Repository navigation
Description
Problem
deploy-site-task.yml hands the caller's deploy hook its environment values one name at a time. Each value is a declared workflow_call.secrets entry, a line in the assert step, and a line in the verify step's env:. #929 added SITE_AUTH_TOKEN_ID/SITE_AUTH_TOKEN, and #2031 added SITE_EXTRA_BASE_URL with a second pair. Each new site or credential needs a hub change, a hub release, and a pin bump in every caller. That works for one or two sites and does not scale. The same shape will recur in any hub task that runs a caller-owned hook against an environment.
What the fleet does today
- Variable-length inputs already exist, and all of them are non-secret. Merge-bot
rulesis a JSON-string input, shape-checked with jq and consumed with--argjson(merge-bot-task.yml).python-versions,python-directories, check-upstreambranches, and build-dockermatrixare JSON strings fed throughfromJSON, each pre-checked with jq so a bad value fails with a named cause. - Nothing in the hub uses
toJSON(secrets),toJSON(vars),secrets[...],vars[...], orGITHUB_ENVwrites. - Secrets cross by explicit name only.
docs/reusable-workflows.md"Secrets and Permissions" rules outsecrets: inherit, since GitHub limits it to one organization or enterprise and the fleet is a personal account. - The environment binding already carries the values. The task's
deployjob bindsenvironment:itself, so values resolve from the caller's environment store. It already readsvars.SITE_BASE_URLwithout declaring it, and its own comment calls the caller's secret mapping ceremony, since the binding supplies the value. - Environment-scope names are checked by nothing. Declare Per-Environment Secrets and Variables Once and Check Them From the Hub #2070 proposes declaring them in the registry and checking them from
spec/audit.py.
GitHub mechanics that bound the design
- A composite action cannot read the
secretscontext (contexts). Community reports say it cannot readvarseither, so values reach a hook only asenv:or inputs. - A reusable workflow job that binds
environment:uses the environment's value over the one the caller passed (reuse-workflows). The documentation does not say whether an environment secret the called workflow never declared appears insecretsortoJSON(secrets). Third-party reports conflict (one says empty until declared, another says visible, but withinherit). This is the question that decides the design. varsin a called workflow resolves against the caller, with environment values winning under a binding (variables). WhethertoJSON(vars)lists every environment variable is unverified.- GitHub advises against a JSON blob in one secret, because redaction matches exact values. A field parsed out of it is unmasked unless registered with
::add-mask::(secure use). - Limits: 100 secrets and 100 variables per environment, 48 KB each (secrets).
GITHUB_*andRUNNER_*cannot be overwritten, butPATH,BASH_ENV, andLD_PRELOADare legal names, so any transfer needs a name filter. - Dynamic indexing such as
secrets[inputs.name]is documented only for literal keys. Community reports say it works, and it is unverified here.
Options
- One JSON or dotenv secret, parsed with an allowlist. One declaration covers any number of values. It goes against GitHub's redaction guidance and needs an
add-maskper field. Not recommended. - A name-list input plus
secrets[name]. The caller chooses what is read, so it needs an allowlist. It also fails if undeclared environment secrets are invisible. - A prefix convention through a filtered
toJSON(vars)/toJSON(secrets). Every value matching, for example,^SITE_[A-Z0-9_]+$goes to the hook as one JSON env var. There is no hub edit per name, and the prefix keeps outPATH-style names. Secrets carry the same visibility risk as option 2. - The caller's own job runs
verify. A caller job withoutuses:can bind the environment and read every value directly, with the release id passed throughneeds. Nothing crosses the boundary. The hub no longer guarantees the live check runs, which is WORKFLOW.md D4.6's reason for owning it. - OIDC to an external secrets manager (Vault, a cloud secret store, 1Password, Doppler, Infisical). It scales, but adds an external dependency and trust setup per environment, which is too heavy for this fleet.
- Environments as the only carrier, with a declared manifest. Declare Per-Environment Secrets and Variables Once and Check Them From the Hub #2070's registry declaration names each environment's variables and secrets. The owner-run audit checks presence and kind, names only. The task transfers by option 3's filtered
toJSON.
Proposal
Option 6, with option 4 as the fallback if the probe below shows undeclared environment secrets are invisible in the called job.
- Declare per Declare Per-Environment Secrets and Variables Once and Check Them From the Hub #2070: registry
environments[]entries gainvariablesandsecretsname lists, andspec/audit.pychecks them, names only. Listing environment secrets needs an admin login, so this stays owner-run, as the audit already is. A required name is enforced there, not at runtime, since the task cannot read the registry. - Group by naming, not by per-name YAML. Each site is
<S>_BASE_URLwith an optional<S>_AUTH_TOKEN_ID/<S>_AUTH_TOKEN. One generic jq loop in the assert step checks both-or-neither, and that a token has its base URL, for every discovered prefix. That replaces the hand-written checks for the two pairs. - Transfer in one hub-owned step:
ALL_VARS: ${{ toJSON(vars) }}andALL_SECRETS: ${{ toJSON(secrets) }}go throughenv:, never inlined into the script. jq keeps the keys matching the site prefix and refuses any name the task uses itself. The result reaches the hook on theverifyinvocation only, as one env var (for exampleSITE_ENV_JSON), which adds no input an existing hook would reject. Nothing is written toGITHUB_ENV, so the values never reach the upload and flip steps. The deploy key and the job token never reach the hook. - Migration. The named pairs keep working through a deprecation window, and callers move by renaming nothing, since the names already match the prefix.
Prove before adopting
A throwaway cross-repository caller in a ptr727 repository, printing key names only (jq 'keys'), never values:
- Does an environment secret not declared under
workflow_call.secretsappear intoJSON(secrets)in the bound called job? This decides between option 6 and option 4. - Does
toJSON(vars)list every environment variable? - Is a secret extracted from
toJSON(secrets)still masked when echoed, including one containing a quote or a backslash? - Does a
required: falsesecret the caller does not map still resolve its environment value? - Does
secrets[expr]work with a computed key?
Activity
ptr727 commentedon Nov 1, 2026
Decision needed (decision triage, 2026-11-01)
Question: Before
deploy-site-task.ymlstops passing itsdeployhook environment values as named pairs, may an agent run throwaway cross-repository probe workflows in a ptr727 repository (printing key names only) to learn whether undeclared environment secrets appear intoJSON(secrets), the answer that decides which transfer design follows?Options, recommended first:
Evidence: deploy-site-task.yml:38-55 declares DEPLOY_SSH_PRIVATE_KEY plus two optional pairs by name, hand-checked at :121-160. 3f4de59 (#938) and 1b061a0 (#2173) each added one pair with a hub release. registry/repos.json:325: Blog is the only entry with production/staging environments. docs/reusable-workflows.md:245, :819 record the earlier cross-repo probe. Option 6 puts every secret in one step's
env:with a jq filter as the only boundary, so masking of an extracted value is a hard requirement.Blocks: none (option 6 depends on #2070)
ptr727 commentedon Nov 1, 2026
Maintainer answer (decision triage, 2026-11-01): Keep the named pairs for now. Deferred until a third site needs a deploy-environment pair, with no probe and no transfer redesign before then. Until that point no agent works this issue.
Improve decision-triage From Its First Full Run (#3760)
Promote Develop: Improve decision-triage From Its First Full Run and …