Yes. Run the build, including the bundler, plugins and tests, in one job that holds no deploy credentials. Pass the finished output as an artifact to a second job that holds the deploy secret, ideally behind an environment with a required reviewer. Build tools then never see the key.
The problem with one big job
A single job that installs, builds, tests and deploys gives every step the same environment. Your bundler, its plugins and every transitive dependency run next to the cloud key. A malicious update to any of them can read it. See is your bundler plugin trusted code for why those tools count as trusted code.
GitHub's security guidance for Actions says that any user with write access to a repository has read access to all secrets configured in it, and that credentials used in workflows should have the least privilege required. It also notes that a secret is redacted from logs only when it is accessible to the runner and exactly matches, so redaction is not a safety net.
What GitHub gives you to split
- Job level permissions. The GITHUB_TOKEN guidance says an action can access the token through the github.token context even if the workflow does not pass it, so you should limit the permissions granted. Set permissions at the job level.
- Environments. GitHub says you can use required reviewers to protect environment secrets, and a job cannot access environment secrets until a reviewer approves.
- Separate jobs. Each job runs in its own runner, so a secret given to the deploy job is not in the build job's environment.
A two job pattern
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: site
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: site
- run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
The deploy script receives only static files. It does not run npm and does not install anything. Pin third party actions to commit SHAs, as in should you pin GitHub Actions to a commit SHA.
Table of who gets what
| Step | Secrets | Token permissions |
|---|---|---|
| Install and build | None | Read contents |
| Unit and browser tests | Only test fixtures | Read contents |
| Deploy | Environment secret behind approval | Only what the deploy needs |
Extra hardening
- Prefer short lived credentials from OIDC over stored keys. See GitHub Actions OIDC to replace long lived cloud secrets.
- Put no network credentials in the build job .npmrc beyond a read only registry token.
- Verify the artifact before deploy. Even a hash check between jobs shows corruption in transit, though it cannot tell you the build was clean.
- Consider a manual approval for production, so a human looks at the commit and the build result before secrets are released.
- Restrict who can edit workflow files, since a workflow change can send secrets somewhere else.
What this does not stop
The build job can still produce a bad artifact if a dependency is malicious. Splitting keeps secrets safe but does not make the output clean. Pair it with lockfile review, dependency scanning and a diff of the output between releases.
Safeguard's policies and gates can check dependency changes before they reach the build, and its compliance and evidence automation can record that approval step. You can run a free scan at gold.safeguard.sh/scan.
Quick check
Do separate jobs share secrets by default?
No. A secret reaches a job only through its configuration, so give the deploy secret to the deploy job alone.
What does a required reviewer on an environment do?
Per GitHub, a job cannot access that environment's secrets until a reviewer approves.
Sources
- GitHub Docs: Security hardening for GitHub Actions
- GitHub Docs: Use GITHUB_TOKEN for authentication in workflows
Last reviewed: 19 October 2026.