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:

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: source or destination. [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 example source-faker. [required]
  • VERSION, --version: Connector version, for example 7.2.1. [required]
  • STORE, --store: Registry store target, for example coral: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 example source-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 example coral: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 root
  • coral:prod → coral prod bucket at root
  • coral:dev/aj-test100 → coral dev bucket under aj-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 index
  • registries/v0/oss_registry.json -- global OSS registry index
  • metadata/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 example source-faker. [required]
  • VERSION, --version: Connector version, for example 7.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        )