airbyte_ops_mcp.cli.secrets
CLI commands for connector integration-test secrets.
Commands:
airbyte-ops secrets fetch - Download connector secrets from GSM into
./secrets/airbyte-ops secrets list - List connector secrets visible in GSM airbyte-ops secrets ci-mask - Emit GitHub Actions::add-mask::lines
These commands operate on the connector integration-test secrets stored in
Google Secret Manager (project dataline-integration-testing by default).
They are intended to replace the airbyte-cdk secrets group in a subsequent
phase; the CDK commands remain functional until that migration lands.
Note:
airbyte-ops devin secret-*is a different subsystem for Devin-internal 1Password on-demand secrets and is unrelated to this group.
CLI reference
The commands below are regenerated by poe docs-generate via cyclopts's
programmatic docs API; see docs/generate_cli.py.
airbyte-ops secrets COMMAND
Manage connector integration-test secrets in GSM.
Commands:
ci-mask: Emit GitHub Actions::add-mask::lines for a connector's fetched secrets.fetch: Fetch connector secrets from GSM into the connector'ssecrets/directory.list: List connector secrets visible in GSM without downloading any values.
airbyte-ops secrets fetch
airbyte-ops secrets fetch [OPTIONS] [ARGS]
Fetch connector secrets from GSM into the connector's secrets/ directory.
Equivalent to the old airbyte-cdk secrets fetch. Runs in three steps:
- Resolve the connector (from
[CONNECTOR], or the cwd). - List GSM secrets labeled
connector=<name>in--gcp-project-id. - Write each secret's latest enabled version to
secrets/<filename>.json(mode0o600). The filename comes from the secret'sfilenamelabel, or defaults toconfig.json.
When --print-ci-secrets-masks is set (or auto-detected in CI), emits
GitHub Actions masking commands after writing.
Parameters:
CONNECTOR, --connector: Connector name (e.g. 'source-pokeapi') or path to a connector directory. If omitted, the current working directory is used and must be a connector directory.--gcp-project-id: GCP project ID for retrieving integration-test credentials. Defaults to theGCP_PROJECT_IDenvironment variable, or 'dataline-integration-testing' when unset. [default: dataline-integration-testing]--print-ci-secrets-masks, --no-print-ci-secrets-masks: Emit GitHub Actions::add-mask::lines for fetched secrets. Ignored outside CI (requires theCIenv var). Defaults to auto-detection fromCI.
airbyte-ops secrets list
airbyte-ops secrets list [OPTIONS] [ARGS]
List connector secrets visible in GSM without downloading any values.
Prints a Rich table with the secret name (linked to its GCP console page), labels, and creation time. Only metadata is read — no secret payloads are fetched.
Parameters:
CONNECTOR, --connector: Connector name (e.g. 'source-pokeapi') or path to a connector directory. If omitted, the current working directory is used and must be a connector directory.--gcp-project-id: GCP project ID for retrieving integration-test credentials. Defaults to theGCP_PROJECT_IDenvironment variable, or 'dataline-integration-testing' when unset. [default: dataline-integration-testing]
airbyte-ops secrets ci-mask
airbyte-ops secrets ci-mask [ARGS]
Emit GitHub Actions ::add-mask:: lines for a connector's fetched secrets.
Reads every *.json file in the connector's local secrets/ directory and
prints a mask command for each secret-valued property (matched against the
canonical
specs_secrets_mask.yaml
list). Intended to run inside a GitHub Actions job; refuses to emit output
when CI is unset to prevent accidentally printing secret values locally.
This subcommand is equivalent to running airbyte-ops secrets fetch
--print-ci-secrets-masks without re-downloading secrets.
Parameters:
CONNECTOR, --connector: Connector name (e.g. 'source-pokeapi') or path to a connector directory. If omitted, the current working directory is used and must be a connector directory.
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""CLI commands for connector integration-test secrets. 3 4Commands: 5 airbyte-ops secrets fetch - Download connector secrets from GSM into `./secrets/` 6 airbyte-ops secrets list - List connector secrets visible in GSM 7 airbyte-ops secrets ci-mask - Emit GitHub Actions `::add-mask::` lines 8 9These commands operate on the connector integration-test secrets stored in 10Google Secret Manager (project `dataline-integration-testing` by default). 11They are intended to replace the `airbyte-cdk secrets` group in a subsequent 12phase; the CDK commands remain functional until that migration lands. 13 14> **Note:** `airbyte-ops devin secret-*` is a different subsystem for 15> Devin-internal 1Password on-demand secrets and is unrelated to this group. 16 17## CLI reference 18 19The commands below are regenerated by `poe docs-generate` via cyclopts's 20programmatic docs API; see `docs/generate_cli.py`. 21 22.. include:: ../../../docs/generated/cli/secrets.md 23 :start-line: 2 24""" 25 26from __future__ import annotations 27 28__all__: list[str] = [] 29 30from typing import Annotated 31 32from airbyte_cdk.utils.connector_paths import resolve_connector_name_and_directory 33from cyclopts import Parameter 34from rich.console import Console 35from rich.table import Table 36 37from airbyte_ops_mcp.cli._base import App, app 38from airbyte_ops_mcp.cli._shared import error_console 39from airbyte_ops_mcp.connector_secrets import ( 40 DEFAULT_GCP_PROJECT_ID, 41 ConnectorSecretWithNoValidVersionsError, 42 extract_gcp_secret_name, 43 fetch_secret_handles, 44 get_gcp_secret_url, 45 get_gsm_secrets_client, 46 get_secret_filepath, 47 get_secrets_dir, 48 write_secret_file, 49) 50from airbyte_ops_mcp.connector_secrets.ci_masks import ( 51 is_running_in_ci, 52 print_ci_secrets_masks, 53) 54 55secrets_app = App( 56 name="secrets", help="Manage connector integration-test secrets in GSM." 57) 58app.command(secrets_app) 59 60_CONNECTOR_HELP = ( 61 "Connector name (e.g. 'source-pokeapi') or path to a connector directory. " 62 "If omitted, the current working directory is used and must be a connector " 63 "directory." 64) 65_GCP_PROJECT_ID_HELP = ( 66 "GCP project ID for retrieving integration-test credentials. Defaults to " 67 "the `GCP_PROJECT_ID` environment variable, or 'dataline-integration-testing' " 68 "when unset." 69) 70 71 72@secrets_app.command(name="fetch") 73def fetch( 74 connector: Annotated[str | None, Parameter(help=_CONNECTOR_HELP)] = None, 75 *, 76 gcp_project_id: Annotated[ 77 str, Parameter(help=_GCP_PROJECT_ID_HELP) 78 ] = DEFAULT_GCP_PROJECT_ID, 79 print_ci_secrets_masks_flag: Annotated[ 80 bool | None, 81 Parameter( 82 name=["--print-ci-secrets-masks"], 83 negative=["--no-print-ci-secrets-masks"], 84 help=( 85 "Emit GitHub Actions `::add-mask::` lines for fetched secrets. " 86 "Ignored outside CI (requires the `CI` env var). Defaults to " 87 "auto-detection from `CI`." 88 ), 89 ), 90 ] = None, 91) -> None: 92 """Fetch connector secrets from GSM into the connector's `secrets/` directory. 93 94 Equivalent to the old `airbyte-cdk secrets fetch`. Runs in three steps: 95 96 1. Resolve the connector (from `[CONNECTOR]`, or the cwd). 97 2. List GSM secrets labeled `connector=<name>` in `--gcp-project-id`. 98 3. Write each secret's latest enabled version to `secrets/<filename>.json` 99 (mode `0o600`). The filename comes from the secret's `filename` label, 100 or defaults to `config.json`. 101 102 When `--print-ci-secrets-masks` is set (or auto-detected in CI), emits 103 GitHub Actions masking commands after writing. 104 """ 105 error_console.print("Fetching secrets...") 106 107 client = get_gsm_secrets_client() 108 connector_name, connector_directory = resolve_connector_name_and_directory( 109 connector 110 ) 111 secrets_dir = get_secrets_dir( 112 connector_directory=connector_directory, 113 ensure_exists=True, 114 ) 115 secrets = fetch_secret_handles( 116 connector_name=connector_name, 117 gcp_project_id=gcp_project_id, 118 client=client, 119 ) 120 121 secret_count = 0 122 exceptions: list[ConnectorSecretWithNoValidVersionsError] = [] 123 124 for secret in secrets: 125 secret_file_path = get_secret_filepath(secrets_dir=secrets_dir, secret=secret) 126 try: 127 write_secret_file( 128 secret=secret, 129 client=client, 130 file_path=secret_file_path, 131 connector_name=connector_name, 132 gcp_project_id=gcp_project_id, 133 ) 134 except ConnectorSecretWithNoValidVersionsError as err: 135 exceptions.append(err) 136 error_console.print( 137 f"Failed to retrieve secret '{err.secret_name}': No enabled version found" 138 ) 139 continue 140 141 error_console.print(f"Secret written to: {secret_file_path.absolute()!s}") 142 secret_count += 1 143 144 if secret_count == 0 and not exceptions: 145 error_console.print(f"No secrets found for connector: '{connector_name}'") 146 147 if exceptions: 148 error_console.print( 149 f"[red]Failed to retrieve {len(exceptions)} secret(s)[/red]" 150 ) 151 if secret_count == 0: 152 raise exceptions[0] 153 154 emit_masks = print_ci_secrets_masks_flag 155 if emit_masks is None: 156 emit_masks = is_running_in_ci() 157 158 if emit_masks: 159 print_ci_secrets_masks( 160 secrets_dir=secrets_dir, 161 strict_ci_env_check=True, 162 ) 163 164 165@secrets_app.command(name="list") 166def list_( 167 connector: Annotated[str | None, Parameter(help=_CONNECTOR_HELP)] = None, 168 *, 169 gcp_project_id: Annotated[ 170 str, Parameter(help=_GCP_PROJECT_ID_HELP) 171 ] = DEFAULT_GCP_PROJECT_ID, 172) -> None: 173 """List connector secrets visible in GSM without downloading any values. 174 175 Prints a Rich table with the secret name (linked to its GCP console page), 176 labels, and creation time. Only metadata is read — no secret payloads are 177 fetched. 178 """ 179 error_console.print("Scanning secrets...") 180 181 if connector and "/" not in connector and "\\" not in connector: 182 connector_name = connector 183 else: 184 connector_name, _ = resolve_connector_name_and_directory(connector) 185 186 secrets = fetch_secret_handles( 187 connector_name=connector_name, 188 gcp_project_id=gcp_project_id, 189 ) 190 if not secrets: 191 error_console.print(f"No secrets found for connector: '{connector_name}'") 192 return 193 194 console = Console() 195 console.print( 196 f"[green]Secrets for connector '{connector_name}' in project " 197 f"'{gcp_project_id}':[/green]" 198 ) 199 table = Table(title=f"'{connector_name}' Secrets") 200 table.add_column("Name", justify="left", style="cyan", overflow="fold") 201 table.add_column("Labels", justify="left", style="magenta", overflow="fold") 202 table.add_column("Created", justify="left", style="blue", overflow="fold") 203 for secret in secrets: 204 secret_name = extract_gcp_secret_name(secret.name) 205 secret_url = get_gcp_secret_url(secret_name, gcp_project_id) 206 table.add_row( 207 f"[link={secret_url}]{secret_name}[/link]", 208 "\n".join(f"{k}={v}" for k, v in secret.labels.items()), 209 str(secret.create_time), 210 ) 211 console.print(table) 212 213 214@secrets_app.command(name="ci-mask") 215def ci_mask( 216 connector: Annotated[str | None, Parameter(help=_CONNECTOR_HELP)] = None, 217) -> None: 218 """Emit GitHub Actions `::add-mask::` lines for a connector's fetched secrets. 219 220 Reads every `*.json` file in the connector's local `secrets/` directory and 221 prints a mask command for each secret-valued property (matched against the 222 canonical 223 [`specs_secrets_mask.yaml`](https://connectors.airbyte.com/files/registries/v0/specs_secrets_mask.yaml) 224 list). Intended to run inside a GitHub Actions job; refuses to emit output 225 when `CI` is unset to prevent accidentally printing secret values locally. 226 227 This subcommand is equivalent to running `airbyte-ops secrets fetch 228 --print-ci-secrets-masks` without re-downloading secrets. 229 """ 230 _, connector_directory = resolve_connector_name_and_directory(connector) 231 secrets_dir = get_secrets_dir( 232 connector_directory=connector_directory, 233 ensure_exists=False, 234 ) 235 if not secrets_dir.is_dir(): 236 error_console.print( 237 f"No secrets directory found at {secrets_dir!s}; nothing to mask." 238 ) 239 return 240 241 print_ci_secrets_masks( 242 secrets_dir=secrets_dir, 243 strict_ci_env_check=True, 244 )