Github actions
Agent skills that give AI agents the operational competence of expert practitioners (Salesforce, AWS, GitHub Actions, web). SKILL.md format; Claude Code plugins. Not test-prep — certification is the scaffold and benchmark, not the product.
npx -y skills add toddkasper/expert-skills --skill github-actionsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Authoring, maintaining, and securing GitHub Actions — CI/CD workflows, triggers, matrix builds, contexts and expressions, reusable and composite workflows, custom actions (action.yml; JS/Docker/composite), self-hosted and GitHub-hosted runners, secrets and OIDC cloud auth, and enterprise governance. Use when building, reviewing, or debugging GitHub Actions workflows, pipelines, release automation, or runner/security policy. Scoped and benchmarked by the GitHub Actions (GH-200) certification blueprint.
SKILL.md
37.4 KB, as published. Nobody here has run it
GitHub Actions — Skills Reference
Overview
This file is an operational playbook, not an exam outline. Each section states the rules an agent must apply when building or reviewing Actions automation: syntax constraints, security invariants, decision criteria, and anti-patterns to catch in review. Benchmarked against the GitHub Actions (GH-200) certification blueprint.
Load this skill when… authoring or reviewing GitHub Actions workflow YAML; designing reusable workflows, composite actions, or custom JS/Docker actions; configuring self-hosted runners, OIDC cloud auth, or enterprise runner/policy governance; or hardening an Actions pipeline against script-injection and supply-chain risks. Not this skill: general Git/GitHub repository administration, branch protection without Actions, or application code in the repository.
Study resources, domain weights, and credential logistics: references/study-resources.md.
Verify steps assume nothing about your tooling — use your project's MCP/automation, the GitHub CLI (
gh) and Actions log/act/workflow-lint, or the GitHub web UI, in that order of preference.
Uncertainty & Escalation
- Always re-verify live — volatile facts: GitHub-hosted runner image versions and pre-installed software (
ubuntu-latest,windows-latest,macos-latestimage mappings change on a rolling basis)[volatile — verify live], action SHA pins for commonly-used actions (e.g.,actions/checkout,actions/setup-node)[volatile — verify live], free-tier minute allotments and per-minute pricing for larger runners[volatile — verify live], reusable workflow nesting limit (currently 10 levels — caller + up to 9 nested workflows)[volatile — verify live], artifact retention defaults and per-repo cache quota[volatile — verify live]. - Live wins: when the live GitHub platform, workflow logs, or official GitHub docs contradict a claim in this file, the live source is authoritative. Log the discrepancy via the Feedback protocol below so the skill can be corrected.
- Escalate to a human — do not silently execute: enterprise runner policy changes (restricting which actions can run org-wide); modifying branch protection rules or required status checks; registering or deregistering self-hosted runners, especially on public repos; force-merging protected branches; rotating or deleting org-level secrets; enabling or disabling Actions for an org or enterprise; any OIDC trust policy change on a production cloud role.
- Confidence taxonomy: every fact in this file is considered stable unless tagged
[volatile — verify live](changes with platform updates) or[opinion — house style](a defensible default, not the only valid choice).
1. Authoring and Managing Workflows
Triggers
Every workflow starts with on:. Pick the narrowest trigger that fits.
| Pattern | Trigger |
|---|---|
| Push/PR to specific branches | push: branches: / pull_request: branches: |
| Manual with inputs | workflow_dispatch: inputs: |
| Called by another workflow | workflow_call: inputs: secrets: |
| Schedule (cron) | schedule: cron: |
| Webhook event (e.g. issue labeled) | on: issues: types: [labeled] |
| After another workflow completes | workflow_run: workflows: types: [completed] |
workflow_dispatch inputs have types: string, boolean, choice, environment, number. Always declare required and default. Pass inputs into a called workflow via with: (for inputs) and secrets: inherit or explicit mapping (for secrets).
Filtering is cost control: use paths:/branches: filters so expensive matrices don't run on unrelated changes; no filter = runs on every push to every branch.
Jobs, Steps, and Needs
- Jobs run in parallel by default. Use
needs: [job-a, job-b]to declare dependencies. A job only starts when all itsneedshave succeeded (or setif: always()/if: failure()to override). - Steps within a job run sequentially and share the runner filesystem. Use
id:on steps whoseoutputsyou need downstream. - Conditional logic:
if:expressions are evaluated with${{ }}syntax. Usesuccess(),failure(),always(),cancelled()status functions.if: always()runs even when a previous step fails — use it for cleanup. Omittingif:means the step runs only if all prior steps in the job succeeded.
Matrix Builds
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
fail-fast: false
max-parallel: 4
fail-fast: true(the default) cancels remaining matrix jobs the moment one fails — useful for cost savings but obscures how many variants are broken. Setfalsewhen you need full coverage data.include:adds specific combinations;exclude:prunes them.- Runner image changes affect matrix silently:
ubuntu-20.04was deprecated;windows-latestmigrated to Windows Server 2025. Check the runner-images release notes before assuming a matrix is stable.
Contexts and Expressions
Key contexts: github, runner, env, vars, secrets, inputs, matrix, needs, strategy, job, steps, github.event, github.ref.
- Contexts are evaluated at runtime (inside
${{ }}); some values are only available in certain job phases. - Secret leakage in expressions: never interpolate
${{ secrets.FOO }}directly into arun:script — pass it viaenv:and reference$MY_SECRETinstead (same env-var pattern as Script Injection, §5). github.refis the full ref (refs/heads/main);github.ref_nameis the short name (main). Usegithub.event_nameto branch behavior between push and PR triggers.
YAML Reuse within a File
YAML anchors and merge keys reduce repetition within a single workflow file but are not cross-file. Full syntax reference in references/advanced-features.md. For cross-workflow reuse, use reusable workflows or composite actions.
Environments, Protections, and Concurrency
environment:
name: production
url: ${{ steps.deploy.outputs.url }}
Environments support required reviewers, wait timers, and deployment branch restrictions. A job targeting a protected environment pauses until a reviewer approves — the primary declarative approval gate in Actions.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: true cancels any in-progress run for the same group when a new one starts — essential for branch-based CD to avoid simultaneous deployments. For the default branch where you never want to drop a run, set cancel-in-progress: false.
Service Containers
Run Docker sidecar services (databases, queues, any network dependency) under services: at the job level:
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test }
ports: ['5432:5432']
options: --health-cmd pg_isready --health-interval 10s --health-retries 5
Key rules:
- Linux runners only (GitHub-hosted
ubuntu-*or self-hosted Linux — not Windows/macOS, where it fails silently). - Map
ports:and reach the service atlocalhost:<host-port>; a job running inside acontainer:instead uses the service name directly (e.g.postgres:5432). - A health check (
--health-cmd/--health-interval/--health-retries) is required, or steps may start before the service accepts connections. Map private-image/DB credentials fromsecrets:, never hard-code them inenv:.
Red flags in review: a service container with no health check, on a Windows/macOS runner, or with hard-coded credentials; on: push: with no branches: filter on a high-traffic repo (every push triggers a full matrix); if: always() on a deploy step that should only run on success; fail-fast: true on a cross-OS matrix where you need all failure data; ${{ secrets.X }} interpolated directly into a run: command string.
2. Consuming Workflows and Troubleshooting
Reusable Workflows vs. Composite Actions vs. Starter Workflows
| Reusable workflow | Composite action | Starter workflow | |
|---|---|---|---|
| Definition location | .github/workflows/ in a repo | action.yml in a repo | .github/workflow-templates/ in org .github repo |
| Invoked via | uses: org/repo/.github/workflows/ci.yml@ref | uses: org/repo/path@ref (as a step) | Copied into a new repo (scaffold, then independent) |
| Gets own runners? | Yes — each job in the called workflow spawns its own runner | No — runs as steps on the caller's runner | N/A — it's a scaffold, not live invocation |
| Passes secrets | secrets: inherit or explicit map | via inputs: (secrets not natively supported; pass via env) | N/A |
| Best for | Full job/pipeline reuse across repos | Step-level logic encapsulation | Giving teams a starting template |
Caller limitations: a reusable workflow can itself call another reusable workflow, up to 10 levels deep (caller + 9 nested workflows). A single workflow file may also call a maximum of 50 unique reusable workflows (counting all unique workflows referenced across the entire call tree rooted at that file). The called workflow's jobs appear in the caller's UI as nested job groups.
Artifacts and Caching
- Artifacts (
actions/upload-artifact/actions/download-artifact) persist files across jobs within a run, or across runs ifretention-days:is set. Default retention is 90 days (configurable at repo/org level). Artifacts are scoped to a workflow run. - Cache (
actions/cache) restores and saves by key + restore-keys. Cache is branch-scoped: a PR branch can read from the default branch cache but not write to it. Cache entries expire after 7 days of no access (or when the repo's cache storage limit is reached and old entries are evicted). The default cache storage limit is 10 GB per repository — this is a default, not a hard cap[volatile — verify live]; enterprise owners, organization owners, and repository admins can raise it (up to 10 TB for repository-level settings); usage beyond 10 GB is billed pay-as-you-go. - Do not confuse them: use cache for build/dependency artifacts that are reproducible (node_modules, Gradle cache, pip wheels); use artifacts for files you need to keep (test reports, build binaries, signed packages).
Passing Data Between Jobs
Three mechanisms, in order of preference:
GITHUB_OUTPUT— writekey=valueto$GITHUB_OUTPUTin a step; reference via${{ needs.job-id.outputs.key }}in downstream jobs. This is the current pattern;::set-outputis deprecated.GITHUB_ENV— writeVAR=valueto$GITHUB_ENV; available in all subsequent steps of the same job only (not across jobs).- Artifacts — when the data is a file or too large for an output string.
Job summaries: write Markdown to $GITHUB_STEP_SUMMARY to generate rich per-job summaries visible in the Actions UI (test results tables, coverage badges, links). No output variable needed — it renders automatically.
Troubleshooting Failed Runs
- Check the job log: expand each step; the red X pinpoints the failing step. Look for exit code, error message, and preceding output.
- Enable debug logging: set repository secret
ACTIONS_STEP_DEBUG=trueandACTIONS_RUNNER_DEBUG=truefor verbose runner/step logs on the next run. - Matrix failures: you can re-run individual matrix jobs (not the whole matrix) from the UI — use this to confirm a fix without burning the full matrix.
Red flags in review: downloading artifacts in a job with no corresponding upload; relying on GITHUB_ENV to pass data across jobs (it only spans within a job); cache keys with no versioning component (a dependency upgrade won't bust the cache).
3. Authoring Custom Actions
Three action types: JavaScript (Node.js, fast, cross-OS, no container spin-up), Docker (custom OS/tools, compiled binaries), Composite (YAML steps, runs on the caller's runner). Full type comparison table and immutable-actions details: references/advanced-features.md → "Action Types" (and §5 on SHA-pinning).
action.yml Structure
name: My Action
description: One-sentence description
inputs:
my-input:
description: What it controls
required: true
default: 'fallback'
outputs:
result:
description: What is returned
value: ${{ steps.compute.outputs.result }}
runs:
using: 'composite' # or 'node20', 'docker'
steps: ...
outputs.valuefor composite actions must reference a step output via${{ steps.<id>.outputs.<name> }}.- For JavaScript actions, outputs are set by calling
core.setOutput('result', value)from the@actions/coretoolkit. branding:(icon+color) is required to publish to the Marketplace but has no effect on functionality.
Versioning and publishing details (semantic tags, Marketplace requirements, private actions): references/advanced-features.md → "Action Versioning and Publishing."
Red flags in review: a JavaScript action without a compiled dist/ committed (the runner has no build step; the source must be pre-compiled); a Docker action with no health check or CMD; an action with hardcoded secrets in action.yml (use inputs mapped from secrets:).
4. Enterprise Management of Runners and Policies
Runner Types
| GitHub-hosted | Self-hosted | |
|---|---|---|
| Provisioning | Automatic | You manage |
| Cost | Billed per minute (beyond free tier) | Infrastructure cost; Actions minutes free |
| Isolation | Fresh VM per job | Shared state between jobs unless you clean up |
| OS choice | Ubuntu, Windows, macOS (fixed image versions) | Any OS you provision |
| Preinstalled software | Fixed; see runner-images release notes | You control |
| Network access to private resources | Not without VNET integration or tunneling | Yes (on-prem or VPC) |
Runner groups (org/enterprise level) gate which repos can use which self-hosted runners. Default group is accessible to all repos; restrict by creating named groups and assigning repos explicitly.
Self-hosted runner hardening:
- Never run self-hosted runners on public repos — any fork PR can trigger a workflow that runs on your runner with your network access.
- Treat the runner machine as untrusted code execution: no persistent credentials on disk, no elevated privileges, ephemeral runners (re-image after each job) preferred for sensitive environments.
- Runner registration tokens expire in 1 hour; do not store them in CI artifacts.
Secrets and Variables
Three scopes, each inherited downward:
| Scope | Visible to |
|---|---|
| Organization secret | All repos in the org (or selected repos) |
| Repository secret | That repo only |
| Environment secret | Jobs targeting that named environment only |
Variables (vars.) follow the same scope hierarchy and are non-sensitive (appear in logs as plain text). Use variables for configuration values (region, URL, feature flags); use secrets for credentials.
- Access in workflows:
${{ secrets.NAME }}/${{ vars.NAME }}. Secrets are masked in logs (replaced with***); variables are not. - Programmatic management: REST API endpoints exist for CRUD on org/repo/environment secrets and variables — useful for rotation automation.
secrets.GITHUB_TOKENis the ephemeral token minted per run (see §5).
Policies and Governance
At the org and enterprise level, administrators can:
- Restrict which actions can run: allow only actions from GitHub, from verified creators, or a specific allowlist (by
owner/repo@refpattern). - Block specific actions with the
!-prefix: prefix an allowlist entry with!to explicitly deny that action (e.g.,!bad-org/bad-action). The blocklist is evaluated last and overrides any permissive policy — use it for rapid response when an action is known-compromised.[volatile — verify live] - Enforce SHA-pinning via policy: an admin checkbox mandates that all workflows pin actions to a full commit SHA; workflows using floating tags or branch refs fail immediately. This proactively prevents tag-mutation supply-chain attacks at the platform level rather than relying on author discipline.
[volatile — verify live] - Require approval for first-time contributors on public repos.
- Restrict self-hosted runner registration to admins (prevent repos from adding their own runners to the org pool).
- Enforce required status checks at the branch protection level — specific workflow job names must pass before merge.
Red flags in review: a self-hosted runner registered to a public repo; secrets scoped to org-wide when only one repo needs them; no branch protection requiring CI to pass before merge on the default branch.
5. Security and Optimization
GITHUB_TOKEN — the ephemeral identity
GITHUB_TOKEN is minted at workflow start, scoped to the repo, and expires when the run ends. Its default permissions are set at the repo/org level (either "permissive" — read/write to most resources — or "restricted" — read only). Override at the workflow or job level:
permissions:
contents: read
pull-requests: write
id-token: write # required for OIDC
Least privilege rule: set permissions: at the workflow level to the minimum required. If different jobs need different scopes, set a restrictive workflow-level default and override per job. Never leave the default permissive setting active for a workflow that writes to the repo or calls external services.
GITHUB_TOKEN cannot trigger other workflow runs by default (prevents infinite loops from push-triggered workflows that commit). Use a PAT or app token only when cross-workflow triggering is genuinely needed, and scope it narrowly.
OIDC — Eliminating Long-Lived Cloud Credentials
OIDC lets a workflow authenticate to a cloud provider (AWS/Azure/GCP) via a short-lived token from GitHub's OIDC provider that the provider validates — no long-lived key stored as a secret.
Requirements:
permissions: id-token: writein the workflow/job.- A cloud provider trust policy that grants access only when the OIDC token's claims match expected values (
repository,ref,environment,workflow). - A setup action provided by the cloud provider (e.g.,
aws-actions/configure-aws-credentials,azure/login,google-github-actions/auth) that exchanges the token.
Scope the trust policy tightly: restrict to a specific repo, branch, or environment in the cloud trust policy — not to the entire org. An overly broad trust allows any repo in the org to assume the role.
Pinning Actions to Commit SHAs
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Pinning to a full SHA is the only guarantee that the action code cannot change between runs. Tags are mutable; a malicious or compromised publisher can repoint v4 to different code. SHA-pinning is especially important for actions that have id-token: write or elevated contents: write permission.
Immutable-actions enforcement resolves tag-pinned actions from a GHCR snapshot on hosted runners (closing tag-mutation there); SHA-pinning remains best practice for auditability and self-hosted consistency.
Org/enterprise SHA-pinning enforcement (the §4 policy checkbox, Aug 2025) makes this mandatory rather than voluntary: with it active, tag-only refs (@v4, @main) fail immediately with "not allowed by policy" — the fix is always to add the full SHA. [volatile — verify live]
Script Injection
Script injection occurs when user-controlled input (e.g., a PR title, issue body, branch name) is interpolated into a run: shell script via ${{ github.event.pull_request.title }}. An attacker can craft a PR title containing shell metacharacters to execute arbitrary code.
Mitigations:
- Pass untrusted context values through an intermediate environment variable, never directly into the script body:
env: PR_TITLE: ${{ github.event.pull_request.title }} run: echo "$PR_TITLE" # shell-quoted variable, not ${{ }} expression - Prefer vetted marketplace actions over inline
run:scripts for complex processing. - Never grant
pull_request_targetworkflows write permissions without careful review — this trigger runs in the context of the base repo (with secrets access) even for fork PRs.
Artifact Attestations (SLSA provenance): generate and verify signed build provenance for release artifacts. Full details, required permissions, and verify steps: references/advanced-features.md → "Artifact Attestations."
Red flags in review: permissions: write-all or unscoped permissions on any workflow; ${{ github.event.*.body }} or similar user-controlled context values inside a run: script; a pull_request_target workflow that checks out the PR head and runs it (classic code-exec attack surface); floating action tags (@main, @v3) without a SHA comment; OIDC trust policies scoped to an entire org rather than a specific repo+branch.
Executable Workflows
Workflow 1 — Ship a Reusable Workflow Safely (Typed Inputs/Secrets → Pin Actions to SHA → Test from a Caller)
- Create
.github/workflows/reusable-build.ymlin the shared repo. Declareon: workflow_call:with typed inputs (string,boolean,choice) and explicit secret declarations; setrequired:anddefault:on every input. → gate:gh workflow list --repo platform-org/platformshows the file; it must NOT appear as a directly triggerable workflow (only callable viaworkflow_call). - Pin every third-party action to a full 40-char SHA:
uses: actions/checkout@<sha> # v4.x.x. → gate:grep -r 'uses:' .github/workflows/reusable-build.yml | grep -v '@[0-9a-f]\{40\}'returns no lines. - Set
permissions:at workflow level to the minimum required (e.g.,contents: read). → gate:permissions:block present; no job declareswrite-allor omitspermissions:. - In a caller repo, invoke the reusable workflow via
uses: platform-org/platform/.github/workflows/reusable-build.yml@mainwithwith:inputs andsecrets: inherit(or explicit mapping). → gate:gh workflow runtriggers it; called workflow's jobs appear as nested groups in the caller UI; a debug step echoing${{ inputs.my-input }}confirms inputs are received. - Confirm secrets are masked: all secret values in step logs must appear as
***; if any appear in plain text the secret was passed viainputs:instead ofsecrets:— move it to thesecrets:declaration.
Workflow 2 — Set Up OIDC Cloud Auth (No Long-Lived Secrets) with a Scoped Trust Policy
- Add
permissions: id-token: writeto the workflow or job that needs cloud access. → gate: run the workflow; if the auth step errors with "credentials could not be loaded" the permission is missing. - Create an IAM OIDC identity provider for
token.actions.githubusercontent.com:aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com --thumbprint-list <thumbprint>. → gate:aws iam list-open-id-connect-providersconfirms the provider;get-open-id-connect-providershows the correct URL and client ID. - Create the IAM role with a trust policy scoped to the specific repo + branch or environment using
StringEquals(notStringLike) on thesubclaim:"repo:my-org/my-service:ref:refs/heads/main"or"repo:my-org/my-service:environment:production". → gate:aws iam get-role --role-name <role> --query 'Role.AssumeRolePolicyDocument'— confirmStringEqualsand the exact repo. Test from a different repo —AssumeRoleWithWebIdentitymust returnAccessDenied. - Add the cloud provider's setup action (e.g.,
aws-actions/configure-aws-credentials@<sha>) withrole-to-assume:andaws-region:set explicitly. → gate: step output showsAssumed role ... with web identity;aws sts get-caller-identityconfirms the expected role ARN. - Confirm no long-lived credentials remain:
gh secret list— delete anyAWS_ACCESS_KEY_IDorAWS_SECRET_ACCESS_KEYafter confirming OIDC works end-to-end.
Workflow 3 — Harden a Public-Repo Workflow Against Fork-PR Injection (pull_request_target, Least-Privilege GITHUB_TOKEN)
- Audit the trigger. If it uses
pull_request_target:, it runs with base-branch secrets access — the highest-risk trigger for public repos. Check whether it also checks out PR-head code. → gate:grep -r 'pull_request_target' .github/workflows/— any hit requires review; acheckoutstep withref: ${{ github.event.pull_request.head.sha }}is actively exploitable and must be fixed immediately. - Set
permissions: {}(all deny) as the workflow-level default; grant minimum overrides per job (typical CI:contents: read,pull-requests: write). → gate:grep -A5 'permissions:' .github/workflows/<workflow>.yml— no job haswrite-allor omitspermissions:against the restrictive default. - For any step processing user-controlled input (PR title, issue body, branch name), pass through an
env:variable and reference$ENV_VARin the shell — never interpolate${{ github.event.pull_request.title }}directly in arun:block. → gate:grep -rn '\${{ github.event' .github/workflows/— every hit must be in anenv:assignment, not inside arun:body. - Pin all external actions to full commit SHAs.
→ gate:
grep -rn 'uses:' .github/workflows/ | grep -v '@[0-9a-f]\{40\}'— no unpinned external actions. - For workflows that must use
pull_request_target(e.g., posting a comment from a fork), use the two-workflow pattern:pull_request(untrusted, no secrets) uploads artifacts;workflow_run:(trusted) downloads artifacts and posts the comment — never executes PR code. → gate: theworkflow_runworkflow must have no checkout step using the PR head SHA; it only downloads artifacts from the untrustedpull_requestworkflow.
Decision Scenarios
Scenario 1 — Composite action vs reusable workflow: which for a shared build step
Situation: A platform team wants to share a "build and push Docker image" sequence across 15 repositories. The sequence is 4 steps: log in to ECR, build image, tag image, push image. A senior engineer proposes creating a reusable workflow (
.github/workflows/docker-build.yml) in a sharedplatformrepo and calling it from each app repo. A colleague suggests a composite action (action.yml) instead. The senior engineer says "they do the same thing, just pick one."
Competent move: Use a composite action, not a reusable workflow, for step-level logic you want to embed as a step inside a calling job. A composite action runs on the caller's runner and shares the runner's filesystem — it can access checked-out source code and build artifacts from preceding steps without artifact uploads/downloads. A reusable workflow spawns its own independent runner(s), requires explicit artifact passing, and counts as a full workflow nesting level against the 10-level nesting limit. For "N steps that run in the context of a caller's job," composite action is the right abstraction.
Tempting-but-wrong: Defaulting to a reusable workflow because it is more familiar. Reusable workflows are the right abstraction for full independent jobs or multi-job pipelines (e.g., a complete deploy pipeline) — not for step sequences that need access to the calling job's runner workspace without artifact overhead.
Verify: In the
action.yml'srunssection, confirmusing: compositeand thatsteps:lists the 4 steps directly; in the calling workflow, the action appears as a singleuses:step within the job (not as a separateuses:at job level).gh workflow listin the shared repo should NOT show a new workflow file for a composite action.
Scenario 2 — GITHUB_OUTPUT vs GITHUB_ENV for cross-job data
Situation: A CI workflow has two jobs:
buildanddeploy. Thebuildjob produces a Docker image tag. A developer writesecho "IMAGE_TAG=$TAG" >> $GITHUB_ENVand references${{ env.IMAGE_TAG }}in thedeployjob. The workflow runs butenv.IMAGE_TAGis empty in thedeployjob.
Competent move:
GITHUB_ENVpropagates environment variables to subsequent steps within the same job only — it does not cross job boundaries. To pass data between jobs, write toGITHUB_OUTPUT(echo "image_tag=$TAG" >> $GITHUB_OUTPUT), declare a job-leveloutputs:block mapping the output (image_tag: ${{ steps.<step-id>.outputs.image_tag }}), and reference it in thedeployjob via${{ needs.build.outputs.image_tag }}. Thedeployjob must also declareneeds: build.
Tempting-but-wrong: Using an artifact to pass a single string value. Artifacts work for files; for scalars,
GITHUB_OUTPUT+ job outputs is the canonical low-overhead pattern.
Verify: Add
- run: echo "${{ needs.build.outputs.image_tag }}"as the first step indeployand confirm the tag appears. In thebuildstep log, the Actions runner logsSet output image_tag=<value>.
Scenario 3 — OIDC trust policy scoped to organization rather than repo+branch
Situation: A team sets up OIDC federation with AWS. The IAM role trust policy condition is
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:my-org/*" }. Production deployments succeed. A security reviewer flags the condition as dangerously broad.
Competent move: A wildcard
repo:my-org/*allows any repository in the org — including forks and any future repo — to assume the production IAM role. Scope the condition to the specific repo and branch or environment:"StringEquals": { "token.actions.githubusercontent.com:sub": "repo:my-org/my-service:ref:refs/heads/main" }orrepo:my-org/my-service:environment:productionfor protected environments.
Tempting-but-wrong: Adding an environment condition alongside the broad org wildcard. If
StringLikewithrepo:my-org/*is the sub-claim check, any repo in the org can still assume the role — the repo restriction must be specific.
Verify:
aws iam get-role --role-name <role> --query 'Role.AssumeRolePolicyDocument'— confirmStringEquals(notStringLike) and the exact repo+branch or environment. Test from a different repo in the org —AssumeRoleWithWebIdentityshould returnAccessDenied.
Scenario 4 — Reusable workflow nesting depth exceeded
Situation: A platform team builds a highly layered shared-pipeline library. Starting from
app-pipeline.yml, each workflow calls the next:app-pipeline.yml→build.yml→test.yml→lint.yml→security-scan.yml→sast.yml→sbom.yml→sign.yml→attest.yml→notify.yml→report.yml. The workflow fails deep in the chain. The team suspects a secrets-inheritance issue.
Competent move: GitHub Actions limits reusable workflow nesting to 10 levels (caller counts as level 1; up to 9 additional nested calls). An eleventh level causes a runtime failure, not a permissions or secrets error. Count the chain:
app-pipeline.yml(1) →build.yml(2) →test.yml(3) →lint.yml(4) →security-scan.yml(5) →sast.yml(6) →sbom.yml(7) →sign.yml(8) →attest.yml(9) →notify.yml(10) →report.yml(11 = over limit). Fix: promotereport.yml's steps into a composite action and call it from level 10, or collapse layers by inlining. The 10-level limit is a hard platform constraint[volatile — verify live]. Additionally, a single workflow file may reference at most 50 unique reusable workflows across its entire call tree.
Tempting-but-wrong: Debugging secrets inheritance or IAM permissions first. The nesting-depth error produces a distinct message referencing call depth — check it before assuming a permissions failure.
Verify: Count the call chain to confirm depth > 10. After refactoring the deepest workflow into a composite action called from level 10,
gh run list --workflow app-pipeline.ymlshould show successful runs.
Further scenario (self-hosted runner on a public repository): references/scenarios.md.
Operational Rules Quick Reference
- DO set
permissions:explicitly at the workflow level; never rely on the org default permissive setting for a workflow that writes or deploys. - DON'T interpolate
${{ secrets.X }}or user-controlled context values directly into arun:script — useenv:to pass them as shell variables. - DO pin third-party actions to a full commit SHA and add a comment with the corresponding tag for readability.
- DON'T register self-hosted runners to public repos — any fork PR can execute arbitrary code on your runner.
- DO use OIDC federation for cloud provider auth; never store long-lived cloud credentials as GitHub secrets.
- DO use
concurrency:withcancel-in-progress: trueon branch-based CD workflows to prevent simultaneous deployments. - DON'T use
GITHUB_ENVto pass data between jobs — it only spans steps within the same job; useGITHUB_OUTPUT+ joboutputs:for cross-job data. - DO add
paths:andbranches:filters to push/PR triggers on large repos to avoid unnecessary matrix runs. - DON'T set
fail-fast: true(the default) on a matrix where you need complete failure data — setfalseexplicitly. - DO treat
pull_request_targetas a high-risk trigger; never check out and execute PR-head code in apull_request_targetworkflow without explicit isolation. - DON'T use the deprecated
::set-outputcommand — write to$GITHUB_OUTPUTinstead. - DO scope OIDC cloud trust policies to a specific repo + branch or environment, never to an entire org.
- DO use ephemeral (re-imaged) self-hosted runners for sensitive workloads; never allow persistent state between jobs on shared runners.
- DON'T store a runner registration token beyond its 1-hour expiry — regenerate at provisioning time.
- DO verify artifact attestations with
gh attestation verifybefore deploying release artifacts in security-critical pipelines.
Feedback protocol
Using this skill and hit a wall? If you find a claim contradicted by the live system or official docs, a missing rule that cost you a wrong attempt, or a decision this skill gave no criteria for — append an entry in the moment to .skill-feedback/github-actions.md at the project root (create it if absent):
date | skill last-reviewed | claim or gap | what you observed instead | evidence (error text / doc URL / query output) | suggested fix
These are harvested back into the skill via the learning loop. When the live system and this file disagree, trust the live system.
Changelog
- 2026-06-11 — D10 right-size (wind-down): merged duplicated guidance (the env-var secret pattern stated twice; immutable-actions explained in three places now cross-referenced to §5), compacted the Service Containers example, and tightened OIDC/filtering notes. Body trimmed ~250 words toward the context-economy ceiling; no facts removed.
- 2026-06-09 — Conformed to the 12-dimension skill standard: task-vocab description + Scope block, Uncertainty & Escalation guidance with inline
[volatile — verify live]marks, executable workflows, tool-agnostic verify steps, and the feedback protocol above. Exam logistics relocated to references/study-resources.md;last-reviewedset to 2026-06-09. - 2026-06-09 — Curation pass (inbox: D9 audit finding): inlined 3 decision scenarios into the body (Scenarios 2–4: GITHUB_OUTPUT vs GITHUB_ENV, OIDC org-scope trust, reusable workflow nesting depth) to meet the teaching-scenario standard (≥4 inline). Scenario 5 remains in references. "Versioning and Publishing" and "Artifact Attestations" subsections moved to references/advanced-features.md to offset body length.
- 2026-06-10 — Cycle-4 curation (inbox): corrected reusable workflow nesting limit from 4 → 10 levels in all three locations (Uncertainty §, §2 body, Scenario 4) and updated Scenario 4's example chain to demonstrate a genuine 11-level over-limit case; added "max 50 unique reusable workflows per file" limit. Corrected cache 10 GB from "hard cap" to "configurable default" (billable above 10 GB, up to 10 TB). Added §1 Service Containers subsection (GH-200 blueprint gap). Added
!-prefix blocklist and SHA-pinning enforcement policy controls to §4 Policies and §5 SHA-Pinning (Aug 2025 feature). Eval probes 13–15 added.
Independent educational content for upskilling AI agents. Not affiliated with, authorized by, endorsed by, or sponsored by GitHub, Microsoft, or any certification body. "GitHub," "GitHub Actions," and related marks are property of GitHub, Inc. / Microsoft and are used here for identification purposes only. Guidance only — verify against official documentation and live systems. No certification outcome is implied or guaranteed.