agentsclimarketplace

Github actions

Skill toddkasper/expert-skills/github/skills/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.

Install
npx -y skills add toddkasper/expert-skills --skill github-actions

Assembled 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-latest image 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.

PatternTrigger
Push/PR to specific branchespush: branches: / pull_request: branches:
Manual with inputsworkflow_dispatch: inputs:
Called by another workflowworkflow_call: inputs: secrets:
Schedule (cron)schedule: cron:
Webhook event (e.g. issue labeled)on: issues: types: [labeled]
After another workflow completesworkflow_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 its needs have succeeded (or set if: always() / if: failure() to override).
  • Steps within a job run sequentially and share the runner filesystem. Use id: on steps whose outputs you need downstream.
  • Conditional logic: if: expressions are evaluated with ${{ }} syntax. Use success(), failure(), always(), cancelled() status functions. if: always() runs even when a previous step fails — use it for cleanup. Omitting if: 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. Set false when you need full coverage data.
  • include: adds specific combinations; exclude: prunes them.
  • Runner image changes affect matrix silently: ubuntu-20.04 was deprecated; windows-latest migrated 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 a run: script — pass it via env: and reference $MY_SECRET instead (same env-var pattern as Script Injection, §5).
  • github.ref is the full ref (refs/heads/main); github.ref_name is the short name (main). Use github.event_name to 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 at localhost:<host-port>; a job running inside a container: 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 from secrets:, never hard-code them in env:.

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 workflowComposite actionStarter workflow
Definition location.github/workflows/ in a repoaction.yml in a repo.github/workflow-templates/ in org .github repo
Invoked viauses: org/repo/.github/workflows/ci.yml@refuses: 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 runnerNo — runs as steps on the caller's runnerN/A — it's a scaffold, not live invocation
Passes secretssecrets: inherit or explicit mapvia inputs: (secrets not natively supported; pass via env)N/A
Best forFull job/pipeline reuse across reposStep-level logic encapsulationGiving 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 if retention-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:

  1. GITHUB_OUTPUT — write key=value to $GITHUB_OUTPUT in a step; reference via ${{ needs.job-id.outputs.key }} in downstream jobs. This is the current pattern; ::set-output is deprecated.
  2. GITHUB_ENV — write VAR=value to $GITHUB_ENV; available in all subsequent steps of the same job only (not across jobs).
  3. 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

  1. Check the job log: expand each step; the red X pinpoints the failing step. Look for exit code, error message, and preceding output.
  2. Enable debug logging: set repository secret ACTIONS_STEP_DEBUG=true and ACTIONS_RUNNER_DEBUG=true for verbose runner/step logs on the next run.
  3. 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.value for 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/core toolkit.
  • 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-hostedSelf-hosted
ProvisioningAutomaticYou manage
CostBilled per minute (beyond free tier)Infrastructure cost; Actions minutes free
IsolationFresh VM per jobShared state between jobs unless you clean up
OS choiceUbuntu, Windows, macOS (fixed image versions)Any OS you provision
Preinstalled softwareFixed; see runner-images release notesYou control
Network access to private resourcesNot without VNET integration or tunnelingYes (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:

ScopeVisible to
Organization secretAll repos in the org (or selected repos)
Repository secretThat repo only
Environment secretJobs 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_TOKEN is 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@ref pattern).
  • 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:

  1. permissions: id-token: write in the workflow/job.
  2. A cloud provider trust policy that grants access only when the OIDC token's claims match expected values (repository, ref, environment, workflow).
  3. 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_target workflows 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)

  1. Create .github/workflows/reusable-build.yml in the shared repo. Declare on: workflow_call: with typed inputs (string, boolean, choice) and explicit secret declarations; set required: and default: on every input. → gate: gh workflow list --repo platform-org/platform shows the file; it must NOT appear as a directly triggerable workflow (only callable via workflow_call).
  2. 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.
  3. Set permissions: at workflow level to the minimum required (e.g., contents: read). → gate: permissions: block present; no job declares write-all or omits permissions:.
  4. In a caller repo, invoke the reusable workflow via uses: platform-org/platform/.github/workflows/reusable-build.yml@main with with: inputs and secrets: inherit (or explicit mapping). → gate: gh workflow run triggers it; called workflow's jobs appear as nested groups in the caller UI; a debug step echoing ${{ inputs.my-input }} confirms inputs are received.
  5. Confirm secrets are masked: all secret values in step logs must appear as ***; if any appear in plain text the secret was passed via inputs: instead of secrets: — move it to the secrets: declaration.

