Skip to content
Français
CI/CD & Automatisation medium

GitHub Actions environments: targeting a deployment

30 min de lecture

Read this page in French

A GitHub environment is a named deployment target: staging, production, preprod. It carries its own secrets, its own variables, its protection rules, and it turns an ordinary job into a tracked deployment in the repository's Deployments tab. This page shows how to create one, how a job attaches to it, and why isolating secrets per environment beats repository secrets prefixed with PROD_.

What you will learn

  • Create an environment and understand the constraints on its name
  • Attach a job to an environment, in the short and the long form
  • Publish the deployment URL and know where GitHub displays it
  • Isolate the secrets per target rather than by a naming convention
  • Spot the plan limit that makes environments inert on a free private repository

Why not simply repository secrets?

The most widespread practice is to store every secret at repository level and tell them apart with a prefix: STAGING_DEPLOY_TOKEN, PROD_DEPLOY_TOKEN. It works, and it protects nothing.

A repository secret is reachable by every workflow of the repository, so by every job, so by every step. A test workflow installing a compromised dependency can read PROD_DEPLOY_TOKEN as easily as the deployment job. The prefix is a human convention; it does not exist for the execution engine.

An environment secret changes the nature of the control: it is only delivered to jobs that declare the environment, and only after that environment's protection rules have been satisfied. The prefix becomes a real boundary.

Repository secretEnvironment secret
Who can read itEvery job of the repositoryThe jobs declaring the environment
WhenFrom the moment the job startsAfter the protection rules are satisfied
staging/prod separationBy a naming conventionBy the GitHub engine
TraceNoneA recorded deployment, with its state

Creating an environment

The environment is created in the interface, not in the YAML. That is deliberate: it carries governance rules, which must not be editable by anyone pushing a commit to a branch.

  1. Open the repository settings, the Settings > Environments section.

  2. Create the environment with New environment, then give it a name.

    Names are not case sensitive, never exceed 255 characters and are unique within the repository. Production and production therefore designate the same environment.

  3. Configure the protection rules if the target deserves them (approvals, a delay, allowed branches). The detail of those four rules is the subject of the next page.

  4. Add the secrets and variables specific to that target, in the Environment secrets and Environment variables sections.

Attaching a job to an environment

Two forms exist. The short form is enough when the deployment exposes no URL:

jobs:
deploy-staging:
runs-on: ubuntu-24.04
environment: staging
steps:
- name: Deploy to staging
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

The DEPLOY_TOKEN read here is the one from the staging environment, not the repository's. The same workflow, with environment: production, reads the production token under the same variable name. That is what lets you write a single deployment job and parameterise it by its target.

The long form adds the deployment URL:

jobs:
deploy-staging:
runs-on: ubuntu-24.04
environment:
name: staging
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Deploy to staging
id: deploy
run: |
./deploy.sh
echo "url=https://staging.my-app.example.com" >> "$GITHUB_OUTPUT"

Where the URL appears

Filling in url: is not cosmetic, it is what ties the deployment to a reachable address. GitHub shows it in three places:

  • on the repository's Deployments page, next to the environment;
  • in the visualisation graph of the workflow run;
  • as a View deployment button in the pull request timeline, where it applies.

That last point is the most useful day to day: the reviewer of a pull request reaches the preview environment without leaving the pull request, and without anyone pasting the URL into a comment.

Environment variables and secrets: what goes where?

Both mechanisms carry a value specific to the target, but they do not protect the same thing. The rule is binary: if leaking the value has an impact, it is a secret; otherwise it is a variable.

Environment variableEnvironment secret
Visible in the logsYes, in clear textNo, masked by GitHub
Readable after creationYesNo
ExamplesAPI URL, bucket name, region, log levelDeployment token, signing key, password
jobs:
deploy:
runs-on: ubuntu-24.04
environment: production
steps:
- name: Deploy
run: ./deploy.sh --region "$AWS_REGION" --bucket "$BUCKET"
env:
AWS_REGION: ${{ vars.AWS_REGION }} # a variable, not sensitive
BUCKET: ${{ vars.BUCKET }} # a variable, not sensitive
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} # a secret, masked

Note the systematic use of an env: block: a secret interpolated straight into the command line shows up in the debug logs, and the automatic masking does not catch a value transformed along the way.

The order in which secrets resolve

The same name can exist at three levels. GitHub applies a precedence, from the most specific to the most general:

Environment secret (the most specific, wins)
|
v
Repository secret
|
v
Organisation secret (the most general)

That precedence is the tool that lets you override cleanly: an organisation defines a shared REGISTRY_TOKEN, a repository replaces it for its particular case, and the production environment replaces it again with a token carrying wider rights. The workflow, meanwhile, does not change a line.

The downside is a classic diagnosis trap: a secret that "does not take" is often a secret defined at the wrong level, masked by a more specific namesake. When in doubt, temporarily renaming one of the two removes the ambiguity in a single run.

The plan limit to check before building on it

Protection rules and environment secrets are not available on every plan. On a Free plan, they only work on public repositories. A private or internal repository requires at least GitHub Pro for a personal account, or a Team or Enterprise plan for an organisation.

The consequence is sneaky: on a free private repository, the environment is created, the job runs, the deployment appears in the history, but the approval never fires. You believe you have a door, you have a sign. Check the plan before founding a production governance on this mechanism.

Key points

  • An environment is a named deployment target carrying its secrets, its variables and its rules, and turning a job into a tracked deployment.
  • A repository secret is readable by every job of the repository; an environment secret is only delivered to the jobs declaring the environment, after its rules are satisfied.
  • The environment: key exists in a short form (a name) and a long form (name: plus url:).
  • The deployment URL appears on the Deployments page, in the run graph, and as a View deployment button in the pull request.
  • An environment named but non-existent is created automatically with no protection: create it first in Settings > Environments.
  • Secrets resolve from the most specific to the most general: environment, then repository, then organisation.
  • On a Free plan, environment protections and secrets only exist on public repositories: a private one requires Pro, Team or Enterprise.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn