Skip to content

Hand a Variable Set of Environment Values to a Hub Task's Hook Instead of Named Pairs #2174

Open

Description

@ptr727

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 rules is a JSON-string input, shape-checked with jq and consumed with --argjson (merge-bot-task.yml). python-versions, python-directories, check-upstream branches, and build-docker matrix are JSON strings fed through fromJSON, 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[...], or GITHUB_ENV writes.
  • Secrets cross by explicit name only. docs/reusable-workflows.md "Secrets and Permissions" rules out secrets: 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 deploy job binds environment: itself, so values resolve from the caller's environment store. It already reads vars.SITE_BASE_URL without 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 secrets context (contexts). Community reports say it cannot read vars either, so values reach a hook only as env: 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 in secrets or toJSON(secrets). Third-party reports conflict (one says empty until declared, another says visible, but with inherit). This is the question that decides the design.
  • vars in a called workflow resolves against the caller, with environment values winning under a binding (variables). Whether toJSON(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_* and RUNNER_* cannot be overwritten, but PATH, BASH_ENV, and LD_PRELOAD are 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

  1. 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-mask per field. Not recommended.
  2. 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.
  3. 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 out PATH-style names. Secrets carry the same visibility risk as option 2.
  4. The caller's own job runs verify. A caller job without uses: can bind the environment and read every value directly, with the release id passed through needs. Nothing crosses the boundary. The hub no longer guarantees the live check runs, which is WORKFLOW.md D4.6's reason for owning it.
  5. 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.
  6. 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 gain variables and secrets name lists, and spec/audit.py checks 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_URL with 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) }} and ALL_SECRETS: ${{ toJSON(secrets) }} go through env:, 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 the verify invocation only, as one env var (for example SITE_ENV_JSON), which adds no input an existing hook would reject. Nothing is written to GITHUB_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:

  1. Does an environment secret not declared under workflow_call.secrets appear in toJSON(secrets) in the bound called job? This decides between option 6 and option 4.
  2. Does toJSON(vars) list every environment variable?
  3. Is a secret extracted from toJSON(secrets) still masked when echoed, including one containing a quote or a backslash?
  4. Does a required: false secret the caller does not map still resolve its environment value?
  5. Does secrets[expr] work with a computed key?

Related: #2031, #2070, #929.

Activity

  1. ptr727 commented on Nov 1, 2026

    @ptr727
    OwnerAuthor

    Decision needed (decision triage, 2026-11-01)

    Question: Before deploy-site-task.yml stops passing its deploy hook 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 in toJSON(secrets), the answer that decides which transfer design follows?

    Options, recommended first:

    1. Approve the probe, then option 6 or 4 by result (recommended): GitHub's docs do not answer probe question 1 and the design turns on it; the hub settled an earlier environment-secret question the same way with a live cross-repo run against Blog. The probe is a write, so the go-ahead names the repository. Option 6 also needs Declare Per-Environment Secrets and Variables Once and Check Them From the Hub #2070 decided first. No work in flight.
    2. Adopt option 4 (caller's job runs verify), no probe: No secret crosses the boundary, but the hub stops guaranteeing the live check runs, the reason WORKFLOW.md D4.6 gives for the hub owning it. No work in flight.
    3. Keep named pairs until a third site needs one: Blog is the only deploy-environment adopter with two pairs; each new pair costs a hub release but no design risk. Defers this and leaves Declare Per-Environment Secrets and Variables Once and Check Them From the Hub #2070 independent. No work in flight.
    4. Name-list input plus secrets[name]: Needs its own allowlist, computed keys are documented only for literals, and it carries option 6's visibility unknown, so still needs the probe. No work in flight.

    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)

  2. added
    decisionA question waiting on the maintainer, alone or beside whatever else the issue carries
    on Nov 1, 2026
  3. ptr727 commented on Nov 1, 2026

    @ptr727
    OwnerAuthor

    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.

  4. removed
    decisionA question waiting on the maintainer, alone or beside whatever else the issue carries
    on Nov 1, 2026
  5. added
    deferredA decision answered not yet, waiting on the condition its answer comment states
    on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    deferredA decision answered not yet, waiting on the condition its answer comment statesenhancementNew feature or requestgithub_actionsPull requests that update GitHub Actions code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions