airbyte_ops_mcp.cli.registry
CLI commands for connector registry operations.
This module provides CLI wrappers for registry operations. The core logic
lives in the airbyte_ops_mcp.registry capability module.
Command groups:
airbyte-ops registry connector list - List all connectors in registry airbyte-ops registry connector-version list - List versions for a connector airbyte-ops registry connector-version next - Compute next version tag (prerelease/RC) airbyte-ops registry connector-version yank - Mark a connector version as yanked airbyte-ops registry connector-version unyank - Remove yank marker from a connector version airbyte-ops registry connector-version metadata get - Read connector metadata from GCS airbyte-ops registry connector-version releases get - Read release attribution info for one connector version airbyte-ops registry connector-version releases list - List release attribution info for connector versions airbyte-ops registry connector-version artifacts generate - Generate version artifacts locally via docker airbyte-ops registry connector-version artifacts publish - Publish version artifacts to GCS airbyte-ops registry store mirror --local|--gcs-bucket|--s3-bucket - Mirror entire registry for testing airbyte-ops registry store compile --store coral:dev|coral:prod - Compile registry indexes and sync latest/ dirs airbyte-ops registry release-attribution build - Build a Git release attribution index airbyte-ops registry marketing-stubs sync --store coral:prod - Sync connector_stubs.json to GCS airbyte-ops registry marketing-stubs check --store coral:prod - Compare local file with GCS
CLI reference
The commands below are regenerated by poe docs-generate via cyclopts's
programmatic docs API; see docs/generate_cli.py.
airbyte-ops registry COMMAND
Connector registry operations (GCS metadata service).
Commands:
connector: Connector listing operations.connector-version: Connector version operations (list, yank, unyank, artifacts).marketing-stubs: Marketing connector stubs GCS operations (whole-file sync).progressive-rollout: Progressive rollout lifecycle operations.release-attribution: Connector release attribution from Git history.store: Whole-registry store operations (mirror, compile).
airbyte-ops registry connector
Connector listing operations.
airbyte-ops registry connector list
airbyte-ops registry connector list [OPTIONS] STORE
List connectors in the registry.
When filters are applied, reads the compiled cloud_registry.json index
for fast lookups. Without filters, falls back to scanning individual
metadata blobs (captures all connectors including OSS-only).
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--certified-only, --no-certified-only: Include only certified connectors. [default: False]--support-level: Exact support level to match (e.g.certified,community,archived). [choices: archived, community, certified]--min-support-level: Minimum support level (inclusive). Levels from lowest to highest:archived,community,certified. [choices: archived, community, certified]--connector-type: Filter by connector type:sourceordestination. [choices: source, destination]--language: Filter by implementation language (e.g.python,java,manifest-only). [choices: python, java, low-code, manifest-only]--format: Output format: 'json' for JSON array, 'text' for newline-separated. [choices: json, text] [default: json]
airbyte-ops registry connector-version
Connector version operations (list, yank, unyank, artifacts).
airbyte-ops registry connector-version metadata
Connector metadata inspection.
Commands:
get: Read a connector version's metadata from the registry.
airbyte-ops registry connector-version metadata get
airbyte-ops registry connector-version metadata get [OPTIONS] NAME STORE
Read a connector version's metadata from the registry.
Returns the full metadata.yaml content for a connector at the specified version.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
NAME, --name: Connector name (e.g., 'source-faker', 'destination-postgres'). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--version: Version to read (e.g., 'latest', '1.2.3'). [default: latest]--format: Output format: 'json' for JSON, 'raw' for YAML. [choices: json, raw] [default: json]
airbyte-ops registry connector-version releases
Inspect which pull request and author released each connector version.
Commands:
get: Read which pull request and author released one connector version.list: List which pull request and author released each connector version.
airbyte-ops registry connector-version releases get
airbyte-ops registry connector-version releases get CONNECTOR VERSION [ARGS]
Read which pull request and author released one connector version.
Parameters:
CONNECTOR, --connector: Connector name, for examplesource-faker. [required]VERSION, --version: Connector version, for example7.2.1. [required]STORE, --store: Registry store target, for examplecoral:prod. [default: coral:prod]
airbyte-ops registry connector-version releases list
airbyte-ops registry connector-version releases list CONNECTOR [ARGS]
List which pull request and author released each connector version.
The default text output labels human and bot authors explicitly. Listing
reads only the version index unless --with-metadata-fallback is supplied.
Parameters:
CONNECTOR, --connector: Connector name, for examplesource-faker. [required]LIMIT, --limit: Maximum number of versions to return, newest first. [default: 50]WITH-METADATA-FALLBACK, --with-metadata-fallback, --no-metadata-fallback: Read metadata for entries without release attribution info. [default: False]FORMAT, --format: Output format: 'json' for machine-readable data, 'text' for a readable PR/author summary. [choices: json, text] [default: text]STORE, --store: Registry store target, for examplecoral:prod. [default: coral:prod]
airbyte-ops registry connector-version artifacts
Version artifact generation and publishing.
Commands:
generate: Generate version artifacts for a connector locally.publish: Publish version artifacts to GCS using fsspec rsync.
airbyte-ops registry connector-version artifacts generate
airbyte-ops registry connector-version artifacts generate METADATA-FILE DOCKER-IMAGE [ARGS]
Generate version artifacts for a connector locally.
Runs the connector's docker image in cloud mode (DEPLOYMENT_MODE=cloud,
AIRBYTE_EDITION=CLOUD) and oss mode (DEPLOYMENT_MODE=oss,
AIRBYTE_EDITION=COMMUNITY) to obtain both spec variants, then generates
the registry entries (cloud.json, oss.json) by applying
registryOverrides from the metadata.
The generated metadata.yaml is enriched with git commit info, SBOM URL,
and (when applicable) components SHA before writing. Validation is run
after generation by default; pass --no-validate to skip.
This is a local-only operation -- no files are uploaded to GCS.
Use artifacts publish to upload generated artifacts to GCS.
Parameters:
METADATA-FILE, --metadata-file: Path to the connector's metadata.yaml file. [required]DOCKER-IMAGE, --docker-image: Docker image to run spec against (e.g., 'airbyte/source-faker:6.2.38'). [required]OUTPUT-DIR, --output-dir: Directory to write artifacts to. If not specified, a temp directory is created.REPO-ROOT, --repo-root: Root of the Airbyte repo checkout (for resolving doc.md). If not specified, inferred by walking up from metadata-file.DRY-RUN, --dry-run, --no-dry-run: Show what would be generated without running docker or writing files. [default: False]WITH-VALIDATE, --with-validate, --no-validate: Run metadata validators after generation (default: enabled). Use --no-validate to skip. [default: True]WITH-SBOM, --with-sbom, --no-sbom: Generate spdx.json (SBOM) for connectors (default: enabled). Use --no-sbom to skip. [default: True]WITH-DEPENDENCY-DUMP, --with-dependency-dump, --no-dependency-dump: Generate dependencies.json for Python connectors (default: enabled). Use --no-dependency-dump to skip. [default: True]PR-NUMBER, --pr-number: Pull request number for prerelease attribution. When provided, it overrides the commit subject.WITH-GITHUB-LOOKUP, --with-github-lookup, --no-github-lookup: Resolve publish PR author metadata through GitHub GraphQL (default: enabled). Use --no-github-lookup for offline runs. [default: True]
airbyte-ops registry connector-version artifacts publish
airbyte-ops registry connector-version artifacts publish [OPTIONS] NAME VERSION ARTIFACTS-DIR STORE
Publish version artifacts to GCS using fsspec rsync.
Uploads locally generated artifacts (from artifacts generate) to the
versioned path in GCS. By default, metadata is validated before upload;
pass --no-validate to skip.
Uses --store to select the destination store and environment:
coral:dev→ coral dev bucket at rootcoral:prod→ coral prod bucket at rootcoral:dev/aj-test100→ coral dev bucket underaj-test100/prefix
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
NAME, --name: Connector name (e.g., 'source-faker'). [required]VERSION, --version: Version to publish artifacts for (e.g., '1.2.3'). [required]ARTIFACTS-DIR, --artifacts-dir: Directory containing generated artifacts to publish (from 'artifacts generate'). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix'). [required]--dry-run, --no-dry-run: Show what would be published without writing to GCS. [default: False]--with-validate, --no-validate: Validate metadata before uploading (default: enabled). Use --no-validate to skip. [default: True]
airbyte-ops registry connector-version next
airbyte-ops registry connector-version next NAME SHA [ARGS]
Compute the next version tag for a connector.
Outputs the version tag to stdout for easy capture in shell scripts. This is the single source of truth for pre-release version format.
The command fetches the connector's metadata.yaml from GitHub at the given SHA to determine the base version. It also compares against the master branch and prints a warning to stderr if no version bump is detected.
If --base-version is provided, it is used directly instead of fetching from GitHub.
Parameters:
NAME, --name: Connector name (e.g., 'source-github'). [required]SHA, --sha: Git commit SHA (full or at least 7 characters). [required]BASE-VERSION, --base-version: Base version override. If not provided, fetched from metadata.yaml at the given SHA.
airbyte-ops registry connector-version list
airbyte-ops registry connector-version list [OPTIONS] NAME STORE
List all versions of a connector in the registry.
Scans the registry bucket to find all versions of a specific connector.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
NAME, --name: Connector name (e.g., 'source-faker'). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--format: Output format: 'json' for JSON array, 'text' for newline-separated. [choices: json, text] [default: json]
airbyte-ops registry connector-version yank
airbyte-ops registry connector-version yank [OPTIONS] NAME VERSION STORE
Mark a connector version as yanked.
Writes a version-yank.yml marker file to the version's directory in GCS. Yanked versions are excluded when determining the latest version of a connector.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
NAME, --name: Connector name (e.g., 'source-faker'). [required]VERSION, --version: Version to yank (e.g., '1.2.3'). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--reason: Reason for yanking this version. [default: ""]--approval-url: Approval evidence URL to record in the yank marker. [default: ""]--dry-run, --no-dry-run: Show what would be done without making changes. [default: False]
airbyte-ops registry connector-version unyank
airbyte-ops registry connector-version unyank [OPTIONS] NAME VERSION STORE
Rename the active yank marker to a dated audit marker.
Moves version-yank.yml to version-unyanked-yyyymmdd.yml, making the
version eligible again when determining the latest version.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
NAME, --name: Connector name (e.g., 'source-faker'). [required]VERSION, --version: Version to unyank (e.g., '1.2.3'). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--dry-run, --no-dry-run: Show what would be done without making changes. [default: False]
airbyte-ops registry connector-version list-yanked
airbyte-ops registry connector-version list-yanked [OPTIONS] STORE
List all yanked connector versions in the registry.
Returns one entry per active version-yank.yml marker, sorted by connector
name then version. Historical version-unyanked-*.yml audit markers are
ignored.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--format: Output format: 'json' for JSON array, 'text' for newline-separated. [choices: json, text] [default: json]
airbyte-ops registry connector-version yank-status
airbyte-ops registry connector-version yank-status NAME VERSION STORE
Show the active yank marker for a single connector version.
Prints the marker's parsed fields (yanked_at, reason, approval_url)
when the version currently has an active version-yank.yml marker, or
reports that the version is not yanked. Historical
version-unyanked-*.yml audit markers are ignored.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
NAME, --name: Connector name (e.g., 'source-faker'). [required]VERSION, --version: Version to inspect (e.g., '1.2.3'). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]
airbyte-ops registry store
Whole-registry store operations (mirror, compile).
airbyte-ops registry store mirror
airbyte-ops registry store mirror [ARGS]
Create a mirror of the connector registry from the source store.
Reads all connector metadata from the source store and copies it to the specified output target. Supports local filesystem, GCS, and S3 as output targets via fsspec.
Output targets are mutually exclusive: specify exactly one of --local, --gcs-bucket, or --s3-bucket.
The production bucket is categorically disallowed as an output target.
To clean up legacy artifacts (e.g. disabled strict-encrypt connectors)
after mirroring, run compile with --with-legacy-migration v1::
airbyte-ops registry store compile --store coral:dev/my-prefix \
--with-legacy-migration v1
Parameters:
LOCAL, --local: Write output to a local directory. Mutually exclusive with --gcs-bucket and --s3-bucket. [default: False]GCS-BUCKET, --gcs-bucket: Write output to a GCS bucket. Must not be the prod bucket. Mutually exclusive with --local and --s3-bucket.S3-BUCKET, --s3-bucket: Write output to an S3 bucket. Mutually exclusive with --local and --gcs-bucket.OUTPUT-PATH-ROOT, --output-path-root: Root path/prefix for the output. For --local, this is a directory path (defaults to a new temp dir if omitted). For --gcs-bucket/--s3-bucket, this prefix is prepended to all blob paths.DRY-RUN, --dry-run, --no-dry-run: Show what would be rebuilt without writing any files. [default: False]SOURCE-STORE, --source-store: Source store to read from (e.g. 'coral:prod', 'coral:dev'). [default: coral:prod]CONNECTOR-NAME, --connector-name, --empty-connector-name: Only rebuild these connectors (by name). Can be specified multiple times, e.g. --connector-name source-faker --connector-name destination-bigquery.
airbyte-ops registry store compile
airbyte-ops registry store compile [OPTIONS] STORE
Compile the registry: sync latest/ dirs, write global and per-connector indexes.
Scans all version directories in the target store, determines the latest GA
semver per connector (excluding yanked and pre-release versions), ensures
each latest/ directory matches the computed latest, and writes:
registries/v0/cloud_registry.json-- global cloud registry indexregistries/v0/oss_registry.json-- global OSS registry indexmetadata/airbyte/<connector>/versions.json-- per-connector version index
With --with-secrets-mask, also regenerates:
registries/v0/specs_secrets_mask.yaml-- properties marked as secrets
With --with-legacy-migration=v1, deletes cloud.json / oss.json
files for connectors whose registryOverrides.cloud.enabled or
registryOverrides.oss.enabled is false.
By default, injects connector quality metrics from the latest analytics
JSONL export into generated.metrics. Use --no-metrics for offline
scenarios.
With --force, resyncs all latest/ directories even if the version marker
matches the computed latest version.
With --with-full-restate, re-reads every version's metadata.yaml to
re-derive release attribution. This is an expensive operation at roughly
33.5k reads for the full registry.
With --release-attribution-index, seeds historical release blocks
directly into versions.json from a Phase 1 Git backfill index.
Uses efficient glob patterns for scanning (no file downloads during discovery).
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix'). [required]--output-store: Write compiled artifacts to<registry>:local:<path>; omit the path to allocate a temporary directory.--connector-name, --empty-connector-name: Only compile these connectors (can be repeated).--dry-run, --no-dry-run: Show what would be done without writing. [default: False]--with-secrets-mask, --no-with-secrets-mask: Also regenerate specs_secrets_mask.yaml by scanning all connector specs for airbyte_secret properties. [default: False]--with-legacy-migration: Run a one-time legacy migration step during compile. Currently supported: 'v1' — delete cloud.json / oss.json files for connectors whose registryOverrides.cloud.enabled or registryOverrides.oss.enabled is false. This cleans up artifacts produced by the legacy pipeline that did not respect the enabled flag.--with-metrics, --no-metrics: Inject latest connector quality metrics from the analytics JSONL export into generated.metrics. [default: True]--force, --no-force: Force resync of latest/ directories even if version markers are current. Useful when metadata changes without a version bump. [default: False]--with-full-restate, --no-full-restate: Re-derive every version's release attribution from metadata.yaml. Expensive for all connectors (~33.5k reads). Without --release-attribution-index, historical blocks unavailable from metadata.yaml are discarded. [default: False]--release-attribution-index: Seed missing release blocks from a Phase 1 attribution index without rewriting historical metadata.yaml files.
airbyte-ops registry store delete-dev-latest
airbyte-ops registry store delete-dev-latest [OPTIONS] STORE
Delete all latest/ directories from a dev registry store.
Discovers every connector that has a latest/ directory and
deletes each one in parallel using a thread pool.
This is useful before a full re-compile to prove that latest/ directories can be correctly regenerated from versioned data.
Only dev stores are allowed (store must begin with 'coral:dev').
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
STORE, --store: Store target (must begin with 'coral:dev'). [required]--connector-name, --empty-connector-name: Only delete latest/ for these connectors (can be repeated).--dry-run, --no-dry-run: Show what would be done without deleting. [default: False]
airbyte-ops registry store compare
airbyte-ops registry store compare [OPTIONS] STORE REFERENCE-STORE
Compare a store against a reference store and report differences.
Evaluates the --store target against --reference-store and reports
per-connector artifact diffs and global index diffs.
Requires GCS_CREDENTIALS environment variable to be set.
Parameters:
STORE, --store: Store target being evaluated (e.g. 'coral:dev/20260306-mirror-compile'). [required]REFERENCE-STORE, --reference-store: Known-good reference store to compare against. [required]--connector-name, --empty-connector-name: Only compare these connectors (can be repeated).--with-artifacts, --no-artifacts: Compare per-connector artifact files (metadata.yaml, cloud.json, oss.json, spec.json). [default: True]--with-indexes, --no-indexes: Compare global registry index files (cloud_registry.json, oss_registry.json, composite_registry.json). [default: True]--assert-stable, --no-assert-stable: Make the exit code reflect registry safety assertions only. [default: False]--with-volatile-fields, --no-with-volatile-fields: Include default volatile-field differences. [default: False]--html-report: Write a self-contained HTML report to this path.--text-report: Write a plain-text diff report to this path.
airbyte-ops registry progressive-rollout
Progressive rollout lifecycle operations.
airbyte-ops registry progressive-rollout list
airbyte-ops registry progressive-rollout list
List all connectors with active release candidates in the compiled registry.
airbyte-ops registry progressive-rollout status
airbyte-ops registry progressive-rollout status [OPTIONS] NAME
Get progressive rollout status for a connector.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]--repo-path: Path to the Airbyte monorepo. Defaults to current directory. [default: /home/runner/work/airbyte-ops-mcp/airbyte-ops-mcp]--active-only, --with-terminal: Only return active non-terminal rollouts. [default: True]--limit: Maximum number of rollout records to return. [default: 100]
airbyte-ops registry progressive-rollout finalize-marker
airbyte-ops registry progressive-rollout finalize-marker [OPTIONS] NAME STORE OUTCOME
Finalize a rollout marker; aborted first yanks the version so it cannot become latest.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]OUTCOME, --outcome: Marker outcome used in the dated audit filename. Aborted first writes version-yank.yml so the version cannot become latest. [required] [choices: promoted, aborted]--version: Version to finalize. If omitted, exactly one active marker must exist.--dry-run, --no-dry-run: Show what would be done without making changes. [default: False]
airbyte-ops registry marketing-stubs
Marketing connector stubs GCS operations (whole-file sync).
airbyte-ops registry marketing-stubs check
airbyte-ops registry marketing-stubs check [OPTIONS] STORE
Compare local connector_stubs.json with the version in GCS.
This command reads the entire local connector_stubs.json file and compares it with the version currently published in GCS.
Exit codes:
0: Local file matches GCS (check passed) 1: Differences found (check failed)
Output:
STDOUT: JSON representation of the comparison result STDERR: Informational messages and comparison details
Parameters:
STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--repo-root: Path to the airbyte-enterprise repository root. Defaults to current directory. [default: /home/runner/work/airbyte-ops-mcp/airbyte-ops-mcp]
airbyte-ops registry marketing-stubs sync
airbyte-ops registry marketing-stubs sync [OPTIONS] STORE
Sync local connector_stubs.json to GCS.
This command uploads the entire local connector_stubs.json file to GCS, replacing the existing file. Use this after merging changes to master in the airbyte-enterprise repository.
Exit codes:
0: Sync successful (or dry-run completed) 1: Error (file not found, validation failed, etc.)
Output:
STDOUT: JSON representation of the sync result STDERR: Informational messages and status updates
Parameters:
STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]--repo-root: Path to the airbyte-enterprise repository root. Defaults to current directory. [default: /home/runner/work/airbyte-ops-mcp/airbyte-ops-mcp]--dry-run, --no-dry-run: Show what would be uploaded without making changes. [default: False]
airbyte-ops registry release-attribution
Connector release attribution from Git history.
airbyte-ops registry release-attribution build
airbyte-ops registry release-attribution build [OPTIONS] REPO-PATH OUTPUT-PATH
Build a connector release attribution index from Git history.
Parameters:
REPO-PATH, --repo-path: Path to a full airbyte monorepo checkout. [required]OUTPUT-PATH, --output-path: Local JSON path to write the attribution index. [required]--connector: Limit the scan to one connector.--with-github-enrichment, --no-github-enrichment: Enrich missing PR author metadata through GitHub GraphQL. [default: False]
airbyte-ops registry release-attribution show
airbyte-ops registry release-attribution show INDEX-PATH CONNECTOR VERSION
Show attribution for one connector version.
Parameters:
INDEX-PATH, --index-path: Attribution index JSON path. [required]CONNECTOR, --connector: Connector name, for examplesource-faker. [required]VERSION, --version: Connector version, for example7.2.1. [required]
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""CLI commands for connector registry operations. 3 4This module provides CLI wrappers for registry operations. The core logic 5lives in the `airbyte_ops_mcp.registry` capability module. 6 7Command groups: 8 airbyte-ops registry connector list - List all connectors in registry 9 airbyte-ops registry connector-version list - List versions for a connector 10 airbyte-ops registry connector-version next - Compute next version tag (prerelease/RC) 11 airbyte-ops registry connector-version yank - Mark a connector version as yanked 12 airbyte-ops registry connector-version unyank - Remove yank marker from a connector version 13 airbyte-ops registry connector-version metadata get - Read connector metadata from GCS 14 airbyte-ops registry connector-version releases get - Read release attribution info for one connector version 15 airbyte-ops registry connector-version releases list - List release attribution info for connector versions 16 airbyte-ops registry connector-version artifacts generate - Generate version artifacts locally via docker 17 airbyte-ops registry connector-version artifacts publish - Publish version artifacts to GCS 18 airbyte-ops registry store mirror --local|--gcs-bucket|--s3-bucket - Mirror entire registry for testing 19 airbyte-ops registry store compile --store coral:dev|coral:prod - Compile registry indexes and sync latest/ dirs 20 airbyte-ops registry release-attribution build - Build a Git release attribution index 21 airbyte-ops registry marketing-stubs sync --store coral:prod - Sync connector_stubs.json to GCS 22 airbyte-ops registry marketing-stubs check --store coral:prod - Compare local file with GCS 23 24## CLI reference 25 26The commands below are regenerated by `poe docs-generate` via cyclopts's 27programmatic docs API; see `docs/generate_cli.py`. 28 29.. include:: ../../../docs/generated/cli/registry.md 30 :start-line: 2 31""" 32 33from __future__ import annotations 34 35# Hide Python-level members from the pdoc page for this module; the rendered 36# docs for this CLI group come entirely from the grafted `.. include::` in 37# the module docstring above. 38__all__: list[str] = [] 39 40import sys 41from pathlib import Path 42from typing import Annotated, Any, Literal 43 44import yaml 45from cyclopts import Parameter 46from fastmcp_extensions.cli import ( 47 exit_with_error, 48 print_error, 49 print_json, 50 print_success, 51) 52from rich.console import Console 53 54from airbyte_ops_mcp.cli._base import App, app 55from airbyte_ops_mcp.github_api import ( 56 get_file_contents_at_ref, 57 resolve_default_github_token, 58) 59from airbyte_ops_mcp.mcp.connector_versions import ( 60 compute_prerelease_docker_image_tag, 61) 62from airbyte_ops_mcp.registry import ( 63 resolve_registry_store, 64) 65from airbyte_ops_mcp.registry._constants import PROD_METADATA_SERVICE_BUCKET_NAME 66from airbyte_ops_mcp.registry._enums import ( 67 ConnectorLanguage, 68 ConnectorType, 69 SupportLevel, 70) 71from airbyte_ops_mcp.registry.compare import ( 72 DEFAULT_TOLERATED_PATHS, 73 compare_stores, 74 write_html_report, 75 write_text_report, 76) 77from airbyte_ops_mcp.registry.connector_stubs import ( 78 CONNECTOR_STUBS_FILE, 79) 80from airbyte_ops_mcp.registry.generate import generate_version_artifacts 81from airbyte_ops_mcp.registry.operations import _read_cloud_registry_index 82from airbyte_ops_mcp.registry.progressive_rollout_status import ( 83 get_connector_rollout_status, 84) 85from airbyte_ops_mcp.registry.registry_store_base import ( 86 Registry, 87 get_registry, 88) 89from airbyte_ops_mcp.registry.release_attribution import ( 90 ACTOR_BOT, 91 ACTOR_USER, 92 KIND_BOT, 93 ReleaseAttributionListResult, 94 enrich_release_attribution, 95 list_release_attribution, 96 lookup_release_attribution, 97 read_release_attribution_index, 98 scan_release_attribution, 99 write_release_attribution_index, 100) 101from airbyte_ops_mcp.registry.store import RegistryStore 102 103error_console = Console(stderr=True) 104 105# Create the registry sub-app 106registry_app = App( 107 name="registry", help="Connector registry operations (GCS metadata service)." 108) 109app.command(registry_app) 110 111# Create the connector sub-app under registry 112connector_app = App(name="connector", help="Connector listing operations.") 113registry_app.command(connector_app) 114 115# Create the connector-version sub-app under registry 116connector_version_app = App( 117 name="connector-version", 118 help="Connector version operations (list, yank, unyank, artifacts).", 119) 120registry_app.command(connector_version_app) 121 122# Create the metadata sub-app under connector-version 123metadata_app = App( 124 name="metadata", 125 help="Connector metadata inspection.", 126) 127connector_version_app.command(metadata_app) 128 129# Create the releases sub-app under connector-version 130releases_app = App( 131 name="releases", 132 help="Inspect which pull request and author released each connector version.", 133) 134connector_version_app.command(releases_app) 135 136# Create the artifacts sub-app under connector-version 137artifacts_app = App( 138 name="artifacts", 139 help="Version artifact generation and publishing.", 140) 141connector_version_app.command(artifacts_app) 142 143# Create the store sub-app under registry (whole-registry operations) 144store_app = App( 145 name="store", 146 help="Whole-registry store operations (mirror, compile).", 147) 148registry_app.command(store_app) 149 150# Create the progressive-rollout sub-app under registry 151progressive_rollout_app = App( 152 name="progressive-rollout", 153 help="Progressive rollout lifecycle operations.", 154) 155registry_app.command(progressive_rollout_app) 156 157# Create the marketing-stubs sub-app under registry (for whole-file GCS operations) 158marketing_stubs_app = App( 159 name="marketing-stubs", 160 help="Marketing connector stubs GCS operations (whole-file sync).", 161) 162registry_app.command(marketing_stubs_app) 163 164# Create the release-attribution sub-app under registry 165release_attribution_app = App( 166 name="release-attribution", 167 help="Connector release attribution from Git history.", 168) 169registry_app.command(release_attribution_app) 170 171 172AIRBYTE_REPO_OWNER = "airbytehq" 173AIRBYTE_ENTERPRISE_REPO_NAME = "airbyte-enterprise" 174AIRBYTE_REPO_NAME = "airbyte" 175CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors" 176 177 178@release_attribution_app.command(name="build") 179def build_release_attribution_cmd( 180 repo_path: Annotated[ 181 Path, 182 Parameter(help="Path to a full airbyte monorepo checkout."), 183 ], 184 output_path: Annotated[ 185 Path, 186 Parameter(help="Local JSON path to write the attribution index."), 187 ], 188 *, 189 connector: Annotated[ 190 str | None, 191 Parameter(help="Limit the scan to one connector."), 192 ] = None, 193 with_github_enrichment: Annotated[ 194 bool, 195 Parameter( 196 help="Enrich missing PR author metadata through GitHub GraphQL.", 197 negative="--no-github-enrichment", 198 ), 199 ] = False, 200) -> None: 201 """Build a connector release attribution index from Git history.""" 202 try: 203 index = scan_release_attribution(repo_path, connector=connector) 204 if with_github_enrichment: 205 index = enrich_release_attribution(index) 206 write_release_attribution_index(index, output_path) 207 except (OSError, ValueError) as exc: 208 exit_with_error(str(exc)) 209 summary = index.summary 210 print_json( 211 { 212 **summary.model_dump(), 213 "pr_percentage": summary.pr_percentage, 214 "author_login_percentage": summary.author_login_percentage, 215 "output_path": str(output_path), 216 } 217 ) 218 219 220@release_attribution_app.command(name="show") 221def show_release_attribution_cmd( 222 index_path: Annotated[ 223 Path, 224 Parameter(help="Attribution index JSON path."), 225 ], 226 connector: Annotated[ 227 str, 228 Parameter(help="Connector name, for example `source-faker`."), 229 ], 230 version: Annotated[ 231 str, 232 Parameter(help="Connector version, for example `7.2.1`."), 233 ], 234) -> None: 235 """Show attribution for one connector version.""" 236 try: 237 index = read_release_attribution_index(index_path) 238 except (OSError, ValueError) as exc: 239 exit_with_error(f"Could not read attribution index {index_path}: {exc}") 240 if connector not in index.connectors: 241 exit_with_error( 242 f"Attribution not found for connector `{connector}` in `{index_path}`." 243 ) 244 if version not in index.connectors[connector]: 245 exit_with_error( 246 f"Attribution not found for `{connector}@{version}` in `{index_path}`." 247 ) 248 record = index.connectors[connector][version] 249 print_json(record.model_dump(mode="json")) 250 251 252@releases_app.command(name="get") 253def release_attribution_get_cmd( 254 connector: Annotated[ 255 str, 256 Parameter(help="Connector name, for example `source-faker`."), 257 ], 258 version: Annotated[ 259 str, 260 Parameter(help="Connector version, for example `7.2.1`."), 261 ], 262 store: Annotated[ 263 str, 264 Parameter(help="Registry store target, for example `coral:prod`."), 265 ] = "coral:prod", 266) -> None: 267 """Read which pull request and author released one connector version.""" 268 try: 269 result = lookup_release_attribution( 270 resolve_registry_store(store=store), 271 connector, 272 version, 273 ) 274 except (OSError, ValueError) as exc: 275 exit_with_error(str(exc)) 276 print_json(result.model_dump(mode="json")) 277 278 279def _print_release_attribution_list( 280 result: ReleaseAttributionListResult, 281 *, 282 format: Literal["json", "text"], 283) -> None: 284 """Render release attribution as JSON or an author-focused text table.""" 285 if format == "json": 286 print_json(result.model_dump(mode="json")) 287 return 288 289 print("VERSION\tSTATUS\tAUTHOR TYPE\tCONTACT\tPULL REQUEST\tRELEASED AT") 290 for item in result.items: 291 attribution = item.attribution 292 if attribution is None: 293 author_type = "UNKNOWN" 294 contact = "no attribution" 295 pull_request = "-" 296 released_at = "-" 297 else: 298 author_type = { 299 ACTOR_USER: "HUMAN", 300 ACTOR_BOT: "BOT", 301 }.get(attribution.pr_author_type or "", "UNKNOWN") 302 if attribution.attributed_to and attribution.attributed_to_kind != KIND_BOT: 303 contact = f"@{attribution.attributed_to}" 304 elif ( 305 attribution.attributed_to_kind == KIND_BOT 306 or attribution.pr_author_type == ACTOR_BOT 307 ): 308 contact = "none (bot-authored)" 309 elif attribution.pr_author_type == ACTOR_USER: 310 contact = "none (human login unavailable)" 311 else: 312 contact = "none (author unknown)" 313 pull_request = ( 314 f"#{attribution.pr_number}" 315 if attribution.pr_number is not None 316 else "-" 317 ) 318 released_at = ( 319 attribution.released_at.isoformat() 320 if attribution.released_at is not None 321 else "-" 322 ) 323 print( 324 f"{item.version}\t{item.status}\t{author_type}\t{contact}\t" 325 f"{pull_request}\t{released_at}" 326 ) 327 if result.error: 328 print(f"ERROR\t{result.error}") 329 330 331@releases_app.command(name="list") 332def release_attribution_list_cmd( 333 connector: Annotated[ 334 str, 335 Parameter(help="Connector name, for example `source-faker`."), 336 ], 337 limit: Annotated[ 338 int, 339 Parameter(help="Maximum number of versions to return, newest first."), 340 ] = 50, 341 with_metadata_fallback: Annotated[ 342 bool, 343 Parameter( 344 help="Read metadata for entries without release attribution info.", 345 negative="--no-metadata-fallback", 346 ), 347 ] = False, 348 format: Annotated[ 349 Literal["json", "text"], 350 Parameter( 351 help="Output format: 'json' for machine-readable data, 'text' for a readable PR/author summary." 352 ), 353 ] = "text", 354 store: Annotated[ 355 str, 356 Parameter(help="Registry store target, for example `coral:prod`."), 357 ] = "coral:prod", 358) -> None: 359 """List which pull request and author released each connector version. 360 361 The default text output labels human and bot authors explicitly. Listing 362 reads only the version index unless `--with-metadata-fallback` is supplied. 363 """ 364 try: 365 result = list_release_attribution( 366 resolve_registry_store(store=store), 367 connector, 368 limit=limit, 369 with_metadata_fallback=with_metadata_fallback, 370 ) 371 except (OSError, ValueError) as exc: 372 exit_with_error(str(exc)) 373 _print_release_attribution_list(result, format=format) 374 375 376def _resolve_store(store: str) -> Registry: 377 """Resolve `--store` to a `Registry` instance. 378 379 Wraps `resolve_registry_store` and `get_registry`, converting 380 `ValueError` into a user-friendly CLI error via `exit_with_error`. 381 """ 382 try: 383 resolved = resolve_registry_store(store=store) 384 except ValueError as e: 385 exit_with_error(str(e)) 386 raise # unreachable; satisfies type checker 387 return get_registry(resolved) 388 389 390def _get_connector_version_from_github( 391 connector_name: str, 392 ref: str, 393 token: str | None = None, 394) -> str | None: 395 """Fetch connector version from metadata.yaml via GitHub API. 396 397 Args: 398 connector_name: Connector name (e.g., "source-github") 399 ref: Git ref (commit SHA, branch name, or tag) 400 token: GitHub API token (optional for public repos) 401 402 Returns: 403 Version string from metadata.yaml, or None if not found. 404 """ 405 path = f"{CONNECTOR_PATH_PREFIX}/{connector_name}/metadata.yaml" 406 contents = get_file_contents_at_ref( 407 owner=AIRBYTE_REPO_OWNER, 408 repo=AIRBYTE_REPO_NAME, 409 path=path, 410 ref=ref, 411 token=token, 412 ) 413 if contents is None: 414 return None 415 416 metadata = yaml.safe_load(contents) 417 return metadata.get("data", {}).get("dockerImageTag") 418 419 420@connector_version_app.command(name="next") 421def compute_next_version( 422 name: Annotated[ 423 str, 424 Parameter(help="Connector name (e.g., 'source-github')."), 425 ], 426 sha: Annotated[ 427 str, 428 Parameter(help="Git commit SHA (full or at least 7 characters)."), 429 ], 430 base_version: Annotated[ 431 str | None, 432 Parameter( 433 help="Base version override. If not provided, fetched from metadata.yaml at the given SHA." 434 ), 435 ] = None, 436) -> None: 437 """Compute the next version tag for a connector. 438 439 Outputs the version tag to stdout for easy capture in shell scripts. 440 This is the single source of truth for pre-release version format. 441 442 The command fetches the connector's metadata.yaml from GitHub at the given SHA 443 to determine the base version. It also compares against the master branch and 444 prints a warning to stderr if no version bump is detected. 445 446 If --base-version is provided, it is used directly instead of fetching from GitHub. 447 448 Examples: 449 airbyte-ops registry connector-version next --name source-github --sha abcdef1234567 450 # Output: 1.2.3-preview.abcdef1 451 452 airbyte-ops registry connector-version next --name source-github --sha abcdef1234567 --base-version 1.2.3 453 # Output: 1.2.3-preview.abcdef1 (uses provided version, skips GitHub API) 454 """ 455 # Try to get a GitHub token (optional, but helps avoid rate limiting) 456 # Token resolution may fail if no token is configured, which is fine for public repos 457 token: str | None = None 458 token = resolve_default_github_token(allow_none=True) 459 460 # Determine base version 461 version: str 462 if base_version: 463 version = base_version 464 else: 465 # Fetch version from metadata.yaml at the given SHA 466 fetched_version = _get_connector_version_from_github(name, sha, token) 467 if fetched_version is None: 468 print( 469 f"Error: Could not fetch metadata.yaml for {name} at ref {sha}", 470 file=sys.stderr, 471 ) 472 sys.exit(1) 473 version = fetched_version 474 475 # Compare with master branch version and warn if no bump detected 476 master_version = _get_connector_version_from_github(name, "master", token) 477 if master_version and master_version == version: 478 print( 479 f"Warning: No version bump detected for {name}. " 480 f"Version {version} matches master branch.", 481 file=sys.stderr, 482 ) 483 484 # Compute and output the prerelease tag 485 tag = compute_prerelease_docker_image_tag(version, sha) 486 print(tag) 487 488 489@progressive_rollout_app.command(name="list") 490def progressive_rollout_list() -> None: 491 """List all connectors with active release candidates in the compiled registry.""" 492 try: 493 entries = _read_cloud_registry_index( 494 bucket_name=PROD_METADATA_SERVICE_BUCKET_NAME, 495 ) 496 except FileNotFoundError as e: 497 exit_with_error(str(e)) 498 499 rc_entries: list[dict[str, Any]] = [] 500 for entry in entries: 501 releases = entry.get("releases", {}) 502 candidates = releases.get("releaseCandidates") 503 if not candidates: 504 continue 505 docker_repo = entry.get("dockerRepository", "") 506 connector_name = ( 507 docker_repo.split("/", 1)[1] if "/" in docker_repo else docker_repo 508 ) 509 rc_entries.append( 510 { 511 "connector": connector_name, 512 "rc_versions": list(candidates.keys()), 513 } 514 ) 515 516 rc_entries.sort(key=lambda x: x["connector"]) 517 print_json(rc_entries) 518 519 520@progressive_rollout_app.command(name="status") 521def progressive_rollout_status( 522 name: Annotated[ 523 str, 524 Parameter(help="Connector technical name (e.g., source-github)."), 525 ], 526 *, 527 repo_path: Annotated[ 528 Path, 529 Parameter(help="Path to the Airbyte monorepo. Defaults to current directory."), 530 ] = Path.cwd(), 531 active_only: Annotated[ 532 bool, 533 Parameter( 534 help="Only return active non-terminal rollouts.", 535 negative="--with-terminal", 536 ), 537 ] = True, 538 limit: Annotated[ 539 int, 540 Parameter(help="Maximum number of rollout records to return."), 541 ] = 100, 542) -> None: 543 """Get progressive rollout status for a connector.""" 544 if not repo_path.exists(): 545 exit_with_error(f"Repository path not found: {repo_path}") 546 try: 547 result = get_connector_rollout_status( 548 repo_path=repo_path, 549 connector_name=name, 550 active_only=active_only, 551 limit=limit, 552 ) 553 except (FileNotFoundError, ValueError) as e: 554 exit_with_error(str(e)) 555 556 print_json(result.model_dump()) 557 558 559@progressive_rollout_app.command(name="finalize-marker") 560def progressive_rollout_finalize_marker( 561 name: Annotated[ 562 str, 563 Parameter(help="Connector technical name (e.g., source-github)."), 564 ], 565 store: Annotated[ 566 str, 567 Parameter(help="Store target (e.g. 'coral:dev', 'coral:prod')."), 568 ], 569 outcome: Annotated[ 570 Literal["promoted", "aborted"], 571 Parameter( 572 help="Marker outcome used in the dated audit filename. Aborted first " 573 "writes version-yank.yml so the version cannot become latest." 574 ), 575 ], 576 *, 577 version: Annotated[ 578 str | None, 579 Parameter( 580 help="Version to finalize. If omitted, exactly one active marker must exist." 581 ), 582 ] = None, 583 dry_run: Annotated[ 584 bool, 585 Parameter(help="Show what would be done without making changes."), 586 ] = False, 587) -> None: 588 """Finalize a rollout marker; aborted first yanks the version so it cannot become latest.""" 589 registry = _resolve_store(store) 590 result = registry.finalize_progressive_rollout_marker( 591 connector_name=name, 592 outcome=outcome, 593 version=version, 594 dry_run=dry_run, 595 ) 596 print_json(result.to_dict()) 597 if result.success: 598 print_success(result.message) 599 else: 600 exit_with_error(result.message, code=1) 601 602 603# ============================================================================= 604# REGISTRY I/O - READ COMMANDS 605# ============================================================================= 606 607 608@metadata_app.command(name="get") 609def get_connector_version_metadata_cmd( 610 name: Annotated[ 611 str, 612 Parameter( 613 help="Connector name (e.g., 'source-faker', 'destination-postgres')." 614 ), 615 ], 616 store: Annotated[ 617 str, 618 Parameter( 619 help="Store target (e.g. 'coral:dev', 'coral:prod').", 620 ), 621 ], 622 *, 623 version: Annotated[ 624 str, 625 Parameter(help="Version to read (e.g., 'latest', '1.2.3')."), 626 ] = "latest", 627 format: Annotated[ 628 Literal["json", "raw"], 629 Parameter(help="Output format: 'json' for JSON, 'raw' for YAML."), 630 ] = "json", 631) -> None: 632 """Read a connector version's metadata from the registry. 633 634 Returns the full metadata.yaml content for a connector at the specified version. 635 636 Requires GCS_CREDENTIALS environment variable to be set. 637 638 Examples: 639 airbyte-ops registry connector-version metadata get --name source-faker --store coral:dev 640 airbyte-ops registry connector-version metadata get --name source-faker --store coral:dev --version 6.2.38 641 airbyte-ops registry connector-version metadata get --name source-faker --store coral:prod 642 """ 643 registry = _resolve_store(store) 644 645 try: 646 metadata = registry.get_connector_metadata( 647 connector_name=name, 648 version=version, 649 ) 650 except FileNotFoundError as e: 651 exit_with_error(str(e), code=1) 652 except Exception as e: 653 exit_with_error(f"Error reading metadata: {e}", code=1) 654 655 if format == "json": 656 print_json(metadata) 657 else: 658 print(yaml.dump(metadata, default_flow_style=False)) 659 660 661@connector_app.command(name="list") 662def list_connectors_cmd( 663 store: Annotated[ 664 str, 665 Parameter( 666 help="Store target (e.g. 'coral:dev', 'coral:prod').", 667 ), 668 ], 669 *, 670 certified_only: Annotated[ 671 bool, 672 Parameter(help="Include only certified connectors."), 673 ] = False, 674 support_level: Annotated[ 675 SupportLevel | None, 676 Parameter( 677 help=( 678 "Exact support level to match " 679 "(e.g. `certified`, `community`, `archived`)." 680 ) 681 ), 682 ] = None, 683 min_support_level: Annotated[ 684 SupportLevel | None, 685 Parameter( 686 help=( 687 "Minimum support level (inclusive). " 688 "Levels from lowest to highest: `archived`, `community`, `certified`." 689 ) 690 ), 691 ] = None, 692 connector_type: Annotated[ 693 ConnectorType | None, 694 Parameter(help="Filter by connector type: `source` or `destination`."), 695 ] = None, 696 language: Annotated[ 697 ConnectorLanguage | None, 698 Parameter( 699 help=( 700 "Filter by implementation language " 701 "(e.g. `python`, `java`, `manifest-only`)." 702 ) 703 ), 704 ] = None, 705 format: Annotated[ 706 Literal["json", "text"], 707 Parameter( 708 help="Output format: 'json' for JSON array, 'text' for newline-separated." 709 ), 710 ] = "json", 711) -> None: 712 """List connectors in the registry. 713 714 When filters are applied, reads the compiled `cloud_registry.json` index 715 for fast lookups. Without filters, falls back to scanning individual 716 metadata blobs (captures all connectors including OSS-only). 717 718 Requires GCS_CREDENTIALS environment variable to be set. 719 """ 720 registry = _resolve_store(store) 721 722 # `--certified-only` is sugar for `--support-level certified`. 723 effective_support_level = support_level 724 if certified_only: 725 if support_level and support_level != SupportLevel.CERTIFIED: 726 exit_with_error( 727 "`--certified-only` conflicts with `--support-level " 728 f"{support_level}`. Use one or the other.", 729 code=1, 730 ) 731 effective_support_level = SupportLevel.CERTIFIED 732 733 try: 734 connectors = registry.list_connectors( 735 support_level=effective_support_level, 736 min_support_level=min_support_level, 737 connector_type=connector_type, 738 language=language, 739 ) 740 except Exception as e: 741 exit_with_error(f"Error listing connectors: {e}", code=1) 742 743 if format == "json": 744 print_json({"connectors": connectors, "count": len(connectors)}) 745 else: 746 for connector in connectors: 747 print(connector) 748 749 750@connector_version_app.command(name="list") 751def list_connector_versions_cmd( 752 name: Annotated[ 753 str, 754 Parameter(help="Connector name (e.g., 'source-faker')."), 755 ], 756 store: Annotated[ 757 str, 758 Parameter( 759 help="Store target (e.g. 'coral:dev', 'coral:prod').", 760 ), 761 ], 762 *, 763 format: Annotated[ 764 Literal["json", "text"], 765 Parameter( 766 help="Output format: 'json' for JSON array, 'text' for newline-separated." 767 ), 768 ] = "json", 769) -> None: 770 """List all versions of a connector in the registry. 771 772 Scans the registry bucket to find all versions of a specific connector. 773 774 Requires GCS_CREDENTIALS environment variable to be set. 775 """ 776 registry = _resolve_store(store) 777 778 try: 779 versions = registry.list_connector_versions( 780 connector_name=name, 781 ) 782 except Exception as e: 783 exit_with_error(f"Error listing versions: {e}", code=1) 784 785 if format == "json": 786 print_json({"connector": name, "versions": versions, "count": len(versions)}) 787 else: 788 for v in versions: 789 print(v) 790 791 792@marketing_stubs_app.command(name="check") 793def marketing_stubs_check( 794 store: Annotated[ 795 str, 796 Parameter( 797 help="Store target (e.g. 'coral:dev', 'coral:prod').", 798 ), 799 ], 800 *, 801 repo_root: Annotated[ 802 Path, 803 Parameter( 804 help="Path to the airbyte-enterprise repository root. Defaults to current directory." 805 ), 806 ] = Path.cwd(), 807) -> None: 808 """Compare local connector_stubs.json with the version in GCS. 809 810 This command reads the entire local connector_stubs.json file and compares it 811 with the version currently published in GCS. 812 813 Exit codes: 814 0: Local file matches GCS (check passed) 815 1: Differences found (check failed) 816 817 Output: 818 STDOUT: JSON representation of the comparison result 819 STDERR: Informational messages and comparison details 820 821 Example: 822 airbyte-ops registry marketing-stubs check --store coral:prod --repo-root /path/to/airbyte-enterprise 823 airbyte-ops registry marketing-stubs check --store coral:dev 824 """ 825 registry = _resolve_store(store) 826 827 try: 828 result = registry.marketing_stubs_check(repo_root=repo_root) 829 except FileNotFoundError as e: 830 exit_with_error(str(e)) 831 except ValueError as e: 832 exit_with_error(str(e)) 833 834 error_console.print( 835 f"Comparing local {CONNECTOR_STUBS_FILE} with {result.get('bucket', '')}/{result.get('path', '')}" 836 ) 837 838 differences = result.get("differences", []) 839 if differences: 840 error_console.print( 841 f"[yellow]Warning:[/yellow] {len(differences)} difference(s) found:" 842 ) 843 for diff in differences: 844 error_console.print(f" {diff['id']}: {diff['status']}") 845 print_json(result) 846 sys.exit(1) 847 848 error_console.print( 849 f"[green]Local file is in sync with GCS ({result.get('local_count', 0)} stubs)[/green]" 850 ) 851 print_json(result) 852 853 854@marketing_stubs_app.command(name="sync") 855def marketing_stubs_sync( 856 store: Annotated[ 857 str, 858 Parameter( 859 help="Store target (e.g. 'coral:dev', 'coral:prod').", 860 ), 861 ], 862 *, 863 repo_root: Annotated[ 864 Path, 865 Parameter( 866 help="Path to the airbyte-enterprise repository root. Defaults to current directory." 867 ), 868 ] = Path.cwd(), 869 dry_run: Annotated[ 870 bool, 871 Parameter(help="Show what would be uploaded without making changes."), 872 ] = False, 873) -> None: 874 """Sync local connector_stubs.json to GCS. 875 876 This command uploads the entire local connector_stubs.json file to GCS, 877 replacing the existing file. Use this after merging changes to master 878 in the airbyte-enterprise repository. 879 880 Exit codes: 881 0: Sync successful (or dry-run completed) 882 1: Error (file not found, validation failed, etc.) 883 884 Output: 885 STDOUT: JSON representation of the sync result 886 STDERR: Informational messages and status updates 887 888 Example: 889 airbyte-ops registry marketing-stubs sync --store coral:prod --repo-root /path/to/airbyte-enterprise 890 airbyte-ops registry marketing-stubs sync --store coral:dev 891 airbyte-ops registry marketing-stubs sync --store coral:dev --dry-run 892 """ 893 registry = _resolve_store(store) 894 895 try: 896 result = registry.marketing_stubs_sync( 897 repo_root=repo_root, 898 dry_run=dry_run, 899 ) 900 except FileNotFoundError as e: 901 exit_with_error(str(e)) 902 except ValueError as e: 903 exit_with_error(str(e)) 904 905 bucket_name = result.get("bucket", "") 906 path = result.get("path", "") 907 stub_count = result.get("stub_count", 0) 908 909 if dry_run: 910 error_console.print( 911 f"[DRY RUN] Would upload {stub_count} stubs to {bucket_name}/{path}" 912 ) 913 else: 914 error_console.print( 915 f"[green]Synced {stub_count} stubs to {bucket_name}/{path}[/green]" 916 ) 917 print_json(result) 918 919 920# ============================================================================= 921# REGISTRY REBUILD COMMANDS 922# ============================================================================= 923 924 925@store_app.command(name="mirror") 926def mirror_cmd( 927 local: Annotated[ 928 bool, 929 Parameter( 930 help="Write output to a local directory. Mutually exclusive with --gcs-bucket and --s3-bucket.", 931 negative="", 932 ), 933 ] = False, 934 gcs_bucket: Annotated[ 935 str | None, 936 Parameter( 937 help="Write output to a GCS bucket. Must not be the prod bucket. " 938 "Mutually exclusive with --local and --s3-bucket.", 939 ), 940 ] = None, 941 s3_bucket: Annotated[ 942 str | None, 943 Parameter( 944 help="Write output to an S3 bucket. " 945 "Mutually exclusive with --local and --gcs-bucket.", 946 ), 947 ] = None, 948 output_path_root: Annotated[ 949 str | None, 950 Parameter( 951 help="Root path/prefix for the output. For --local, this is a directory path " 952 "(defaults to a new temp dir if omitted). For --gcs-bucket/--s3-bucket, " 953 "this prefix is prepended to all blob paths.", 954 ), 955 ] = None, 956 dry_run: Annotated[ 957 bool, 958 Parameter(help="Show what would be rebuilt without writing any files."), 959 ] = False, 960 source_store: Annotated[ 961 str, 962 Parameter( 963 help="Source store to read from (e.g. 'coral:prod', 'coral:dev').", 964 ), 965 ] = "coral:prod", 966 connector_name: Annotated[ 967 tuple[str, ...] | None, 968 Parameter( 969 help="Only rebuild these connectors (by name). " 970 "Can be specified multiple times, e.g. " 971 "--connector-name source-faker --connector-name destination-bigquery.", 972 ), 973 ] = None, 974) -> None: 975 """Create a mirror of the connector registry from the source store. 976 977 Reads all connector metadata from the source store and copies it 978 to the specified output target. Supports local filesystem, GCS, and S3 979 as output targets via fsspec. 980 981 Output targets are mutually exclusive: specify exactly one of 982 --local, --gcs-bucket, or --s3-bucket. 983 984 The production bucket is categorically disallowed as an output target. 985 986 To clean up legacy artifacts (e.g. disabled strict-encrypt connectors) 987 after mirroring, run compile with `--with-legacy-migration v1`:: 988 989 airbyte-ops registry store compile --store coral:dev/my-prefix \\ 990 --with-legacy-migration v1 991 992 Examples: 993 airbyte-ops registry store mirror --local 994 airbyte-ops registry store mirror --local --source-store coral:dev 995 airbyte-ops registry store mirror --gcs-bucket dev-airbyte-cloud-connector-metadata-service-2 \\ 996 --output-path-root test-run-123 997 airbyte-ops registry store mirror --s3-bucket my-test-bucket --dry-run 998 """ 999 # Validate mutually exclusive output targets 1000 targets = [local, gcs_bucket is not None, s3_bucket is not None] 1001 if sum(targets) != 1: 1002 exit_with_error( 1003 "Specify exactly one output target: --local, --gcs-bucket, or --s3-bucket." 1004 ) 1005 1006 if local: 1007 output_mode = "local" 1008 elif gcs_bucket is not None: 1009 output_mode = "gcs" 1010 else: 1011 output_mode = "s3" 1012 1013 registry = _resolve_store(source_store) 1014 1015 result = registry.mirror( 1016 output_mode=output_mode, 1017 output_path_root=output_path_root, 1018 gcs_bucket=gcs_bucket, 1019 s3_bucket=s3_bucket, 1020 dry_run=dry_run, 1021 connector_name=list(connector_name) if connector_name else None, 1022 ) 1023 1024 print_json( 1025 { 1026 "status": result.status, 1027 "source_bucket": result.source_bucket, 1028 "output_mode": result.output_mode, 1029 "output_root": result.output_root, 1030 "connectors_processed": result.connectors_processed, 1031 "blobs_copied": result.blobs_copied, 1032 "blobs_skipped": result.blobs_skipped, 1033 "error_count": len(result.errors), 1034 "dry_run": result.dry_run, 1035 } 1036 ) 1037 1038 if result.errors: 1039 for err in result.errors[:10]: 1040 print_error(err) 1041 if len(result.errors) > 10: 1042 print_error(f"... and {len(result.errors) - 10} more errors") 1043 1044 if result.status == "success": 1045 print_success(result.summary()) 1046 elif result.status == "dry-run": 1047 print_success(f"[DRY RUN] {result.summary()}") 1048 else: 1049 error_console.print(f"[yellow]{result.summary()}[/yellow]") 1050 1051 1052# ============================================================================= 1053# VERSION YANK COMMANDS 1054# ============================================================================= 1055 1056 1057@connector_version_app.command(name="yank") 1058def yank_cmd( 1059 name: Annotated[ 1060 str, 1061 Parameter(help="Connector name (e.g., 'source-faker')."), 1062 ], 1063 version: Annotated[ 1064 str, 1065 Parameter(help="Version to yank (e.g., '1.2.3')."), 1066 ], 1067 store: Annotated[ 1068 str, 1069 Parameter( 1070 help="Store target (e.g. 'coral:dev', 'coral:prod').", 1071 ), 1072 ], 1073 *, 1074 reason: Annotated[ 1075 str, 1076 Parameter(help="Reason for yanking this version."), 1077 ] = "", 1078 approval_url: Annotated[ 1079 str, 1080 Parameter(help="Approval evidence URL to record in the yank marker."), 1081 ] = "", 1082 dry_run: Annotated[ 1083 bool, 1084 Parameter(help="Show what would be done without making changes."), 1085 ] = False, 1086) -> None: 1087 """Mark a connector version as yanked. 1088 1089 Writes a version-yank.yml marker file to the version's directory in GCS. 1090 Yanked versions are excluded when determining the latest version of a 1091 connector. 1092 1093 Requires GCS_CREDENTIALS environment variable to be set. 1094 1095 Examples: 1096 airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:dev 1097 airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:dev --reason "Critical bug" 1098 airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:prod 1099 """ 1100 registry = _resolve_store(store) 1101 1102 result = registry.yank( 1103 connector_name=name, 1104 version=version, 1105 reason=reason, 1106 approval_url=approval_url, 1107 dry_run=dry_run, 1108 ) 1109 1110 print_json(result.to_dict()) 1111 1112 if result.success: 1113 print_success(result.message) 1114 if not dry_run: 1115 error_console.print( 1116 "\n[yellow]Note:[/yellow] The registry indexes are now stale. " 1117 "To update them, run:\n\n" 1118 f" airbyte-ops registry store compile --store {store}\n\n" 1119 "Or wait for the next scheduled compile operation." 1120 ) 1121 else: 1122 exit_with_error(result.message, code=1) 1123 1124 1125@connector_version_app.command(name="unyank") 1126def unyank_cmd( 1127 name: Annotated[ 1128 str, 1129 Parameter(help="Connector name (e.g., 'source-faker')."), 1130 ], 1131 version: Annotated[ 1132 str, 1133 Parameter(help="Version to unyank (e.g., '1.2.3')."), 1134 ], 1135 store: Annotated[ 1136 str, 1137 Parameter( 1138 help="Store target (e.g. 'coral:dev', 'coral:prod').", 1139 ), 1140 ], 1141 *, 1142 dry_run: Annotated[ 1143 bool, 1144 Parameter(help="Show what would be done without making changes."), 1145 ] = False, 1146) -> None: 1147 """Rename the active yank marker to a dated audit marker. 1148 1149 Moves `version-yank.yml` to `version-unyanked-yyyymmdd.yml`, making the 1150 version eligible again when determining the latest version. 1151 1152 Requires GCS_CREDENTIALS environment variable to be set. 1153 1154 Examples: 1155 airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:dev 1156 airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:prod 1157 """ 1158 registry = _resolve_store(store) 1159 1160 result = registry.unyank( 1161 connector_name=name, 1162 version=version, 1163 dry_run=dry_run, 1164 ) 1165 1166 print_json(result.to_dict()) 1167 1168 if result.success: 1169 print_success(result.message) 1170 if not dry_run: 1171 error_console.print( 1172 "\n[yellow]Note:[/yellow] The registry indexes are now stale. " 1173 "To update them, run:\n\n" 1174 f" airbyte-ops registry store compile --store {store}\n\n" 1175 "Or wait for the next scheduled compile operation." 1176 ) 1177 else: 1178 exit_with_error(result.message, code=1) 1179 1180 1181@connector_version_app.command(name="list-yanked") 1182def list_yanked_cmd( 1183 store: Annotated[ 1184 str, 1185 Parameter( 1186 help="Store target (e.g. 'coral:dev', 'coral:prod').", 1187 ), 1188 ], 1189 *, 1190 format: Annotated[ 1191 Literal["json", "text"], 1192 Parameter( 1193 help="Output format: 'json' for JSON array, 'text' for newline-separated." 1194 ), 1195 ] = "json", 1196) -> None: 1197 """List all yanked connector versions in the registry. 1198 1199 Returns one entry per active `version-yank.yml` marker, sorted by connector 1200 name then version. Historical `version-unyanked-*.yml` audit markers are 1201 ignored. 1202 1203 Requires GCS_CREDENTIALS environment variable to be set. 1204 1205 Examples: 1206 airbyte-ops registry connector-version list-yanked --store coral:prod 1207 airbyte-ops registry connector-version list-yanked --store coral:prod --format text 1208 """ 1209 registry = _resolve_store(store) 1210 yanked = registry.list_yanked_versions() 1211 1212 if format == "json": 1213 print_json( 1214 { 1215 "yanked_versions": [item.to_dict() for item in yanked], 1216 "count": len(yanked), 1217 } 1218 ) 1219 else: 1220 for item in yanked: 1221 print(f"{item.connector_name}\t{item.version}") 1222 1223 1224@connector_version_app.command(name="yank-status") 1225def yank_status_cmd( 1226 name: Annotated[ 1227 str, 1228 Parameter(help="Connector name (e.g., 'source-faker')."), 1229 ], 1230 version: Annotated[ 1231 str, 1232 Parameter(help="Version to inspect (e.g., '1.2.3')."), 1233 ], 1234 store: Annotated[ 1235 str, 1236 Parameter( 1237 help="Store target (e.g. 'coral:dev', 'coral:prod').", 1238 ), 1239 ], 1240) -> None: 1241 """Show the active yank marker for a single connector version. 1242 1243 Prints the marker's parsed fields (`yanked_at`, `reason`, `approval_url`) 1244 when the version currently has an active `version-yank.yml` marker, or 1245 reports that the version is not yanked. Historical 1246 `version-unyanked-*.yml` audit markers are ignored. 1247 1248 Requires GCS_CREDENTIALS environment variable to be set. 1249 1250 Examples: 1251 airbyte-ops registry connector-version yank-status --name source-faker --version 1.2.3 --store coral:prod 1252 """ 1253 registry = _resolve_store(store) 1254 marker = registry.get_yank_marker(connector_name=name, version=version) 1255 1256 if marker is None: 1257 print_json( 1258 { 1259 "connector_name": name, 1260 "version": version, 1261 "yanked": False, 1262 } 1263 ) 1264 print_success(f"{name} {version} is not yanked.") 1265 return 1266 1267 print_json({"yanked": True, **marker.to_dict()}) 1268 print_success(f"{name} {version} is yanked.") 1269 1270 1271# ============================================================================= 1272# ARTIFACT GENERATION COMMANDS 1273# ============================================================================= 1274 1275 1276@artifacts_app.command(name="generate") 1277def generate_version_artifacts_cmd( 1278 metadata_file: Annotated[ 1279 Path, 1280 Parameter(help="Path to the connector's metadata.yaml file."), 1281 ], 1282 docker_image: Annotated[ 1283 str, 1284 Parameter( 1285 help="Docker image to run spec against (e.g., 'airbyte/source-faker:6.2.38')." 1286 ), 1287 ], 1288 output_dir: Annotated[ 1289 Path | None, 1290 Parameter( 1291 help="Directory to write artifacts to. If not specified, a temp directory is created." 1292 ), 1293 ] = None, 1294 repo_root: Annotated[ 1295 Path | None, 1296 Parameter( 1297 help=( 1298 "Root of the Airbyte repo checkout (for resolving doc.md). " 1299 "If not specified, inferred by walking up from metadata-file." 1300 ), 1301 ), 1302 ] = None, 1303 dry_run: Annotated[ 1304 bool, 1305 Parameter( 1306 help="Show what would be generated without running docker or writing files." 1307 ), 1308 ] = False, 1309 with_validate: Annotated[ 1310 bool, 1311 Parameter( 1312 help=( 1313 "Run metadata validators after generation (default: enabled). " 1314 "Use --no-validate to skip." 1315 ), 1316 negative="--no-validate", 1317 ), 1318 ] = True, 1319 with_sbom: Annotated[ 1320 bool, 1321 Parameter( 1322 help=( 1323 "Generate spdx.json (SBOM) for connectors " 1324 "(default: enabled). Use --no-sbom to skip." 1325 ), 1326 negative="--no-sbom", 1327 ), 1328 ] = True, 1329 with_dependency_dump: Annotated[ 1330 bool, 1331 Parameter( 1332 help=( 1333 "Generate dependencies.json for Python connectors " 1334 "(default: enabled). Use --no-dependency-dump to skip." 1335 ), 1336 negative="--no-dependency-dump", 1337 ), 1338 ] = True, 1339 pr_number: Annotated[ 1340 int | None, 1341 Parameter( 1342 help=( 1343 "Pull request number for prerelease attribution. " 1344 "When provided, it overrides the commit subject." 1345 ), 1346 ), 1347 ] = None, 1348 with_github_lookup: Annotated[ 1349 bool, 1350 Parameter( 1351 help=( 1352 "Resolve publish PR author metadata through GitHub GraphQL " 1353 "(default: enabled). Use --no-github-lookup for offline runs." 1354 ), 1355 negative="--no-github-lookup", 1356 ), 1357 ] = True, 1358) -> None: 1359 """Generate version artifacts for a connector locally. 1360 1361 Runs the connector's docker image in cloud mode (`DEPLOYMENT_MODE=cloud`, 1362 `AIRBYTE_EDITION=CLOUD`) and oss mode (`DEPLOYMENT_MODE=oss`, 1363 `AIRBYTE_EDITION=COMMUNITY`) to obtain both spec variants, then generates 1364 the registry entries (cloud.json, oss.json) by applying 1365 registryOverrides from the metadata. 1366 1367 The generated metadata.yaml is enriched with git commit info, SBOM URL, 1368 and (when applicable) components SHA before writing. Validation is run 1369 after generation by default; pass `--no-validate` to skip. 1370 1371 This is a local-only operation -- no files are uploaded to GCS. 1372 Use `artifacts publish` to upload generated artifacts to GCS. 1373 1374 Examples: 1375 airbyte-ops registry connector-version artifacts generate \\ 1376 --metadata-file path/to/metadata.yaml \\ 1377 --docker-image airbyte/source-faker:6.2.38 1378 1379 airbyte-ops registry connector-version artifacts generate \\ 1380 --metadata-file path/to/metadata.yaml \\ 1381 --docker-image airbyte/source-faker:6.2.38 \\ 1382 --output-dir ./artifacts --with-validate 1383 """ 1384 result = generate_version_artifacts( 1385 metadata_file=metadata_file, 1386 docker_image=docker_image, 1387 output_dir=output_dir, 1388 repo_root=repo_root, 1389 dry_run=dry_run, 1390 with_validate=with_validate, 1391 with_dependency_dump=with_dependency_dump, 1392 with_sbom=with_sbom, 1393 pr_number=pr_number, 1394 with_github_lookup=with_github_lookup, 1395 ) 1396 1397 print_json(result.to_dict()) 1398 1399 if result.success: 1400 print_success( 1401 f"Generated {len(result.artifacts_written)} artifacts to {result.output_dir}" 1402 ) 1403 else: 1404 all_errors = result.errors + result.validation_errors 1405 exit_with_error( 1406 f"Generation completed with {len(all_errors)} error(s): " 1407 + "; ".join(all_errors), 1408 code=1, 1409 ) 1410 1411 1412@artifacts_app.command(name="publish") 1413def publish_version_artifacts_cmd( 1414 name: Annotated[ 1415 str, 1416 Parameter(help="Connector name (e.g., 'source-faker')."), 1417 ], 1418 version: Annotated[ 1419 str, 1420 Parameter(help="Version to publish artifacts for (e.g., '1.2.3')."), 1421 ], 1422 artifacts_dir: Annotated[ 1423 Path, 1424 Parameter( 1425 help="Directory containing generated artifacts to publish (from 'artifacts generate')." 1426 ), 1427 ], 1428 store: Annotated[ 1429 str, 1430 Parameter( 1431 help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix').", 1432 ), 1433 ], 1434 *, 1435 dry_run: Annotated[ 1436 bool, 1437 Parameter(help="Show what would be published without writing to GCS."), 1438 ] = False, 1439 with_validate: Annotated[ 1440 bool, 1441 Parameter( 1442 help=( 1443 "Validate metadata before uploading (default: enabled). " 1444 "Use --no-validate to skip." 1445 ), 1446 negative="--no-validate", 1447 ), 1448 ] = True, 1449) -> None: 1450 """Publish version artifacts to GCS using fsspec rsync. 1451 1452 Uploads locally generated artifacts (from `artifacts generate`) to the 1453 versioned path in GCS. By default, metadata is validated before upload; 1454 pass `--no-validate` to skip. 1455 1456 Uses `--store` to select the destination store and environment: 1457 1458 * `coral:dev` → coral dev bucket at root 1459 * `coral:prod` → coral prod bucket at root 1460 * `coral:dev/aj-test100` → coral dev bucket under `aj-test100/` prefix 1461 1462 Requires GCS_CREDENTIALS environment variable to be set. 1463 1464 Examples: 1465 airbyte-ops registry connector-version artifacts publish \\ 1466 --name source-faker --version 6.2.38 \\ 1467 --artifacts-dir ./artifacts --store coral:dev --with-validate 1468 1469 airbyte-ops registry connector-version artifacts publish \\ 1470 --name source-faker --version 6.2.38 \\ 1471 --artifacts-dir ./artifacts --store coral:prod 1472 1473 airbyte-ops registry connector-version artifacts publish \\ 1474 --name source-faker --version 6.2.38 \\ 1475 --artifacts-dir ./artifacts --store coral:dev/aj-test100 1476 """ 1477 registry = _resolve_store(store) 1478 1479 result = registry.publish_version_artifacts( 1480 connector_name=name, 1481 version=version, 1482 artifacts_dir=artifacts_dir, 1483 dry_run=dry_run, 1484 with_validate=with_validate, 1485 ) 1486 1487 print_json( 1488 { 1489 "status": result.status, 1490 "connector_name": result.connector_name, 1491 "version": result.version, 1492 "target": result.target, 1493 "gcs_destination": result.gcs_destination, 1494 "files_uploaded": result.files_uploaded, 1495 "errors": result.errors, 1496 "validation_errors": result.validation_errors, 1497 "progressive_rollout_overridden_by_breaking_change": result.progressive_rollout_overridden_by_breaking_change, 1498 "progressive_rollout_overridden_by_published_ga": result.progressive_rollout_overridden_by_published_ga, 1499 "dry_run": result.dry_run, 1500 } 1501 ) 1502 1503 if result.success: 1504 print_success( 1505 f"Published {len(result.files_uploaded)} artifacts for " 1506 f"{result.connector_name}@{result.version} → {result.gcs_destination}" 1507 ) 1508 else: 1509 all_errors = result.errors + result.validation_errors 1510 exit_with_error( 1511 f"Publish completed with {len(all_errors)} error(s): " 1512 + "; ".join(all_errors), 1513 code=1, 1514 ) 1515 1516 1517# ============================================================================= 1518# COMPILE COMMAND 1519# ============================================================================= 1520 1521 1522@store_app.command(name="compile") 1523def compile_cmd( 1524 store: Annotated[ 1525 str, 1526 Parameter( 1527 help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix').", 1528 ), 1529 ], 1530 *, 1531 output_store: Annotated[ 1532 str | None, 1533 Parameter( 1534 help="Write compiled artifacts to `<registry>:local:<path>`; omit the path to allocate a temporary directory.", 1535 ), 1536 ] = None, 1537 connector_name: Annotated[ 1538 tuple[str, ...] | None, 1539 Parameter( 1540 help="Only compile these connectors (can be repeated).", 1541 ), 1542 ] = None, 1543 dry_run: Annotated[ 1544 bool, 1545 Parameter(help="Show what would be done without writing."), 1546 ] = False, 1547 with_secrets_mask: Annotated[ 1548 bool, 1549 Parameter( 1550 help=( 1551 "Also regenerate specs_secrets_mask.yaml by scanning all " 1552 "connector specs for airbyte_secret properties." 1553 ), 1554 ), 1555 ] = False, 1556 with_legacy_migration: Annotated[ 1557 str | None, 1558 Parameter( 1559 help=( 1560 "Run a one-time legacy migration step during compile. " 1561 "Currently supported: 'v1' — delete cloud.json / oss.json " 1562 "files for connectors whose registryOverrides.cloud.enabled " 1563 "or registryOverrides.oss.enabled is false. This cleans up " 1564 "artifacts produced by the legacy pipeline that did not " 1565 "respect the enabled flag." 1566 ), 1567 ), 1568 ] = None, 1569 with_metrics: Annotated[ 1570 bool, 1571 Parameter( 1572 help=( 1573 "Inject latest connector quality metrics from the analytics " 1574 "JSONL export into generated.metrics." 1575 ), 1576 negative="--no-metrics", 1577 ), 1578 ] = True, 1579 force: Annotated[ 1580 bool, 1581 Parameter( 1582 help=( 1583 "Force resync of latest/ directories even if version markers are current. " 1584 "Useful when metadata changes without a version bump." 1585 ), 1586 ), 1587 ] = False, 1588 with_full_restate: Annotated[ 1589 bool, 1590 Parameter( 1591 help=( 1592 "Re-derive every version's release attribution from metadata.yaml. " 1593 "Expensive for all connectors (~33.5k reads). Without " 1594 "--release-attribution-index, historical blocks unavailable " 1595 "from metadata.yaml are discarded." 1596 ), 1597 negative="--no-full-restate", 1598 ), 1599 ] = False, 1600 release_attribution_index: Annotated[ 1601 Path | None, 1602 Parameter( 1603 help=( 1604 "Seed missing release blocks from a Phase 1 attribution index " 1605 "without rewriting historical metadata.yaml files." 1606 ), 1607 ), 1608 ] = None, 1609) -> None: 1610 """Compile the registry: sync latest/ dirs, write global and per-connector indexes. 1611 1612 Scans all version directories in the target store, determines the latest GA 1613 semver per connector (excluding yanked and pre-release versions), ensures 1614 each `latest/` directory matches the computed latest, and writes: 1615 1616 * `registries/v0/cloud_registry.json` -- global cloud registry index 1617 * `registries/v0/oss_registry.json` -- global OSS registry index 1618 * `metadata/airbyte/<connector>/versions.json` -- per-connector version index 1619 1620 With `--with-secrets-mask`, also regenerates: 1621 1622 * `registries/v0/specs_secrets_mask.yaml` -- properties marked as secrets 1623 1624 With `--with-legacy-migration=v1`, deletes `cloud.json` / `oss.json` 1625 files for connectors whose `registryOverrides.cloud.enabled` or 1626 `registryOverrides.oss.enabled` is `false`. 1627 1628 By default, injects connector quality metrics from the latest analytics 1629 JSONL export into `generated.metrics`. Use `--no-metrics` for offline 1630 scenarios. 1631 1632 With `--force`, resyncs all latest/ directories even if the version marker 1633 matches the computed latest version. 1634 1635 With `--with-full-restate`, re-reads every version's metadata.yaml to 1636 re-derive release attribution. This is an expensive operation at roughly 1637 33.5k reads for the full registry. 1638 1639 With `--release-attribution-index`, seeds historical `release` blocks 1640 directly into versions.json from a Phase 1 Git backfill index. 1641 1642 Uses efficient glob patterns for scanning (no file downloads during discovery). 1643 1644 Requires GCS_CREDENTIALS environment variable to be set. 1645 1646 Examples: 1647 airbyte-ops registry store compile --store coral:dev --dry-run 1648 1649 airbyte-ops registry store compile --store coral:dev/aj-test100 \\ 1650 --connector-name source-faker --connector-name destination-bigquery 1651 1652 airbyte-ops registry store compile --store coral:prod --with-secrets-mask 1653 1654 airbyte-ops registry store compile --store coral:dev \\ 1655 --with-legacy-migration v1 1656 """ 1657 registry = _resolve_store(store) 1658 parsed_output_store = ( 1659 RegistryStore.parse(output_store) if output_store is not None else None 1660 ) 1661 if parsed_output_store is not None and parsed_output_store.env != "local": 1662 exit_with_error( 1663 "--output-store must use `<registry>:local:<path>`; an empty path allocates a temporary directory." 1664 ) 1665 1666 result = registry.compile( 1667 output_store=parsed_output_store, 1668 connector_name=list(connector_name) if connector_name else None, 1669 dry_run=dry_run, 1670 with_secrets_mask=with_secrets_mask, 1671 with_legacy_migration=with_legacy_migration, 1672 with_metrics=with_metrics, 1673 force=force, 1674 with_full_restate=with_full_restate, 1675 release_attribution_index=release_attribution_index, 1676 ) 1677 1678 print_json( 1679 { 1680 "status": result.status, 1681 "target": result.target, 1682 "connectors_scanned": result.connectors_scanned, 1683 "versions_found": result.versions_found, 1684 "yanked_versions": result.yanked_versions, 1685 "latest_updated": result.latest_updated, 1686 "latest_already_current": result.latest_already_current, 1687 "cloud_registry_entries": result.cloud_registry_entries, 1688 "oss_registry_entries": result.oss_registry_entries, 1689 "composite_registry_entries": result.composite_registry_entries, 1690 "metrics_connector_count": result.metrics_connector_count, 1691 "metrics_registry_entries": result.metrics_registry_entries, 1692 "metrics_source": result.metrics_source, 1693 "metrics_error": result.metrics_error, 1694 "version_indexes_written": result.version_indexes_written, 1695 "version_indexes_skipped": result.version_indexes_skipped, 1696 "specs_secrets_mask_properties": result.specs_secrets_mask_properties, 1697 "errors": result.errors, 1698 "dry_run": result.dry_run, 1699 } 1700 ) 1701 1702 if result.status == "success" or result.status == "dry-run": 1703 print_success(result.summary()) 1704 else: 1705 exit_with_error( 1706 f"Compile completed with {len(result.errors)} error(s): " 1707 + "; ".join(result.errors), 1708 code=1, 1709 ) 1710 1711 1712# ============================================================================= 1713# DELETE-DEV-LATEST COMMAND 1714# ============================================================================= 1715 1716 1717@store_app.command(name="delete-dev-latest") 1718def delete_dev_latest_cmd( 1719 store: Annotated[ 1720 str, 1721 Parameter( 1722 help="Store target (must begin with 'coral:dev').", 1723 ), 1724 ], 1725 *, 1726 connector_name: Annotated[ 1727 tuple[str, ...] | None, 1728 Parameter( 1729 help="Only delete latest/ for these connectors (can be repeated).", 1730 ), 1731 ] = None, 1732 dry_run: Annotated[ 1733 bool, 1734 Parameter(help="Show what would be done without deleting."), 1735 ] = False, 1736) -> None: 1737 """Delete all latest/ directories from a dev registry store. 1738 1739 Discovers every connector that has a `latest/` directory and 1740 deletes each one in parallel using a thread pool. 1741 1742 This is useful before a full re-compile to prove that latest/ 1743 directories can be correctly regenerated from versioned data. 1744 1745 Only dev stores are allowed (store must begin with 'coral:dev'). 1746 1747 Requires GCS_CREDENTIALS environment variable to be set. 1748 1749 Examples: 1750 airbyte-ops registry store delete-dev-latest --store coral:dev --dry-run 1751 1752 airbyte-ops registry store delete-dev-latest --store coral:dev/aj-test100 1753 1754 airbyte-ops registry store delete-dev-latest --store coral:dev \\ 1755 --connector-name source-faker --connector-name destination-bigquery 1756 """ 1757 if not store.startswith("coral:dev"): 1758 exit_with_error( 1759 "delete-dev-latest only supports dev stores " 1760 f"(store must begin with 'coral:dev', got '{store}').", 1761 code=1, 1762 ) 1763 1764 registry = _resolve_store(store) 1765 1766 result = registry.delete_dev_latest( 1767 connector_name=list(connector_name) if connector_name else None, 1768 dry_run=dry_run, 1769 ) 1770 1771 print_json( 1772 { 1773 "status": result.status, 1774 "target": result.target, 1775 "connectors_found": result.connectors_found, 1776 "latest_dirs_deleted": result.latest_dirs_deleted, 1777 "errors": result.errors, 1778 "dry_run": result.dry_run, 1779 } 1780 ) 1781 1782 if result.status in ("success", "dry-run"): 1783 print_success(result.summary()) 1784 else: 1785 exit_with_error( 1786 f"Delete completed with {len(result.errors)} error(s): " 1787 + "; ".join(result.errors[:5]), 1788 code=1, 1789 ) 1790 1791 1792# ============================================================================= 1793# STORE COMPARE COMMAND 1794# ============================================================================= 1795 1796 1797@store_app.command(name="compare") 1798def compare_cmd( 1799 store: Annotated[ 1800 str, 1801 Parameter( 1802 help="Store target being evaluated (e.g. 'coral:dev/20260306-mirror-compile').", 1803 ), 1804 ], 1805 reference_store: Annotated[ 1806 str, 1807 Parameter( 1808 help="Known-good reference store to compare against.", 1809 ), 1810 ], 1811 *, 1812 connector_name: Annotated[ 1813 tuple[str, ...] | None, 1814 Parameter( 1815 help="Only compare these connectors (can be repeated).", 1816 ), 1817 ] = None, 1818 with_artifacts: Annotated[ 1819 bool, 1820 Parameter( 1821 help="Compare per-connector artifact files " 1822 "(metadata.yaml, cloud.json, oss.json, spec.json).", 1823 negative="--no-artifacts", 1824 ), 1825 ] = True, 1826 with_indexes: Annotated[ 1827 bool, 1828 Parameter( 1829 help="Compare global registry index files " 1830 "(cloud_registry.json, oss_registry.json, composite_registry.json).", 1831 negative="--no-indexes", 1832 ), 1833 ] = True, 1834 assert_stable: Annotated[ 1835 bool, 1836 Parameter(help="Make the exit code reflect registry safety assertions only."), 1837 ] = False, 1838 with_volatile_fields: Annotated[ 1839 bool, 1840 Parameter(help="Include default volatile-field differences."), 1841 ] = False, 1842 html_report: Annotated[ 1843 Path | None, 1844 Parameter(help="Write a self-contained HTML report to this path."), 1845 ] = None, 1846 text_report: Annotated[ 1847 Path | None, 1848 Parameter(help="Write a plain-text diff report to this path."), 1849 ] = None, 1850) -> None: 1851 """Compare a store against a reference store and report differences. 1852 1853 Evaluates the `--store` target against `--reference-store` and reports 1854 per-connector artifact diffs and global index diffs. 1855 1856 Requires GCS_CREDENTIALS environment variable to be set. 1857 1858 Examples: 1859 airbyte-ops registry store compare --store coral:dev/20260306-mirror \\ 1860 --reference-store coral:prod 1861 1862 airbyte-ops registry store compare --store coral:dev/my-test \\ 1863 --connector-name source-faker --no-indexes 1864 1865 airbyte-ops registry store compare --store coral:dev/my-test \\ 1866 --no-artifacts 1867 """ 1868 store_target = resolve_registry_store(store=store) 1869 ref_target = resolve_registry_store(store=reference_store) 1870 either_side_local = store_target.env == "local" or ref_target.env == "local" 1871 1872 result = compare_stores( 1873 store=store_target, 1874 reference=ref_target, 1875 connector_name=list(connector_name) if connector_name else None, 1876 with_artifacts=(with_artifacts and not either_side_local), 1877 with_indexes=with_indexes, 1878 tolerated_paths=(() if with_volatile_fields else DEFAULT_TOLERATED_PATHS), 1879 ) 1880 1881 print_json(result.to_dict()) 1882 if text_report is not None: 1883 write_text_report(result, str(text_report)) 1884 if html_report is not None: 1885 write_html_report(result, str(html_report)) 1886 1887 if assert_stable and ( 1888 result.connectors_only_in_store 1889 or result.connectors_only_in_reference 1890 or result.connectors_latest_forward 1891 or result.connectors_latest_backward 1892 ): 1893 exit_with_error("Registry safety assertions failed.", code=1) 1894 1895 if result.status == "match": 1896 print_success(result.summary()) 1897 elif result.status == "differences-found": 1898 error_console.print(f"[yellow]{result.summary()}[/yellow]") 1899 1900 # Print a concise per-connector diff summary 1901 for diff in result.connector_diffs: 1902 if diff.status in ("only_in_store", "only_in_reference"): 1903 error_console.print(f" {diff.connector}: {diff.status}") 1904 else: 1905 for ad in diff.artifact_diffs: 1906 error_console.print( 1907 f" {diff.connector}/{ad.file}: {ad.status}" 1908 + (f" ({ad.details})" if ad.details else "") 1909 ) 1910 1911 for idx_diff in result.index_diffs: 1912 if idx_diff.status != "match": 1913 error_console.print( 1914 f" [index] {idx_diff.file}: {idx_diff.status}" 1915 + ( 1916 f" (store={idx_diff.entry_count_store}," 1917 f" ref={idx_diff.entry_count_reference})" 1918 if idx_diff.entry_count_store or idx_diff.entry_count_reference 1919 else "" 1920 ) 1921 ) 1922 1923 if not assert_stable: 1924 sys.exit(1) 1925 else: 1926 exit_with_error( 1927 f"Compare completed with {len(result.errors)} error(s): " 1928 + "; ".join(result.errors[:5]), 1929 code=1, 1930 )