- Difficulty:
- intermediate
- Tools:
- github-actions, npm
Problem
Deploying by hand from a laptop means the deployed thing was built on that laptop, with whatever happened to be installed that day, and two people deploying at once race each other.
Trigger
A push to the main branch.
Prerequisites
- 01A repository on GitHub with Actions enabled.
- 02A package.json with a build script. Tests are optional: --if-present skips cleanly when there are none.
- 03A deploy credential that can be revoked on its own without rotating anything else.
Steps
- 01
Save the workflow at .github/workflows/deploy.yml github-actions
- 02
Put the deploy credential in repository secrets as DEPLOY_TOKEN github-actions
Settings > Secrets and variables > Actions. Never in the workflow file.
- 03
Write scripts/publish.sh so it is the only step that touches production
Keeping the publish in one script means it can still be run by hand in an emergency.
- 04
Push a trivial change and watch the run github-actions
The Actions tab shows every step. A green run that skipped the tests is not a green run: check that npm test actually ran.
Payload · yaml
name: Deploy
on:
push:
branches: [main]
# One deploy at a time. A newer push cancels an older run.
concurrency:
group: deploy
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test --if-present
- run: npm run build
- name: Publish
run: ./scripts/publish.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Failure modes
- !cancel-in-progress cancels a run that was already mid publish. Make publish.sh safe to run twice, or drop the concurrency block.
- !npm ci fails when package-lock.json is out of date with package.json. Commit the lockfile.
- !The build passes on the runner and fails on a developer machine, or the reverse. The runner is the source of truth from now on.
- !A secret is only masked in logs when it matches exactly. Do not echo credentials, even partially.
JSON-LD · HowTo
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "Build, test and deploy on every push to main",
"description": "A GitHub Actions workflow installs, tests and builds on every push to main and then runs one publish step, cancelling an older run when a newer push arrives.",
"url": "https://automationsanonymous.com/automations/build-test-and-deploy-on-every-push",
"tool": [
{
"@type": "HowToTool",
"name": "github-actions",
"url": "https://automationsanonymous.com/tools/github-actions"
},
{
"@type": "HowToTool",
"name": "npm",
"url": "https://automationsanonymous.com/tools/npm"
}
],
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Save the workflow at .github/workflows/deploy.yml",
"text": "Save the workflow at .github/workflows/deploy.yml",
"url": "https://automationsanonymous.com/tools/github-actions"
},
{
"@type": "HowToStep",
"position": 2,
"name": "Put the deploy credential in repository secrets as DEPLOY_TOKEN",
"text": "Settings > Secrets and variables > Actions. Never in the workflow file.",
"url": "https://automationsanonymous.com/tools/github-actions"
},
{
"@type": "HowToStep",
"position": 3,
"name": "Write scripts/publish.sh so it is the only step that touches production",
"text": "Keeping the publish in one script means it can still be run by hand in an emergency."
},
{
"@type": "HowToStep",
"position": 4,
"name": "Push a trivial change and watch the run",
"text": "The Actions tab shows every step. A green run that skipped the tests is not a green run: check that npm test actually ran.",
"url": "https://automationsanonymous.com/tools/github-actions"
}
],
"datePublished": "2026-09-05T16:53:27.173Z"
}