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

Rename an active progressive-rollout.yml marker to an audit marker.

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. [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(help="Marker outcome used in the dated audit filename."),
 572    ],
 573    *,
 574    version: Annotated[
 575        str | None,
 576        Parameter(
 577            help="Version to finalize. If omitted, exactly one active marker must exist."
 578        ),
 579    ] = None,
 580    dry_run: Annotated[
 581        bool,
 582        Parameter(help="Show what would be done without making changes."),
 583    ] = False,
 584) -> None:
 585    """Rename an active `progressive-rollout.yml` marker to an audit marker."""
 586    registry = _resolve_store(store)
 587    result = registry.finalize_progressive_rollout_marker(
 588        connector_name=name,
 589        outcome=outcome,
 590        version=version,
 591        dry_run=dry_run,
 592    )
 593    print_json(result.to_dict())
 594    if result.success:
 595        print_success(result.message)
 596    else:
 597        exit_with_error(result.message, code=1)
 598
 599
 600# =============================================================================
 601# REGISTRY I/O - READ COMMANDS
 602# =============================================================================
 603
 604
 605@metadata_app.command(name="get")
 606def get_connector_version_metadata_cmd(
 607    name: Annotated[
 608        str,
 609        Parameter(
 610            help="Connector name (e.g., 'source-faker', 'destination-postgres')."
 611        ),
 612    ],
 613    store: Annotated[
 614        str,
 615        Parameter(
 616            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 617        ),
 618    ],
 619    *,
 620    version: Annotated[
 621        str,
 622        Parameter(help="Version to read (e.g., 'latest', '1.2.3')."),
 623    ] = "latest",
 624    format: Annotated[
 625        Literal["json", "raw"],
 626        Parameter(help="Output format: 'json' for JSON, 'raw' for YAML."),
 627    ] = "json",
 628) -> None:
 629    """Read a connector version's metadata from the registry.
 630
 631    Returns the full metadata.yaml content for a connector at the specified version.
 632
 633    Requires GCS_CREDENTIALS environment variable to be set.
 634
 635    Examples:
 636        airbyte-ops registry connector-version metadata get --name source-faker --store coral:dev
 637        airbyte-ops registry connector-version metadata get --name source-faker --store coral:dev --version 6.2.38
 638        airbyte-ops registry connector-version metadata get --name source-faker --store coral:prod
 639    """
 640    registry = _resolve_store(store)
 641
 642    try:
 643        metadata = registry.get_connector_metadata(
 644            connector_name=name,
 645            version=version,
 646        )
 647    except FileNotFoundError as e:
 648        exit_with_error(str(e), code=1)
 649    except Exception as e:
 650        exit_with_error(f"Error reading metadata: {e}", code=1)
 651
 652    if format == "json":
 653        print_json(metadata)
 654    else:
 655        print(yaml.dump(metadata, default_flow_style=False))
 656
 657
 658@connector_app.command(name="list")
 659def list_connectors_cmd(
 660    store: Annotated[
 661        str,
 662        Parameter(
 663            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 664        ),
 665    ],
 666    *,
 667    certified_only: Annotated[
 668        bool,
 669        Parameter(help="Include only certified connectors."),
 670    ] = False,
 671    support_level: Annotated[
 672        SupportLevel | None,
 673        Parameter(
 674            help=(
 675                "Exact support level to match "
 676                "(e.g. `certified`, `community`, `archived`)."
 677            )
 678        ),
 679    ] = None,
 680    min_support_level: Annotated[
 681        SupportLevel | None,
 682        Parameter(
 683            help=(
 684                "Minimum support level (inclusive). "
 685                "Levels from lowest to highest: `archived`, `community`, `certified`."
 686            )
 687        ),
 688    ] = None,
 689    connector_type: Annotated[
 690        ConnectorType | None,
 691        Parameter(help="Filter by connector type: `source` or `destination`."),
 692    ] = None,
 693    language: Annotated[
 694        ConnectorLanguage | None,
 695        Parameter(
 696            help=(
 697                "Filter by implementation language "
 698                "(e.g. `python`, `java`, `manifest-only`)."
 699            )
 700        ),
 701    ] = None,
 702    format: Annotated[
 703        Literal["json", "text"],
 704        Parameter(
 705            help="Output format: 'json' for JSON array, 'text' for newline-separated."
 706        ),
 707    ] = "json",
 708) -> None:
 709    """List connectors in the registry.
 710
 711    When filters are applied, reads the compiled `cloud_registry.json` index
 712    for fast lookups. Without filters, falls back to scanning individual
 713    metadata blobs (captures all connectors including OSS-only).
 714
 715    Requires GCS_CREDENTIALS environment variable to be set.
 716    """
 717    registry = _resolve_store(store)
 718
 719    # `--certified-only` is sugar for `--support-level certified`.
 720    effective_support_level = support_level
 721    if certified_only:
 722        if support_level and support_level != SupportLevel.CERTIFIED:
 723            exit_with_error(
 724                "`--certified-only` conflicts with `--support-level "
 725                f"{support_level}`. Use one or the other.",
 726                code=1,
 727            )
 728        effective_support_level = SupportLevel.CERTIFIED
 729
 730    try:
 731        connectors = registry.list_connectors(
 732            support_level=effective_support_level,
 733            min_support_level=min_support_level,
 734            connector_type=connector_type,
 735            language=language,
 736        )
 737    except Exception as e:
 738        exit_with_error(f"Error listing connectors: {e}", code=1)
 739
 740    if format == "json":
 741        print_json({"connectors": connectors, "count": len(connectors)})
 742    else:
 743        for connector in connectors:
 744            print(connector)
 745
 746
 747@connector_version_app.command(name="list")
 748def list_connector_versions_cmd(
 749    name: Annotated[
 750        str,
 751        Parameter(help="Connector name (e.g., 'source-faker')."),
 752    ],
 753    store: Annotated[
 754        str,
 755        Parameter(
 756            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 757        ),
 758    ],
 759    *,
 760    format: Annotated[
 761        Literal["json", "text"],
 762        Parameter(
 763            help="Output format: 'json' for JSON array, 'text' for newline-separated."
 764        ),
 765    ] = "json",
 766) -> None:
 767    """List all versions of a connector in the registry.
 768
 769    Scans the registry bucket to find all versions of a specific connector.
 770
 771    Requires GCS_CREDENTIALS environment variable to be set.
 772    """
 773    registry = _resolve_store(store)
 774
 775    try:
 776        versions = registry.list_connector_versions(
 777            connector_name=name,
 778        )
 779    except Exception as e:
 780        exit_with_error(f"Error listing versions: {e}", code=1)
 781
 782    if format == "json":
 783        print_json({"connector": name, "versions": versions, "count": len(versions)})
 784    else:
 785        for v in versions:
 786            print(v)
 787
 788
 789@marketing_stubs_app.command(name="check")
 790def marketing_stubs_check(
 791    store: Annotated[
 792        str,
 793        Parameter(
 794            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 795        ),
 796    ],
 797    *,
 798    repo_root: Annotated[
 799        Path,
 800        Parameter(
 801            help="Path to the airbyte-enterprise repository root. Defaults to current directory."
 802        ),
 803    ] = Path.cwd(),
 804) -> None:
 805    """Compare local connector_stubs.json with the version in GCS.
 806
 807    This command reads the entire local connector_stubs.json file and compares it
 808    with the version currently published in GCS.
 809
 810    Exit codes:
 811        0: Local file matches GCS (check passed)
 812        1: Differences found (check failed)
 813
 814    Output:
 815        STDOUT: JSON representation of the comparison result
 816        STDERR: Informational messages and comparison details
 817
 818    Example:
 819        airbyte-ops registry marketing-stubs check --store coral:prod --repo-root /path/to/airbyte-enterprise
 820        airbyte-ops registry marketing-stubs check --store coral:dev
 821    """
 822    registry = _resolve_store(store)
 823
 824    try:
 825        result = registry.marketing_stubs_check(repo_root=repo_root)
 826    except FileNotFoundError as e:
 827        exit_with_error(str(e))
 828    except ValueError as e:
 829        exit_with_error(str(e))
 830
 831    error_console.print(
 832        f"Comparing local {CONNECTOR_STUBS_FILE} with {result.get('bucket', '')}/{result.get('path', '')}"
 833    )
 834
 835    differences = result.get("differences", [])
 836    if differences:
 837        error_console.print(
 838            f"[yellow]Warning:[/yellow] {len(differences)} difference(s) found:"
 839        )
 840        for diff in differences:
 841            error_console.print(f"  {diff['id']}: {diff['status']}")
 842        print_json(result)
 843        sys.exit(1)
 844
 845    error_console.print(
 846        f"[green]Local file is in sync with GCS ({result.get('local_count', 0)} stubs)[/green]"
 847    )
 848    print_json(result)
 849
 850
 851@marketing_stubs_app.command(name="sync")
 852def marketing_stubs_sync(
 853    store: Annotated[
 854        str,
 855        Parameter(
 856            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 857        ),
 858    ],
 859    *,
 860    repo_root: Annotated[
 861        Path,
 862        Parameter(
 863            help="Path to the airbyte-enterprise repository root. Defaults to current directory."
 864        ),
 865    ] = Path.cwd(),
 866    dry_run: Annotated[
 867        bool,
 868        Parameter(help="Show what would be uploaded without making changes."),
 869    ] = False,
 870) -> None:
 871    """Sync local connector_stubs.json to GCS.
 872
 873    This command uploads the entire local connector_stubs.json file to GCS,
 874    replacing the existing file. Use this after merging changes to master
 875    in the airbyte-enterprise repository.
 876
 877    Exit codes:
 878        0: Sync successful (or dry-run completed)
 879        1: Error (file not found, validation failed, etc.)
 880
 881    Output:
 882        STDOUT: JSON representation of the sync result
 883        STDERR: Informational messages and status updates
 884
 885    Example:
 886        airbyte-ops registry marketing-stubs sync --store coral:prod --repo-root /path/to/airbyte-enterprise
 887        airbyte-ops registry marketing-stubs sync --store coral:dev
 888        airbyte-ops registry marketing-stubs sync --store coral:dev --dry-run
 889    """
 890    registry = _resolve_store(store)
 891
 892    try:
 893        result = registry.marketing_stubs_sync(
 894            repo_root=repo_root,
 895            dry_run=dry_run,
 896        )
 897    except FileNotFoundError as e:
 898        exit_with_error(str(e))
 899    except ValueError as e:
 900        exit_with_error(str(e))
 901
 902    bucket_name = result.get("bucket", "")
 903    path = result.get("path", "")
 904    stub_count = result.get("stub_count", 0)
 905
 906    if dry_run:
 907        error_console.print(
 908            f"[DRY RUN] Would upload {stub_count} stubs to {bucket_name}/{path}"
 909        )
 910    else:
 911        error_console.print(
 912            f"[green]Synced {stub_count} stubs to {bucket_name}/{path}[/green]"
 913        )
 914    print_json(result)
 915
 916
 917# =============================================================================
 918# REGISTRY REBUILD COMMANDS
 919# =============================================================================
 920
 921
 922@store_app.command(name="mirror")
 923def mirror_cmd(
 924    local: Annotated[
 925        bool,
 926        Parameter(
 927            help="Write output to a local directory. Mutually exclusive with --gcs-bucket and --s3-bucket.",
 928            negative="",
 929        ),
 930    ] = False,
 931    gcs_bucket: Annotated[
 932        str | None,
 933        Parameter(
 934            help="Write output to a GCS bucket. Must not be the prod bucket. "
 935            "Mutually exclusive with --local and --s3-bucket.",
 936        ),
 937    ] = None,
 938    s3_bucket: Annotated[
 939        str | None,
 940        Parameter(
 941            help="Write output to an S3 bucket. "
 942            "Mutually exclusive with --local and --gcs-bucket.",
 943        ),
 944    ] = None,
 945    output_path_root: Annotated[
 946        str | None,
 947        Parameter(
 948            help="Root path/prefix for the output. For --local, this is a directory path "
 949            "(defaults to a new temp dir if omitted). For --gcs-bucket/--s3-bucket, "
 950            "this prefix is prepended to all blob paths.",
 951        ),
 952    ] = None,
 953    dry_run: Annotated[
 954        bool,
 955        Parameter(help="Show what would be rebuilt without writing any files."),
 956    ] = False,
 957    source_store: Annotated[
 958        str,
 959        Parameter(
 960            help="Source store to read from (e.g. 'coral:prod', 'coral:dev').",
 961        ),
 962    ] = "coral:prod",
 963    connector_name: Annotated[
 964        tuple[str, ...] | None,
 965        Parameter(
 966            help="Only rebuild these connectors (by name). "
 967            "Can be specified multiple times, e.g. "
 968            "--connector-name source-faker --connector-name destination-bigquery.",
 969        ),
 970    ] = None,
 971) -> None:
 972    """Create a mirror of the connector registry from the source store.
 973
 974    Reads all connector metadata from the source store and copies it
 975    to the specified output target. Supports local filesystem, GCS, and S3
 976    as output targets via fsspec.
 977
 978    Output targets are mutually exclusive: specify exactly one of
 979    --local, --gcs-bucket, or --s3-bucket.
 980
 981    The production bucket is categorically disallowed as an output target.
 982
 983    To clean up legacy artifacts (e.g. disabled strict-encrypt connectors)
 984    after mirroring, run compile with `--with-legacy-migration v1`::
 985
 986        airbyte-ops registry store compile --store coral:dev/my-prefix \\
 987            --with-legacy-migration v1
 988
 989    Examples:
 990        airbyte-ops registry store mirror --local
 991        airbyte-ops registry store mirror --local --source-store coral:dev
 992        airbyte-ops registry store mirror --gcs-bucket dev-airbyte-cloud-connector-metadata-service-2 \\
 993            --output-path-root test-run-123
 994        airbyte-ops registry store mirror --s3-bucket my-test-bucket --dry-run
 995    """
 996    # Validate mutually exclusive output targets
 997    targets = [local, gcs_bucket is not None, s3_bucket is not None]
 998    if sum(targets) != 1:
 999        exit_with_error(
1000            "Specify exactly one output target: --local, --gcs-bucket, or --s3-bucket."
1001        )
1002
1003    if local:
1004        output_mode = "local"
1005    elif gcs_bucket is not None:
1006        output_mode = "gcs"
1007    else:
1008        output_mode = "s3"
1009
1010    registry = _resolve_store(source_store)
1011
1012    result = registry.mirror(
1013        output_mode=output_mode,
1014        output_path_root=output_path_root,
1015        gcs_bucket=gcs_bucket,
1016        s3_bucket=s3_bucket,
1017        dry_run=dry_run,
1018        connector_name=list(connector_name) if connector_name else None,
1019    )
1020
1021    print_json(
1022        {
1023            "status": result.status,
1024            "source_bucket": result.source_bucket,
1025            "output_mode": result.output_mode,
1026            "output_root": result.output_root,
1027            "connectors_processed": result.connectors_processed,
1028            "blobs_copied": result.blobs_copied,
1029            "blobs_skipped": result.blobs_skipped,
1030            "error_count": len(result.errors),
1031            "dry_run": result.dry_run,
1032        }
1033    )
1034
1035    if result.errors:
1036        for err in result.errors[:10]:
1037            print_error(err)
1038        if len(result.errors) > 10:
1039            print_error(f"... and {len(result.errors) - 10} more errors")
1040
1041    if result.status == "success":
1042        print_success(result.summary())
1043    elif result.status == "dry-run":
1044        print_success(f"[DRY RUN] {result.summary()}")
1045    else:
1046        error_console.print(f"[yellow]{result.summary()}[/yellow]")
1047
1048
1049# =============================================================================
1050# VERSION YANK COMMANDS
1051# =============================================================================
1052
1053
1054@connector_version_app.command(name="yank")
1055def yank_cmd(
1056    name: Annotated[
1057        str,
1058        Parameter(help="Connector name (e.g., 'source-faker')."),
1059    ],
1060    version: Annotated[
1061        str,
1062        Parameter(help="Version to yank (e.g., '1.2.3')."),
1063    ],
1064    store: Annotated[
1065        str,
1066        Parameter(
1067            help="Store target (e.g. 'coral:dev', 'coral:prod').",
1068        ),
1069    ],
1070    *,
1071    reason: Annotated[
1072        str,
1073        Parameter(help="Reason for yanking this version."),
1074    ] = "",
1075    approval_url: Annotated[
1076        str,
1077        Parameter(help="Approval evidence URL to record in the yank marker."),
1078    ] = "",
1079    dry_run: Annotated[
1080        bool,
1081        Parameter(help="Show what would be done without making changes."),
1082    ] = False,
1083) -> None:
1084    """Mark a connector version as yanked.
1085
1086    Writes a version-yank.yml marker file to the version's directory in GCS.
1087    Yanked versions are excluded when determining the latest version of a
1088    connector.
1089
1090    Requires GCS_CREDENTIALS environment variable to be set.
1091
1092    Examples:
1093        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:dev
1094        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:dev --reason "Critical bug"
1095        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:prod
1096    """
1097    registry = _resolve_store(store)
1098
1099    result = registry.yank(
1100        connector_name=name,
1101        version=version,
1102        reason=reason,
1103        approval_url=approval_url,
1104        dry_run=dry_run,
1105    )
1106
1107    print_json(result.to_dict())
1108
1109    if result.success:
1110        print_success(result.message)
1111        if not dry_run:
1112            error_console.print(
1113                "\n[yellow]Note:[/yellow] The registry indexes are now stale. "
1114                "To update them, run:\n\n"
1115                f"    airbyte-ops registry store compile --store {store}\n\n"
1116                "Or wait for the next scheduled compile operation."
1117            )
1118    else:
1119        exit_with_error(result.message, code=1)
1120
1121
1122@connector_version_app.command(name="unyank")
1123def unyank_cmd(
1124    name: Annotated[
1125        str,
1126        Parameter(help="Connector name (e.g., 'source-faker')."),
1127    ],
1128    version: Annotated[
1129        str,
1130        Parameter(help="Version to unyank (e.g., '1.2.3')."),
1131    ],
1132    store: Annotated[
1133        str,
1134        Parameter(
1135            help="Store target (e.g. 'coral:dev', 'coral:prod').",
1136        ),
1137    ],
1138    *,
1139    dry_run: Annotated[
1140        bool,
1141        Parameter(help="Show what would be done without making changes."),
1142    ] = False,
1143) -> None:
1144    """Rename the active yank marker to a dated audit marker.
1145
1146    Moves `version-yank.yml` to `version-unyanked-yyyymmdd.yml`, making the
1147    version eligible again when determining the latest version.
1148
1149    Requires GCS_CREDENTIALS environment variable to be set.
1150
1151    Examples:
1152        airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:dev
1153        airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:prod
1154    """
1155    registry = _resolve_store(store)
1156
1157    result = registry.unyank(
1158        connector_name=name,
1159        version=version,
1160        dry_run=dry_run,
1161    )
1162
1163    print_json(result.to_dict())
1164
1165    if result.success:
1166        print_success(result.message)
1167        if not dry_run:
1168            error_console.print(
1169                "\n[yellow]Note:[/yellow] The registry indexes are now stale. "
1170                "To update them, run:\n\n"
1171                f"    airbyte-ops registry store compile --store {store}\n\n"
1172                "Or wait for the next scheduled compile operation."
1173            )
1174    else:
1175        exit_with_error(result.message, code=1)
1176
1177
1178@connector_version_app.command(name="list-yanked")
1179def list_yanked_cmd(
1180    store: Annotated[
1181        str,
1182        Parameter(
1183            help="Store target (e.g. 'coral:dev', 'coral:prod').",
1184        ),
1185    ],
1186    *,
1187    format: Annotated[
1188        Literal["json", "text"],
1189        Parameter(
1190            help="Output format: 'json' for JSON array, 'text' for newline-separated."
1191        ),
1192    ] = "json",
1193) -> None:
1194    """List all yanked connector versions in the registry.
1195
1196    Returns one entry per active `version-yank.yml` marker, sorted by connector
1197    name then version. Historical `version-unyanked-*.yml` audit markers are
1198    ignored.
1199
1200    Requires GCS_CREDENTIALS environment variable to be set.
1201
1202    Examples:
1203        airbyte-ops registry connector-version list-yanked --store coral:prod
1204        airbyte-ops registry connector-version list-yanked --store coral:prod --format text
1205    """
1206    registry = _resolve_store(store)
1207    yanked = registry.list_yanked_versions()
1208
1209    if format == "json":
1210        print_json(
1211            {
1212                "yanked_versions": [item.to_dict() for item in yanked],
1213                "count": len(yanked),
1214            }
1215        )
1216    else:
1217        for item in yanked:
1218            print(f"{item.connector_name}\t{item.version}")
1219
1220
1221@connector_version_app.command(name="yank-status")
1222def yank_status_cmd(
1223    name: Annotated[
1224        str,
1225        Parameter(help="Connector name (e.g., 'source-faker')."),
1226    ],
1227    version: Annotated[
1228        str,
1229        Parameter(help="Version to inspect (e.g., '1.2.3')."),
1230    ],
1231    store: Annotated[
1232        str,
1233        Parameter(
1234            help="Store target (e.g. 'coral:dev', 'coral:prod').",
1235        ),
1236    ],
1237) -> None:
1238    """Show the active yank marker for a single connector version.
1239
1240    Prints the marker's parsed fields (`yanked_at`, `reason`, `approval_url`)
1241    when the version currently has an active `version-yank.yml` marker, or
1242    reports that the version is not yanked. Historical
1243    `version-unyanked-*.yml` audit markers are ignored.
1244
1245    Requires GCS_CREDENTIALS environment variable to be set.
1246
1247    Examples:
1248        airbyte-ops registry connector-version yank-status --name source-faker --version 1.2.3 --store coral:prod
1249    """
1250    registry = _resolve_store(store)
1251    marker = registry.get_yank_marker(connector_name=name, version=version)
1252
1253    if marker is None:
1254        print_json(
1255            {
1256                "connector_name": name,
1257                "version": version,
1258                "yanked": False,
1259            }
1260        )
1261        print_success(f"{name} {version} is not yanked.")
1262        return
1263
1264    print_json({"yanked": True, **marker.to_dict()})
1265    print_success(f"{name} {version} is yanked.")
1266
1267
1268# =============================================================================
1269# ARTIFACT GENERATION COMMANDS
1270# =============================================================================
1271
1272
1273@artifacts_app.command(name="generate")
1274def generate_version_artifacts_cmd(
1275    metadata_file: Annotated[
1276        Path,
1277        Parameter(help="Path to the connector's metadata.yaml file."),
1278    ],
1279    docker_image: Annotated[
1280        str,
1281        Parameter(
1282            help="Docker image to run spec against (e.g., 'airbyte/source-faker:6.2.38')."
1283        ),
1284    ],
1285    output_dir: Annotated[
1286        Path | None,
1287        Parameter(
1288            help="Directory to write artifacts to. If not specified, a temp directory is created."
1289        ),
1290    ] = None,
1291    repo_root: Annotated[
1292        Path | None,
1293        Parameter(
1294            help=(
1295                "Root of the Airbyte repo checkout (for resolving doc.md). "
1296                "If not specified, inferred by walking up from metadata-file."
1297            ),
1298        ),
1299    ] = None,
1300    dry_run: Annotated[
1301        bool,
1302        Parameter(
1303            help="Show what would be generated without running docker or writing files."
1304        ),
1305    ] = False,
1306    with_validate: Annotated[
1307        bool,
1308        Parameter(
1309            help=(
1310                "Run metadata validators after generation (default: enabled). "
1311                "Use --no-validate to skip."
1312            ),
1313            negative="--no-validate",
1314        ),
1315    ] = True,
1316    with_sbom: Annotated[
1317        bool,
1318        Parameter(
1319            help=(
1320                "Generate spdx.json (SBOM) for connectors "
1321                "(default: enabled). Use --no-sbom to skip."
1322            ),
1323            negative="--no-sbom",
1324        ),
1325    ] = True,
1326    with_dependency_dump: Annotated[
1327        bool,
1328        Parameter(
1329            help=(
1330                "Generate dependencies.json for Python connectors "
1331                "(default: enabled). Use --no-dependency-dump to skip."
1332            ),
1333            negative="--no-dependency-dump",
1334        ),
1335    ] = True,
1336    pr_number: Annotated[
1337        int | None,
1338        Parameter(
1339            help=(
1340                "Pull request number for prerelease attribution. "
1341                "When provided, it overrides the commit subject."
1342            ),
1343        ),
1344    ] = None,
1345    with_github_lookup: Annotated[
1346        bool,
1347        Parameter(
1348            help=(
1349                "Resolve publish PR author metadata through GitHub GraphQL "
1350                "(default: enabled). Use --no-github-lookup for offline runs."
1351            ),
1352            negative="--no-github-lookup",
1353        ),
1354    ] = True,
1355) -> None:
1356    """Generate version artifacts for a connector locally.
1357
1358    Runs the connector's docker image in cloud mode (`DEPLOYMENT_MODE=cloud`,
1359    `AIRBYTE_EDITION=CLOUD`) and oss mode (`DEPLOYMENT_MODE=oss`,
1360    `AIRBYTE_EDITION=COMMUNITY`) to obtain both spec variants, then generates
1361    the registry entries (cloud.json, oss.json) by applying
1362    registryOverrides from the metadata.
1363
1364    The generated metadata.yaml is enriched with git commit info, SBOM URL,
1365    and (when applicable) components SHA before writing.  Validation is run
1366    after generation by default; pass `--no-validate` to skip.
1367
1368    This is a local-only operation -- no files are uploaded to GCS.
1369    Use `artifacts publish` to upload generated artifacts to GCS.
1370
1371    Examples:
1372        airbyte-ops registry connector-version artifacts generate \\
1373            --metadata-file path/to/metadata.yaml \\
1374            --docker-image airbyte/source-faker:6.2.38
1375
1376        airbyte-ops registry connector-version artifacts generate \\
1377            --metadata-file path/to/metadata.yaml \\
1378            --docker-image airbyte/source-faker:6.2.38 \\
1379            --output-dir ./artifacts --with-validate
1380    """
1381    result = generate_version_artifacts(
1382        metadata_file=metadata_file,
1383        docker_image=docker_image,
1384        output_dir=output_dir,
1385        repo_root=repo_root,
1386        dry_run=dry_run,
1387        with_validate=with_validate,
1388        with_dependency_dump=with_dependency_dump,
1389        with_sbom=with_sbom,
1390        pr_number=pr_number,
1391        with_github_lookup=with_github_lookup,
1392    )
1393
1394    print_json(result.to_dict())
1395
1396    if result.success:
1397        print_success(
1398            f"Generated {len(result.artifacts_written)} artifacts to {result.output_dir}"
1399        )
1400    else:
1401        all_errors = result.errors + result.validation_errors
1402        exit_with_error(
1403            f"Generation completed with {len(all_errors)} error(s): "
1404            + "; ".join(all_errors),
1405            code=1,
1406        )
1407
1408
1409@artifacts_app.command(name="publish")
1410def publish_version_artifacts_cmd(
1411    name: Annotated[
1412        str,
1413        Parameter(help="Connector name (e.g., 'source-faker')."),
1414    ],
1415    version: Annotated[
1416        str,
1417        Parameter(help="Version to publish artifacts for (e.g., '1.2.3')."),
1418    ],
1419    artifacts_dir: Annotated[
1420        Path,
1421        Parameter(
1422            help="Directory containing generated artifacts to publish (from 'artifacts generate')."
1423        ),
1424    ],
1425    store: Annotated[
1426        str,
1427        Parameter(
1428            help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix').",
1429        ),
1430    ],
1431    *,
1432    dry_run: Annotated[
1433        bool,
1434        Parameter(help="Show what would be published without writing to GCS."),
1435    ] = False,
1436    with_validate: Annotated[
1437        bool,
1438        Parameter(
1439            help=(
1440                "Validate metadata before uploading (default: enabled). "
1441                "Use --no-validate to skip."
1442            ),
1443            negative="--no-validate",
1444        ),
1445    ] = True,
1446) -> None:
1447    """Publish version artifacts to GCS using fsspec rsync.
1448
1449    Uploads locally generated artifacts (from `artifacts generate`) to the
1450    versioned path in GCS.  By default, metadata is validated before upload;
1451    pass `--no-validate` to skip.
1452
1453    Uses `--store` to select the destination store and environment:
1454
1455    * `coral:dev`              → coral dev bucket at root
1456    * `coral:prod`             → coral prod bucket at root
1457    * `coral:dev/aj-test100`   → coral dev bucket under `aj-test100/` prefix
1458
1459    Requires GCS_CREDENTIALS environment variable to be set.
1460
1461    Examples:
1462        airbyte-ops registry connector-version artifacts publish \\
1463            --name source-faker --version 6.2.38 \\
1464            --artifacts-dir ./artifacts --store coral:dev --with-validate
1465
1466        airbyte-ops registry connector-version artifacts publish \\
1467            --name source-faker --version 6.2.38 \\
1468            --artifacts-dir ./artifacts --store coral:prod
1469
1470        airbyte-ops registry connector-version artifacts publish \\
1471            --name source-faker --version 6.2.38 \\
1472            --artifacts-dir ./artifacts --store coral:dev/aj-test100
1473    """
1474    registry = _resolve_store(store)
1475
1476    result = registry.publish_version_artifacts(
1477        connector_name=name,
1478        version=version,
1479        artifacts_dir=artifacts_dir,
1480        dry_run=dry_run,
1481        with_validate=with_validate,
1482    )
1483
1484    print_json(
1485        {
1486            "status": result.status,
1487            "connector_name": result.connector_name,
1488            "version": result.version,
1489            "target": result.target,
1490            "gcs_destination": result.gcs_destination,
1491            "files_uploaded": result.files_uploaded,
1492            "errors": result.errors,
1493            "validation_errors": result.validation_errors,
1494            "progressive_rollout_overridden_by_breaking_change": result.progressive_rollout_overridden_by_breaking_change,
1495            "progressive_rollout_overridden_by_published_ga": result.progressive_rollout_overridden_by_published_ga,
1496            "dry_run": result.dry_run,
1497        }
1498    )
1499
1500    if result.success:
1501        print_success(
1502            f"Published {len(result.files_uploaded)} artifacts for "
1503            f"{result.connector_name}@{result.version} → {result.gcs_destination}"
1504        )
1505    else:
1506        all_errors = result.errors + result.validation_errors
1507        exit_with_error(
1508            f"Publish completed with {len(all_errors)} error(s): "
1509            + "; ".join(all_errors),
1510            code=1,
1511        )
1512
1513
1514# =============================================================================
1515# COMPILE COMMAND
1516# =============================================================================
1517
1518
1519@store_app.command(name="compile")
1520def compile_cmd(
1521    store: Annotated[
1522        str,
1523        Parameter(
1524            help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix').",
1525        ),
1526    ],
1527    *,
1528    output_store: Annotated[
1529        str | None,
1530        Parameter(
1531            help="Write compiled artifacts to `<registry>:local:<path>`; omit the path to allocate a temporary directory.",
1532        ),
1533    ] = None,
1534    connector_name: Annotated[
1535        tuple[str, ...] | None,
1536        Parameter(
1537            help="Only compile these connectors (can be repeated).",
1538        ),
1539    ] = None,
1540    dry_run: Annotated[
1541        bool,
1542        Parameter(help="Show what would be done without writing."),
1543    ] = False,
1544    with_secrets_mask: Annotated[
1545        bool,
1546        Parameter(
1547            help=(
1548                "Also regenerate specs_secrets_mask.yaml by scanning all "
1549                "connector specs for airbyte_secret properties."
1550            ),
1551        ),
1552    ] = False,
1553    with_legacy_migration: Annotated[
1554        str | None,
1555        Parameter(
1556            help=(
1557                "Run a one-time legacy migration step during compile. "
1558                "Currently supported: 'v1' — delete cloud.json / oss.json "
1559                "files for connectors whose registryOverrides.cloud.enabled "
1560                "or registryOverrides.oss.enabled is false. This cleans up "
1561                "artifacts produced by the legacy pipeline that did not "
1562                "respect the enabled flag."
1563            ),
1564        ),
1565    ] = None,
1566    with_metrics: Annotated[
1567        bool,
1568        Parameter(
1569            help=(
1570                "Inject latest connector quality metrics from the analytics "
1571                "JSONL export into generated.metrics."
1572            ),
1573            negative="--no-metrics",
1574        ),
1575    ] = True,
1576    force: Annotated[
1577        bool,
1578        Parameter(
1579            help=(
1580                "Force resync of latest/ directories even if version markers are current. "
1581                "Useful when metadata changes without a version bump."
1582            ),
1583        ),
1584    ] = False,
1585    with_full_restate: Annotated[
1586        bool,
1587        Parameter(
1588            help=(
1589                "Re-derive every version's release attribution from metadata.yaml. "
1590                "Expensive for all connectors (~33.5k reads). Without "
1591                "--release-attribution-index, historical blocks unavailable "
1592                "from metadata.yaml are discarded."
1593            ),
1594            negative="--no-full-restate",
1595        ),
1596    ] = False,
1597    release_attribution_index: Annotated[
1598        Path | None,
1599        Parameter(
1600            help=(
1601                "Seed missing release blocks from a Phase 1 attribution index "
1602                "without rewriting historical metadata.yaml files."
1603            ),
1604        ),
1605    ] = None,
1606) -> None:
1607    """Compile the registry: sync latest/ dirs, write global and per-connector indexes.
1608
1609    Scans all version directories in the target store, determines the latest GA
1610    semver per connector (excluding yanked and pre-release versions), ensures
1611    each `latest/` directory matches the computed latest, and writes:
1612
1613    * `registries/v0/cloud_registry.json` -- global cloud registry index
1614    * `registries/v0/oss_registry.json`   -- global OSS registry index
1615    * `metadata/airbyte/<connector>/versions.json` -- per-connector version index
1616
1617    With `--with-secrets-mask`, also regenerates:
1618
1619    * `registries/v0/specs_secrets_mask.yaml` -- properties marked as secrets
1620
1621    With `--with-legacy-migration=v1`, deletes `cloud.json` / `oss.json`
1622    files for connectors whose `registryOverrides.cloud.enabled` or
1623    `registryOverrides.oss.enabled` is `false`.
1624
1625    By default, injects connector quality metrics from the latest analytics
1626    JSONL export into `generated.metrics`. Use `--no-metrics` for offline
1627    scenarios.
1628
1629    With `--force`, resyncs all latest/ directories even if the version marker
1630    matches the computed latest version.
1631
1632    With `--with-full-restate`, re-reads every version's metadata.yaml to
1633    re-derive release attribution. This is an expensive operation at roughly
1634    33.5k reads for the full registry.
1635
1636    With `--release-attribution-index`, seeds historical `release` blocks
1637    directly into versions.json from a Phase 1 Git backfill index.
1638
1639    Uses efficient glob patterns for scanning (no file downloads during discovery).
1640
1641    Requires GCS_CREDENTIALS environment variable to be set.
1642
1643    Examples:
1644        airbyte-ops registry store compile --store coral:dev --dry-run
1645
1646        airbyte-ops registry store compile --store coral:dev/aj-test100 \\
1647            --connector-name source-faker --connector-name destination-bigquery
1648
1649        airbyte-ops registry store compile --store coral:prod --with-secrets-mask
1650
1651        airbyte-ops registry store compile --store coral:dev \\
1652            --with-legacy-migration v1
1653    """
1654    registry = _resolve_store(store)
1655    parsed_output_store = (
1656        RegistryStore.parse(output_store) if output_store is not None else None
1657    )
1658    if parsed_output_store is not None and parsed_output_store.env != "local":
1659        exit_with_error(
1660            "--output-store must use `<registry>:local:<path>`; an empty path allocates a temporary directory."
1661        )
1662
1663    result = registry.compile(
1664        output_store=parsed_output_store,
1665        connector_name=list(connector_name) if connector_name else None,
1666        dry_run=dry_run,
1667        with_secrets_mask=with_secrets_mask,
1668        with_legacy_migration=with_legacy_migration,
1669        with_metrics=with_metrics,
1670        force=force,
1671        with_full_restate=with_full_restate,
1672        release_attribution_index=release_attribution_index,
1673    )
1674
1675    print_json(
1676        {
1677            "status": result.status,
1678            "target": result.target,
1679            "connectors_scanned": result.connectors_scanned,
1680            "versions_found": result.versions_found,
1681            "yanked_versions": result.yanked_versions,
1682            "latest_updated": result.latest_updated,
1683            "latest_already_current": result.latest_already_current,
1684            "cloud_registry_entries": result.cloud_registry_entries,
1685            "oss_registry_entries": result.oss_registry_entries,
1686            "composite_registry_entries": result.composite_registry_entries,
1687            "metrics_connector_count": result.metrics_connector_count,
1688            "metrics_registry_entries": result.metrics_registry_entries,
1689            "metrics_source": result.metrics_source,
1690            "metrics_error": result.metrics_error,
1691            "version_indexes_written": result.version_indexes_written,
1692            "version_indexes_skipped": result.version_indexes_skipped,
1693            "specs_secrets_mask_properties": result.specs_secrets_mask_properties,
1694            "errors": result.errors,
1695            "dry_run": result.dry_run,
1696        }
1697    )
1698
1699    if result.status == "success" or result.status == "dry-run":
1700        print_success(result.summary())
1701    else:
1702        exit_with_error(
1703            f"Compile completed with {len(result.errors)} error(s): "
1704            + "; ".join(result.errors),
1705            code=1,
1706        )
1707
1708
1709# =============================================================================
1710# DELETE-DEV-LATEST COMMAND
1711# =============================================================================
1712
1713
1714@store_app.command(name="delete-dev-latest")
1715def delete_dev_latest_cmd(
1716    store: Annotated[
1717        str,
1718        Parameter(
1719            help="Store target (must begin with 'coral:dev').",
1720        ),
1721    ],
1722    *,
1723    connector_name: Annotated[
1724        tuple[str, ...] | None,
1725        Parameter(
1726            help="Only delete latest/ for these connectors (can be repeated).",
1727        ),
1728    ] = None,
1729    dry_run: Annotated[
1730        bool,
1731        Parameter(help="Show what would be done without deleting."),
1732    ] = False,
1733) -> None:
1734    """Delete all latest/ directories from a dev registry store.
1735
1736    Discovers every connector that has a `latest/` directory and
1737    deletes each one in parallel using a thread pool.
1738
1739    This is useful before a full re-compile to prove that latest/
1740    directories can be correctly regenerated from versioned data.
1741
1742    Only dev stores are allowed (store must begin with 'coral:dev').
1743
1744    Requires GCS_CREDENTIALS environment variable to be set.
1745
1746    Examples:
1747        airbyte-ops registry store delete-dev-latest --store coral:dev --dry-run
1748
1749        airbyte-ops registry store delete-dev-latest --store coral:dev/aj-test100
1750
1751        airbyte-ops registry store delete-dev-latest --store coral:dev \\
1752            --connector-name source-faker --connector-name destination-bigquery
1753    """
1754    if not store.startswith("coral:dev"):
1755        exit_with_error(
1756            "delete-dev-latest only supports dev stores "
1757            f"(store must begin with 'coral:dev', got '{store}').",
1758            code=1,
1759        )
1760
1761    registry = _resolve_store(store)
1762
1763    result = registry.delete_dev_latest(
1764        connector_name=list(connector_name) if connector_name else None,
1765        dry_run=dry_run,
1766    )
1767
1768    print_json(
1769        {
1770            "status": result.status,
1771            "target": result.target,
1772            "connectors_found": result.connectors_found,
1773            "latest_dirs_deleted": result.latest_dirs_deleted,
1774            "errors": result.errors,
1775            "dry_run": result.dry_run,
1776        }
1777    )
1778
1779    if result.status in ("success", "dry-run"):
1780        print_success(result.summary())
1781    else:
1782        exit_with_error(
1783            f"Delete completed with {len(result.errors)} error(s): "
1784            + "; ".join(result.errors[:5]),
1785            code=1,
1786        )
1787
1788
1789# =============================================================================
1790# STORE COMPARE COMMAND
1791# =============================================================================
1792
1793
1794@store_app.command(name="compare")
1795def compare_cmd(
1796    store: Annotated[
1797        str,
1798        Parameter(
1799            help="Store target being evaluated (e.g. 'coral:dev/20260306-mirror-compile').",
1800        ),
1801    ],
1802    reference_store: Annotated[
1803        str,
1804        Parameter(
1805            help="Known-good reference store to compare against.",
1806        ),
1807    ],
1808    *,
1809    connector_name: Annotated[
1810        tuple[str, ...] | None,
1811        Parameter(
1812            help="Only compare these connectors (can be repeated).",
1813        ),
1814    ] = None,
1815    with_artifacts: Annotated[
1816        bool,
1817        Parameter(
1818            help="Compare per-connector artifact files "
1819            "(metadata.yaml, cloud.json, oss.json, spec.json).",
1820            negative="--no-artifacts",
1821        ),
1822    ] = True,
1823    with_indexes: Annotated[
1824        bool,
1825        Parameter(
1826            help="Compare global registry index files "
1827            "(cloud_registry.json, oss_registry.json, composite_registry.json).",
1828            negative="--no-indexes",
1829        ),
1830    ] = True,
1831    assert_stable: Annotated[
1832        bool,
1833        Parameter(help="Make the exit code reflect registry safety assertions only."),
1834    ] = False,
1835    with_volatile_fields: Annotated[
1836        bool,
1837        Parameter(help="Include default volatile-field differences."),
1838    ] = False,
1839    html_report: Annotated[
1840        Path | None,
1841        Parameter(help="Write a self-contained HTML report to this path."),
1842    ] = None,
1843    text_report: Annotated[
1844        Path | None,
1845        Parameter(help="Write a plain-text diff report to this path."),
1846    ] = None,
1847) -> None:
1848    """Compare a store against a reference store and report differences.
1849
1850    Evaluates the `--store` target against `--reference-store` and reports
1851    per-connector artifact diffs and global index diffs.
1852
1853    Requires GCS_CREDENTIALS environment variable to be set.
1854
1855    Examples:
1856        airbyte-ops registry store compare --store coral:dev/20260306-mirror \\
1857            --reference-store coral:prod
1858
1859        airbyte-ops registry store compare --store coral:dev/my-test \\
1860            --connector-name source-faker --no-indexes
1861
1862        airbyte-ops registry store compare --store coral:dev/my-test \\
1863            --no-artifacts
1864    """
1865    store_target = resolve_registry_store(store=store)
1866    ref_target = resolve_registry_store(store=reference_store)
1867    either_side_local = store_target.env == "local" or ref_target.env == "local"
1868
1869    result = compare_stores(
1870        store=store_target,
1871        reference=ref_target,
1872        connector_name=list(connector_name) if connector_name else None,
1873        with_artifacts=(with_artifacts and not either_side_local),
1874        with_indexes=with_indexes,
1875        tolerated_paths=(() if with_volatile_fields else DEFAULT_TOLERATED_PATHS),
1876    )
1877
1878    print_json(result.to_dict())
1879    if text_report is not None:
1880        write_text_report(result, str(text_report))
1881    if html_report is not None:
1882        write_html_report(result, str(html_report))
1883
1884    if assert_stable and (
1885        result.connectors_only_in_store
1886        or result.connectors_only_in_reference
1887        or result.connectors_latest_forward
1888        or result.connectors_latest_backward
1889    ):
1890        exit_with_error("Registry safety assertions failed.", code=1)
1891
1892    if result.status == "match":
1893        print_success(result.summary())
1894    elif result.status == "differences-found":
1895        error_console.print(f"[yellow]{result.summary()}[/yellow]")
1896
1897        # Print a concise per-connector diff summary
1898        for diff in result.connector_diffs:
1899            if diff.status in ("only_in_store", "only_in_reference"):
1900                error_console.print(f"  {diff.connector}: {diff.status}")
1901            else:
1902                for ad in diff.artifact_diffs:
1903                    error_console.print(
1904                        f"  {diff.connector}/{ad.file}: {ad.status}"
1905                        + (f" ({ad.details})" if ad.details else "")
1906                    )
1907
1908        for idx_diff in result.index_diffs:
1909            if idx_diff.status != "match":
1910                error_console.print(
1911                    f"  [index] {idx_diff.file}: {idx_diff.status}"
1912                    + (
1913                        f" (store={idx_diff.entry_count_store},"
1914                        f" ref={idx_diff.entry_count_reference})"
1915                        if idx_diff.entry_count_store or idx_diff.entry_count_reference
1916                        else ""
1917                    )
1918                )
1919
1920        if not assert_stable:
1921            sys.exit(1)
1922    else:
1923        exit_with_error(
1924            f"Compare completed with {len(result.errors)} error(s): "
1925            + "; ".join(result.errors[:5]),
1926            code=1,
1927        )