airbyte_ops_mcp.cli.local
CLI commands for local Airbyte monorepo operations.
Commands:
airbyte-ops local connector list - List connectors in the monorepo airbyte-ops local connector info - Get metadata for a single connector airbyte-ops local connector get-version - Get connector version (current or next) airbyte-ops local connector bump-version - Bump connector version airbyte-ops local connector qa - Run QA checks on a connector airbyte-ops local connector qa-docs-generate - Generate QA checks documentation airbyte-ops local connector changelog check - Check changelog entries for issues airbyte-ops local connector changelog fix - Fix changelog entry dates airbyte-ops local connector bump-deps - Update Poetry-managed dependencies airbyte-ops local connector marketing-stub check - Validate marketing stub entries airbyte-ops local connector marketing-stub sync - Sync stub from connector metadata airbyte-ops local connector release-block add - Add a connector release block marker airbyte-ops local connector release-block clear - Clear a connector release block marker airbyte-ops local connector release-block list - List connector release block markers
CLI reference
The commands below are regenerated by poe docs-generate via cyclopts's
programmatic docs API; see docs/generate_cli.py.
airbyte-ops local COMMAND
Local Airbyte monorepo operations.
Commands:
connector: Connector operations in the monorepo.
airbyte-ops local connector
Connector operations in the monorepo.
airbyte-ops local connector list
airbyte-ops local connector list REPO-PATH [ARGS]
List connectors in the Airbyte monorepo with filtering options.
Parameters:
REPO-PATH, --repo-path: Absolute path to the Airbyte monorepo. [required]CERTIFIED-ONLY, --certified-only, --no-certified-only: Include only certified connectors. [default: False]MODIFIED-ONLY, --modified-only, --no-modified-only: Include only modified connectors (requires PR context). [default: False]LOCAL-CDK, --local-cdk, --no-local-cdk: Include connectors using local CDK reference. When combined with --modified-only, adds local-CDK connectors to the modified set. [default: False]LANGUAGE, --language, --empty-language: Languages to include (python, java, low-code, manifest-only).EXCLUDE-LANGUAGE, --exclude-language, --empty-exclude-language: Languages to exclude.CONNECTOR-TYPE, --connector-type: Connector types to include (source, destination). Accepts CSV or newline-delimited values.MIN-SUPPORT-LEVEL, --min-support-level: Minimum support level (inclusive). Accepts integer (100, 200, 300) or keyword (archived, community, certified).MAX-SUPPORT-LEVEL, --max-support-level: Maximum support level (inclusive). Accepts integer (100, 200, 300) or keyword (archived, community, certified).PR, --pr: PR number or GitHub URL for modification detection.GH-TOKEN, --gh-token: GitHub API token. When provided together with --pr, the GitHub API is used to detect modified files instead of local git diff (avoids shallow-clone issues).EXCLUDE-CONNECTORS, --exclude-connectors, --empty-exclude-connectors: Connectors to exclude from results. Accepts CSV or newline-delimited values. Can be specified multiple times.FORCE-INCLUDE-CONNECTORS, --force-include-connectors, --empty-force-include-connectors: Connectors to force-include regardless of other filters. Accepts CSV or newline-delimited values. Can be specified multiple times.CONNECTORS-FILTER, --connectors-filter, --empty-connectors-filter: Intersect results with this explicit set of connector names. Only connectors present in both the filtered results and this set are returned. Useful for composing multiple filter passes (e.g. combining separate source and destination lists). Accepts CSV or newline-delimited values. Can be specified multiple times.OUTPUT-FORMAT, --output-format: Output format: "csv" (comma-separated), "lines" (one connector per line), "json-gh-matrix" (GitHub Actions matrix JSON). [choices: csv, lines, json-gh-matrix] [default: lines]UNPUBLISHED, --unpublished, --no-unpublished: Filter to only connectors whose local dockerImageTag has not been published to the GCS registry. Requires --store. [default: False]STORE, --store: Store target for unpublished check (e.g. 'coral:prod', 'coral:dev'). Required when --unpublished is set.ASSERT-NONE, --assert-none, --no-assert-none: Exit with non-zero status if any connectors match the filters. Useful for audit/CI checks (e.g. --unpublished --assert-none). [default: False]SORT-BY, --sort-by: Sort connectors by the given key. Only 'name' is supported for now. [choices: name] [default: name]SORT-DIRECTION, --sort-direction: Sort direction: 'asc' (ascending, default) or 'desc' (descending). [choices: asc, desc] [default: asc]LIMIT, --limit: Maximum number of connectors to return. Applied after sorting. Useful for batched processing.OFFSET, --offset: Number of connectors to skip from the start of the (sorted) list. Applied after sorting, before --limit. Useful for batched processing (e.g. --offset=200 --limit=200 for batch 2). [default: 0]
airbyte-ops local connector info
airbyte-ops local connector info CONNECTOR-NAME [ARGS]
Get metadata for a single connector.
Prints JSON output with connector metadata. When running in GitHub Actions (CI env var set), also writes each field to GitHub step outputs.
Parameters:
CONNECTOR-NAME, --connector-name: Name of the connector (e.g., source-github). [required]REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.DPATH, --dpath: Evaluate this dpath expression against the parsed metadata.yaml object and print only that value (e.g., data/dockerImageTag).
airbyte-ops local connector get-version
airbyte-ops local connector get-version NAME REPO-PATH [ARGS]
Get the current or next version for a connector.
By default, prints the current version from metadata.yaml.
This is analogous to local connector info --dpath data/dockerImageTag and
uses the same dpath evaluation internally.
With --next, computes and prints the next version. Requires either --bump-type or --prerelease to be specified.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]REPO-PATH, --repo-path: Absolute path to the Airbyte monorepo. [required]NEXT, --next, --no-next: Compute the next version instead of the current version. [default: False]BUMP-TYPE, --bump-type: Version bump type (requires --next). Standard: patch, minor, major. RC: patch_rc, minor_rc, major_rc, rc, promote. [choices: patch, minor, major, patch_rc, minor_rc, major_rc, rc, promote]PRERELEASE, --prerelease, --no-prerelease: Compute a prerelease (preview) tag using the repo HEAD SHA (requires --next). [default: False]
airbyte-ops local connector bump-version
airbyte-ops local connector bump-version NAME REPO-PATH [ARGS]
Bump a connector's version across all relevant files.
Updates version in metadata.yaml (always), pyproject.toml (if exists), and documentation changelog (if --changelog-message provided).
Note: --changelog-message is ignored when --no-changelog is set.
Either --bump-type or --new-version must be provided.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]REPO-PATH, --repo-path: Absolute path to the Airbyte monorepo. [required]BUMP-TYPE, --bump-type: Version bump type. Standard: patch, minor, major. RC: patch_rc, minor_rc, major_rc, rc, promote. [choices: patch, minor, major, patch_rc, minor_rc, major_rc, rc, promote]NEW-VERSION, --new-version: Explicit new version (overrides --bump-type if provided).CHANGELOG-MESSAGE, --changelog-message: Message to add to changelog. Ignored if --no-changelog is set.PR-NUMBER, --pr-number: PR number for changelog entry.DRY-RUN, --dry-run, --no-dry-run: Show what would be changed without modifying files. [default: False]NO-CHANGELOG, --no-changelog, --no-no-changelog: Skip changelog updates even if --changelog-message is provided. Useful for ephemeral version bumps (e.g. pre-release artifact generation). [default: False]PROGRESSIVE-ROLLOUT-ENABLED, --progressive-rollout-enabled, --no-progressive-rollout-enabled: Explicitly setenableProgressiveRolloutin metadata.yaml. Passfalseto disable progressive rollout (e.g. for preview builds). When omitted, the automatic behaviour based on --bump-type is used.
airbyte-ops local connector bump-base-image
airbyte-ops local connector bump-base-image NAME REPO-PATH [ARGS]
Update a connector's base image.
Two modes:
- Default: bump to the latest stable tag within the same major version. Major version changes are treated as breaking-change boundaries.
- --force-latest: bump to the absolute latest stable tag regardless of semver.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]REPO-PATH, --repo-path: Absolute path to the Airbyte monorepo. [required]FORCE-LATEST, --force-latest, --no-force-latest: Bump to the absolute latest stable base image, ignoring major-version boundaries. Without this flag the bump stays within the current major version. [default: False]DRY-RUN, --dry-run, --no-dry-run: Show what would be changed without modifying files. [default: False]
airbyte-ops local connector bump-cdk
airbyte-ops local connector bump-cdk NAME REPO-PATH [ARGS]
Bump a connector's CDK dependency.
Two modes:
- Default: refresh the lock file so it resolves the newest CDK that satisfies the existing constraint. The constraint is NOT changed.
- --force-latest: rewrite the constraint to >=LATEST,
For Java connectors, updates build.gradle to the latest CDK version.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]REPO-PATH, --repo-path: Absolute path to the Airbyte monorepo. [required]FORCE-LATEST, --force-latest, --no-force-latest: Rewrite the CDK constraint to >=LATEST,[default: False] DRY-RUN, --dry-run, --no-dry-run: Show what would be changed without modifying files. [default: False]
airbyte-ops local connector bump-deps
airbyte-ops local connector bump-deps NAME REPO-PATH [ARGS]
Update a connector's dependencies.
For Python / low-code connectors using Poetry, this runs
poetry update --lock to refresh the lock file with the latest
versions allowed by existing constraints.
For connectors that do not use Poetry (manifest-only, Java, etc.), this is a no-op.
Parameters:
NAME, --name: Connector technical name (e.g., source-github). [required]REPO-PATH, --repo-path: Absolute path to the Airbyte monorepo. [required]DRY-RUN, --dry-run, --no-dry-run: Show what would be changed without modifying files. [default: False]
airbyte-ops local connector qa
airbyte-ops local connector qa [ARGS]
Run QA checks on connector(s).
Validates connector metadata, documentation, packaging, security, and versioning. Exit code is non-zero if any checks fail.
Parameters:
NAME, --name, --empty-name: Connector technical name(s) (e.g., source-github). Can be specified multiple times.CONNECTOR-DIRECTORY, --connector-directory: Directory containing connectors to run checks on all connectors in this directory.CHECK, --check, --empty-check: Specific check(s) to run. Can be specified multiple times.REPORT-PATH, --report-path: Path to write the JSON report file.
airbyte-ops local connector qa-docs-generate
airbyte-ops local connector qa-docs-generate OUTPUT-FILE
Generate documentation for QA checks.
Creates a markdown file documenting all available QA checks organized by category.
Parameters:
OUTPUT-FILE, --output-file: Path to write the generated documentation file. [required]
airbyte-ops local connector changelog
Changelog operations for connectors.
Commands:
add: Add a changelog entry for a connector using its current version.check: Check changelog entries for issues.fix: Fix changelog entry dates to match PR merge dates.
airbyte-ops local connector changelog check
airbyte-ops local connector changelog check [ARGS]
Check changelog entries for issues.
Validates changelog dates match PR merge dates and checks for PR number mismatches.
Parameters:
CONNECTOR-NAME, --connector-name: Connector technical name (e.g., source-github).ALL, --all, --no-all: Check all connectors in the repository. [default: False]REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.LOOKBACK-DAYS, --lookback-days: Only check entries with dates within this many days.STRICT, --strict, --no-strict: Exit with error code if any issues are found. [default: False]
airbyte-ops local connector changelog fix
airbyte-ops local connector changelog fix [ARGS]
Fix changelog entry dates to match PR merge dates.
Looks up the actual merge date for each PR referenced in the changelog and updates the date column to match.
Parameters:
CONNECTOR-NAME, --connector-name: Connector technical name (e.g., source-github).ALL, --all, --no-all: Fix all connectors in the repository. [default: False]REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.LOOKBACK-DAYS, --lookback-days: Only fix entries with dates within this many days.DRY-RUN, --dry-run, --no-dry-run: Print changes without modifying files. [default: False]
airbyte-ops local connector changelog add
airbyte-ops local connector changelog add CONNECTOR-NAME PR-NUMBER MESSAGE [ARGS]
Add a changelog entry for a connector using its current version.
Reads the version from metadata.yaml and writes a single changelog entry to the connector's documentation file. Does not modify any version files.
Parameters:
CONNECTOR-NAME, --connector-name: Connector technical name (e.g., source-github). [required]PR-NUMBER, --pr-number: PR number for the changelog entry. [required]MESSAGE, --message: Changelog entry message. [required]REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.DRY-RUN, --dry-run, --no-dry-run: Print changes without modifying files. [default: False]
airbyte-ops local connector marketing-stub
Marketing connector stub operations (local file validation and updates).
Commands:
check: Validate marketing connector stub entries.sync: Sync connector stub(s) from connector metadata.yaml file(s).
airbyte-ops local connector marketing-stub check
airbyte-ops local connector marketing-stub check [ARGS]
Validate marketing connector stub entries.
Checks that stub entries have valid required fields (id, name, url, icon) and optionally validates that the stub matches the connector's metadata.yaml.
Exit codes:
0: All checks passed 1: Validation errors found
Output:
STDOUT: JSON validation result STDERR: Informational messages
Parameters:
CONNECTOR, --connector: Connector name to check (e.g., 'source-oracle-enterprise').ALL, --all, --no-all: Check all stubs in the file. [default: False]REPO-ROOT, --repo-root: Path to the airbyte-enterprise repository root. Defaults to current directory.
airbyte-ops local connector marketing-stub sync
airbyte-ops local connector marketing-stub sync [ARGS]
Sync connector stub(s) from connector metadata.yaml file(s).
Reads the connector's metadata.yaml file and updates the corresponding entry in connector_stubs.json with the current values.
Exit codes:
0: Sync successful (or dry-run completed) 1: Error (connector not found, no metadata, etc.)
Output:
STDOUT: JSON representation of the synced stub(s) STDERR: Informational messages
Parameters:
CONNECTOR, --connector: Connector name to sync (e.g., 'source-oracle-enterprise').ALL, --all, --no-all: Sync all connectors that have metadata.yaml files. [default: False]REPO-ROOT, --repo-root: Path to the airbyte-enterprise repository root. Defaults to current directory.DRY-RUN, --dry-run, --no-dry-run: Show what would be synced without making changes. [default: False]
airbyte-ops local connector release-block
Manage release block markers for connectors.
Commands:
add: Add ablock-release.yamlmarker to prevent publishing a connector.clear: Remove theblock-release.yamlmarker to allow publishing a connector.list: List all connectors that have ablock-release.yamlmarker.
airbyte-ops local connector release-block add
airbyte-ops local connector release-block add NAME REASON [ARGS]
Add a block-release.yaml marker to prevent publishing a connector.
Creates a marker file in the connector's directory that causes the publish pipeline to skip the connector with a warning.
Parameters:
NAME, --name: Connector technical name (e.g., source-faker). [required]REASON, --reason: Human-readable reason for blocking the release. [required]REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.YANKED-VERSION, --yanked-version: Version that was yanked (for reference).BLOCKED-BY, --blocked-by: Email or identifier of the person requesting the block.
airbyte-ops local connector release-block clear
airbyte-ops local connector release-block clear NAME [ARGS]
Remove the block-release.yaml marker to allow publishing a connector.
Parameters:
NAME, --name: Connector technical name (e.g., source-faker). [required]REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.
airbyte-ops local connector release-block list
airbyte-ops local connector release-block list [ARGS]
List all connectors that have a block-release.yaml marker.
Parameters:
REPO-PATH, --repo-path: Path to the Airbyte monorepo. Can be inferred from context.OUTPUT-FORMAT, --output-format: Output format: "text" (human-readable), "json" (blocked connectors with block file contents), "csv" (comma-delimited connector names). [choices: text, json, csv] [default: text]
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""CLI commands for local Airbyte monorepo operations. 3 4Commands: 5 airbyte-ops local connector list - List connectors in the monorepo 6 airbyte-ops local connector info - Get metadata for a single connector 7 airbyte-ops local connector get-version - Get connector version (current or next) 8 airbyte-ops local connector bump-version - Bump connector version 9 airbyte-ops local connector qa - Run QA checks on a connector 10 airbyte-ops local connector qa-docs-generate - Generate QA checks documentation 11 airbyte-ops local connector changelog check - Check changelog entries for issues 12 airbyte-ops local connector changelog fix - Fix changelog entry dates 13 airbyte-ops local connector bump-deps - Update Poetry-managed dependencies 14 airbyte-ops local connector marketing-stub check - Validate marketing stub entries 15 airbyte-ops local connector marketing-stub sync - Sync stub from connector metadata 16 airbyte-ops local connector release-block add - Add a connector release block marker 17 airbyte-ops local connector release-block clear - Clear a connector release block marker 18 airbyte-ops local connector release-block list - List connector release block markers 19 20## CLI reference 21 22The commands below are regenerated by `poe docs-generate` via cyclopts's 23programmatic docs API; see `docs/generate_cli.py`. 24 25.. include:: ../../../docs/generated/cli/local.md 26 :start-line: 2 27""" 28 29from __future__ import annotations 30 31# Hide Python-level members from the pdoc page for this module; the rendered 32# docs for this CLI group come entirely from the grafted `.. include::` in 33# the module docstring above. 34__all__: list[str] = [] 35 36import json 37import os 38import subprocess 39import sys 40from pathlib import Path 41from typing import Annotated, Literal 42 43import yaml 44from cyclopts import Parameter 45from jinja2 import Environment, PackageLoader, select_autoescape 46from rich.console import Console 47 48from airbyte_ops_mcp.airbyte_repo.bump_base_image import ( 49 BaseImageError, 50 bump_base_image, 51) 52from airbyte_ops_mcp.airbyte_repo.bump_cdk import ( 53 CdkBumpError, 54 bump_cdk, 55) 56from airbyte_ops_mcp.airbyte_repo.bump_deps import ( 57 DepsError, 58 bump_deps, 59) 60from airbyte_ops_mcp.airbyte_repo.bump_version import ( 61 ConnectorNotFoundError, 62 InvalidVersionError, 63 VersionNotFoundError, 64 bump_connector_version, 65 calculate_new_version, 66 get_connector_doc_path, 67 get_connector_path, 68 get_current_version, 69 update_changelog, 70) 71from airbyte_ops_mcp.airbyte_repo.changelog_fix import ( 72 ChangelogCheckResult, 73 ChangelogFixResult, 74 check_all_changelogs, 75 check_changelog, 76 fix_all_changelog_dates, 77 fix_changelog_dates, 78) 79from airbyte_ops_mcp.airbyte_repo.list_connectors import ( 80 CONNECTOR_PATH_PREFIX, 81 METADATA_FILE_NAME, 82 _detect_connector_language, 83 get_connectors_with_local_cdk, 84) 85from airbyte_ops_mcp.airbyte_repo.release_block import ( 86 add_release_block, 87 clear_release_block, 88 list_release_blocks, 89) 90from airbyte_ops_mcp.cli._base import App, app 91from airbyte_ops_mcp.cli._shared import error_console, exit_with_error, print_json 92from airbyte_ops_mcp.connector_metadata import ( 93 ConnectorMetadataDpathError, 94 ConnectorMetadataDpathNotFoundError, 95 format_metadata_dpath_value, 96 get_connector_version_from_metadata, 97 load_raw_connector_metadata_from_local, 98) 99from airbyte_ops_mcp.connector_ops.utils import Connector 100from airbyte_ops_mcp.connector_qa.checks import ENABLED_CHECKS 101from airbyte_ops_mcp.connector_qa.consts import CONNECTORS_QA_DOC_TEMPLATE_NAME 102from airbyte_ops_mcp.connector_qa.models import ( 103 Check, 104 CheckCategory, 105 CheckStatus, 106 Report, 107) 108from airbyte_ops_mcp.connector_qa.utils import ( 109 get_all_connectors_in_directory, 110 remove_strict_encrypt_suffix, 111) 112from airbyte_ops_mcp.mcp.github_repo_ops import list_connectors_in_repo 113from airbyte_ops_mcp.mcp.prerelease import compute_prerelease_docker_image_tag 114from airbyte_ops_mcp.registry._enums import ConnectorType, SupportLevel 115from airbyte_ops_mcp.registry.audit import ( 116 AuditResult, 117 find_unpublished_connectors, 118 generate_connector_list_summary, 119) 120from airbyte_ops_mcp.registry.connector_stubs import ( 121 CONNECTOR_STUBS_FILE, 122 ConnectorStub, 123 find_stub_by_connector, 124 load_local_stubs, 125 save_local_stubs, 126) 127from airbyte_ops_mcp.registry.store import resolve_registry_store 128from airbyte_ops_mcp.regression_tests.ci_output import write_github_summary 129 130console = Console() 131 132OutputFormat = Literal["csv", "lines", "json-gh-matrix"] 133SortBy = Literal["name"] 134 135 136def _parse_support_level(value: str) -> SupportLevel: 137 """Parse a support level string into a `SupportLevel` enum member. 138 139 Accepts a keyword ("certified", "community", "archived") or a legacy 140 integer string ("100", "200", "300"). 141 """ 142 return SupportLevel.parse(value.strip().lower()) 143 144 145def _get_connector_support_level(connector_dir: Path) -> SupportLevel | None: 146 """Read support level from connector's metadata.yaml.""" 147 metadata_file = connector_dir / METADATA_FILE_NAME 148 if not metadata_file.exists(): 149 return None 150 metadata = yaml.safe_load(metadata_file.read_text()) 151 support_level_str = metadata.get("data", {}).get("supportLevel") 152 if not support_level_str: 153 return None 154 try: 155 return SupportLevel(support_level_str.lower()) 156 except ValueError: 157 return None 158 159 160def _parse_connector_types(value: str) -> set[ConnectorType]: 161 """Parse connector types from CSV or newline-delimited string.""" 162 types: set[ConnectorType] = set() 163 for item in value.replace(",", "\n").split("\n"): 164 item = item.strip().lower() 165 if item: 166 types.add(ConnectorType.parse(item)) 167 return types 168 169 170def _get_connector_type(connector_name: str) -> str: 171 """Derive connector type from name prefix.""" 172 if connector_name.startswith("source-"): 173 return ConnectorType.SOURCE 174 elif connector_name.startswith("destination-"): 175 return ConnectorType.DESTINATION 176 return "unknown" 177 178 179def _parse_connector_names(value: str) -> set[str]: 180 """Parse connector names from CSV or newline-delimited string.""" 181 names = set() 182 for item in value.replace(",", "\n").split("\n"): 183 item = item.strip() 184 if item: 185 names.add(item) 186 return names 187 188 189def _get_connector_version(connector_dir: Path) -> str | None: 190 """Read connector version (dockerImageTag) from metadata.yaml.""" 191 metadata_file = connector_dir / METADATA_FILE_NAME 192 if not metadata_file.exists(): 193 return None 194 metadata = yaml.safe_load(metadata_file.read_text()) 195 return metadata.get("data", {}).get("dockerImageTag") 196 197 198def _support_level_to_int(level: SupportLevel | None) -> int | None: 199 """Convert a `SupportLevel` to its legacy integer representation for JSON output.""" 200 if level is None: 201 return None 202 return level.precedence 203 204 205def _get_connector_info( 206 connector_name: str, connector_dir: Path 207) -> dict[str, str | int | None]: 208 """Get full connector metadata as a dict with connector_ prefixed keys. 209 210 This is shared between the `list --output-format json-gh-matrix` and `info` commands. 211 """ 212 return { 213 "connector": connector_name, 214 "connector_type": _get_connector_type(connector_name), 215 "connector_language": _detect_connector_language(connector_dir, connector_name) 216 or "unknown", 217 "connector_support_level": _support_level_to_int( 218 _get_connector_support_level(connector_dir) 219 ), 220 "connector_version": _get_connector_version(connector_dir), 221 "connector_dir": f"{CONNECTOR_PATH_PREFIX}/{connector_name}", 222 } 223 224 225# Create the local sub-app 226local_app = App(name="local", help="Local Airbyte monorepo operations.") 227app.command(local_app) 228 229# Create the connector sub-app under local 230connector_app = App(name="connector", help="Connector operations in the monorepo.") 231local_app.command(connector_app) 232 233 234@connector_app.command(name="list") 235def list_connectors( 236 repo_path: Annotated[ 237 str, 238 Parameter(help="Absolute path to the Airbyte monorepo."), 239 ], 240 certified_only: Annotated[ 241 bool, 242 Parameter(help="Include only certified connectors."), 243 ] = False, 244 modified_only: Annotated[ 245 bool, 246 Parameter(help="Include only modified connectors (requires PR context)."), 247 ] = False, 248 local_cdk: Annotated[ 249 bool, 250 Parameter( 251 help=( 252 "Include connectors using local CDK reference. " 253 "When combined with --modified-only, adds local-CDK connectors to the modified set." 254 ) 255 ), 256 ] = False, 257 language: Annotated[ 258 list[str] | None, 259 Parameter(help="Languages to include (python, java, low-code, manifest-only)."), 260 ] = None, 261 exclude_language: Annotated[ 262 list[str] | None, 263 Parameter(help="Languages to exclude."), 264 ] = None, 265 connector_type: Annotated[ 266 str | None, 267 Parameter( 268 help=( 269 "Connector types to include (source, destination). " 270 "Accepts CSV or newline-delimited values." 271 ) 272 ), 273 ] = None, 274 min_support_level: Annotated[ 275 str | None, 276 Parameter( 277 help=( 278 "Minimum support level (inclusive). " 279 "Accepts integer (100, 200, 300) or keyword (archived, community, certified)." 280 ) 281 ), 282 ] = None, 283 max_support_level: Annotated[ 284 str | None, 285 Parameter( 286 help=( 287 "Maximum support level (inclusive). " 288 "Accepts integer (100, 200, 300) or keyword (archived, community, certified)." 289 ) 290 ), 291 ] = None, 292 pr: Annotated[ 293 str | None, 294 Parameter(help="PR number or GitHub URL for modification detection."), 295 ] = None, 296 gh_token: Annotated[ 297 str | None, 298 Parameter( 299 help=( 300 "GitHub API token. When provided together with --pr, the GitHub API " 301 "is used to detect modified files instead of local git diff " 302 "(avoids shallow-clone issues)." 303 ) 304 ), 305 ] = None, 306 exclude_connectors: Annotated[ 307 list[str] | None, 308 Parameter( 309 help=( 310 "Connectors to exclude from results. " 311 "Accepts CSV or newline-delimited values. Can be specified multiple times." 312 ) 313 ), 314 ] = None, 315 force_include_connectors: Annotated[ 316 list[str] | None, 317 Parameter( 318 help=( 319 "Connectors to force-include regardless of other filters. " 320 "Accepts CSV or newline-delimited values. Can be specified multiple times." 321 ) 322 ), 323 ] = None, 324 connectors_filter: Annotated[ 325 list[str] | None, 326 Parameter( 327 help=( 328 "Intersect results with this explicit set of connector names. " 329 "Only connectors present in both the filtered results and this set are returned. " 330 "Useful for composing multiple filter passes (e.g. combining separate source " 331 "and destination lists). " 332 "Accepts CSV or newline-delimited values. Can be specified multiple times." 333 ) 334 ), 335 ] = None, 336 output_format: Annotated[ 337 OutputFormat, 338 Parameter( 339 help=( 340 'Output format: "csv" (comma-separated), ' 341 '"lines" (one connector per line), ' 342 '"json-gh-matrix" (GitHub Actions matrix JSON).' 343 ) 344 ), 345 ] = "lines", 346 unpublished: Annotated[ 347 bool, 348 Parameter( 349 help=( 350 "Filter to only connectors whose local dockerImageTag " 351 "has not been published to the GCS registry. Requires --store." 352 ) 353 ), 354 ] = False, 355 store: Annotated[ 356 str | None, 357 Parameter( 358 help=( 359 "Store target for unpublished check (e.g. 'coral:prod', 'coral:dev'). " 360 "Required when --unpublished is set." 361 ) 362 ), 363 ] = None, 364 assert_none: Annotated[ 365 bool, 366 Parameter( 367 help=( 368 "Exit with non-zero status if any connectors match the filters. " 369 "Useful for audit/CI checks (e.g. --unpublished --assert-none)." 370 ) 371 ), 372 ] = False, 373 sort_by: Annotated[ 374 SortBy, 375 Parameter( 376 help="Sort connectors by the given key. Only 'name' is supported for now." 377 ), 378 ] = "name", 379 sort_direction: Annotated[ 380 Literal["asc", "desc"], 381 Parameter( 382 help="Sort direction: 'asc' (ascending, default) or 'desc' (descending)." 383 ), 384 ] = "asc", 385 limit: Annotated[ 386 int | None, 387 Parameter( 388 help=( 389 "Maximum number of connectors to return. " 390 "Applied after sorting. Useful for batched processing." 391 ) 392 ), 393 ] = None, 394 offset: Annotated[ 395 int, 396 Parameter( 397 help=( 398 "Number of connectors to skip from the start of the (sorted) list. " 399 "Applied after sorting, before --limit. " 400 "Useful for batched processing (e.g. --offset=200 --limit=200 for batch 2)." 401 ) 402 ), 403 ] = 0, 404) -> None: 405 """List connectors in the Airbyte monorepo with filtering options.""" 406 # Validate mutually exclusive flags 407 if language and exclude_language: 408 exit_with_error("Cannot specify both --language and --exclude-language.") 409 if assert_none and (offset or limit is not None): 410 exit_with_error( 411 "Cannot combine --assert-none with --offset or --limit. " 412 "--assert-none checks all connectors matching the filters, " 413 "which is incompatible with pagination." 414 ) 415 416 # Map CLI flags to MCP tool parameters 417 certified: bool | None = True if certified_only else None 418 modified: bool | None = True if modified_only else None 419 420 language_filter: set[str] | None = set(language) if language else None 421 language_exclude: set[str] | None = ( 422 set(exclude_language) if exclude_language else None 423 ) 424 425 # Parse connector type filter 426 connector_type_filter: set[str] | None = None 427 if connector_type: 428 try: 429 connector_type_filter = _parse_connector_types(connector_type) 430 except ValueError as e: 431 exit_with_error(str(e)) 432 433 # Parse support level filters 434 min_level: SupportLevel | None = None 435 max_level: SupportLevel | None = None 436 if min_support_level: 437 try: 438 min_level = _parse_support_level(min_support_level) 439 except ValueError as e: 440 exit_with_error(str(e)) 441 if max_support_level: 442 try: 443 max_level = _parse_support_level(max_support_level) 444 except ValueError as e: 445 exit_with_error(str(e)) 446 447 # Parse exclude/force-include connector lists (merge multiple flag values) 448 exclude_set: set[str] = set() 449 if exclude_connectors: 450 for value in exclude_connectors: 451 exclude_set.update(_parse_connector_names(value)) 452 453 force_include_set: set[str] = set() 454 if force_include_connectors: 455 for value in force_include_connectors: 456 force_include_set.update(_parse_connector_names(value)) 457 458 connectors_filter_set: set[str] | None = None 459 if connectors_filter: 460 connectors_filter_set = set() 461 for value in connectors_filter: 462 connectors_filter_set.update(_parse_connector_names(value)) 463 464 result = list_connectors_in_repo( 465 repo_path=repo_path, 466 certified=certified, 467 modified=modified, 468 language_filter=language_filter, 469 language_exclude=language_exclude, 470 pr_num_or_url=pr, 471 gh_token=gh_token, 472 ) 473 connectors = list(result.connectors) 474 repo_path_obj = Path(repo_path) 475 476 # Add connectors with local CDK reference if --local-cdk flag is set 477 if local_cdk: 478 local_cdk_connectors = get_connectors_with_local_cdk(repo_path) 479 connectors = sorted(set(connectors) | local_cdk_connectors) 480 481 # Apply connector type filter 482 if connector_type_filter: 483 connectors = [ 484 name 485 for name in connectors 486 if _get_connector_type(name) in connector_type_filter 487 ] 488 489 # Apply support level filters (requires reading metadata) 490 if min_level is not None or max_level is not None: 491 filtered_connectors = [] 492 for name in connectors: 493 connector_dir = repo_path_obj / CONNECTOR_PATH_PREFIX / name 494 level = _get_connector_support_level(connector_dir) 495 if level is None: 496 continue # Skip connectors without support level 497 if min_level is not None and level.precedence < min_level.precedence: 498 continue 499 if max_level is not None and level.precedence > max_level.precedence: 500 continue 501 filtered_connectors.append(name) 502 connectors = filtered_connectors 503 504 # Apply exclude filter 505 if exclude_set: 506 connectors = [name for name in connectors if name not in exclude_set] 507 508 # Apply connectors filter (intersection, narrows the set) 509 if connectors_filter_set is not None: 510 connectors = [name for name in connectors if name in connectors_filter_set] 511 512 # Apply force-include (union, overrides all other filters) 513 if force_include_set: 514 connectors_set = set(connectors) 515 connectors_set.update(force_include_set) 516 connectors = sorted(connectors_set) 517 518 # Apply unpublished filter (requires GCS check) 519 audit_result: AuditResult | None = None 520 if unpublished: 521 if not store: 522 exit_with_error("--store is required when --unpublished is set.") 523 524 target = resolve_registry_store(store=store) 525 audit_result = find_unpublished_connectors( 526 repo_path=repo_path, 527 bucket_name=target.bucket, 528 connector_names=connectors, 529 ) 530 unpublished_names = {entry.connector_name for entry in audit_result.unpublished} 531 connectors = [name for name in connectors if name in unpublished_names] 532 533 # Log audit summary to stderr 534 error_console.print( 535 f"Audit: {audit_result.checked_count} checked, " 536 f"{len(audit_result.unpublished)} unpublished, " 537 f"{len(audit_result.skipped_archived)} archived-skipped, " 538 f"{len(audit_result.skipped_disabled)} disabled-skipped, " 539 f"{len(audit_result.skipped_rc)} rc-skipped" 540 ) 541 if audit_result.errors: 542 for err in audit_result.errors: 543 error_console.print(f" Warning: {err}") 544 545 # --- Sort, offset, limit (applied after all filters) --- 546 if offset < 0: 547 exit_with_error("--offset must be non-negative.") 548 if limit is not None and limit < 0: 549 exit_with_error("--limit must be non-negative.") 550 551 total_before_slice = len(connectors) 552 reverse = sort_direction == "desc" 553 if sort_by == "name": 554 connectors = sorted(connectors, reverse=reverse) 555 556 if offset: 557 connectors = connectors[offset:] 558 if limit is not None: 559 connectors = connectors[:limit] 560 561 if offset or limit is not None: 562 error_console.print( 563 f"Slice: showing {len(connectors)} of {total_before_slice} " 564 f"(offset={offset}, limit={limit})" 565 ) 566 567 if output_format == "csv": 568 sys.stdout.write(",".join(connectors) + "\n") 569 elif output_format == "lines": 570 for name in connectors: 571 sys.stdout.write(name + "\n") 572 elif output_format == "json-gh-matrix": 573 # Build matrix with full connector metadata 574 include_list = [] 575 for name in connectors: 576 connector_dir = repo_path_obj / CONNECTOR_PATH_PREFIX / name 577 include_list.append(_get_connector_info(name, connector_dir)) 578 matrix = {"include": include_list} 579 sys.stdout.write( 580 json.dumps( 581 matrix, 582 indent=2, 583 default=str, 584 ) 585 + "\n" 586 ) 587 588 # Write GitHub Step Summary when GITHUB_STEP_SUMMARY is set 589 _write_connector_list_summary( 590 connectors=connectors, 591 assert_none=assert_none, 592 is_unpublished=unpublished, 593 audit_result=audit_result, 594 ) 595 596 # Exit non-zero if --assert-none and any connectors matched 597 if assert_none and connectors: 598 error_console.print(f"FAIL: {len(connectors)} connector(s) matched.") 599 sys.exit(1) 600 601 602def _write_github_step_outputs(outputs: dict[str, str | int | None]) -> None: 603 """Write outputs to GitHub Actions step output file if running in CI.""" 604 github_output = os.getenv("GITHUB_OUTPUT") 605 if not (os.getenv("CI") and github_output): 606 return 607 608 with open(github_output, "a", encoding="utf-8") as f: 609 for key, value in outputs.items(): 610 if value is None: 611 continue 612 f.write(f"{key}={value}\n") 613 614 615def _write_connector_list_summary( 616 connectors: list[str], 617 *, 618 assert_none: bool, 619 is_unpublished: bool, 620 audit_result: AuditResult | None, 621) -> None: 622 """Write a GitHub Step Summary for the connector list command. 623 624 This is a no-op when `GITHUB_STEP_SUMMARY` is not set. 625 """ 626 github_summary = os.getenv("GITHUB_STEP_SUMMARY") 627 if not github_summary: 628 return 629 630 summary = generate_connector_list_summary( 631 connectors, 632 assert_none=assert_none, 633 unpublished=is_unpublished, 634 audit_result=audit_result, 635 ) 636 write_github_summary(summary) 637 638 639@connector_app.command(name="info") 640def connector_info( 641 connector_name: Annotated[ 642 str, 643 Parameter(help="Name of the connector (e.g., source-github)."), 644 ], 645 repo_path: Annotated[ 646 str | None, 647 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 648 ] = None, 649 dpath_expression: Annotated[ 650 str | None, 651 Parameter( 652 name="--dpath", 653 help=( 654 "Evaluate this dpath expression against the parsed metadata.yaml " 655 "object and print only that value (e.g., data/dockerImageTag)." 656 ), 657 ), 658 ] = None, 659) -> None: 660 """Get metadata for a single connector. 661 662 Prints JSON output with connector metadata. When running in GitHub Actions 663 (CI env var set), also writes each field to GitHub step outputs. 664 """ 665 # Infer repo_path from current directory if not provided 666 if repo_path is None: 667 # Check if we're in an airbyte repo by looking for the connectors directory 668 cwd = Path.cwd() 669 # Walk up to find airbyte-integrations/connectors 670 for parent in [cwd, *cwd.parents]: 671 if (parent / CONNECTOR_PATH_PREFIX).exists(): 672 repo_path = str(parent) 673 break 674 if repo_path is None: 675 exit_with_error( 676 "Could not infer repo path. Please provide --repo-path or run from within the Airbyte monorepo." 677 ) 678 679 repo_path_obj = Path(repo_path) 680 connector_dir = repo_path_obj / CONNECTOR_PATH_PREFIX / connector_name 681 682 if not connector_dir.exists(): 683 exit_with_error(f"Connector directory not found: {connector_dir}") 684 685 try: 686 value = load_raw_connector_metadata_from_local( 687 repo_path_obj, 688 connector_name, 689 dpath_expression=dpath_expression, 690 ) 691 except ( 692 ConnectorMetadataDpathError, 693 ConnectorMetadataDpathNotFoundError, 694 FileNotFoundError, 695 ValueError, 696 ) as e: 697 exit_with_error(str(e)) 698 699 if dpath_expression is None: 700 print_json(value) 701 return 702 703 sys.stdout.write(format_metadata_dpath_value(value) + "\n") 704 705 706BumpType = Literal[ 707 "patch", 708 "minor", 709 "major", 710 "patch_rc", 711 "minor_rc", 712 "major_rc", 713 "rc", 714 "promote", 715] 716 717 718def _get_git_head_sha(repo_path: Path) -> str: 719 """Get the HEAD commit SHA from a git repository. 720 721 Args: 722 repo_path: Path to the git repository. 723 724 Returns: 725 Full commit SHA string. 726 727 Raises: 728 SystemExit: If the git command fails (e.g., not a git repo). 729 """ 730 try: 731 result = subprocess.run( 732 ["git", "rev-parse", "HEAD"], 733 cwd=repo_path, 734 capture_output=True, 735 text=True, 736 check=True, 737 ) 738 except subprocess.CalledProcessError: 739 exit_with_error( 740 f"Failed to get HEAD SHA from {repo_path}. Is it a git repository?" 741 ) 742 except OSError: 743 exit_with_error("Could not run 'git'. Is git installed and on PATH?") 744 return result.stdout.strip() 745 746 747@connector_app.command(name="get-version") 748def get_version( 749 name: Annotated[ 750 str, 751 Parameter(help="Connector technical name (e.g., source-github)."), 752 ], 753 repo_path: Annotated[ 754 str, 755 Parameter(help="Absolute path to the Airbyte monorepo."), 756 ], 757 compute_next: Annotated[ 758 bool, 759 Parameter( 760 name="--next", 761 help="Compute the next version instead of the current version.", 762 ), 763 ] = False, 764 bump_type: Annotated[ 765 BumpType | None, 766 Parameter( 767 help="Version bump type (requires --next). " 768 "Standard: patch, minor, major. RC: patch_rc, minor_rc, major_rc, rc, promote." 769 ), 770 ] = None, 771 prerelease: Annotated[ 772 bool, 773 Parameter( 774 help="Compute a prerelease (preview) tag using the repo HEAD SHA (requires --next)." 775 ), 776 ] = False, 777) -> None: 778 """Get the current or next version for a connector. 779 780 By default, prints the current version from metadata.yaml. 781 This is analogous to `local connector info --dpath data/dockerImageTag` and 782 uses the same dpath evaluation internally. 783 784 With --next, computes and prints the next version. Requires either 785 --bump-type or --prerelease to be specified. 786 787 Examples: 788 # Current version 789 airbyte-ops local connector get-version --name source-github --repo-path /path/to/airbyte 790 791 # Next patch version 792 VERSION=$(airbyte-ops local connector get-version --name source-github --repo-path /path/to/airbyte --next --bump-type patch) 793 794 # Next prerelease (preview) tag based on HEAD SHA 795 TAG=$(airbyte-ops local connector get-version --name source-github --repo-path /path/to/airbyte --next --prerelease) 796 """ 797 repo = Path(repo_path) 798 799 # Validate flag combinations 800 if bump_type and not compute_next: 801 exit_with_error("--bump-type requires --next.") 802 if prerelease and not compute_next: 803 exit_with_error("--prerelease requires --next.") 804 if bump_type and prerelease: 805 exit_with_error("--bump-type and --prerelease are mutually exclusive.") 806 if compute_next and not bump_type and not prerelease: 807 exit_with_error( 808 "--next requires either --bump-type or --prerelease. " 809 "Automatic detection of bump type is not yet supported." 810 ) 811 812 try: 813 get_connector_path(repo, name) 814 metadata = load_raw_connector_metadata_from_local(repo, name) 815 current_version = get_connector_version_from_metadata(metadata) 816 except ( 817 ConnectorMetadataDpathError, 818 ConnectorMetadataDpathNotFoundError, 819 ConnectorNotFoundError, 820 FileNotFoundError, 821 ValueError, 822 ) as e: 823 exit_with_error(str(e)) 824 825 if not compute_next: 826 sys.stdout.write(current_version + "\n") 827 return 828 829 if prerelease: 830 sha = _get_git_head_sha(repo) 831 try: 832 tag = compute_prerelease_docker_image_tag(current_version, sha) 833 except InvalidVersionError as e: 834 exit_with_error(str(e)) 835 sys.stdout.write(tag + "\n") 836 return 837 838 # bump_type is set (validated above) 839 try: 840 new_version = calculate_new_version( 841 current_version=current_version, 842 bump_type=bump_type, 843 ) 844 except (InvalidVersionError, ValueError) as e: 845 exit_with_error(str(e)) 846 847 sys.stdout.write(new_version + "\n") 848 849 850@connector_app.command(name="bump-version") 851def bump_version( 852 name: Annotated[ 853 str, 854 Parameter(help="Connector technical name (e.g., source-github)."), 855 ], 856 repo_path: Annotated[ 857 str, 858 Parameter(help="Absolute path to the Airbyte monorepo."), 859 ], 860 bump_type: Annotated[ 861 BumpType | None, 862 Parameter( 863 help="Version bump type. Standard: patch, minor, major. RC: patch_rc, minor_rc, major_rc, rc, promote." 864 ), 865 ] = None, 866 new_version: Annotated[ 867 str | None, 868 Parameter(help="Explicit new version (overrides --bump-type if provided)."), 869 ] = None, 870 changelog_message: Annotated[ 871 str | None, 872 Parameter( 873 help="Message to add to changelog. Ignored if --no-changelog is set." 874 ), 875 ] = None, 876 pr_number: Annotated[ 877 int | None, 878 Parameter(help="PR number for changelog entry."), 879 ] = None, 880 dry_run: Annotated[ 881 bool, 882 Parameter(help="Show what would be changed without modifying files."), 883 ] = False, 884 no_changelog: Annotated[ 885 bool, 886 Parameter( 887 help="Skip changelog updates even if --changelog-message is provided. " 888 "Useful for ephemeral version bumps (e.g. pre-release artifact generation)." 889 ), 890 ] = False, 891 progressive_rollout_enabled: Annotated[ 892 bool | None, 893 Parameter( 894 help="Explicitly set `enableProgressiveRollout` in metadata.yaml. " 895 "Pass `false` to disable progressive rollout (e.g. for preview builds). " 896 "When omitted, the automatic behaviour based on --bump-type is used.", 897 ), 898 ] = None, 899) -> None: 900 """Bump a connector's version across all relevant files. 901 902 Updates version in metadata.yaml (always), pyproject.toml (if exists), 903 and documentation changelog (if --changelog-message provided). 904 905 Note: --changelog-message is ignored when --no-changelog is set. 906 907 Either --bump-type or --new-version must be provided. 908 909 Examples: 910 airbyte-ops local connector bump-version --name source-github --repo-path /path/to/airbyte --bump-type patch 911 airbyte-ops local connector bump-version --name source-github --repo-path /path/to/airbyte --new-version 1.2.3-preview.abc1234 --no-changelog 912 airbyte-ops local connector bump-version --name source-github --repo-path /path/to/airbyte --new-version 1.2.3-preview.abc1234 --no-changelog --progressive-rollout-enabled=false 913 """ 914 try: 915 result = bump_connector_version( 916 repo_path=repo_path, 917 connector_name=name, 918 bump_type=bump_type, 919 new_version=new_version, 920 changelog_message=changelog_message, 921 pr_number=pr_number, 922 dry_run=dry_run, 923 no_changelog=no_changelog, 924 progressive_rollout_enabled=progressive_rollout_enabled, 925 ) 926 except ConnectorNotFoundError as e: 927 exit_with_error(str(e)) 928 except VersionNotFoundError as e: 929 exit_with_error(str(e)) 930 except InvalidVersionError as e: 931 exit_with_error(str(e)) 932 except ValueError as e: 933 exit_with_error(str(e)) 934 935 # Build output matching the issue spec 936 output = { 937 "connector": result.connector, 938 "previous_version": result.previous_version, 939 "new_version": result.new_version, 940 "files_modified": result.files_modified, 941 "dry_run": result.dry_run, 942 } 943 print_json(output) 944 945 # Write to GitHub step outputs if in CI 946 _write_github_step_outputs( 947 { 948 "connector": result.connector, 949 "previous_version": result.previous_version, 950 "new_version": result.new_version, 951 } 952 ) 953 954 955@connector_app.command(name="bump-base-image") 956def bump_base_image_cmd( 957 name: Annotated[ 958 str, 959 Parameter(help="Connector technical name (e.g., source-github)."), 960 ], 961 repo_path: Annotated[ 962 str, 963 Parameter(help="Absolute path to the Airbyte monorepo."), 964 ], 965 force_latest: Annotated[ 966 bool, 967 Parameter( 968 help=( 969 "Bump to the absolute latest stable base image, ignoring " 970 "major-version boundaries. Without this flag the bump stays " 971 "within the current major version." 972 ) 973 ), 974 ] = False, 975 dry_run: Annotated[ 976 bool, 977 Parameter(help="Show what would be changed without modifying files."), 978 ] = False, 979) -> None: 980 """Update a connector's base image. 981 982 Two modes: 983 984 * Default: bump to the latest stable tag within the same major version. 985 Major version changes are treated as breaking-change boundaries. 986 * --force-latest: bump to the absolute latest stable tag regardless of semver. 987 988 Examples: 989 airbyte-ops local connector bump-base-image --name source-github --repo-path /path/to/airbyte 990 airbyte-ops local connector bump-base-image --name source-github --repo-path /path/to/airbyte --force-latest 991 """ 992 try: 993 result = bump_base_image( 994 repo_path=repo_path, 995 connector_name=name, 996 force_latest=force_latest, 997 dry_run=dry_run, 998 ) 999 except (ConnectorNotFoundError, BaseImageError) as e: 1000 exit_with_error(str(e)) 1001 except ValueError as e: 1002 exit_with_error(str(e)) 1003 1004 output = { 1005 "connector": result.connector, 1006 "previous_base_image": result.previous_base_image, 1007 "new_base_image": result.new_base_image, 1008 "updated": result.updated, 1009 "dry_run": result.dry_run, 1010 "files_modified": result.files_modified, 1011 "message": result.message, 1012 } 1013 print_json(output) 1014 1015 1016@connector_app.command(name="bump-cdk") 1017def bump_cdk_cmd( 1018 name: Annotated[ 1019 str, 1020 Parameter(help="Connector technical name (e.g., source-github)."), 1021 ], 1022 repo_path: Annotated[ 1023 str, 1024 Parameter(help="Absolute path to the Airbyte monorepo."), 1025 ], 1026 force_latest: Annotated[ 1027 bool, 1028 Parameter( 1029 help=( 1030 "Rewrite the CDK constraint to >=LATEST,<NEXT_MAJOR and refresh " 1031 "the lock file. Without this flag only the lock file is refreshed " 1032 "(constraint unchanged)." 1033 ) 1034 ), 1035 ] = False, 1036 dry_run: Annotated[ 1037 bool, 1038 Parameter(help="Show what would be changed without modifying files."), 1039 ] = False, 1040) -> None: 1041 """Bump a connector's CDK dependency. 1042 1043 Two modes: 1044 1045 * Default: refresh the lock file so it resolves the newest CDK that 1046 satisfies the existing constraint. The constraint is NOT changed. 1047 * --force-latest: rewrite the constraint to >=LATEST,<NEXT_MAJOR and refresh 1048 the lock file. Extras (e.g. file-based) are preserved. 1049 1050 For Java connectors, updates `build.gradle` to the latest CDK version. 1051 1052 Examples: 1053 airbyte-ops local connector bump-cdk --name source-github --repo-path /path/to/airbyte 1054 airbyte-ops local connector bump-cdk --name source-github --repo-path /path/to/airbyte --force-latest 1055 """ 1056 try: 1057 result = bump_cdk( 1058 repo_path=repo_path, 1059 connector_name=name, 1060 force_latest=force_latest, 1061 dry_run=dry_run, 1062 ) 1063 except (ConnectorNotFoundError, CdkBumpError) as e: 1064 exit_with_error(str(e)) 1065 except ValueError as e: 1066 exit_with_error(str(e)) 1067 1068 output = { 1069 "connector": result.connector, 1070 "language": result.language, 1071 "previous_version": result.previous_version, 1072 "new_version": result.new_version, 1073 "updated": result.updated, 1074 "dry_run": result.dry_run, 1075 "files_modified": result.files_modified, 1076 "message": result.message, 1077 } 1078 print_json(output) 1079 1080 1081@connector_app.command(name="bump-deps") 1082def bump_deps_cmd( 1083 name: Annotated[ 1084 str, 1085 Parameter(help="Connector technical name (e.g., source-github)."), 1086 ], 1087 repo_path: Annotated[ 1088 str, 1089 Parameter(help="Absolute path to the Airbyte monorepo."), 1090 ], 1091 dry_run: Annotated[ 1092 bool, 1093 Parameter(help="Show what would be changed without modifying files."), 1094 ] = False, 1095) -> None: 1096 """Update a connector's dependencies. 1097 1098 For Python / low-code connectors using Poetry, this runs 1099 `poetry update --lock` to refresh the lock file with the latest 1100 versions allowed by existing constraints. 1101 1102 For connectors that do not use Poetry (manifest-only, Java, etc.), 1103 this is a no-op. 1104 1105 Examples: 1106 airbyte-ops local connector bump-deps --name source-github --repo-path /path/to/airbyte 1107 airbyte-ops local connector bump-deps --name source-github --repo-path /path/to/airbyte --dry-run 1108 """ 1109 try: 1110 result = bump_deps( 1111 repo_path=repo_path, 1112 connector_name=name, 1113 dry_run=dry_run, 1114 ) 1115 except (ConnectorNotFoundError, DepsError) as e: 1116 exit_with_error(str(e)) 1117 except ValueError as e: 1118 exit_with_error(str(e)) 1119 1120 output = { 1121 "connector": result.connector, 1122 "language": result.language, 1123 "updated": result.updated, 1124 "dry_run": result.dry_run, 1125 "files_modified": result.files_modified, 1126 "outdated_packages": result.outdated_packages, 1127 "message": result.message, 1128 } 1129 print_json(output) 1130 1131 1132@connector_app.command(name="qa") 1133def run_qa_checks( 1134 name: Annotated[ 1135 list[str] | None, 1136 Parameter( 1137 help="Connector technical name(s) (e.g., source-github). Can be specified multiple times." 1138 ), 1139 ] = None, 1140 connector_directory: Annotated[ 1141 str | None, 1142 Parameter( 1143 help="Directory containing connectors to run checks on all connectors in this directory." 1144 ), 1145 ] = None, 1146 check: Annotated[ 1147 list[str] | None, 1148 Parameter(help="Specific check(s) to run. Can be specified multiple times."), 1149 ] = None, 1150 report_path: Annotated[ 1151 str | None, 1152 Parameter(help="Path to write the JSON report file."), 1153 ] = None, 1154) -> None: 1155 """Run QA checks on connector(s). 1156 1157 Validates connector metadata, documentation, packaging, security, and versioning. 1158 Exit code is non-zero if any checks fail. 1159 """ 1160 # Determine which checks to run 1161 checks_to_run = ENABLED_CHECKS 1162 if check: 1163 check_names = set(check) 1164 checks_to_run = [c for c in ENABLED_CHECKS if type(c).__name__ in check_names] 1165 if not checks_to_run: 1166 exit_with_error( 1167 f"No matching checks found. Available checks: {[type(c).__name__ for c in ENABLED_CHECKS]}" 1168 ) 1169 1170 # Collect connectors to check 1171 connectors: list[Connector] = [] 1172 if name: 1173 connectors.extend(Connector(remove_strict_encrypt_suffix(n)) for n in name) 1174 if connector_directory: 1175 connectors.extend(get_all_connectors_in_directory(Path(connector_directory))) 1176 1177 if not connectors: 1178 exit_with_error("No connectors specified. Use --name or --connector-directory.") 1179 1180 connectors = sorted(connectors, key=lambda c: c.technical_name) 1181 1182 # Run checks synchronously (simpler than async for CLI) 1183 all_results = [] 1184 for connector in connectors: 1185 for qa_check in checks_to_run: 1186 result = qa_check.run(connector) 1187 if result.status == CheckStatus.PASSED: 1188 status_icon = "[green]✅ PASS[/green]" 1189 elif result.status == CheckStatus.SKIPPED: 1190 status_icon = "[yellow]🔶 SKIP[/yellow]" 1191 else: 1192 status_icon = "[red]❌ FAIL[/red]" 1193 console.print( 1194 f"{status_icon} {connector.technical_name}: {result.check.name}" 1195 ) 1196 if result.message: 1197 console.print(f" {result.message}") 1198 all_results.append(result) 1199 1200 # Write report if requested 1201 if report_path: 1202 Report(check_results=all_results).write(Path(report_path)) 1203 console.print(f"Report written to {report_path}") 1204 1205 # Exit with error if any checks failed 1206 failed = [r for r in all_results if r.status == CheckStatus.FAILED] 1207 if failed: 1208 exit_with_error(f"{len(failed)} check(s) failed") 1209 1210 1211@connector_app.command(name="qa-docs-generate") 1212def generate_qa_docs( 1213 output_file: Annotated[ 1214 str, 1215 Parameter(help="Path to write the generated documentation file."), 1216 ], 1217) -> None: 1218 """Generate documentation for QA checks. 1219 1220 Creates a markdown file documenting all available QA checks organized by category. 1221 """ 1222 checks_by_category: dict[CheckCategory, list[Check]] = {} 1223 for qa_check in ENABLED_CHECKS: 1224 checks_by_category.setdefault(qa_check.category, []).append(qa_check) 1225 1226 jinja_env = Environment( 1227 loader=PackageLoader("airbyte_ops_mcp.connector_qa", "templates"), 1228 autoescape=select_autoescape(), 1229 trim_blocks=False, 1230 lstrip_blocks=True, 1231 ) 1232 template = jinja_env.get_template(CONNECTORS_QA_DOC_TEMPLATE_NAME) 1233 documentation = template.render(checks_by_category=checks_by_category) 1234 1235 output_path = Path(output_file) 1236 output_path.write_text(documentation) 1237 console.print(f"Documentation written to {output_file}") 1238 1239 1240# Create the changelog sub-app under connector 1241changelog_app = App(name="changelog", help="Changelog operations for connectors.") 1242connector_app.command(changelog_app) 1243 1244 1245@changelog_app.command(name="check") 1246def changelog_check( 1247 connector_name: Annotated[ 1248 str | None, 1249 Parameter(help="Connector technical name (e.g., source-github)."), 1250 ] = None, 1251 all_connectors: Annotated[ 1252 bool, 1253 Parameter("--all", help="Check all connectors in the repository."), 1254 ] = False, 1255 repo_path: Annotated[ 1256 str | None, 1257 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 1258 ] = None, 1259 lookback_days: Annotated[ 1260 int | None, 1261 Parameter(help="Only check entries with dates within this many days."), 1262 ] = None, 1263 strict: Annotated[ 1264 bool, 1265 Parameter(help="Exit with error code if any issues are found."), 1266 ] = False, 1267) -> None: 1268 """Check changelog entries for issues. 1269 1270 Validates changelog dates match PR merge dates and checks for PR number mismatches. 1271 """ 1272 if not connector_name and not all_connectors: 1273 exit_with_error("Either --connector-name or --all must be specified.") 1274 1275 if connector_name and all_connectors: 1276 exit_with_error("Cannot specify both --connector-name and --all.") 1277 1278 if repo_path is None: 1279 cwd = Path.cwd() 1280 for parent in [cwd, *cwd.parents]: 1281 if (parent / CONNECTOR_PATH_PREFIX).exists(): 1282 repo_path = str(parent) 1283 break 1284 if repo_path is None: 1285 exit_with_error( 1286 "Could not infer repo path. Please provide --repo-path or run from within the Airbyte monorepo." 1287 ) 1288 1289 total_issues = 0 1290 1291 if all_connectors: 1292 results = check_all_changelogs(repo_path=repo_path, lookback_days=lookback_days) 1293 for result in results: 1294 if result.has_issues or result.errors: 1295 _print_check_result(result) 1296 total_issues += result.issue_count 1297 else: 1298 result = check_changelog( 1299 repo_path=repo_path, 1300 connector_name=connector_name, 1301 lookback_days=lookback_days, 1302 ) 1303 _print_check_result(result) 1304 total_issues = result.issue_count 1305 1306 if total_issues > 0: 1307 console.print(f"\n[bold]Total issues found: {total_issues}[/bold]") 1308 if strict: 1309 exit_with_error(f"Found {total_issues} issue(s) in changelog(s).") 1310 else: 1311 console.print("[green]No issues found.[/green]") 1312 1313 1314def _print_check_result(result: ChangelogCheckResult) -> None: 1315 """Print a changelog check result.""" 1316 if not result.has_issues and not result.errors: 1317 return 1318 1319 console.print(f"\n[bold]{result.connector}[/bold]") 1320 1321 for warning in result.pr_mismatch_warnings: 1322 console.print( 1323 f" [yellow]WARNING[/yellow] Line {warning.line_number} (v{warning.version}): {warning.message}" 1324 ) 1325 1326 for fix in result.date_issues: 1327 if fix.changed: 1328 console.print( 1329 f" [red]DATE MISMATCH[/red] Line {fix.line_number} (v{fix.version}): " 1330 f"changelog has {fix.old_date}, PR merged on {fix.new_date}" 1331 ) 1332 1333 for error in result.errors: 1334 console.print(f" [red]ERROR[/red] {error}") 1335 1336 1337@changelog_app.command(name="fix") 1338def changelog_fix( 1339 connector_name: Annotated[ 1340 str | None, 1341 Parameter(help="Connector technical name (e.g., source-github)."), 1342 ] = None, 1343 all_connectors: Annotated[ 1344 bool, 1345 Parameter("--all", help="Fix all connectors in the repository."), 1346 ] = False, 1347 repo_path: Annotated[ 1348 str | None, 1349 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 1350 ] = None, 1351 lookback_days: Annotated[ 1352 int | None, 1353 Parameter(help="Only fix entries with dates within this many days."), 1354 ] = None, 1355 dry_run: Annotated[ 1356 bool, 1357 Parameter(help="Print changes without modifying files."), 1358 ] = False, 1359) -> None: 1360 """Fix changelog entry dates to match PR merge dates. 1361 1362 Looks up the actual merge date for each PR referenced in the changelog 1363 and updates the date column to match. 1364 """ 1365 if not connector_name and not all_connectors: 1366 exit_with_error("Either --connector-name or --all must be specified.") 1367 1368 if connector_name and all_connectors: 1369 exit_with_error("Cannot specify both --connector-name and --all.") 1370 1371 if repo_path is None: 1372 cwd = Path.cwd() 1373 for parent in [cwd, *cwd.parents]: 1374 if (parent / CONNECTOR_PATH_PREFIX).exists(): 1375 repo_path = str(parent) 1376 break 1377 if repo_path is None: 1378 exit_with_error( 1379 "Could not infer repo path. Please provide --repo-path or run from within the Airbyte monorepo." 1380 ) 1381 1382 total_fixed = 0 1383 total_warnings = 0 1384 1385 if all_connectors: 1386 results = fix_all_changelog_dates( 1387 repo_path=repo_path, dry_run=dry_run, lookback_days=lookback_days 1388 ) 1389 for result in results: 1390 if result.has_changes or result.warnings or result.errors: 1391 _print_fix_result(result) 1392 total_fixed += result.changed_count 1393 total_warnings += len(result.warnings) 1394 else: 1395 result = fix_changelog_dates( 1396 repo_path=repo_path, 1397 connector_name=connector_name, 1398 dry_run=dry_run, 1399 lookback_days=lookback_days, 1400 ) 1401 _print_fix_result(result) 1402 total_fixed = result.changed_count 1403 total_warnings = len(result.warnings) 1404 1405 action = "Would fix" if dry_run else "Fixed" 1406 console.print(f"\n[bold]{action} {total_fixed} date(s).[/bold]") 1407 if total_warnings > 0: 1408 console.print( 1409 f"[yellow]{total_warnings} warning(s) about PR number mismatches.[/yellow]" 1410 ) 1411 1412 1413def _print_fix_result(result: ChangelogFixResult) -> None: 1414 """Print a changelog fix result.""" 1415 if not result.has_changes and not result.warnings and not result.errors: 1416 return 1417 1418 console.print(f"\n[bold]{result.connector}[/bold]") 1419 1420 for warning in result.warnings: 1421 console.print( 1422 f" [yellow]WARNING[/yellow] Line {warning.line_number} (v{warning.version}): {warning.message}" 1423 ) 1424 1425 for fix in result.fixes: 1426 if fix.changed: 1427 action = "Would fix" if result.dry_run else "Fixed" 1428 console.print( 1429 f" [green]{action}[/green] Line {fix.line_number} (v{fix.version}): " 1430 f"{fix.old_date} -> {fix.new_date}" 1431 ) 1432 1433 for error in result.errors: 1434 console.print(f" [red]ERROR[/red] {error}") 1435 1436 1437@changelog_app.command(name="add") 1438def changelog_add( 1439 connector_name: Annotated[ 1440 str, 1441 Parameter(help="Connector technical name (e.g., source-github)."), 1442 ], 1443 pr_number: Annotated[ 1444 int, 1445 Parameter(help="PR number for the changelog entry."), 1446 ], 1447 message: Annotated[ 1448 str, 1449 Parameter(help="Changelog entry message."), 1450 ], 1451 repo_path: Annotated[ 1452 str | None, 1453 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 1454 ] = None, 1455 dry_run: Annotated[ 1456 bool, 1457 Parameter(help="Print changes without modifying files."), 1458 ] = False, 1459) -> None: 1460 """Add a changelog entry for a connector using its current version. 1461 1462 Reads the version from metadata.yaml and writes a single changelog 1463 entry to the connector's documentation file. Does not modify any 1464 version files. 1465 """ 1466 if repo_path is None: 1467 cwd = Path.cwd() 1468 for parent in [cwd, *cwd.parents]: 1469 if (parent / CONNECTOR_PATH_PREFIX).exists(): 1470 repo_path = str(parent) 1471 break 1472 if repo_path is None: 1473 exit_with_error( 1474 "Could not infer repo path. Please provide --repo-path or run from within the Airbyte monorepo." 1475 ) 1476 1477 try: 1478 connector_path = get_connector_path(Path(repo_path), connector_name) 1479 version = get_current_version(connector_path) 1480 except (ConnectorNotFoundError, VersionNotFoundError) as e: 1481 exit_with_error(str(e)) 1482 1483 doc_path = get_connector_doc_path(Path(repo_path), connector_name) 1484 if doc_path is None or not doc_path.exists(): 1485 exit_with_error(f"Documentation file not found for {connector_name}.") 1486 1487 modified = update_changelog( 1488 doc_path=doc_path, 1489 new_version=version, 1490 changelog_message=message, 1491 pr_number=pr_number, 1492 dry_run=dry_run, 1493 ) 1494 1495 action = "Would add" if dry_run else "Added" 1496 if modified: 1497 console.print( 1498 f"[green]{action} changelog entry for {connector_name} v{version}[/green]" 1499 ) 1500 else: 1501 console.print( 1502 f"[yellow]No changes needed for {connector_name} v{version}[/yellow]" 1503 ) 1504 1505 1506# Create the marketing-stub sub-app under connector 1507marketing_stub_app = App( 1508 name="marketing-stub", 1509 help="Marketing connector stub operations (local file validation and updates).", 1510) 1511connector_app.command(marketing_stub_app) 1512 1513# Path to connectors in the airbyte-enterprise repo 1514ENTERPRISE_CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors" 1515 1516 1517def _build_stub_from_metadata( 1518 connector_name: str, 1519 metadata: dict, 1520 existing_stub: dict | None = None, 1521) -> dict: 1522 """Build a connector stub from metadata.yaml. 1523 1524 Args: 1525 connector_name: The connector name (e.g., 'source-oracle-enterprise'). 1526 metadata: The parsed metadata.yaml content. 1527 existing_stub: Optional existing stub to preserve extra fields from. 1528 1529 Returns: 1530 A connector stub dictionary. 1531 """ 1532 data = metadata.get("data", {}) 1533 1534 # Determine connector type for the stub 1535 connector_type = data.get("connectorType", "source") 1536 stub_type = f"enterprise_{connector_type}" 1537 1538 # Preserve existing stub ID if available, otherwise use connector name 1539 stub_id = (existing_stub.get("id") if existing_stub else None) or connector_name 1540 1541 # Get the icon URL - construct from icon filename if available 1542 icon_filename = data.get("icon", "") 1543 if icon_filename and not icon_filename.startswith("http"): 1544 # Construct icon URL from the standard GCS path 1545 icon_url = f"https://storage.googleapis.com/prod-airbyte-cloud-connector-metadata-service/resources/connector_stubs/v0/icons/{icon_filename}" 1546 else: 1547 icon_url = icon_filename or "" 1548 1549 # Build the stub 1550 stub: dict = { 1551 "id": stub_id, 1552 "name": data.get("name", connector_name.replace("-", " ").title()), 1553 "label": "enterprise", 1554 "icon": icon_url, 1555 "url": data.get("documentationUrl", ""), 1556 "type": stub_type, 1557 } 1558 1559 # Add definitionId if available 1560 definition_id = data.get("definitionId") 1561 if definition_id: 1562 stub["definitionId"] = definition_id 1563 1564 # Preserve extra fields from existing stub (like codename) 1565 if existing_stub: 1566 for key in existing_stub: 1567 if key not in stub: 1568 stub[key] = existing_stub[key] 1569 1570 return stub 1571 1572 1573@marketing_stub_app.command(name="check") 1574def marketing_stub_check( 1575 connector: Annotated[ 1576 str | None, 1577 Parameter(help="Connector name to check (e.g., 'source-oracle-enterprise')."), 1578 ] = None, 1579 all_connectors: Annotated[ 1580 bool, 1581 Parameter("--all", help="Check all stubs in the file."), 1582 ] = False, 1583 repo_root: Annotated[ 1584 Path | None, 1585 Parameter( 1586 help="Path to the airbyte-enterprise repository root. Defaults to current directory." 1587 ), 1588 ] = None, 1589) -> None: 1590 """Validate marketing connector stub entries. 1591 1592 Checks that stub entries have valid required fields (id, name, url, icon) 1593 and optionally validates that the stub matches the connector's metadata.yaml. 1594 1595 Exit codes: 1596 0: All checks passed 1597 1: Validation errors found 1598 1599 Output: 1600 STDOUT: JSON validation result 1601 STDERR: Informational messages 1602 1603 Example: 1604 airbyte-ops local connector marketing-stub check --connector source-oracle-enterprise --repo-root /path/to/airbyte-enterprise 1605 airbyte-ops local connector marketing-stub check --all --repo-root /path/to/airbyte-enterprise 1606 """ 1607 if not connector and not all_connectors: 1608 exit_with_error("Either --connector or --all must be specified.") 1609 1610 if connector and all_connectors: 1611 exit_with_error("Cannot specify both --connector and --all.") 1612 1613 if repo_root is None: 1614 repo_root = Path.cwd() 1615 1616 # Load local stubs 1617 try: 1618 stubs = load_local_stubs(repo_root) 1619 except FileNotFoundError as e: 1620 exit_with_error(str(e)) 1621 except ValueError as e: 1622 exit_with_error(str(e)) 1623 1624 stubs_to_check = stubs if all_connectors else [] 1625 if connector: 1626 stub = find_stub_by_connector(stubs, connector) 1627 if stub is None: 1628 exit_with_error( 1629 f"Connector stub '{connector}' not found in {CONNECTOR_STUBS_FILE}" 1630 ) 1631 stubs_to_check = [stub] 1632 1633 errors: list[dict] = [] 1634 warnings: list[dict] = [] 1635 placeholders: list[dict] = [] 1636 1637 for stub in stubs_to_check: 1638 stub_id = stub.get("id", "<unknown>") 1639 stub_name = stub.get("name", stub_id) 1640 1641 # Check required fields 1642 required_fields = ["id", "name", "url", "icon"] 1643 for field in required_fields: 1644 if not stub.get(field): 1645 errors.append( 1646 {"stub_id": stub_id, "error": f"Missing required field: {field}"} 1647 ) 1648 1649 # Check if corresponding connector exists and validate against metadata 1650 connector_dir = repo_root / ENTERPRISE_CONNECTOR_PATH_PREFIX / stub_id 1651 metadata_file = connector_dir / METADATA_FILE_NAME 1652 1653 if metadata_file.exists(): 1654 metadata = yaml.safe_load(metadata_file.read_text()) 1655 data = metadata.get("data", {}) 1656 1657 # Check if definitionId matches 1658 metadata_def_id = data.get("definitionId") 1659 stub_def_id = stub.get("definitionId") 1660 if metadata_def_id and stub_def_id and metadata_def_id != stub_def_id: 1661 errors.append( 1662 { 1663 "stub_id": stub_id, 1664 "error": f"definitionId mismatch: stub has '{stub_def_id}', metadata has '{metadata_def_id}'", 1665 } 1666 ) 1667 1668 # Check if name matches 1669 metadata_name = data.get("name") 1670 if metadata_name and stub_name and metadata_name != stub_name: 1671 warnings.append( 1672 { 1673 "stub_id": stub_id, 1674 "warning": f"name mismatch: stub has '{stub_name}', metadata has '{metadata_name}'", 1675 } 1676 ) 1677 else: 1678 # No connector directory - this is a registry placeholder for a future connector 1679 placeholders.append( 1680 { 1681 "stub_id": stub_id, 1682 "name": stub_name, 1683 } 1684 ) 1685 1686 result = { 1687 "checked_count": len(stubs_to_check), 1688 "error_count": len(errors), 1689 "warning_count": len(warnings), 1690 "placeholder_count": len(placeholders), 1691 "valid": len(errors) == 0, 1692 "errors": errors, 1693 "warnings": warnings, 1694 "placeholders": placeholders, 1695 } 1696 1697 # Print placeholders as info (not warnings - these are valid registry placeholders) 1698 if placeholders: 1699 error_console.print( 1700 f"[blue]Found {len(placeholders)} registry placeholder(s) (no local directory):[/blue]" 1701 ) 1702 for placeholder in placeholders: 1703 error_console.print( 1704 f" Found Connector Registry Placeholder (no local directory): {placeholder['name']}" 1705 ) 1706 1707 if errors: 1708 error_console.print(f"[red]Found {len(errors)} error(s):[/red]") 1709 for err in errors: 1710 error_console.print(f" {err['stub_id']}: {err['error']}") 1711 1712 if warnings: 1713 error_console.print(f"[yellow]Found {len(warnings)} warning(s):[/yellow]") 1714 for warn in warnings: 1715 error_console.print(f" {warn['stub_id']}: {warn['warning']}") 1716 1717 if not errors and not warnings: 1718 error_console.print( 1719 f"[green]All {len(stubs_to_check)} stub(s) passed validation[/green]" 1720 ) 1721 1722 print_json(result) 1723 1724 if errors: 1725 exit_with_error("Validation failed", code=1) 1726 1727 1728@marketing_stub_app.command(name="sync") 1729def marketing_stub_sync( 1730 connector: Annotated[ 1731 str | None, 1732 Parameter(help="Connector name to sync (e.g., 'source-oracle-enterprise')."), 1733 ] = None, 1734 all_connectors: Annotated[ 1735 bool, 1736 Parameter("--all", help="Sync all connectors that have metadata.yaml files."), 1737 ] = False, 1738 repo_root: Annotated[ 1739 Path | None, 1740 Parameter( 1741 help="Path to the airbyte-enterprise repository root. Defaults to current directory." 1742 ), 1743 ] = None, 1744 dry_run: Annotated[ 1745 bool, 1746 Parameter(help="Show what would be synced without making changes."), 1747 ] = False, 1748) -> None: 1749 """Sync connector stub(s) from connector metadata.yaml file(s). 1750 1751 Reads the connector's metadata.yaml file and updates the corresponding 1752 entry in connector_stubs.json with the current values. 1753 1754 Exit codes: 1755 0: Sync successful (or dry-run completed) 1756 1: Error (connector not found, no metadata, etc.) 1757 1758 Output: 1759 STDOUT: JSON representation of the synced stub(s) 1760 STDERR: Informational messages 1761 1762 Example: 1763 airbyte-ops local connector marketing-stub sync --connector source-oracle-enterprise --repo-root /path/to/airbyte-enterprise 1764 airbyte-ops local connector marketing-stub sync --all --repo-root /path/to/airbyte-enterprise 1765 airbyte-ops local connector marketing-stub sync --connector source-oracle-enterprise --dry-run 1766 """ 1767 if not connector and not all_connectors: 1768 exit_with_error("Either --connector or --all must be specified.") 1769 1770 if connector and all_connectors: 1771 exit_with_error("Cannot specify both --connector and --all.") 1772 1773 if repo_root is None: 1774 repo_root = Path.cwd() 1775 1776 # Load existing stubs 1777 try: 1778 stubs = load_local_stubs(repo_root) 1779 except FileNotFoundError: 1780 stubs = [] 1781 except ValueError as e: 1782 exit_with_error(str(e)) 1783 1784 # Determine which connectors to sync 1785 connectors_to_sync: list[str] = [] 1786 if connector: 1787 connectors_to_sync = [connector] 1788 else: 1789 # Find all connectors with metadata.yaml in the enterprise connectors directory 1790 connectors_dir = repo_root / ENTERPRISE_CONNECTOR_PATH_PREFIX 1791 if connectors_dir.exists(): 1792 for item in connectors_dir.iterdir(): 1793 if item.is_dir() and (item / METADATA_FILE_NAME).exists(): 1794 connectors_to_sync.append(item.name) 1795 connectors_to_sync.sort() 1796 1797 if not connectors_to_sync: 1798 exit_with_error("No connectors found to sync.") 1799 1800 synced_stubs: list[dict] = [] 1801 updated_count = 0 1802 added_count = 0 1803 1804 for conn_name in connectors_to_sync: 1805 connector_dir = repo_root / ENTERPRISE_CONNECTOR_PATH_PREFIX / conn_name 1806 metadata_file = connector_dir / METADATA_FILE_NAME 1807 1808 if not connector_dir.exists(): 1809 if connector: 1810 exit_with_error(f"Connector directory not found: {connector_dir}") 1811 continue 1812 1813 if not metadata_file.exists(): 1814 if connector: 1815 exit_with_error(f"Metadata file not found: {metadata_file}") 1816 continue 1817 1818 # Load metadata 1819 metadata = yaml.safe_load(metadata_file.read_text()) 1820 1821 # Find existing stub if any 1822 existing_stub = find_stub_by_connector(stubs, conn_name) 1823 1824 # Build new stub from metadata 1825 new_stub = _build_stub_from_metadata(conn_name, metadata, existing_stub) 1826 1827 # Validate the new stub 1828 ConnectorStub(**new_stub) 1829 1830 if dry_run: 1831 action = "update" if existing_stub else "create" 1832 error_console.print(f"[DRY RUN] Would {action} stub for '{conn_name}'") 1833 synced_stubs.append(new_stub) 1834 continue 1835 1836 # Update or add the stub 1837 if existing_stub: 1838 # Find and replace 1839 for i, stub in enumerate(stubs): 1840 if stub.get("id") == existing_stub.get("id"): 1841 stubs[i] = new_stub 1842 break 1843 updated_count += 1 1844 else: 1845 stubs.append(new_stub) 1846 added_count += 1 1847 1848 synced_stubs.append(new_stub) 1849 1850 if not dry_run: 1851 # Save the updated stubs 1852 save_local_stubs(repo_root, stubs) 1853 error_console.print( 1854 f"[green]Synced {len(synced_stubs)} stub(s) to {CONNECTOR_STUBS_FILE} " 1855 f"({added_count} added, {updated_count} updated)[/green]" 1856 ) 1857 else: 1858 error_console.print( 1859 f"[DRY RUN] Would sync {len(synced_stubs)} stub(s) to {CONNECTOR_STUBS_FILE}" 1860 ) 1861 1862 print_json( 1863 synced_stubs if all_connectors else synced_stubs[0] if synced_stubs else {} 1864 ) 1865 1866 1867# Create the release-block sub-app under connector 1868release_block_app = App( 1869 name="release-block", 1870 help="Manage release block markers for connectors.", 1871) 1872connector_app.command(release_block_app) 1873 1874 1875@release_block_app.command(name="add") 1876def release_block_add( 1877 name: Annotated[ 1878 str, 1879 Parameter(help="Connector technical name (e.g., source-faker)."), 1880 ], 1881 reason: Annotated[ 1882 str, 1883 Parameter(help="Human-readable reason for blocking the release."), 1884 ], 1885 repo_path: Annotated[ 1886 str | None, 1887 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 1888 ] = None, 1889 yanked_version: Annotated[ 1890 str | None, 1891 Parameter(help="Version that was yanked (for reference)."), 1892 ] = None, 1893 blocked_by: Annotated[ 1894 str | None, 1895 Parameter(help="Email or identifier of the person requesting the block."), 1896 ] = None, 1897) -> None: 1898 """Add a `block-release.yaml` marker to prevent publishing a connector. 1899 1900 Creates a marker file in the connector's directory that causes the publish 1901 pipeline to skip the connector with a warning. 1902 1903 Example: 1904 1905 airbyte-ops local connector release-block add \\ 1906 --name source-faker \\ 1907 --reason "Version 5.0.1 yanked due to regression" \\ 1908 --repo-path /path/to/airbyte 1909 """ 1910 resolved_path = repo_path or os.environ.get("AIRBYTE_REPO_PATH", ".") 1911 result = add_release_block( 1912 repo_path=resolved_path, 1913 connector_name=name, 1914 reason=reason, 1915 yanked_version=yanked_version, 1916 blocked_by=blocked_by, 1917 ) 1918 if result.success: 1919 error_console.print(f"[green]{result.message}[/green]") 1920 else: 1921 exit_with_error(result.message) 1922 print_json(result.to_dict()) 1923 1924 1925@release_block_app.command(name="clear") 1926def release_block_clear( 1927 name: Annotated[ 1928 str, 1929 Parameter(help="Connector technical name (e.g., source-faker)."), 1930 ], 1931 repo_path: Annotated[ 1932 str | None, 1933 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 1934 ] = None, 1935) -> None: 1936 """Remove the `block-release.yaml` marker to allow publishing a connector. 1937 1938 Example: 1939 1940 airbyte-ops local connector release-block clear \\ 1941 --name source-faker \\ 1942 --repo-path /path/to/airbyte 1943 """ 1944 resolved_path = repo_path or os.environ.get("AIRBYTE_REPO_PATH", ".") 1945 result = clear_release_block( 1946 repo_path=resolved_path, 1947 connector_name=name, 1948 ) 1949 if result.success: 1950 error_console.print(f"[green]{result.message}[/green]") 1951 else: 1952 exit_with_error(result.message) 1953 print_json(result.to_dict()) 1954 1955 1956ReleaseBlockListFormat = Literal["text", "json", "csv"] 1957 1958 1959@release_block_app.command(name="list") 1960def release_block_list( 1961 repo_path: Annotated[ 1962 str | None, 1963 Parameter(help="Path to the Airbyte monorepo. Can be inferred from context."), 1964 ] = None, 1965 output_format: Annotated[ 1966 ReleaseBlockListFormat, 1967 Parameter( 1968 help=( 1969 'Output format: "text" (human-readable), ' 1970 '"json" (blocked connectors with block file contents), ' 1971 '"csv" (comma-delimited connector names).' 1972 ) 1973 ), 1974 ] = "text", 1975) -> None: 1976 """List all connectors that have a `block-release.yaml` marker. 1977 1978 Example: 1979 1980 airbyte-ops local connector release-block list --repo-path /path/to/airbyte 1981 airbyte-ops local connector release-block list --output-format json 1982 airbyte-ops local connector release-block list --output-format csv 1983 """ 1984 resolved_path = repo_path or os.environ.get("AIRBYTE_REPO_PATH", ".") 1985 result = list_release_blocks(repo_path=resolved_path) 1986 1987 if output_format == "json": 1988 print_json(result.to_dict()) 1989 elif output_format == "csv": 1990 names = [info.connector_name for info in result.blocked_connectors] 1991 sys.stdout.write(",".join(names) + "\n") 1992 else: 1993 # text format (default) 1994 if result.count == 0: 1995 console.print( 1996 "[green]No connectors are currently blocked from release.[/green]" 1997 ) 1998 else: 1999 console.print( 2000 f"[yellow]{result.count} connector(s) blocked from release:[/yellow]" 2001 ) 2002 for info in result.blocked_connectors: 2003 version_str = ( 2004 f" (yanked: {info.yanked_version})" if info.yanked_version else "" 2005 ) 2006 console.print(f" - {info.connector_name}: {info.reason}{version_str}")