AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL unreviewed Apache-2.0 Self-run

Github Actions

skill-bcgov-agent-skills-github-actions · by bcgov

BC Gov hardening patterns for GitHub Actions workflows — deny-all `permissions: {}` baseline with per-job step-ups, first-party tag vs third-party SHA pinning, fork-gate, single results-aggregator required check, pinned ubuntu-24.04 runners, script-injection-safe env handling, and signed-commit ruleset interaction. Covers `.github/workflows/*.yml`, `dependabot.yml`, and the branch-protection / re…

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add skill-bcgov-agent-skills-github-actions

Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.

Security review

⚠ Flagged

1 finding(s); flagged for manual review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures
  • high Destructive filesystem operation.

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Reliability & compatibility

Not yet reviewed
0 installs to date
no reviews yet
3d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Github Actions? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

GitHub Actions — BC Gov Hardening Patterns

Use When

  • Authoring or reviewing any file under .github/workflows/ in a BC Gov repo.
  • Wiring branch protection / a repository ruleset and need to pick the required status check(s).
  • Hardening a public repo's pipeline against fork-PR token exfiltration, action tag hijacks, or script injection from PR titles / branch names / issue bodies.
  • Debugging Resource not accessible by integration, Refusing to allow a Personal Access Token to create workflow, or other GITHUB_TOKEN permission errors.
  • Adding or auditing Dependabot — both .github/dependabot.yml and an auto-merge workflow for low-risk bumps.
  • Picking a runner OS / version, action pin format (tag vs SHA), or permissions: scope set for a new job.
  • Publishing artifacts, npm packages, container images, or GitHub Pages from a workflow and need OIDC / least-privilege guidance.
  • Migrating an existing repo onto BC Gov defaults (signed commits, single required check, no force-push).

Don't Use When

  • Authoring generic GitHub Actions workflows with no BC Gov / security angle — prefer the upstream GitHub Actions docs or a general CI catalogue.
  • Authoring reusable / callable workflows that deploy or validate into the BC Gov Azure Landing Zone (e.g., platform-team-owned deploy templates, OpenShift or AKS deploy callers) — defer to the BC Gov gha-workflows skill (when installed in your agent environment); this skill covers only the workflow chassis (permissions, pinning, fork-gate, aggregator, ruleset) around such callers, not their deploy logic.
  • Designing the workflow's domain logic (Bicep what-if, Terraform plan, npm publish mechanics, Pages build) — those belong to the domain skill: sibling [azure-networking](../azure-networking/SKILL.md) in this catalogue, or upstream Microsoft azure-prepare / azure-deploy skills (if installed). This skill covers only the workflow chassis around them.
  • Provisioning the Azure infrastructure the workflow deploys to — use the matching domain skill (upstream Microsoft azure-* skills, if installed).
  • Defining branch-protection rules in code via a Terraform provider — out of scope; this skill describes the settings a ruleset should carry, not the IaC to apply them.
  • Debugging self-hosted runner setup, ARC (Actions Runner Controller), or runner-image customization — out of scope.

Workflow

  1. Set repository defaults before writing any workflow. In the repo's Settings → Actions → General: set Workflow permissions to Read repository contents and packages permissions (default-deny write); leave Allow GitHub Actions to create and approve pull requests off unless a workflow truly needs it; under Fork pull request workflows from outside collaborators pick Require approval for first-time contributors who are new to GitHub at minimum. These defaults make every later permissions: block additive instead of subtractive.
  2. Pin the runner OS to a specific version label, never the *-latest aliases. Use runs-on: ubuntu-24.04 for Linux jobs, runs-on: windows-2025 (or windows-2022) for Windows jobs, and runs-on: macos-26 (or macos-15) for macOS jobs — or whichever specific label the repo standardises on. A pinned runner makes the OS upgrade a deliberate PR you review, not a silent rollover during an incident; ubuntu-latest, windows-latest, and macos-latest each roll forward on GitHub's own cadence and the breaking-change blast radius differs by platform (Xcode majors and SDK churn on macOS, Visual Studio component updates on Windows, glibc / Python / Node default bumps on Ubuntu).
  3. Pin actions by class: first-party actions owned by GitHub (actions/*) pin to a major tag (actions/checkout@v6) — GitHub guarantees those tags are forward-only and Dependabot still bumps majors. Third-party actions pin to an immutable commit SHA with the human-readable version in a trailing comment (uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0) so a tag hijack cannot swap the action's code under you.
  4. Declare top-level least-privilege permissions: — prefer the deny-all permissions: {} baseline. Start every workflow with permissions: {} (empty mapping = deny everything) and add only the specific scopes each job needs (e.g. contents: read on the checkout job, packages: write on the publish job, pages: write + id-token: write on the deploy job, pull-requests: write on the bot-comment job). Anything that doesn't appear in the block is implicitly denied. If a workflow legitimately needs contents: read everywhere (e.g. all jobs check out the repo), set that at workflow scope and keep every elevation per-job.
  5. Harden every non-trivial run: block with set -euo pipefail as the first line (Bash is the default shell on ubuntu-* runners). Chain related commands with && rather than newlines when you want them to be one logical step that stops on the first failure. Without pipefail, a failure on the left side of a | is silently dropped; without -u, an unset variable becomes an empty string that quietly corrupts commands.
  6. Add a fork-gate as the first job on any pull_request workflow in a public repo. The job compares github.event.pull_request.head.repo.full_name to github.repository and exit 1s when they differ, so maintainers re-land fork PRs from an in-repo branch after review. This keeps CI minutes (and any future token) off untrusted code.
  7. Wire a results aggregator job with if: always(), needs: [every-other-job], and a step that fails when any needs.*.result is failure or cancelled. Make this single job the only required status check in branch protection — then renaming or splitting upstream jobs never requires touching the ruleset. Treat skipped as a pass so conditional jobs don't false-fail the gate.
  8. Constrain the trigger surface. On push/pull_request, add paths: filters so the workflow only runs when files it cares about change (skills/** for the publish workflow, docs/** for Pages, etc.). On scheduled or long-running workflows add concurrency: { group: "", cancel-in-progress: }true for build/test, false for deploy / publish to avoid mid-flight cancellation.
  9. Pass untrusted input only through env:, never via ${{ … }} interpolation in a run: block. PR titles, branch names, issue bodies, and PR head SHAs are attacker-controlled. Bind them to environment variables in an env: map and reference them with shell quoting ("$PR_TITLE") so a ; rm -rf / payload becomes a literal string, not a command.
  10. Use pull_request_target only when you must mutate the PR (approve, label, auto-merge) and never combine it with actions/checkout of the PR head without an explicit reviewer gate — pull_request_target runs with the base repo's tokens. Limit those workflows to if: github.event.pull_request.user.login == 'dependabot[bot]' (or an equivalent allow-list) and a tight concurrency group to avoid race conditions on synchronize.
  11. Configure Dependabot in two files. .github/dependabot.yml enables the github-actions ecosystem (and any others — npm, pip, uv, docker) so every pinned action and runtime gets PR-bumped, and uses a Conventional-Commit commit-message.prefix keyed to the ecosystem (deps(actions), deps(npm), deps(pip), deps(uv)). Pair it with a small dependabot-auto-merge.yml triggered on pull_request_target that approves and enables auto-merge only for dependabot[bot] PRs, with a deny-all permissions: {} at the workflow scope and permissions: { contents: write, pull-requests: write } scoped to the single auto-merge job.
  12. Require signed commits on main. Either via the GitHub branch ruleset (Settings → Rules → Rulesets) or the legacy branch protection page: enable Require signed commits, Require a pull request before merging, point Require status checks at the single results job, set Require strict status checks, and disable Allow force pushes and Allow deletions. Signed commits + a single required check is the BC Gov default — keep workflow PRs themselves signed so they don't lock you out.
  13. Use Conventional Commits for workflow / action PRs, with the scope derived from the primary directory touched: ci(workflows): ... for .github/workflows/** edits, ci(scripts): ... for .github/scripts/**, deps(actions): ... for pinned-action bumps. This is what the BC Gov coding standard expects and what Dependabot is already configured to emit — keep human PRs consistent so changelogs and release-notes generation stay clean.
  14. Validate locally before opening a PR. Lint YAML (yamllint .github/workflows/), shellcheck any non-trivial run: blocks (or use actionlint which extracts and shellchecks them for you), and dry-run with act for fast iteration on the matrix surface. For this repo specifically, also run make lint test validate so the results check passes on the first push.

Rules

  • Always pin first-party actions/* to a major tag, third-party actions to an immutable commit SHA + version comment, and runner tools/binaries to a specific release version (never pipe unpinned installer scripts from raw CDN URLs). (Why: GitHub forward-guarantees actions/* major tags; third-party tags are mutable and a hijacked release can ship an exfil payload to every workflow that uses it; unpinned CDN scripts are a security and rate-limiting risk. Dependabot bumps both actions styles, so you lose nothing.)
  • Always declare a top-level permissions: block — permissions: {} (deny-all) as the strongest default, or permissions: { contents: read } when every job legitimately needs to read the repo — and step up to any write scope only on the single job that needs it. (Why: workflows inherit the repo's Workflow permissions default; setting it at workflow scope makes least-privilege a code-review artefact instead of a UI checkbox that can drift. permissions: {} is the BC Gov standard baseline.)
  • Always start non-trivial run: blocks with set -euo pipefail and chain commands you want short-circuited with &&. (Why: Bash without pipefail silently swallows failures upstream of a |; without -u an unset variable becomes "" and a missing secret turns into a quietly successful no-op instead of a loud failure. && makes the dependency between commands part of the shell script, not assumed.)
  • Always pin the runner to a specific OS version label — ubuntu-24.04 for Linux, windows-2025 (or windows-2022) for Windows, macos-26 (or macos-15) for macOS — never the *-latest aliases (ubuntu-latest, windows-latest, macos-latest). (Why: every *-latest alias silently rolls to a new major version on GitHub's own cadence — Ubuntu LTS roughly yearly, Windows Server every few years, macOS once a year — and the breaking-change blast radius is real on every platform: Xcode majors and SDK churn on macOS, Visual Studio component updates on Windows, glibc / Python / Node default bumps on Ubuntu. A pinned label makes the upgrade a deliberate, reviewable PR instead of an incident-time surprise. GitHub only supports the latest two GA images per OS, so the pinned label also tells you when the OS itself is approaching deprecation.)
  • Always make a single results aggregator job the only required status check in branch protection. (Why: required-check names are duplicated into the ruleset by string; renaming or splitting any underlying job is otherwise a coordinated change across two systems. The aggregator decouples them and if: always() lets it run even when an upstream job fails.)
  • Always add a fork-gate job that fails fast on PRs opened from a fork in any public-repo pull_request workflow. (Why: even with permissions: read, fork PRs spend CI minutes and any future expansion of the workflow's privileges immediately becomes a token-exfil hole. Maintainers re-land fork work from an in-repo branch.)
  • Always pass attacker-controlled values (github.event.pull_request.title, head_ref, issue.body, comment.body) through env: and quote them in shell. (Why: ${{ … }} is interpolated by the runner before the shell parses, so a payload like "; curl …" becomes a command, not a string. env: + "$VAR" is the only safe pattern.)
  • Always check the actor on a pull_request_target workflow before doing anything that uses the elevated token. (Why: pull_request_target runs in the base repo's context with its secrets; without a strict actor gate, a fork PR can trigger a privileged path.)
  • Never check out the PR's head SHA on a pull_request_target workflow and then run code from it (build/test/install hooks). (Why: that re-introduces the untrusted code into a privileged context — the exact attack pull_request_target was designed around.)
  • Never use a long-lived Personal Access Token where the built-in GITHUB_TOKEN or OIDC will work. (Why: PATs grant their owner's full permissions, don't expire on PR close, and bypass the per-workflow permissions: ceiling; they also block on the owner leaving the org.)
  • Always log into a public cloud (Azure or AWS) via OIDC federation, not a stored secret credential. Grant id-token: write on the deploy job only, use the official provider action SHA-pinned per Rule #1 (azure/login or aws-actions/configure-aws-credentials), and pin the cloud-side trust by a subject claim scoped to repo + branch / environment (repo:OWNER/REPO:environment:prod is the strongest shape because it composes with GitHub Environments' required-reviewers gate). (Why: a long-lived AZURE_CLIENT_SECRET or AWS access key in secrets.* is a stealable, non-expiring, environment-blind credential — exactly the blast-radius profile that an action-tag hijack, a pull_request_target slip, or an insider is designed to harvest. OIDC tokens expire in ~5 minutes, are minted only when id-token: write is granted on a specific job, and the cloud-side subject pins the GitHub repo + ref so a fork or a non-main branch cannot exchange them.)
  • Never disable signed-commit enforcement on main, even temporarily. (Why: the BC Gov ruleset treats it as a hard floor; disabling it for one merge requires re-enabling and means any unsigned commit that landed during the gap is permanent.)

Examples

  • "Add CI to a new repo so PRs run lint and tests" → workflow with on: pull_request, permissions: { contents: read }, jobs fork-gate → lint-tests → results, actions/checkout@v6 + a pinned setup action, runs-on: ubuntu-24.04, single required check = results.
  • "Publish an npm package from main on a path change" → workflow with on: push: { branches: [main], paths: ["packages/**"] }, permissions: { contents: read, packages: write } on the publish job only, actions/setup-node@v6 with registry-url: https://npm.pkg.github.com, NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}, and a version-skip step that no-ops when the version is already on the registry.
  • "Deploy GitHub Pages from docs/" → workflow with permissions: { contents: read, pages: write, id-token: write } at workflow scope, concurrency: { group: pages, cancel-in-progress: false }, build job → deploy job, actions/configure-pages@v6, actions/upload-pages-artifact@v5, actions/deploy-pages@v5.
  • "Deploy to Azure / AWS from a workflow without storing cloud secrets" → OIDC federation on the deploy job: permissions: { contents: read, id-token: write }, environment: prod, then uses: azure/login@ # v3.0.0 with only client-id / tenant-id / subscription-id (no client-secret), or uses: aws-actions/configure-aws-credentials@ # v6 with role-to-assume + aws-region (no aws-access-key-id / aws-secret-access-key). Cloud-side: create a Federated Identity Creden

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.