{"post":{"slug":"how-to-write-a-github-action-2026","title":"How to write a GitHub Action in 2026: CI workflows, secrets, and reuse","excerpt":"GitHub Actions is the de facto CI/CD platform for open-source and most private projects. This guide covers writing workflows, managing secrets, and reusing logic.","category":"code","date":"March 1, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">G</span>itHub Actions powers CI/CD for millions of projects in 2026 — it is the platform most developers will reach for first for automated testing, linting, and deployment. The YAML syntax is approachable but has enough depth to trip up even experienced engineers. This guide covers the patterns that matter in production.\n\n## What changed in 2026\n\n- **Actions Runner Controller (ARC) v2** is stable for self-hosted Kubernetes runners, giving teams autoscaling CI without per-seat costs.\n- **GitHub-hosted runners include 4 vCPUs** for public repos and paid tiers; `ubuntu-24.04` is the current default label.\n- **Larger GitHub-hosted runners** (16 vCPUs, 64 GB RAM) are available for orgs that need faster build times.\n- **Attestations and artifact signing** via `actions/attest-build-provenance` are now expected for packages published to npm or container registries.\n- **Merge queues** became generally available and integrate with required status checks — the recommended pattern for high-velocity repos.\n\n## Anatomy of a workflow file\n\n```yaml\n# .github/workflows/ci.yml\nname: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '22'\n          cache: 'npm'\n      - run: npm ci\n      - run: npm test\n```\n\nKey concepts: `on` defines triggers, `jobs` are parallel by default, `steps` run sequentially within a job.\n\n## Pinning action versions securely\n\n```yaml\n# BAD — @v4 tag can be force-pushed to a different commit\n- uses: actions/checkout@v4\n\n# GOOD — pinned to a specific commit SHA\n- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2\n```\n\nPin to full SHAs for actions in security-sensitive workflows. Tools like `Dependabot` or `pin-github-action` automate this.\n\n## Secrets and environment variables\n\n```yaml\njobs:\n  deploy:\n    runs-on: ubuntu-24.04\n    environment: production   # requires manual approval if configured\n    env:\n      NODE_ENV: production\n    steps:\n      - uses: actions/checkout@v4\n      - name: Deploy\n        env:\n          API_KEY: ${{ secrets.DEPLOY_API_KEY }}\n        run: ./scripts/deploy.sh\n```\n\n`secrets.*` values are masked in logs — they never appear as plain text. `vars.*` (repository variables) are for non-sensitive config.\n\n## Dependency caching\n\n```yaml\n- uses: actions/setup-node@v4\n  with:\n    node-version: '22'\n    cache: 'npm'         # caches ~/.npm based on package-lock.json hash\n```\n\nFor more control:\n\n```yaml\n- uses: actions/cache@v4\n  with:\n    path: ~/.cache/pip\n    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}\n    restore-keys: |\n      ${{ runner.os }}-pip-\n```\n\nGood caching cuts install times from ~2 minutes to ~10 seconds on warm runs.\n\n## Build matrix\n\n```yaml\njobs:\n  test:\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        node-version: ['20', '22', '23']\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: npm ci && npm test\n```\n\n## Workflow comparison\n\n| Pattern | When to use |\n| --- | --- |\n| `push` trigger | Run on every commit to main |\n| `pull_request` trigger | Run on every PR — block merge on failure |\n| `workflow_dispatch` | Manual run with optional inputs |\n| `schedule` (cron) | Nightly builds, dependency scans |\n| `workflow_call` | Reusable workflow called from another |\n| `release` trigger | Publish artifacts on GitHub release |\n\n## Docker build and push example\n\n```yaml\njobs:\n  build-push:\n    runs-on: ubuntu-24.04\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - uses: actions/checkout@v4\n      - uses: docker/setup-buildx-action@v3\n      - uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - uses: docker/build-push-action@v6\n        with:\n          push: true\n          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n```\n\n## How to pick between actions approaches\n\n| Need | Pattern |\n| --- | --- |\n| Share steps within one repo | Composite action in `.github/actions/` |\n| Share full workflow across repos | Reusable workflow with `workflow_call` |\n| Call a third-party action | Marketplace action (pin to SHA) |\n| Run a script inline | `run:` step |\n| Deploy to a cloud provider | Official action (AWS, GCP, Azure) |\n\n## Common mistakes\n\n**Not caching dependencies.** A cold `npm ci` or `pip install` adds 1–3 minutes to every run.\n\n**Over-permissioned workflows.** The default `GITHUB_TOKEN` permissions are broad. Set `permissions:` explicitly in every workflow to the minimum required.\n\n**Hardcoded tokens or passwords.** Even if rotated later, they appear in git history. Always use `secrets.*`.\n\n**Not using `continue-on-error: true`** for steps that are allowed to fail (e.g., coverage upload) — a failure there should not block the entire job.\n\n**Running all steps as one job.** Separate fast linting from slow integration tests. Fast checks give quick feedback; slow builds run only on success.\n\n## What to skip\n\n- **Self-hosted runners for every project** — use GitHub-hosted runners unless you need specific hardware, GPU access, or private network access.\n- **Complex bash inline in workflow YAML** — move logic into scripts in the repo; YAML strings are painful to test and debug.\n- **Third-party actions you have not reviewed** — the Actions marketplace has many low-quality or abandoned actions; prefer official or well-maintained ones.\n\n## FAQ\n\n**How do I pass data between steps in a job?**\nUse `$GITHUB_OUTPUT`: `echo \"my_key=my_value\" >> \"$GITHUB_OUTPUT\"` and reference with `${{ steps.step-id.outputs.my_key }}`.\n\n**How do I trigger a workflow from another workflow?**\nUse `workflow_dispatch` with `github.rest.actions.createWorkflowDispatch` via the API, or use `workflow_call` for synchronous reusable workflows.\n\n**How do I debug a failing workflow?**\nEnable debug logging by setting the secret `ACTIONS_STEP_DEBUG` to `true`. Alternatively, use `tmate` or the `nektos/act` local runner for local testing.\n\n**What are environments used for?**\nEnvironments add protection rules (required reviewers, wait timers) to deployment jobs. Use them for `staging` and `production` deploys to require manual approval.\n\n## Where to go next\n\n- [How to containerize an app in 2026](/blog/how-to-containerize-an-app-2026)\n- [How to set up pre-commit hooks in 2026](/blog/how-to-set-up-pre-commit-hooks-2026)\n- [How to set up a dev environment in 2026](/blog/how-to-set-up-a-dev-environment-2026)","hue":311,"metaTitle":"How to Write a GitHub Action in 2026: CI Workflows Guide","metaDescription":"Write GitHub Actions workflows in 2026 — triggers, jobs, secrets, caching, and reusable actions. Practical CI/CD patterns for modern projects.","keywords":["how to write a github action 2026","github actions workflow tutorial","github actions ci cd 2026","github actions secrets","github actions reusable workflow","github actions caching","github actions docker build","github actions node example","github actions best practices 2026","github actions composite action"],"tldr":["**A workflow is a YAML file in `.github/workflows/`** — triggered by events like push, PR, or a cron schedule.","**Pin action versions to a full commit SHA** — `@v4` tags can be rewritten; pinning to a SHA prevents supply-chain attacks.","**Store secrets in Settings > Secrets** and reference them as `${{ secrets.NAME }}` — never hardcode credentials.","**Use reusable workflows or composite actions** to share logic across repositories without copy-pasting YAML.","**Skip:** running long jobs on the default `ubuntu-latest` runner for every commit — split into fast check and slow build/deploy jobs."],"readTime":"4 min read","wordCount":917},"related":[{"slug":"how-to-deploy-with-github-actions-2026","title":"How to deploy with GitHub Actions in 2026","excerpt":"GitHub Actions is the default CI/CD choice for most teams in 2026. This guide covers fast, secure deployment pipelines that actually reach production safely.","category":"code","date":"March 12, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":306},{"slug":"how-to-containerize-an-app-2026","title":"How to containerize an app in 2026: Docker, layers, and production-ready images","excerpt":"Containerizing an app is now a baseline skill for every backend developer. This guide covers Dockerfiles, multi-stage builds, and production best practices.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":310},{"slug":"how-to-set-up-ci-cd-2026","title":"How to set up CI/CD in 2026: a practical pipeline from zero to deploy","excerpt":"A CI/CD pipeline that catches bugs before production is one of the highest-ROI investments a team can make. This 2026 guide walks through the full setup.","category":"code","date":"February 26, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":311}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-write-a-cover-letter-2026","title":"How to write a cover letter in 2026: a concrete step-by-step guide","excerpt":"Most cover letters are ignored because they are generic. Here is how to write one in 2026 that hiring managers actually read — in under an hour.","category":"guides","date":"March 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":75},"older":{"slug":"how-to-write-a-readme-2026","title":"How to write a README in 2026","excerpt":"A great README is the first thing contributors and users see. This guide covers the structure, tone, and tooling that make READMEs actually useful in 2026.","category":"code","date":"March 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":305}}