Workflow 2 — Set Up OIDC Cloud Auth (No Long-Lived Secrets) with a Scoped Trust Policy

  1. Add permissions: id-token: write to 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.
  2. 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-providers confirms the provider; get-open-id-connect-provider shows the correct URL and client ID.
  3. Create the IAM role with a trust policy scoped to the specific repo + branch or environment using StringEquals (not StringLike) on the sub claim: "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' — confirm StringEquals and the exact repo. Test from a different repo — AssumeRoleWithWebIdentity must return AccessDenied.
  4. Add the cloud provider's setup action (e.g., aws-actions/configure-aws-credentials@<sha>) with role-to-assume: and aws-region: set explicitly. → gate: step output shows Assumed role ... with web identity; aws sts get-caller-identity confirms the expected role ARN.
  5. Confirm no long-lived credentials remain: gh secret list — delete any AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY after confirming OIDC works end-to-end.

Workflow 3 — Harden a Public-Repo Workflow Against Fork-PR Injection (pull_request_target, Least-Privilege GITHUB_TOKEN)

  1. 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; a checkout step with ref: ${{ github.event.pull_request.head.sha }} is actively exploitable and must be fixed immediately.
  2. 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 has write-all or omits permissions: against the restrictive default.
  3. For any step processing user-controlled input (PR title, issue body, branch name), pass through an env: variable and reference $ENV_VAR in the shell — never interpolate ${{ github.event.pull_request.title }} directly in a run: block. → gate: grep -rn '\${{ github.event' .github/workflows/ — every hit must be in an env: assignment, not inside a run: body.
  4. Pin all external actions to full commit SHAs. → gate: grep -rn 'uses:' .github/workflows/ | grep -v '@[0-9a-f]\{40\}' — no unpinned external actions.
  5. 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: the workflow_run workflow must have no checkout step using the PR head SHA; it only downloads artifacts from the untrusted pull_request workflow.

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 shared platform repo 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's runs section, confirm using: composite and that steps: lists the 4 steps directly; in the calling workflow, the action appears as a single uses: step within the job (not as a separate uses: at job level). gh workflow list in 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: build and deploy. The build job produces a Docker image tag. A developer writes echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV and references ${{ env.IMAGE_TAG }} in the deploy job. The workflow runs but env.IMAGE_TAG is empty in the deploy job.

Competent move: GITHUB_ENV propagates environment variables to subsequent steps within the same job only — it does not cross job boundaries. To pass data between jobs, write to GITHUB_OUTPUT (echo "image_tag=$TAG" >> $GITHUB_OUTPUT), declare a job-level outputs: block mapping the output (image_tag: ${{ steps.<step-id>.outputs.image_tag }}), and reference it in the deploy job via ${{ needs.build.outputs.image_tag }}. The deploy job must also declare needs: 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 in deploy and confirm the tag appears. In the build step log, the Actions runner logs Set 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" } or repo:my-org/my-service:environment:production for protected environments.

Tempting-but-wrong: Adding an environment condition alongside the broad org wildcard. If StringLike with repo: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' — confirm StringEquals (not StringLike) and the exact repo+branch or environment. Test from a different repo in the org — AssumeRoleWithWebIdentity should return AccessDenied.


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.ymlbuild.ymltest.ymllint.ymlsecurity-scan.ymlsast.ymlsbom.ymlsign.ymlattest.ymlnotify.ymlreport.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: promote report.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.yml should 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 a run: script — use env: 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: with cancel-in-progress: true on branch-based CD workflows to prevent simultaneous deployments.
  • DON'T use GITHUB_ENV to pass data between jobs — it only spans steps within the same job; use GITHUB_OUTPUT + job outputs: for cross-job data.
  • DO add paths: and branches: 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 — set false explicitly.
  • DO treat pull_request_target as a high-risk trigger; never check out and execute PR-head code in a pull_request_target workflow without explicit isolation.
  • DON'T use the deprecated ::set-output command — write to $GITHUB_OUTPUT instead.
  • 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 verify before 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-reviewed set 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.