wads.scripts.export_ci_env¶
Export the pyproject-declared CI environment to $GITHUB_ENV.
This is the run-time half of wads’ two-layer secret model (see
wads.ci_secrets). The reusable workflow receives the caller’s secrets
either as named pass-throughs (the default stub) or as one double-encoded JSON
blob under WADS_CI_SECRETS_JSON (opt-in JSON stubs); this script decides which
values actually become job environment variables, driven entirely by
[tool.wads.ci.env] in the consumer’s pyproject.toml (read via the
read-ci-config action):
defaults— literalKEY=valuepairs, always written.required_envvars— must resolve non-empty, else CI fails with a precise message.test_envvars— exported if set; a warning is emitted if missing.extra_envvars— exported if set; silent if missing.secret_aliases— mapENV_VAR -> SECRET_NAMEfor the (rare) case where the env var the code reads differs from the GitHub secret name.
Each declared name resolves against secrets first (named ones merged with
the transport blob) and the caller’s repository variables second (the
vars context resolves to the caller’s repo in a reusable workflow) — so a
non-sensitive value like a test verbosity level can live in a repo variable
instead of being mis-classified as a secret.
Secret values are never printed. Values extracted from the transport blob
are not auto-masked by GitHub (only the blob as a whole is), so every
secret-sourced value is re-registered with ::add-mask:: (per line, the
documented form for multiline values) before being written to the job env.
Functions
|
Compute the env assignments to write, plus any missing-required errors. |
|
True when |
|
Read inputs from env, write assignments to |
|
Flatten the secrets mapping, expanding the |
Classes
|
What |
- class wads.scripts.export_ci_env.ExportPlan(assignments, exported, missing_required, missing_test, mask_values)[source]¶
Bases:
NamedTupleWhat
export_ci_env()decided, ready formain()to apply.
- wads.scripts.export_ci_env.export_ci_env(*, required=(), test=(), extra=(), defaults=None, aliases=None, secrets=None, vars_=None, withhold_publish=True, warn=<function <lambda>>)[source]¶
Compute the env assignments to write, plus any missing-required errors.
Pure function (no I/O) so it can be unit-tested without GitHub. Each declared name resolves secrets-first, then falls back to the caller’s repository variables (
vars_). Secret-sourced values are collected inmask_valuesfor re-masking; variable- and default-sourced values are not masked (they are not sensitive by definition).>>> plan = export_ci_env( ... required=["OPENAI_API_KEY"], ... test=["TAVILY_API_KEY"], ... extra=["UNSET_THING", "COSMO_TEST_LEVEL"], ... defaults={"LOG_LEVEL": "DEBUG"}, ... secrets={"OPENAI_API_KEY": "sk-xxx", "TAVILY_API_KEY": ""}, ... vars_={"COSMO_TEST_LEVEL": "3"}, ... ) >>> plan.exported ['LOG_LEVEL', 'OPENAI_API_KEY', 'COSMO_TEST_LEVEL'] >>> plan.missing_required [] >>> plan.missing_test ['TAVILY_API_KEY'] >>> plan.mask_values # only the secret-sourced value ['sk-xxx']
A required var with no backing secret or variable is reported (caller should fail CI):
>>> export_ci_env(required=["OPENAI_API_KEY"], secrets={}).missing_required [('OPENAI_API_KEY', 'OPENAI_API_KEY')]
Aliases let an env var read a differently-named secret:
>>> plan = export_ci_env( ... test=["HF_TOKEN"], ... aliases={"HF_TOKEN": "HF_WRITE_TOKEN"}, ... secrets={"HF_WRITE_TOKEN": "hf_xxx"}) >>> plan.exported ['HF_TOKEN'] >>> plan.assignments ['HF_TOKEN=hf_xxx']
A publish credential (see
is_publish_credential()) is never exported, under any name, and never fails the job, unlesswithhold_publish=False(the publish job). The token must not reach a job that runs tests (issue #110):>>> plan = export_ci_env( ... required=["PYPI_PASSWORD"], ... extra=["UV_PUBLISH_TOKEN"], ... aliases={"UV_PUBLISH_TOKEN": "PYPI_PASSWORD"}, ... secrets={"PYPI_PASSWORD": "pypi-xxx"}, ... warn=lambda msg: None) >>> plan.exported, plan.missing_required, plan.mask_values ([], [], [])
- wads.scripts.export_ci_env.is_publish_credential(env_var, secret=None)[source]¶
True when
env_var(backed bysecret) carries a PyPI publish credential.Matches the publish env-var names tools read, the wads publish secrets, and any other PyPI-ish credential name, case-insensitively (GitHub secret names are case-insensitive).
- Return type:
>>> is_publish_credential("UV_PUBLISH_TOKEN", "PYPI_PASSWORD") True >>> is_publish_credential("TWINE_PASSWORD", "WHATEVER") True >>> is_publish_credential("MY_TOKEN", "pypi_api_token") True >>> is_publish_credential("PYPI_USERNAME") False >>> is_publish_credential("OPENAI_API_KEY") False
- wads.scripts.export_ci_env.main()[source]¶
Read inputs from env, write assignments to
$GITHUB_ENV, fail if required missing.- Return type:
- wads.scripts.export_ci_env.merge_transported_secrets(secrets)[source]¶
Flatten the secrets mapping, expanding the
WADS_CI_SECRETS_JSONblob.The blob is the caller’s whole secrets context, serialized by the stub with
toJSON(toJSON(secrets))(double-encoded => single-line). A single-encoded blob is also accepted. Named entries win over blob entries (they are the same caller values anyway), and non-exportable keys (the blob itself,github_token) are dropped.>>> merged = merge_transported_secrets({ ... "WADS_CI_SECRETS_JSON": '"{\\n \\"MY_KEY\\": \\"v1\\",\\n \\"github_token\\": \\"t\\"\\n}"', ... "OPENAI_API_KEY": "sk-named", ... }) >>> sorted(merged) ['MY_KEY', 'OPENAI_API_KEY'] >>> merged["MY_KEY"] 'v1'
Malformed blobs are ignored rather than fatal (the named layer still works, and required-validation reports any name that failed to resolve):
>>> merge_transported_secrets({"WADS_CI_SECRETS_JSON": "not json", "A": "x"}) {'A': 'x'}