skip to content

In GitHub Actions, which secret wins when org, repo, and environment names collide?

level: middleimportance: should knowfreq 62%

answer

  1. three places a secret can live
  2. narrowest definition wins
  3. one of the three needs a job-level key
  4. the vars context behaves identically
  5. removing an override does not remove the name

basics

~20 s

The most specific scope wins: an environment secret overrides a repository secret, which overrides an organization secret of the same name. The environment value only applies to jobs that declare that environment; other jobs still see the repository or organization value.

solid answer

~50 s

GitHub Actions resolves a secret name from the narrowest scope outward. If the same name exists as an environment secret, a repository secret, and an organization secret, a job that declares `environment: production` gets the environment value; a job without an `environment:` key never sees it and falls back to the repository secret, and only if that is absent does the organization secret apply. Organization secrets additionally have a visibility setting — all repositories, private repositories, or a selected list — so a repository outside that list resolves nothing at all. The identical precedence applies to configuration variables read through the `vars` context, which is the non-sensitive counterpart to `secrets`. In practice this is how teams give staging and production different values for one name such as `DEPLOY_KEY`, while a shared org secret supplies the default everywhere else.

code

yaml · 19 lines
yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # no environment: key here, so secrets.API_KEY resolves the
      # repository (or organization) value, never the production one
      - run: ./gradlew test
        env:
          API_KEY: ${{ secrets.API_KEY }}

  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment: production        # unlocks the production-scoped secrets
    steps:
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}   # production override wins
          REGION: ${{ vars.REGION }}        # same precedence, non-sensitive

go deeper

for a junior

Know the three places a secret can live and that a job must declare environment: to use an environment secret. Recall that the most specific definition is the one the job receives.

for a middle

State the precedence order confidently and explain the design it enables: one workflow name, per-environment values, with build jobs unable to reach the production one. Mention that vars behaves the same way.

for a senior

Show how you use the scoping as a control — credentials on environments, protection rules on top, secrets: inherit avoided for workflows you do not own — and name the trap where deleting an override silently resurfaces a broader value.

for a principal

Own the organization-wide layout: which credentials are org-level with restricted visibility, which live on environments, how rotation is coordinated across repositories, and how you audit which workflows can resolve which names.

## Three scopes, one name GitHub Actions can store a secret at three levels: - **Organization** — defined once for the org, with a visibility setting: all repositories, all private repositories, or a selected list of repositories. - **Repository** — defined on one repository, available to every workflow in it (subject to the fork rule below). - **Environment** — defined on a named environment such as `staging` or `production`, available only to a job that declares `environment: <name>`. When the same name exists at more than one level, the **most specific scope wins**: environment overrides repository, repository overrides organization. There is no merge and no warning — the job simply resolves one value. ## Why this ordering is useful The pattern it enables is one workflow, one secret name, several targets. A deploy job parameterised by environment can be written once: jobs: deploy: environment: ${{ inputs.target }} steps: - run: ./deploy.sh env: API_KEY: ${{ secrets.API_KEY }} Staging and production each define their own `API_KEY`, and the workflow text never mentions which one it is using. Meanwhile an organization-level `API_KEY` can serve every repository that has not overridden it. The corollary matters just as much: **a job with no `environment:` key cannot reach environment secrets at all.** This is the mechanism behind the common review question "how do you make sure only the deploy job can see the production credential?" — you put the credential on the `production` environment, and every job that does not declare that environment resolves the name to nothing (or to a less-privileged repository value). ## Variables follow the same rules Non-sensitive configuration uses the parallel `vars` context, with the same three scopes and the same precedence. `${{ vars.REGION }}` resolves the environment variable definition first, then repository, then organization. Variables are readable in the UI and are printed in logs unmasked, so they are for things like a region name, a URL, or a feature flag — never a credential. Separately, `env:` blocks in workflow YAML have their own precedence that people often conflate with this: a step-level `env` entry overrides the same name at job level, which overrides workflow level. That is ordinary variable shadowing inside the file and is unrelated to where a secret is stored. ## Inheritance across workflow boundaries Secrets do not flow automatically into a reusable workflow called with `workflow_call`. The caller either lists them explicitly under `secrets:` or passes `secrets: inherit`, which forwards the secrets available to the caller. `inherit` is convenient and blunt — it hands the callee everything, so a reusable workflow you do not control should get named secrets instead. A composite action, by contrast, is not a trust boundary in this sense: it runs inside the calling job and can read the job's environment, which is why secrets are passed to it as declared inputs and why third-party actions are pinned. ## Gotchas worth naming in an interview - **Fork pull requests** get no secrets at any scope, so precedence never comes into play there. - **Organization visibility** can silently produce an empty value: the name exists in the org but this repository is not on the selected list. - **Deleted overrides resurface the parent value.** Removing an environment secret does not disable the name; the repository or org value quietly takes over, which can point a production job at a staging credential. - **Names are case-insensitive** and cannot start with `GITHUB_`, so you cannot shadow the built-in namespace. - **You cannot read a secret back** to check which one resolved. Diagnose by scope inspection and by asserting on an effect (for example a step that fails cleanly when the value is empty), not by echoing it. ## How to answer concisely "Narrowest scope wins — environment, then repository, then organization — and environment secrets require the job to declare that environment, which is exactly how you keep production credentials away from build and test jobs."

  • How do you stop a build job in the same workflow from reading a production credential?
    Store it as an environment secret on `production` and declare `environment: production` only on the deploy job. Jobs that do not name the environment cannot resolve environment secrets at all. Add protection rules on that environment so the deploy job additionally waits for approval before it starts and before the secret is issued.
  • Does a reusable workflow called with workflow_call automatically receive the caller's secrets?
    No. The caller must declare each secret under `secrets:` in the `with`-style block, or pass `secrets: inherit` to forward everything available to it. `inherit` is convenient for workflows inside your own organization but hands the callee every secret, so a reusable workflow you do not own should receive only the names it needs.
  • When would you use the vars context instead of secrets?
    For non-sensitive configuration that differs per repository or environment — a region, a base URL, a feature flag, an image tag prefix. Variables use the same three scopes and the same precedence, but they are readable in the UI and appear unmasked in logs, so anything whose disclosure matters must remain a secret.

saying these in an interview costs you the question

  • Says the organization secret wins because it is broader
  • Thinks environment secrets are readable from any job
  • Expects colliding scopes to merge or to error
  • Confuses env: block shadowing with secret scope
  • Assumes workflow_call passes secrets automatically

context