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 rc create - Create the release_candidate/ marker in GCS airbyte-ops registry rc cleanup - Delete the release_candidate/ marker from GCS 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 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 marketing-stubs sync --store coral:prod - Sync connector_stubs.json to GCS airbyte-ops registry marketing-stubs check --store coral:prod - Compare local file with GCS

CLI reference

The commands below are regenerated by poe docs-generate via cyclopts's programmatic docs API; see docs/generate_cli.py.

airbyte-ops registry COMMAND

Connector registry operations (GCS metadata service).

Commands:

  • connector: Connector listing operations.
  • connector-version: Connector version operations (list, yank, unyank, artifacts).
  • marketing-stubs: Marketing connector stubs GCS operations (whole-file sync).
  • progressive-rollout: Progressive rollout lifecycle operations (create/cleanup rollout marker in GCS).
  • store: Whole-registry store operations (mirror, compile).

airbyte-ops registry connector

Connector listing operations.

airbyte-ops registry connector list

airbyte-ops registry connector list [OPTIONS] STORE

List connectors in the registry.

When filters are applied, reads the compiled cloud_registry.json index for fast lookups. Without filters, falls back to scanning individual metadata blobs (captures all connectors including OSS-only).

Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

  • STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]
  • --certified-only, --no-certified-only: Include only certified connectors. [default: False]
  • --support-level: Exact support level to match (e.g. certified, community, archived). [choices: archived, community, certified]
  • --min-support-level: Minimum support level (inclusive). Levels from lowest to highest: archived, community, certified. [choices: archived, community, certified]
  • --connector-type: Filter by connector type: 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 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 with DEPLOYMENT_MODE=cloud and DEPLOYMENT_MODE=oss 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]
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: ""]
  • --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

Remove the yank marker from a connector version.

Deletes the version-yank.yml marker file from the version's directory in GCS, 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 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.

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]
  • --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]

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). [default: True]

airbyte-ops registry progressive-rollout

Progressive rollout lifecycle operations (create/cleanup rollout marker in GCS).

airbyte-ops registry progressive-rollout create

airbyte-ops registry progressive-rollout create [OPTIONS] NAME STORE

Create the release_candidate/ metadata marker in GCS.

Copies the versioned metadata (e.g. 1.2.3-rc.1/metadata.yaml or 2.1.0/metadata.yaml) to release_candidate/metadata.yaml so the platform knows a progressive rollout is active for this connector.

The connector's metadata.yaml on disk must declare a version that is valid for progressive rollout (i.e. not a -preview build) and the versioned blob must already exist in GCS.

Uses --store to select the registry store and environment.

Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

  • NAME, --name: Connector technical name (e.g., source-github). [required]
  • STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]
  • --repo-path: Path to the Airbyte monorepo. Defaults to current directory. [default: /home/runner/work/airbyte-ops-mcp/airbyte-ops-mcp]
  • --dry-run, --no-dry-run: Show what would be done without making changes. [default: False]

airbyte-ops registry progressive-rollout cleanup

airbyte-ops registry progressive-rollout cleanup [OPTIONS] NAME STORE

Delete the release_candidate/ metadata marker from GCS.

This command deletes only the release_candidate/metadata.yaml file for the given connector. The versioned directory (e.g. 1.2.3-rc.1/) is intentionally preserved as an audit trail of what was actually deployed during the progressive rollout.

Both promote and rollback workflows call this same command -- the only difference between the two is in the git-side steps (version bump vs. not), which are handled by the finalize_rollout GitHub Actions workflow.

Uses --store to select the registry store and environment.

Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

  • NAME, --name: Connector technical name (e.g., source-github). [required]
  • STORE, --store: Store target (e.g. 'coral:dev', 'coral:prod'). [required]
  • --repo-path: Path to the Airbyte monorepo. Defaults to current directory. [default: /home/runner/work/airbyte-ops-mcp/airbyte-ops-mcp]
  • --dry-run, --no-dry-run: Show what would be done without making changes. [default: False]

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 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]
   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 rc create - Create the release_candidate/ marker in GCS
   9    airbyte-ops registry rc cleanup - Delete the release_candidate/ marker from GCS
  10    airbyte-ops registry connector list - List all connectors in registry
  11    airbyte-ops registry connector-version list - List versions for a connector
  12    airbyte-ops registry connector-version next - Compute next version tag (prerelease/RC)
  13    airbyte-ops registry connector-version yank - Mark a connector version as yanked
  14    airbyte-ops registry connector-version unyank - Remove yank marker from a connector version
  15    airbyte-ops registry connector-version metadata get - Read connector metadata from GCS
  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 marketing-stubs sync --store coral:prod - Sync connector_stubs.json to GCS
  21    airbyte-ops registry marketing-stubs check --store coral:prod - Compare local file with GCS
  22
  23## CLI reference
  24
  25The commands below are regenerated by `poe docs-generate` via cyclopts's
  26programmatic docs API; see `docs/generate_cli.py`.
  27
  28.. include:: ../../../docs/generated/cli/registry.md
  29   :start-line: 2
  30"""
  31
  32from __future__ import annotations
  33
  34# Hide Python-level members from the pdoc page for this module; the rendered
  35# docs for this CLI group come entirely from the grafted `.. include::` in
  36# the module docstring above.
  37__all__: list[str] = []
  38
  39import sys
  40from pathlib import Path
  41from typing import Annotated, Literal
  42
  43import yaml
  44from cyclopts import Parameter
  45
  46from airbyte_ops_mcp.cli._base import App, app
  47from airbyte_ops_mcp.cli._shared import (
  48    error_console,
  49    exit_with_error,
  50    print_error,
  51    print_json,
  52    print_success,
  53)
  54from airbyte_ops_mcp.github_api import (
  55    get_file_contents_at_ref,
  56    resolve_default_github_token,
  57)
  58from airbyte_ops_mcp.mcp.prerelease import (
  59    compute_prerelease_docker_image_tag,
  60)
  61from airbyte_ops_mcp.registry import (
  62    resolve_registry_store,
  63)
  64from airbyte_ops_mcp.registry._enums import (
  65    ConnectorLanguage,
  66    ConnectorType,
  67    SupportLevel,
  68)
  69from airbyte_ops_mcp.registry.compare import compare_stores
  70from airbyte_ops_mcp.registry.connector_stubs import (
  71    CONNECTOR_STUBS_FILE,
  72)
  73from airbyte_ops_mcp.registry.generate import generate_version_artifacts
  74from airbyte_ops_mcp.registry.progressive_rollout_status import (
  75    get_connector_rollout_status,
  76)
  77from airbyte_ops_mcp.registry.registry_store_base import (
  78    Registry,
  79    get_registry,
  80)
  81
  82# Create the registry sub-app
  83registry_app = App(
  84    name="registry", help="Connector registry operations (GCS metadata service)."
  85)
  86app.command(registry_app)
  87
  88# Create the connector sub-app under registry
  89connector_app = App(name="connector", help="Connector listing operations.")
  90registry_app.command(connector_app)
  91
  92# Create the connector-version sub-app under registry
  93connector_version_app = App(
  94    name="connector-version",
  95    help="Connector version operations (list, yank, unyank, artifacts).",
  96)
  97registry_app.command(connector_version_app)
  98
  99# Create the metadata sub-app under connector-version
 100metadata_app = App(
 101    name="metadata",
 102    help="Connector metadata inspection.",
 103)
 104connector_version_app.command(metadata_app)
 105
 106# Create the artifacts sub-app under connector-version
 107artifacts_app = App(
 108    name="artifacts",
 109    help="Version artifact generation and publishing.",
 110)
 111connector_version_app.command(artifacts_app)
 112
 113# Create the store sub-app under registry (whole-registry operations)
 114store_app = App(
 115    name="store",
 116    help="Whole-registry store operations (mirror, compile).",
 117)
 118registry_app.command(store_app)
 119
 120# Create the progressive-rollout sub-app under registry
 121progressive_rollout_app = App(
 122    name="progressive-rollout",
 123    help="Progressive rollout lifecycle operations (create/cleanup rollout marker in GCS).",
 124)
 125registry_app.command(progressive_rollout_app)
 126
 127# Create the marketing-stubs sub-app under registry (for whole-file GCS operations)
 128marketing_stubs_app = App(
 129    name="marketing-stubs",
 130    help="Marketing connector stubs GCS operations (whole-file sync).",
 131)
 132registry_app.command(marketing_stubs_app)
 133
 134
 135AIRBYTE_REPO_OWNER = "airbytehq"
 136AIRBYTE_ENTERPRISE_REPO_NAME = "airbyte-enterprise"
 137AIRBYTE_REPO_NAME = "airbyte"
 138CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors"
 139
 140
 141def _resolve_store(store: str) -> Registry:
 142    """Resolve `--store` to a `Registry` instance.
 143
 144    Wraps `resolve_registry_store` and `get_registry`, converting
 145    `ValueError` into a user-friendly CLI error via `exit_with_error`.
 146    """
 147    try:
 148        resolved = resolve_registry_store(store=store)
 149    except ValueError as e:
 150        exit_with_error(str(e))
 151        raise  # unreachable; satisfies type checker
 152    return get_registry(resolved)
 153
 154
 155def _get_connector_version_from_github(
 156    connector_name: str,
 157    ref: str,
 158    token: str | None = None,
 159) -> str | None:
 160    """Fetch connector version from metadata.yaml via GitHub API.
 161
 162    Args:
 163        connector_name: Connector name (e.g., "source-github")
 164        ref: Git ref (commit SHA, branch name, or tag)
 165        token: GitHub API token (optional for public repos)
 166
 167    Returns:
 168        Version string from metadata.yaml, or None if not found.
 169    """
 170    path = f"{CONNECTOR_PATH_PREFIX}/{connector_name}/metadata.yaml"
 171    contents = get_file_contents_at_ref(
 172        owner=AIRBYTE_REPO_OWNER,
 173        repo=AIRBYTE_REPO_NAME,
 174        path=path,
 175        ref=ref,
 176        token=token,
 177    )
 178    if contents is None:
 179        return None
 180
 181    metadata = yaml.safe_load(contents)
 182    return metadata.get("data", {}).get("dockerImageTag")
 183
 184
 185@connector_version_app.command(name="next")
 186def compute_next_version(
 187    name: Annotated[
 188        str,
 189        Parameter(help="Connector name (e.g., 'source-github')."),
 190    ],
 191    sha: Annotated[
 192        str,
 193        Parameter(help="Git commit SHA (full or at least 7 characters)."),
 194    ],
 195    base_version: Annotated[
 196        str | None,
 197        Parameter(
 198            help="Base version override. If not provided, fetched from metadata.yaml at the given SHA."
 199        ),
 200    ] = None,
 201) -> None:
 202    """Compute the next version tag for a connector.
 203
 204    Outputs the version tag to stdout for easy capture in shell scripts.
 205    This is the single source of truth for pre-release version format.
 206
 207    The command fetches the connector's metadata.yaml from GitHub at the given SHA
 208    to determine the base version. It also compares against the master branch and
 209    prints a warning to stderr if no version bump is detected.
 210
 211    If --base-version is provided, it is used directly instead of fetching from GitHub.
 212
 213    Examples:
 214        airbyte-ops registry connector-version next --name source-github --sha abcdef1234567
 215        # Output: 1.2.3-preview.abcdef1
 216
 217        airbyte-ops registry connector-version next --name source-github --sha abcdef1234567 --base-version 1.2.3
 218        # Output: 1.2.3-preview.abcdef1 (uses provided version, skips GitHub API)
 219    """
 220    # Try to get a GitHub token (optional, but helps avoid rate limiting)
 221    # Token resolution may fail if no token is configured, which is fine for public repos
 222    token: str | None = None
 223    token = resolve_default_github_token(allow_none=True)
 224
 225    # Determine base version
 226    version: str
 227    if base_version:
 228        version = base_version
 229    else:
 230        # Fetch version from metadata.yaml at the given SHA
 231        fetched_version = _get_connector_version_from_github(name, sha, token)
 232        if fetched_version is None:
 233            print(
 234                f"Error: Could not fetch metadata.yaml for {name} at ref {sha}",
 235                file=sys.stderr,
 236            )
 237            sys.exit(1)
 238        version = fetched_version
 239
 240    # Compare with master branch version and warn if no bump detected
 241    master_version = _get_connector_version_from_github(name, "master", token)
 242    if master_version and master_version == version:
 243        print(
 244            f"Warning: No version bump detected for {name}. "
 245            f"Version {version} matches master branch.",
 246            file=sys.stderr,
 247        )
 248
 249    # Compute and output the prerelease tag
 250    tag = compute_prerelease_docker_image_tag(version, sha)
 251    print(tag)
 252
 253
 254# =============================================================================
 255# PROGRESSIVE ROLLOUT COMMANDS
 256# =============================================================================
 257
 258
 259@progressive_rollout_app.command(name="create")
 260def progressive_rollout_create(
 261    name: Annotated[
 262        str,
 263        Parameter(help="Connector technical name (e.g., source-github)."),
 264    ],
 265    store: Annotated[
 266        str,
 267        Parameter(
 268            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 269        ),
 270    ],
 271    *,
 272    repo_path: Annotated[
 273        Path,
 274        Parameter(help="Path to the Airbyte monorepo. Defaults to current directory."),
 275    ] = Path.cwd(),
 276    dry_run: Annotated[
 277        bool,
 278        Parameter(help="Show what would be done without making changes."),
 279    ] = False,
 280) -> None:
 281    """Create the release_candidate/ metadata marker in GCS.
 282
 283    Copies the versioned metadata (e.g. `1.2.3-rc.1/metadata.yaml` or
 284    `2.1.0/metadata.yaml`) to `release_candidate/metadata.yaml` so the
 285    platform knows a progressive rollout is active for this connector.
 286
 287    The connector's metadata.yaml on disk must declare a version that
 288    is valid for progressive rollout (i.e. not a -preview build) and the
 289    versioned blob must already exist in GCS.
 290
 291    Uses `--store` to select the registry store and environment.
 292
 293    Requires `GCS_CREDENTIALS` environment variable to be set.
 294    """
 295    if not repo_path.exists():
 296        exit_with_error(f"Repository path not found: {repo_path}")
 297
 298    registry = _resolve_store(store)
 299
 300    result = registry.progressive_rollout_create(
 301        repo_path=repo_path,
 302        connector_name=name,
 303        dry_run=dry_run,
 304    )
 305
 306    print_json(result.model_dump())
 307
 308    if result.status == "failure":
 309        exit_with_error(result.message or "Operation failed", code=1)
 310
 311
 312@progressive_rollout_app.command(name="cleanup")
 313def progressive_rollout_cleanup(
 314    name: Annotated[
 315        str,
 316        Parameter(help="Connector technical name (e.g., source-github)."),
 317    ],
 318    store: Annotated[
 319        str,
 320        Parameter(
 321            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 322        ),
 323    ],
 324    *,
 325    repo_path: Annotated[
 326        Path,
 327        Parameter(help="Path to the Airbyte monorepo. Defaults to current directory."),
 328    ] = Path.cwd(),
 329    dry_run: Annotated[
 330        bool,
 331        Parameter(help="Show what would be done without making changes."),
 332    ] = False,
 333) -> None:
 334    """Delete the release_candidate/ metadata marker from GCS.
 335
 336    This command deletes *only* the `release_candidate/metadata.yaml` file
 337    for the given connector.  The versioned directory (e.g. `1.2.3-rc.1/`)
 338    is intentionally preserved as an audit trail of what was actually deployed
 339    during the progressive rollout.
 340
 341    Both promote and rollback workflows call this same command -- the only
 342    difference between the two is in the git-side steps (version bump vs.
 343    not), which are handled by the `finalize_rollout` GitHub Actions
 344    workflow.
 345
 346    Uses `--store` to select the registry store and environment.
 347
 348    Requires `GCS_CREDENTIALS` environment variable to be set.
 349    """
 350    if not repo_path.exists():
 351        exit_with_error(f"Repository path not found: {repo_path}")
 352
 353    registry = _resolve_store(store)
 354
 355    result = registry.progressive_rollout_cleanup(
 356        repo_path=repo_path,
 357        connector_name=name,
 358        dry_run=dry_run,
 359    )
 360
 361    print_json(result.model_dump())
 362
 363    if result.status == "failure":
 364        exit_with_error(result.message or "Operation failed", code=1)
 365
 366
 367@progressive_rollout_app.command(name="status")
 368def progressive_rollout_status(
 369    name: Annotated[
 370        str,
 371        Parameter(help="Connector technical name (e.g., source-github)."),
 372    ],
 373    *,
 374    repo_path: Annotated[
 375        Path,
 376        Parameter(help="Path to the Airbyte monorepo. Defaults to current directory."),
 377    ] = Path.cwd(),
 378    active_only: Annotated[
 379        bool,
 380        Parameter(
 381            help="Only return active non-terminal rollouts.",
 382            negative="--with-terminal",
 383        ),
 384    ] = True,
 385    limit: Annotated[
 386        int,
 387        Parameter(help="Maximum number of rollout records to return."),
 388    ] = 100,
 389) -> None:
 390    """Get progressive rollout status for a connector."""
 391    if not repo_path.exists():
 392        exit_with_error(f"Repository path not found: {repo_path}")
 393    try:
 394        result = get_connector_rollout_status(
 395            repo_path=repo_path,
 396            connector_name=name,
 397            active_only=active_only,
 398            limit=limit,
 399        )
 400    except (FileNotFoundError, ValueError) as e:
 401        exit_with_error(str(e))
 402
 403    print_json(result.model_dump())
 404
 405
 406# =============================================================================
 407# REGISTRY I/O - READ COMMANDS
 408# =============================================================================
 409
 410
 411@metadata_app.command(name="get")
 412def get_connector_version_metadata_cmd(
 413    name: Annotated[
 414        str,
 415        Parameter(
 416            help="Connector name (e.g., 'source-faker', 'destination-postgres')."
 417        ),
 418    ],
 419    store: Annotated[
 420        str,
 421        Parameter(
 422            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 423        ),
 424    ],
 425    *,
 426    version: Annotated[
 427        str,
 428        Parameter(help="Version to read (e.g., 'latest', '1.2.3')."),
 429    ] = "latest",
 430    format: Annotated[
 431        Literal["json", "raw"],
 432        Parameter(help="Output format: 'json' for JSON, 'raw' for YAML."),
 433    ] = "json",
 434) -> None:
 435    """Read a connector version's metadata from the registry.
 436
 437    Returns the full metadata.yaml content for a connector at the specified version.
 438
 439    Requires GCS_CREDENTIALS environment variable to be set.
 440
 441    Examples:
 442        airbyte-ops registry connector-version metadata get --name source-faker --store coral:dev
 443        airbyte-ops registry connector-version metadata get --name source-faker --store coral:dev --version 6.2.38
 444        airbyte-ops registry connector-version metadata get --name source-faker --store coral:prod
 445    """
 446    registry = _resolve_store(store)
 447
 448    try:
 449        metadata = registry.get_connector_metadata(
 450            connector_name=name,
 451            version=version,
 452        )
 453    except FileNotFoundError as e:
 454        exit_with_error(str(e), code=1)
 455    except Exception as e:
 456        exit_with_error(f"Error reading metadata: {e}", code=1)
 457
 458    if format == "json":
 459        print_json(metadata)
 460    else:
 461        print(yaml.dump(metadata, default_flow_style=False))
 462
 463
 464@connector_app.command(name="list")
 465def list_connectors_cmd(
 466    store: Annotated[
 467        str,
 468        Parameter(
 469            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 470        ),
 471    ],
 472    *,
 473    certified_only: Annotated[
 474        bool,
 475        Parameter(help="Include only certified connectors."),
 476    ] = False,
 477    support_level: Annotated[
 478        SupportLevel | None,
 479        Parameter(
 480            help=(
 481                "Exact support level to match "
 482                "(e.g. `certified`, `community`, `archived`)."
 483            )
 484        ),
 485    ] = None,
 486    min_support_level: Annotated[
 487        SupportLevel | None,
 488        Parameter(
 489            help=(
 490                "Minimum support level (inclusive). "
 491                "Levels from lowest to highest: `archived`, `community`, `certified`."
 492            )
 493        ),
 494    ] = None,
 495    connector_type: Annotated[
 496        ConnectorType | None,
 497        Parameter(help="Filter by connector type: `source` or `destination`."),
 498    ] = None,
 499    language: Annotated[
 500        ConnectorLanguage | None,
 501        Parameter(
 502            help=(
 503                "Filter by implementation language "
 504                "(e.g. `python`, `java`, `manifest-only`)."
 505            )
 506        ),
 507    ] = None,
 508    format: Annotated[
 509        Literal["json", "text"],
 510        Parameter(
 511            help="Output format: 'json' for JSON array, 'text' for newline-separated."
 512        ),
 513    ] = "json",
 514) -> None:
 515    """List connectors in the registry.
 516
 517    When filters are applied, reads the compiled `cloud_registry.json` index
 518    for fast lookups. Without filters, falls back to scanning individual
 519    metadata blobs (captures all connectors including OSS-only).
 520
 521    Requires GCS_CREDENTIALS environment variable to be set.
 522    """
 523    registry = _resolve_store(store)
 524
 525    # `--certified-only` is sugar for `--support-level certified`.
 526    effective_support_level = support_level
 527    if certified_only:
 528        if support_level and support_level != SupportLevel.CERTIFIED:
 529            exit_with_error(
 530                "`--certified-only` conflicts with `--support-level "
 531                f"{support_level}`. Use one or the other.",
 532                code=1,
 533            )
 534        effective_support_level = SupportLevel.CERTIFIED
 535
 536    try:
 537        connectors = registry.list_connectors(
 538            support_level=effective_support_level,
 539            min_support_level=min_support_level,
 540            connector_type=connector_type,
 541            language=language,
 542        )
 543    except Exception as e:
 544        exit_with_error(f"Error listing connectors: {e}", code=1)
 545
 546    if format == "json":
 547        print_json({"connectors": connectors, "count": len(connectors)})
 548    else:
 549        for connector in connectors:
 550            print(connector)
 551
 552
 553@connector_version_app.command(name="list")
 554def list_connector_versions_cmd(
 555    name: Annotated[
 556        str,
 557        Parameter(help="Connector name (e.g., 'source-faker')."),
 558    ],
 559    store: Annotated[
 560        str,
 561        Parameter(
 562            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 563        ),
 564    ],
 565    *,
 566    format: Annotated[
 567        Literal["json", "text"],
 568        Parameter(
 569            help="Output format: 'json' for JSON array, 'text' for newline-separated."
 570        ),
 571    ] = "json",
 572) -> None:
 573    """List all versions of a connector in the registry.
 574
 575    Scans the registry bucket to find all versions of a specific connector.
 576
 577    Requires GCS_CREDENTIALS environment variable to be set.
 578    """
 579    registry = _resolve_store(store)
 580
 581    try:
 582        versions = registry.list_connector_versions(
 583            connector_name=name,
 584        )
 585    except Exception as e:
 586        exit_with_error(f"Error listing versions: {e}", code=1)
 587
 588    if format == "json":
 589        print_json({"connector": name, "versions": versions, "count": len(versions)})
 590    else:
 591        for v in versions:
 592            print(v)
 593
 594
 595@marketing_stubs_app.command(name="check")
 596def marketing_stubs_check(
 597    store: Annotated[
 598        str,
 599        Parameter(
 600            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 601        ),
 602    ],
 603    *,
 604    repo_root: Annotated[
 605        Path,
 606        Parameter(
 607            help="Path to the airbyte-enterprise repository root. Defaults to current directory."
 608        ),
 609    ] = Path.cwd(),
 610) -> None:
 611    """Compare local connector_stubs.json with the version in GCS.
 612
 613    This command reads the entire local connector_stubs.json file and compares it
 614    with the version currently published in GCS.
 615
 616    Exit codes:
 617        0: Local file matches GCS (check passed)
 618        1: Differences found (check failed)
 619
 620    Output:
 621        STDOUT: JSON representation of the comparison result
 622        STDERR: Informational messages and comparison details
 623
 624    Example:
 625        airbyte-ops registry marketing-stubs check --store coral:prod --repo-root /path/to/airbyte-enterprise
 626        airbyte-ops registry marketing-stubs check --store coral:dev
 627    """
 628    registry = _resolve_store(store)
 629
 630    try:
 631        result = registry.marketing_stubs_check(repo_root=repo_root)
 632    except FileNotFoundError as e:
 633        exit_with_error(str(e))
 634    except ValueError as e:
 635        exit_with_error(str(e))
 636
 637    error_console.print(
 638        f"Comparing local {CONNECTOR_STUBS_FILE} with {result.get('bucket', '')}/{result.get('path', '')}"
 639    )
 640
 641    differences = result.get("differences", [])
 642    if differences:
 643        error_console.print(
 644            f"[yellow]Warning:[/yellow] {len(differences)} difference(s) found:"
 645        )
 646        for diff in differences:
 647            error_console.print(f"  {diff['id']}: {diff['status']}")
 648        print_json(result)
 649        sys.exit(1)
 650
 651    error_console.print(
 652        f"[green]Local file is in sync with GCS ({result.get('local_count', 0)} stubs)[/green]"
 653    )
 654    print_json(result)
 655
 656
 657@marketing_stubs_app.command(name="sync")
 658def marketing_stubs_sync(
 659    store: Annotated[
 660        str,
 661        Parameter(
 662            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 663        ),
 664    ],
 665    *,
 666    repo_root: Annotated[
 667        Path,
 668        Parameter(
 669            help="Path to the airbyte-enterprise repository root. Defaults to current directory."
 670        ),
 671    ] = Path.cwd(),
 672    dry_run: Annotated[
 673        bool,
 674        Parameter(help="Show what would be uploaded without making changes."),
 675    ] = False,
 676) -> None:
 677    """Sync local connector_stubs.json to GCS.
 678
 679    This command uploads the entire local connector_stubs.json file to GCS,
 680    replacing the existing file. Use this after merging changes to master
 681    in the airbyte-enterprise repository.
 682
 683    Exit codes:
 684        0: Sync successful (or dry-run completed)
 685        1: Error (file not found, validation failed, etc.)
 686
 687    Output:
 688        STDOUT: JSON representation of the sync result
 689        STDERR: Informational messages and status updates
 690
 691    Example:
 692        airbyte-ops registry marketing-stubs sync --store coral:prod --repo-root /path/to/airbyte-enterprise
 693        airbyte-ops registry marketing-stubs sync --store coral:dev
 694        airbyte-ops registry marketing-stubs sync --store coral:dev --dry-run
 695    """
 696    registry = _resolve_store(store)
 697
 698    try:
 699        result = registry.marketing_stubs_sync(
 700            repo_root=repo_root,
 701            dry_run=dry_run,
 702        )
 703    except FileNotFoundError as e:
 704        exit_with_error(str(e))
 705    except ValueError as e:
 706        exit_with_error(str(e))
 707
 708    bucket_name = result.get("bucket", "")
 709    path = result.get("path", "")
 710    stub_count = result.get("stub_count", 0)
 711
 712    if dry_run:
 713        error_console.print(
 714            f"[DRY RUN] Would upload {stub_count} stubs to {bucket_name}/{path}"
 715        )
 716    else:
 717        error_console.print(
 718            f"[green]Synced {stub_count} stubs to {bucket_name}/{path}[/green]"
 719        )
 720    print_json(result)
 721
 722
 723# =============================================================================
 724# REGISTRY REBUILD COMMANDS
 725# =============================================================================
 726
 727
 728@store_app.command(name="mirror")
 729def mirror_cmd(
 730    local: Annotated[
 731        bool,
 732        Parameter(
 733            help="Write output to a local directory. Mutually exclusive with --gcs-bucket and --s3-bucket.",
 734            negative="",
 735        ),
 736    ] = False,
 737    gcs_bucket: Annotated[
 738        str | None,
 739        Parameter(
 740            help="Write output to a GCS bucket. Must not be the prod bucket. "
 741            "Mutually exclusive with --local and --s3-bucket.",
 742        ),
 743    ] = None,
 744    s3_bucket: Annotated[
 745        str | None,
 746        Parameter(
 747            help="Write output to an S3 bucket. "
 748            "Mutually exclusive with --local and --gcs-bucket.",
 749        ),
 750    ] = None,
 751    output_path_root: Annotated[
 752        str | None,
 753        Parameter(
 754            help="Root path/prefix for the output. For --local, this is a directory path "
 755            "(defaults to a new temp dir if omitted). For --gcs-bucket/--s3-bucket, "
 756            "this prefix is prepended to all blob paths.",
 757        ),
 758    ] = None,
 759    dry_run: Annotated[
 760        bool,
 761        Parameter(help="Show what would be rebuilt without writing any files."),
 762    ] = False,
 763    source_store: Annotated[
 764        str,
 765        Parameter(
 766            help="Source store to read from (e.g. 'coral:prod', 'coral:dev').",
 767        ),
 768    ] = "coral:prod",
 769    connector_name: Annotated[
 770        tuple[str, ...] | None,
 771        Parameter(
 772            help="Only rebuild these connectors (by name). "
 773            "Can be specified multiple times, e.g. "
 774            "--connector-name source-faker --connector-name destination-bigquery.",
 775        ),
 776    ] = None,
 777) -> None:
 778    """Create a mirror of the connector registry from the source store.
 779
 780    Reads all connector metadata from the source store and copies it
 781    to the specified output target. Supports local filesystem, GCS, and S3
 782    as output targets via fsspec.
 783
 784    Output targets are mutually exclusive: specify exactly one of
 785    --local, --gcs-bucket, or --s3-bucket.
 786
 787    The production bucket is categorically disallowed as an output target.
 788
 789    To clean up legacy artifacts (e.g. disabled strict-encrypt connectors)
 790    after mirroring, run compile with `--with-legacy-migration v1`::
 791
 792        airbyte-ops registry store compile --store coral:dev/my-prefix \\
 793            --with-legacy-migration v1
 794
 795    Examples:
 796        airbyte-ops registry store mirror --local
 797        airbyte-ops registry store mirror --local --source-store coral:dev
 798        airbyte-ops registry store mirror --gcs-bucket dev-airbyte-cloud-connector-metadata-service-2 \\
 799            --output-path-root test-run-123
 800        airbyte-ops registry store mirror --s3-bucket my-test-bucket --dry-run
 801    """
 802    # Validate mutually exclusive output targets
 803    targets = [local, gcs_bucket is not None, s3_bucket is not None]
 804    if sum(targets) != 1:
 805        exit_with_error(
 806            "Specify exactly one output target: --local, --gcs-bucket, or --s3-bucket."
 807        )
 808
 809    if local:
 810        output_mode = "local"
 811    elif gcs_bucket is not None:
 812        output_mode = "gcs"
 813    else:
 814        output_mode = "s3"
 815
 816    registry = _resolve_store(source_store)
 817
 818    result = registry.mirror(
 819        output_mode=output_mode,
 820        output_path_root=output_path_root,
 821        gcs_bucket=gcs_bucket,
 822        s3_bucket=s3_bucket,
 823        dry_run=dry_run,
 824        connector_name=list(connector_name) if connector_name else None,
 825    )
 826
 827    print_json(
 828        {
 829            "status": result.status,
 830            "source_bucket": result.source_bucket,
 831            "output_mode": result.output_mode,
 832            "output_root": result.output_root,
 833            "connectors_processed": result.connectors_processed,
 834            "blobs_copied": result.blobs_copied,
 835            "blobs_skipped": result.blobs_skipped,
 836            "error_count": len(result.errors),
 837            "dry_run": result.dry_run,
 838        }
 839    )
 840
 841    if result.errors:
 842        for err in result.errors[:10]:
 843            print_error(err)
 844        if len(result.errors) > 10:
 845            print_error(f"... and {len(result.errors) - 10} more errors")
 846
 847    if result.status == "success":
 848        print_success(result.summary())
 849    elif result.status == "dry-run":
 850        print_success(f"[DRY RUN] {result.summary()}")
 851    else:
 852        error_console.print(f"[yellow]{result.summary()}[/yellow]")
 853
 854
 855# =============================================================================
 856# VERSION YANK COMMANDS
 857# =============================================================================
 858
 859
 860@connector_version_app.command(name="yank")
 861def yank_cmd(
 862    name: Annotated[
 863        str,
 864        Parameter(help="Connector name (e.g., 'source-faker')."),
 865    ],
 866    version: Annotated[
 867        str,
 868        Parameter(help="Version to yank (e.g., '1.2.3')."),
 869    ],
 870    store: Annotated[
 871        str,
 872        Parameter(
 873            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 874        ),
 875    ],
 876    *,
 877    reason: Annotated[
 878        str,
 879        Parameter(help="Reason for yanking this version."),
 880    ] = "",
 881    dry_run: Annotated[
 882        bool,
 883        Parameter(help="Show what would be done without making changes."),
 884    ] = False,
 885) -> None:
 886    """Mark a connector version as yanked.
 887
 888    Writes a version-yank.yml marker file to the version's directory in GCS.
 889    Yanked versions are excluded when determining the latest version of a
 890    connector.
 891
 892    Requires GCS_CREDENTIALS environment variable to be set.
 893
 894    Examples:
 895        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:dev
 896        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:dev --reason "Critical bug"
 897        airbyte-ops registry connector-version yank --name source-faker --version 1.2.3 --store coral:prod
 898    """
 899    registry = _resolve_store(store)
 900
 901    result = registry.yank(
 902        connector_name=name,
 903        version=version,
 904        reason=reason,
 905        dry_run=dry_run,
 906    )
 907
 908    print_json(result.to_dict())
 909
 910    if result.success:
 911        print_success(result.message)
 912        if not dry_run:
 913            error_console.print(
 914                "\n[yellow]Note:[/yellow] The registry indexes are now stale. "
 915                "To update them, run:\n\n"
 916                f"    airbyte-ops registry store compile --store {store}\n\n"
 917                "Or wait for the next scheduled compile operation."
 918            )
 919    else:
 920        exit_with_error(result.message, code=1)
 921
 922
 923@connector_version_app.command(name="unyank")
 924def unyank_cmd(
 925    name: Annotated[
 926        str,
 927        Parameter(help="Connector name (e.g., 'source-faker')."),
 928    ],
 929    version: Annotated[
 930        str,
 931        Parameter(help="Version to unyank (e.g., '1.2.3')."),
 932    ],
 933    store: Annotated[
 934        str,
 935        Parameter(
 936            help="Store target (e.g. 'coral:dev', 'coral:prod').",
 937        ),
 938    ],
 939    *,
 940    dry_run: Annotated[
 941        bool,
 942        Parameter(help="Show what would be done without making changes."),
 943    ] = False,
 944) -> None:
 945    """Remove the yank marker from a connector version.
 946
 947    Deletes the version-yank.yml marker file from the version's directory in GCS,
 948    making the version eligible again when determining the latest version.
 949
 950    Requires GCS_CREDENTIALS environment variable to be set.
 951
 952    Examples:
 953        airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:dev
 954        airbyte-ops registry connector-version unyank --name source-faker --version 1.2.3 --store coral:prod
 955    """
 956    registry = _resolve_store(store)
 957
 958    result = registry.unyank(
 959        connector_name=name,
 960        version=version,
 961        dry_run=dry_run,
 962    )
 963
 964    print_json(result.to_dict())
 965
 966    if result.success:
 967        print_success(result.message)
 968        if not dry_run:
 969            error_console.print(
 970                "\n[yellow]Note:[/yellow] The registry indexes are now stale. "
 971                "To update them, run:\n\n"
 972                f"    airbyte-ops registry store compile --store {store}\n\n"
 973                "Or wait for the next scheduled compile operation."
 974            )
 975    else:
 976        exit_with_error(result.message, code=1)
 977
 978
 979# =============================================================================
 980# ARTIFACT GENERATION COMMANDS
 981# =============================================================================
 982
 983
 984@artifacts_app.command(name="generate")
 985def generate_version_artifacts_cmd(
 986    metadata_file: Annotated[
 987        Path,
 988        Parameter(help="Path to the connector's metadata.yaml file."),
 989    ],
 990    docker_image: Annotated[
 991        str,
 992        Parameter(
 993            help="Docker image to run spec against (e.g., 'airbyte/source-faker:6.2.38')."
 994        ),
 995    ],
 996    output_dir: Annotated[
 997        Path | None,
 998        Parameter(
 999            help="Directory to write artifacts to. If not specified, a temp directory is created."
1000        ),
1001    ] = None,
1002    repo_root: Annotated[
1003        Path | None,
1004        Parameter(
1005            help=(
1006                "Root of the Airbyte repo checkout (for resolving doc.md). "
1007                "If not specified, inferred by walking up from metadata-file."
1008            ),
1009        ),
1010    ] = None,
1011    dry_run: Annotated[
1012        bool,
1013        Parameter(
1014            help="Show what would be generated without running docker or writing files."
1015        ),
1016    ] = False,
1017    with_validate: Annotated[
1018        bool,
1019        Parameter(
1020            help=(
1021                "Run metadata validators after generation (default: enabled). "
1022                "Use --no-validate to skip."
1023            ),
1024            negative="--no-validate",
1025        ),
1026    ] = True,
1027    with_sbom: Annotated[
1028        bool,
1029        Parameter(
1030            help=(
1031                "Generate spdx.json (SBOM) for connectors "
1032                "(default: enabled). Use --no-sbom to skip."
1033            ),
1034            negative="--no-sbom",
1035        ),
1036    ] = True,
1037    with_dependency_dump: Annotated[
1038        bool,
1039        Parameter(
1040            help=(
1041                "Generate dependencies.json for Python connectors "
1042                "(default: enabled). Use --no-dependency-dump to skip."
1043            ),
1044            negative="--no-dependency-dump",
1045        ),
1046    ] = True,
1047) -> None:
1048    """Generate version artifacts for a connector locally.
1049
1050    Runs the connector's docker image with DEPLOYMENT_MODE=cloud and
1051    DEPLOYMENT_MODE=oss to obtain both spec variants, then generates
1052    the registry entries (cloud.json, oss.json) by applying
1053    registryOverrides from the metadata.
1054
1055    The generated metadata.yaml is enriched with git commit info, SBOM URL,
1056    and (when applicable) components SHA before writing.  Validation is run
1057    after generation by default; pass `--no-validate` to skip.
1058
1059    This is a local-only operation -- no files are uploaded to GCS.
1060    Use `artifacts publish` to upload generated artifacts to GCS.
1061
1062    Examples:
1063        airbyte-ops registry connector-version artifacts generate \\
1064            --metadata-file path/to/metadata.yaml \\
1065            --docker-image airbyte/source-faker:6.2.38
1066
1067        airbyte-ops registry connector-version artifacts generate \\
1068            --metadata-file path/to/metadata.yaml \\
1069            --docker-image airbyte/source-faker:6.2.38 \\
1070            --output-dir ./artifacts --with-validate
1071    """
1072    result = generate_version_artifacts(
1073        metadata_file=metadata_file,
1074        docker_image=docker_image,
1075        output_dir=output_dir,
1076        repo_root=repo_root,
1077        dry_run=dry_run,
1078        with_validate=with_validate,
1079        with_dependency_dump=with_dependency_dump,
1080        with_sbom=with_sbom,
1081    )
1082
1083    print_json(result.to_dict())
1084
1085    if result.success:
1086        print_success(
1087            f"Generated {len(result.artifacts_written)} artifacts to {result.output_dir}"
1088        )
1089    else:
1090        all_errors = result.errors + result.validation_errors
1091        exit_with_error(
1092            f"Generation completed with {len(all_errors)} error(s): "
1093            + "; ".join(all_errors),
1094            code=1,
1095        )
1096
1097
1098@artifacts_app.command(name="publish")
1099def publish_version_artifacts_cmd(
1100    name: Annotated[
1101        str,
1102        Parameter(help="Connector name (e.g., 'source-faker')."),
1103    ],
1104    version: Annotated[
1105        str,
1106        Parameter(help="Version to publish artifacts for (e.g., '1.2.3')."),
1107    ],
1108    artifacts_dir: Annotated[
1109        Path,
1110        Parameter(
1111            help="Directory containing generated artifacts to publish (from 'artifacts generate')."
1112        ),
1113    ],
1114    store: Annotated[
1115        str,
1116        Parameter(
1117            help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix').",
1118        ),
1119    ],
1120    *,
1121    dry_run: Annotated[
1122        bool,
1123        Parameter(help="Show what would be published without writing to GCS."),
1124    ] = False,
1125    with_validate: Annotated[
1126        bool,
1127        Parameter(
1128            help=(
1129                "Validate metadata before uploading (default: enabled). "
1130                "Use --no-validate to skip."
1131            ),
1132            negative="--no-validate",
1133        ),
1134    ] = True,
1135) -> None:
1136    """Publish version artifacts to GCS using fsspec rsync.
1137
1138    Uploads locally generated artifacts (from `artifacts generate`) to the
1139    versioned path in GCS.  By default, metadata is validated before upload;
1140    pass `--no-validate` to skip.
1141
1142    Uses `--store` to select the destination store and environment:
1143
1144    * `coral:dev`              → coral dev bucket at root
1145    * `coral:prod`             → coral prod bucket at root
1146    * `coral:dev/aj-test100`   → coral dev bucket under `aj-test100/` prefix
1147
1148    Requires GCS_CREDENTIALS environment variable to be set.
1149
1150    Examples:
1151        airbyte-ops registry connector-version artifacts publish \\
1152            --name source-faker --version 6.2.38 \\
1153            --artifacts-dir ./artifacts --store coral:dev --with-validate
1154
1155        airbyte-ops registry connector-version artifacts publish \\
1156            --name source-faker --version 6.2.38 \\
1157            --artifacts-dir ./artifacts --store coral:prod
1158
1159        airbyte-ops registry connector-version artifacts publish \\
1160            --name source-faker --version 6.2.38 \\
1161            --artifacts-dir ./artifacts --store coral:dev/aj-test100
1162    """
1163    registry = _resolve_store(store)
1164
1165    result = registry.publish_version_artifacts(
1166        connector_name=name,
1167        version=version,
1168        artifacts_dir=artifacts_dir,
1169        dry_run=dry_run,
1170        with_validate=with_validate,
1171    )
1172
1173    print_json(
1174        {
1175            "status": result.status,
1176            "connector_name": result.connector_name,
1177            "version": result.version,
1178            "target": result.target,
1179            "gcs_destination": result.gcs_destination,
1180            "files_uploaded": result.files_uploaded,
1181            "errors": result.errors,
1182            "validation_errors": result.validation_errors,
1183            "dry_run": result.dry_run,
1184        }
1185    )
1186
1187    if result.success:
1188        print_success(
1189            f"Published {len(result.files_uploaded)} artifacts for "
1190            f"{result.connector_name}@{result.version}{result.gcs_destination}"
1191        )
1192    else:
1193        all_errors = result.errors + result.validation_errors
1194        exit_with_error(
1195            f"Publish completed with {len(all_errors)} error(s): "
1196            + "; ".join(all_errors),
1197            code=1,
1198        )
1199
1200
1201# =============================================================================
1202# COMPILE COMMAND
1203# =============================================================================
1204
1205
1206@store_app.command(name="compile")
1207def compile_cmd(
1208    store: Annotated[
1209        str,
1210        Parameter(
1211            help="Store target (e.g. 'coral:dev', 'coral:prod', 'coral:dev/prefix').",
1212        ),
1213    ],
1214    *,
1215    connector_name: Annotated[
1216        tuple[str, ...] | None,
1217        Parameter(
1218            help="Only compile these connectors (can be repeated).",
1219        ),
1220    ] = None,
1221    dry_run: Annotated[
1222        bool,
1223        Parameter(help="Show what would be done without writing."),
1224    ] = False,
1225    with_secrets_mask: Annotated[
1226        bool,
1227        Parameter(
1228            help=(
1229                "Also regenerate specs_secrets_mask.yaml by scanning all "
1230                "connector specs for airbyte_secret properties."
1231            ),
1232        ),
1233    ] = False,
1234    with_legacy_migration: Annotated[
1235        str | None,
1236        Parameter(
1237            help=(
1238                "Run a one-time legacy migration step during compile. "
1239                "Currently supported: 'v1' — delete cloud.json / oss.json "
1240                "files for connectors whose registryOverrides.cloud.enabled "
1241                "or registryOverrides.oss.enabled is false. This cleans up "
1242                "artifacts produced by the legacy pipeline that did not "
1243                "respect the enabled flag."
1244            ),
1245        ),
1246    ] = None,
1247    with_metrics: Annotated[
1248        bool,
1249        Parameter(
1250            help=(
1251                "Inject latest connector quality metrics from the analytics "
1252                "JSONL export into generated.metrics."
1253            ),
1254            negative="--no-metrics",
1255        ),
1256    ] = True,
1257    force: Annotated[
1258        bool,
1259        Parameter(
1260            help=(
1261                "Force resync of latest/ directories even if version markers are current. "
1262                "Useful when metadata changes without a version bump."
1263            ),
1264        ),
1265    ] = False,
1266) -> None:
1267    """Compile the registry: sync latest/ dirs, write global and per-connector indexes.
1268
1269    Scans all version directories in the target store, determines the latest GA
1270    semver per connector (excluding yanked and pre-release versions), ensures
1271    each `latest/` directory matches the computed latest, and writes:
1272
1273    * `registries/v0/cloud_registry.json` -- global cloud registry index
1274    * `registries/v0/oss_registry.json`   -- global OSS registry index
1275    * `metadata/airbyte/<connector>/versions.json` -- per-connector version index
1276
1277    With `--with-secrets-mask`, also regenerates:
1278
1279    * `registries/v0/specs_secrets_mask.yaml` -- properties marked as secrets
1280
1281    With `--with-legacy-migration=v1`, deletes `cloud.json` / `oss.json`
1282    files for connectors whose `registryOverrides.cloud.enabled` or
1283    `registryOverrides.oss.enabled` is `false`.
1284
1285    By default, injects connector quality metrics from the latest analytics
1286    JSONL export into `generated.metrics`. Use `--no-metrics` for offline
1287    scenarios.
1288
1289    With `--force`, resyncs all latest/ directories even if the version marker
1290    matches the computed latest version.
1291
1292    Uses efficient glob patterns for scanning (no file downloads during discovery).
1293
1294    Requires GCS_CREDENTIALS environment variable to be set.
1295
1296    Examples:
1297        airbyte-ops registry store compile --store coral:dev --dry-run
1298
1299        airbyte-ops registry store compile --store coral:dev/aj-test100 \\
1300            --connector-name source-faker --connector-name destination-bigquery
1301
1302        airbyte-ops registry store compile --store coral:prod --with-secrets-mask
1303
1304        airbyte-ops registry store compile --store coral:dev \\
1305            --with-legacy-migration v1
1306    """
1307    registry = _resolve_store(store)
1308
1309    result = registry.compile(
1310        connector_name=list(connector_name) if connector_name else None,
1311        dry_run=dry_run,
1312        with_secrets_mask=with_secrets_mask,
1313        with_legacy_migration=with_legacy_migration,
1314        with_metrics=with_metrics,
1315        force=force,
1316    )
1317
1318    print_json(
1319        {
1320            "status": result.status,
1321            "target": result.target,
1322            "connectors_scanned": result.connectors_scanned,
1323            "versions_found": result.versions_found,
1324            "yanked_versions": result.yanked_versions,
1325            "latest_updated": result.latest_updated,
1326            "latest_already_current": result.latest_already_current,
1327            "cloud_registry_entries": result.cloud_registry_entries,
1328            "oss_registry_entries": result.oss_registry_entries,
1329            "composite_registry_entries": result.composite_registry_entries,
1330            "metrics_connector_count": result.metrics_connector_count,
1331            "metrics_registry_entries": result.metrics_registry_entries,
1332            "metrics_source": result.metrics_source,
1333            "metrics_error": result.metrics_error,
1334            "version_indexes_written": result.version_indexes_written,
1335            "specs_secrets_mask_properties": result.specs_secrets_mask_properties,
1336            "errors": result.errors,
1337            "dry_run": result.dry_run,
1338        }
1339    )
1340
1341    if result.status == "success" or result.status == "dry-run":
1342        print_success(result.summary())
1343    else:
1344        exit_with_error(
1345            f"Compile completed with {len(result.errors)} error(s): "
1346            + "; ".join(result.errors),
1347            code=1,
1348        )
1349
1350
1351# =============================================================================
1352# DELETE-DEV-LATEST COMMAND
1353# =============================================================================
1354
1355
1356@store_app.command(name="delete-dev-latest")
1357def delete_dev_latest_cmd(
1358    store: Annotated[
1359        str,
1360        Parameter(
1361            help="Store target (must begin with 'coral:dev').",
1362        ),
1363    ],
1364    *,
1365    connector_name: Annotated[
1366        tuple[str, ...] | None,
1367        Parameter(
1368            help="Only delete latest/ for these connectors (can be repeated).",
1369        ),
1370    ] = None,
1371    dry_run: Annotated[
1372        bool,
1373        Parameter(help="Show what would be done without deleting."),
1374    ] = False,
1375) -> None:
1376    """Delete all latest/ directories from a dev registry store.
1377
1378    Discovers every connector that has a `latest/` directory and
1379    deletes each one in parallel using a thread pool.
1380
1381    This is useful before a full re-compile to prove that latest/
1382    directories can be correctly regenerated from versioned data.
1383
1384    Only dev stores are allowed (store must begin with 'coral:dev').
1385
1386    Requires GCS_CREDENTIALS environment variable to be set.
1387
1388    Examples:
1389        airbyte-ops registry store delete-dev-latest --store coral:dev --dry-run
1390
1391        airbyte-ops registry store delete-dev-latest --store coral:dev/aj-test100
1392
1393        airbyte-ops registry store delete-dev-latest --store coral:dev \\
1394            --connector-name source-faker --connector-name destination-bigquery
1395    """
1396    if not store.startswith("coral:dev"):
1397        exit_with_error(
1398            "delete-dev-latest only supports dev stores "
1399            f"(store must begin with 'coral:dev', got '{store}').",
1400            code=1,
1401        )
1402
1403    registry = _resolve_store(store)
1404
1405    result = registry.delete_dev_latest(
1406        connector_name=list(connector_name) if connector_name else None,
1407        dry_run=dry_run,
1408    )
1409
1410    print_json(
1411        {
1412            "status": result.status,
1413            "target": result.target,
1414            "connectors_found": result.connectors_found,
1415            "latest_dirs_deleted": result.latest_dirs_deleted,
1416            "errors": result.errors,
1417            "dry_run": result.dry_run,
1418        }
1419    )
1420
1421    if result.status in ("success", "dry-run"):
1422        print_success(result.summary())
1423    else:
1424        exit_with_error(
1425            f"Delete completed with {len(result.errors)} error(s): "
1426            + "; ".join(result.errors[:5]),
1427            code=1,
1428        )
1429
1430
1431# =============================================================================
1432# STORE COMPARE COMMAND
1433# =============================================================================
1434
1435
1436@store_app.command(name="compare")
1437def compare_cmd(
1438    store: Annotated[
1439        str,
1440        Parameter(
1441            help="Store target being evaluated (e.g. 'coral:dev/20260306-mirror-compile').",
1442        ),
1443    ],
1444    reference_store: Annotated[
1445        str,
1446        Parameter(
1447            help="Known-good reference store to compare against.",
1448        ),
1449    ],
1450    *,
1451    connector_name: Annotated[
1452        tuple[str, ...] | None,
1453        Parameter(
1454            help="Only compare these connectors (can be repeated).",
1455        ),
1456    ] = None,
1457    with_artifacts: Annotated[
1458        bool,
1459        Parameter(
1460            help="Compare per-connector artifact files "
1461            "(metadata.yaml, cloud.json, oss.json, spec.json).",
1462            negative="--no-artifacts",
1463        ),
1464    ] = True,
1465    with_indexes: Annotated[
1466        bool,
1467        Parameter(
1468            help="Compare global registry index files "
1469            "(cloud_registry.json, oss_registry.json).",
1470            negative="--no-indexes",
1471        ),
1472    ] = True,
1473) -> None:
1474    """Compare a store against a reference store and report differences.
1475
1476    Evaluates the `--store` target against `--reference-store` and reports
1477    per-connector artifact diffs and global index diffs.
1478
1479    Requires GCS_CREDENTIALS environment variable to be set.
1480
1481    Examples:
1482        airbyte-ops registry store compare --store coral:dev/20260306-mirror \\
1483            --reference-store coral:prod
1484
1485        airbyte-ops registry store compare --store coral:dev/my-test \\
1486            --connector-name source-faker --no-indexes
1487
1488        airbyte-ops registry store compare --store coral:dev/my-test \\
1489            --no-artifacts
1490    """
1491    store_target = _resolve_store(store)
1492    ref_target = _resolve_store(reference_store)
1493
1494    store_prefix = f"{store_target.prefix}/" if store_target.prefix else ""
1495    ref_prefix = f"{ref_target.prefix}/" if ref_target.prefix else ""
1496
1497    result = compare_stores(
1498        store_bucket=store_target.bucket_name,
1499        store_prefix=store_prefix,
1500        reference_bucket=ref_target.bucket_name,
1501        reference_prefix=ref_prefix,
1502        connector_name=list(connector_name) if connector_name else None,
1503        with_artifacts=with_artifacts,
1504        with_indexes=with_indexes,
1505    )
1506
1507    print_json(result.to_dict())
1508
1509    if result.status == "match":
1510        print_success(result.summary())
1511    elif result.status == "differences-found":
1512        error_console.print(f"[yellow]{result.summary()}[/yellow]")
1513
1514        # Print a concise per-connector diff summary
1515        for diff in result.connector_diffs:
1516            if diff.status in ("only_in_store", "only_in_reference"):
1517                error_console.print(f"  {diff.connector}: {diff.status}")
1518            else:
1519                for ad in diff.artifact_diffs:
1520                    error_console.print(
1521                        f"  {diff.connector}/{ad.file}: {ad.status}"
1522                        + (f" ({ad.details})" if ad.details else "")
1523                    )
1524
1525        for idx_diff in result.index_diffs:
1526            if idx_diff.status != "match":
1527                error_console.print(
1528                    f"  [index] {idx_diff.file}: {idx_diff.status}"
1529                    + (
1530                        f" (store={idx_diff.entry_count_store},"
1531                        f" ref={idx_diff.entry_count_reference})"
1532                        if idx_diff.entry_count_store or idx_diff.entry_count_reference
1533                        else ""
1534                    )
1535                )
1536
1537        sys.exit(1)
1538    else:
1539        exit_with_error(
1540            f"Compare completed with {len(result.errors)} error(s): "
1541            + "; ".join(result.errors[:5]),
1542            code=1,
1543        )