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