Purpose: Automatically deploy your React app to GitHub Pages whenever you push to the master branch.
What It Does:
- Builds your React app for production
- Uploads the build as an artifact
- Deploys to GitHub Pages
- Provides a live public URL
Your Results:
- ✅ Total duration: 43 seconds
- ✅ Build job: 27 seconds
- ✅ Deploy job: 10 seconds
- ✅ Live URL:
https://kirti.github.io/github-actions-pr...
File: .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [ master ] # Only deploy from master
workflow_dispatch: # Allow manual trigger
# Sets permissions for GITHUB_TOKEN
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
CI: false # Treat warnings as warnings, not errors
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./build
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build # Wait for build to complete
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4name: Deploy to GitHub Pages- Human-readable name shown in Actions tab
- Appears in status badges and notifications
on:
push:
branches: [ master ]
workflow_dispatch:push: branches: [ master ]
- Only triggers when pushing to master branch
- Won't run on feature branches
- Why? We only want to deploy production code, not development branches
workflow_dispatch:
- Allows manual triggering from GitHub UI
- Useful for:
- Redeploying without new commits
- Testing the deployment workflow
- Emergency deployments
permissions:
contents: read
pages: write
id-token: writeWhat are permissions?
- Controls what
GITHUB_TOKENcan do - By default, workflows have broad permissions
- Explicitly defining them follows security best practice (principle of least privilege)
Breaking it down:
| Permission | Purpose |
|---|---|
contents: read |
Read repository code (for checkout) |
pages: write |
Write/publish to GitHub Pages |
id-token: write |
Generate OIDC token for secure deployment |
Why OIDC (id-token)?
- More secure than using passwords/tokens
- Temporary credentials that expire
- GitHub generates them on-demand
- Industry standard for authentication
concurrency:
group: "pages"
cancel-in-progress: falseWhat is concurrency control?
- Prevents multiple deployments from running simultaneously
- Ensures only one deployment happens at a time
group: "pages"
- Creates a concurrency group named "pages"
- All workflows with the same group share the limit
- If a workflow is running, new ones wait
cancel-in-progress: false
- Don't cancel running deployments when new one starts
- Let them finish, queue the new one
- Why false? Canceling mid-deployment could leave the site in a broken state
Alternative:
concurrency:
group: "pages"
cancel-in-progress: true # Cancel old deployment, start new oneUse true if you want latest code deployed faster (but risks incomplete deployments).
jobs:
build:
runs-on: ubuntu-latestFirst job: Build the React app
- name: Checkout
uses: actions/checkout@v4- Downloads repository code to runner
- Required for accessing source files
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'node-version: '22'
- Uses Node.js 22 (latest stable)
- Ensures consistent builds
cache: 'npm'
- Caches npm dependencies
- Speeds up subsequent builds
- Automatically invalidates when package-lock.json changes
- name: Install dependencies
run: npm ciWhy npm ci?
- Clean install (removes node_modules first)
- Uses exact versions from package-lock.json
- Faster and more reliable in CI
- Won't modify package-lock.json
- name: Build
run: npm run build
env:
CI: falsenpm run build
- Runs the build script from package.json
- For React: creates optimized production build in
build/folder - Minifies JS, optimizes assets, etc.
CI: false
- By default,
CI=truetreats warnings as errors - In production React builds, warnings will fail the build
- Setting
CI=falsetreats warnings as warnings - Use case: ESLint warnings shouldn't block deployment
When to use CI: true?
env:
CI: trueUse in pull requests to enforce strict quality checks.
- name: Setup Pages
uses: actions/configure-pages@v4What does this do?
- Configures GitHub Pages environment
- Sets up base URL and path
- Prepares deployment metadata
- Required before uploading artifacts
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./buildupload-pages-artifact@v3
- Special artifact uploader for GitHub Pages
- Different from regular
upload-artifact@v4 - Validates content is suitable for web hosting
path: ./build
- Location of built files (React's build output)
- Everything in
build/folder gets deployed
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: buildenvironment:
- Uses GitHub Environments feature
github-pagesis a special built-in environment- Provides deployment history and rollback
url: ${{ steps.deployment.outputs.page_url }}
- Displays the deployed URL in the UI
- Clickable link to your live site
- Retrieved from deployment step's output
needs: build
- Critical! Deploy only runs after build succeeds
- If build fails, deploy is skipped
- Creates job dependency chain
Job dependency visualization:
build (27s)
↓
deploy (10s)
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4id: deployment
- Gives this step an ID
- Allows referencing outputs:
${{ steps.deployment.outputs.page_url }}
actions/deploy-pages@v4
- Official GitHub action for Pages deployment
- Downloads the artifact from build job
- Publishes to GitHub Pages
- Returns the live URL
Add the homepage field:
{
"name": "my-react-app",
"version": "0.1.0",
"homepage": "https://USERNAME.github.io/REPO-NAME",
"dependencies": {
...
}
}Why is this needed?
- React uses this for asset paths
- Without it, CSS/JS files won't load correctly
- Must match your GitHub Pages URL exactly
How React uses it:
// Without homepage:
<script src="/static/js/main.js"></script> // ❌ 404 error
// With homepage:
<script src="/REPO-NAME/static/js/main.js"></script> // ✅ Works!Enable GitHub Pages:
- Repository → Settings → Pages
- Source: "GitHub Actions" (not "Deploy from a branch")
- This allows workflow-based deployments
GitHub Pages requirements:
- ✅ Public repositories: Free
- ❌ Private repositories: Requires GitHub Enterprise (paid)
Your case: Made repository public to use free GitHub Pages.
jobs:
build:
# ... build steps
deploy:
needs: build # Waits for build
# ... deploy stepsBenefits:
- Sequential execution: build → deploy
- Deploy only runs if build succeeds
- Failed builds don't trigger deployments
- Clean separation of concerns
Visualizing:
┌─────────┐
│ build │ (27s)
└────┬────┘
│
↓
┌─────────┐
│ deploy │ (10s)
└─────────┘
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}What are environments?
- Named deployment targets (dev, staging, production)
- Track deployment history
- Support protection rules
- Provide deployment URLs
github-pages environment:
- Special built-in environment for GitHub Pages
- Automatically created when you deploy
- Visible in repo's Environments tab
Benefits:
- Deployment history tracking
- Rollback to previous deployments
- View current deployment status
- Protection rules (required reviewers, wait timers)
Advanced protection rules:
environment:
name: production
# Requires manual approval before deployingWhy use artifacts instead of direct deployment?
# Build job
- uses: actions/upload-pages-artifact@v3
with:
path: ./build
# Deploy job (different runner)
- uses: actions/deploy-pages@v4 # Downloads artifact automaticallyBenefits:
- Separation of concerns: Build and deploy are independent
- Reusability: Can deploy same artifact to multiple environments
- Reliability: Artifact persists even if job fails
- Debugging: Can download artifact to inspect build output
Artifact flow:
Build Job Deploy Job
--------- -----------
npm run build → Download artifact
Create artifact → Publish to Pages
Upload artifact → Return URL
- name: Deploy to GitHub Pages
id: deployment # Give step an ID
uses: actions/deploy-pages@v4
# Later, reference the output:
url: ${{ steps.deployment.outputs.page_url }}How it works:
- Action sets output:
page_url=https://... - Reference with:
steps.STEP_ID.outputs.OUTPUT_NAME - Use in other steps or jobs
Common use case:
- name: Deploy
id: deploy
run: echo "url=https://example.com" >> $GITHUB_OUTPUT
- name: Comment URL
run: echo "Deployed to ${{ steps.deploy.outputs.url }}"┌──────────────────────────────────────────────────────────┐
│ TRIGGER: Push to master │
└────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────┐
│ JOB 1: BUILD │
├──────────────────────┤
│ 1. Checkout code │
│ 2. Setup Node.js 22 │
│ 3. npm ci │
│ 4. npm run build │ → Creates ./build folder
│ 5. Setup Pages │
│ 6. Upload artifact │ → Saves ./build
└──────────┬───────────┘
│
✅ Build Success
│
▼
┌──────────────────────┐
│ JOB 2: DEPLOY │
├──────────────────────┤
│ 1. Download artifact │ ← Gets ./build from Job 1
│ 2. Deploy to Pages │ → Publishes to GitHub
│ 3. Return URL │
└──────────┬───────────┘
│
▼
🌐 LIVE SITE!
https://username.github.io/repo-name
| Phase | Duration | What Happened |
|---|---|---|
| Build | 27s | Installed deps, built React app |
| Deploy | 10s | Uploaded and published to Pages |
| Total | 43s | From push to live site |
Build Job (27s):
- Checkout: ~2s
- Setup Node + Cache restore: ~3s
- npm ci: ~12s
- npm run build: ~8s
- Upload artifact: ~2s
Deploy Job (10s):
- Download artifact: ~2s
- Deploy to Pages: ~6s
- DNS propagation: ~2s
permissions:
contents: read # Only read repo
pages: write # Only write to Pages
id-token: write # Only for OIDC auth✅ Good: Explicit minimal permissions ❌ Bad: Using default broad permissions
environment:
name: production
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}Benefits:
- Different secrets per environment
- Production secrets not accessible in staging
- Better secret management
Configure in GitHub:
- Require PR reviews before merging to main
- Require status checks to pass
- Require signed commits
- Restrict who can push to main
environment:
name: production
# In GitHub UI, add required reviewersBenefits:
- Manual approval before production deploys
- Review deployment changes
- Prevent accidental deployments
# Different URLs for each environment
- develop branch → https://dev.myapp.com
- staging branch → https://staging.myapp.com
- main branch → https://myapp.com# Deploy to blue environment
# Test blue environment
# Switch traffic from green to blue
# Keep green as backup for rollback# Deploy to 10% of users
# Monitor metrics
# Gradually increase to 100%
# Rollback if issues detected- ✅ Two-job deployment pattern (build → deploy)
- ✅ Job dependencies with
needs - ✅ Environments for deployment tracking
- ✅ Concurrency control to prevent conflicts
- ✅ Permissions for security
- ✅ Artifact sharing between jobs
- ✅ Step outputs for dynamic values
- Build time: 27 seconds
- Deploy time: 10 seconds
- Total: 43 seconds from push to live! 🚀
https://kirti.github.io/github-actions-pr...