# GitHub Actions Advanced Skill - Detailed Guide > This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations. These YAML blocks are fragments to adapt, not validated complete workflows. Resolve every action SHA from its owning repository and inspect it; a full-length hash alone proves neither existence nor safety. Historical runtime/action versions are not current support recommendations. The Azure login SHA was malformed and is deliberately a blocking placeholder until verified. ## Step 1: Understand Context Before Responding When invoked, first gather context: ```bash # Discover existing workflows in the repo find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20 # Check for composite actions find .github/actions -name "action.yml" 2>/dev/null # Detect tech stack (influences runner OS, language setup actions) ls package.json requirements.txt Gemfile go.mod Cargo.toml pom.xml 2>/dev/null ``` Then adapt recommendations to: - Existing workflow patterns in the repo - The tech stack and language runtime - Whether this is a monorepo or single-project repo - Whether self-hosted or GitHub-hosted runners are in use --- ## Workflow Structure Reference ```yaml name: Workflow Name on: # Triggers (see Triggers section) push: branches: [main] permissions: # Always declare — principle of least privilege contents: read env: # Workflow-level env vars NODE_VERSION: '20' concurrency: # Prevent duplicate runs group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # Cancel older runs for same branch jobs: job-id: name: Human-readable name runs-on: ubuntu-24.04 # Pin OS version — never use -latest in prod timeout-minutes: 15 # Always set — prevents runaway jobs environment: production # Links to GitHub Environment (approvals/secrets) steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Step name run: echo "hello" ``` --- ## Triggers (`on:`) ### Common Patterns ```yaml on: push: branches: [main, 'release/**'] paths-ignore: ['**.md', 'docs/**'] # Skip docs-only changes pull_request: types: [opened, synchronize, reopened] branches: [main] workflow_dispatch: # Manual trigger with inputs inputs: environment: description: 'Deploy target' required: true type: choice options: [staging, production] dry-run: description: 'Dry run only?' type: boolean default: false schedule: - cron: '0 2 * * 1' # Monday 2am UTC workflow_call: # Called by other workflows (reusable) inputs: image-tag: type: string required: true secrets: deploy-token: required: true release: types: [published] # Trigger only on published releases pull_request_target: # Runs with repo secrets — use with care! types: [labeled] # Gate with label + author_association check ``` > **Security Warning:** A maintainer label or author association is not sufficient approval of the current PR head. Keep privileged target workflows on trusted base code; never build/test/execute untrusted PR code, dependencies or artifacts with secrets/write credentials. Use pull_request with least privilege for those tests. --- ## Reusable Workflows Split large pipelines into composable units stored in `.github/workflows/`. **Convention:** Prefix internal/reusable workflows with `_` (e.g., `_build.yml`). ### Caller (`.github/workflows/deploy.yml`) ```yaml jobs: call-build: uses: ./.github/workflows/_build.yml # Same-repo reusable # uses: org/repo/.github/workflows/build.yml@main # Cross-repo with: image-tag: ${{ github.sha }} # No secrets needed for this build-only example; pass explicit secrets only when required call-test: uses: ./.github/workflows/_test.yml with: node-version: '20' secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Explicit secret passing ``` ### Reusable Workflow (`.github/workflows/_build.yml`) ```yaml on: workflow_call: inputs: image-tag: type: string required: true push: type: boolean default: false secrets: registry-token: required: false outputs: digest: description: "Image digest" value: ${{ jobs.build.outputs.digest }} jobs: build: runs-on: ubuntu-24.04 timeout-minutes: 20 outputs: digest: ${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: build uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: push: ${{ inputs.push }} tags: myapp:${{ inputs.image-tag }} ``` --- ## Matrix Builds ```yaml jobs: test: strategy: fail-fast: false # Don't cancel others if one fails max-parallel: 4 # Limit concurrent runners matrix: os: [ubuntu-24.04, windows-2022, macos-14] node: ['18', '20', '22'] exclude: - os: windows-2022 node: '18' include: - os: ubuntu-24.04 node: '22' experimental: true # Custom matrix variable runs-on: ${{ matrix.os }} timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: ${{ matrix.node }} cache: 'npm' - run: npm ci - run: npm test continue-on-error: ${{ matrix.experimental == true }} ``` ### Dynamic Matrix via Script ```yaml jobs: generate-matrix: runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: set-matrix run: | SERVICES=$(find services -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') printf 'matrix={"service":%s}\n' "$SERVICES" >> "$GITHUB_OUTPUT" build: needs: generate-matrix strategy: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} runs-on: ubuntu-24.04 steps: - env: SERVICE: ${{ matrix.service }} run: echo "Building $SERVICE" ``` --- ## Caching Strategies ### Language Setup Actions (Preferred — No Extra Step Needed) ```yaml # Node.js - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: '20' cache: 'npm' # or 'yarn' or 'pnpm' # Python - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.12' cache: 'pip' # Go - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: '1.23' cache: true # Java / Gradle / Maven - uses: actions/setup-java@7a6d8a8234af8eb26422e24052f73b12b0e46a27 # v4.6.0 with: distribution: 'temurin' java-version: '21' cache: 'maven' # or 'gradle' ``` ### Manual Cache (Any Tool) ```yaml - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache-deps with: path: | ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: | ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} ${{ runner.os }}-pip- - name: Install deps (only on cache miss) run: pip install -r requirements.txt # cache contains downloads; always install into the current environment ``` ### Docker Layer Caching ```yaml - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: cache-from: type=gha cache-to: type=gha,mode=max # For registry-backed cache (cross-branch): # cache-from: type=registry,ref=ghcr.io/myorg/myapp:buildcache # cache-to: type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max ``` --- ## OIDC Authentication (Keyless Cloud Auth) **Never store long-lived cloud credentials as secrets.** Use OIDC to get short-lived tokens that expire automatically. ### AWS ```yaml permissions: id-token: write contents: read steps: - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 with: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole aws-region: us-east-1 role-session-name: GitHubActions-${{ github.run_id }} # Trust policy on the IAM role must include: # "token.actions.githubusercontent.com" as OIDC provider # Condition: "repo:org/repo:ref:refs/heads/main" (restrict to branch) ``` ### GCP (Workload Identity Federation) ```yaml permissions: id-token: write contents: read steps: - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider service_account: github-actions@my-project.iam.gserviceaccount.com token_format: access_token # or 'id_token' ``` ### Azure (Federated Identity) ```yaml permissions: id-token: write contents: read steps: - uses: azure/login@ # v2.2.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} # No client secret needed! Uses OIDC federated credentials ``` --- ## Environments & Deployment Protection ```yaml jobs: deploy-staging: environment: name: staging url: https://staging.myapp.com runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - run: ./scripts/deploy.sh staging deploy-production: needs: deploy-staging environment: name: production url: https://myapp.com # Shown in the GitHub UI deployment panel runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - run: ./scripts/deploy.sh production ``` **Configure in Settings → Environments:** - **Required reviewers** — manual approval gate before run - **Wait timer** — delay after approval (e.g., 10-minute buffer) - **Branch/tag restrictions** — only `main` or `v*` tags can deploy to prod - **Environment-specific secrets** — override repo-level secrets per environment - **Deployment branches** — whitelist which branches can target this environment --- ## Secrets Management ```yaml # Access repo/org/environment secrets env: DB_PASSWORD: ${{ secrets.DB_PASSWORD }} # Auto-provided token — no setup needed - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} # Hierarchy (most specific wins): # environment secret > repo secret > org secret ``` ### Masking Dynamic Values ```yaml - name: Generate and mask dynamic token run: | TOKEN=$(./scripts/generate-token.sh) echo "::add-mask::$TOKEN" # Mask in all subsequent logs echo "DEPLOY_TOKEN=$TOKEN" >> $GITHUB_ENV ``` ### Secrets in Composite Actions ```yaml # Composite actions cannot directly use the secrets context; callers can pass # secrets explicitly as inputs or env. Minimize either and never log them: - uses: ./.github/actions/my-action env: SECRET_VALUE: ${{ secrets.MY_SECRET }} ``` --- ## Composite Actions Package reusable step sequences into local actions. No container spin-up, no separate workflow file needed. ### Action Definition (`.github/actions/setup-app/action.yml`) ```yaml name: Setup App description: Install and configure application dependencies inputs: node-version: description: 'Node.js version' required: false default: '20' install-flags: description: 'Additional npm install flags' required: false default: '' outputs: cache-hit: description: 'Whether the dependency cache was hit' value: ${{ steps.cache.outputs.cache-hit }} runs: using: composite steps: - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: ${{ inputs.node-version }} cache: npm - id: cache uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: node_modules key: ${{ runner.os }}-node-${{ inputs.node-version }}-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' shell: bash env: INSTALL_FLAGS: ${{ inputs.install-flags }} run: | args=() case "$INSTALL_FLAGS" in "") ;; "--ignore-scripts") args+=(--ignore-scripts) ;; *) echo "Unsupported install flags" >&2; exit 1 ;; esac npm ci "${args[@]}" - name: Build shell: bash run: npm run build ``` ### Usage in a Workflow ```yaml steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./.github/actions/setup-app with: node-version: '22' install-flags: '--ignore-scripts' ``` --- ## Self-Hosted Runners ```yaml jobs: build-gpu: runs-on: [self-hosted, linux, x64, gpu] # Label matching timeout-minutes: 60 build-arm: runs-on: [self-hosted, linux, arm64] ``` ### Runner Best Practices | Practice | Details | |---|---| | **Ephemeral runners** | Use Actions Runner Controller (ARC) on Kubernetes for fresh runners per job | | **Isolation** | Never share prod runners with untrusted/fork PR workflows | | **Cleanup hooks** | Set `ACTIONS_RUNNER_HOOK_JOB_COMPLETED` to reset environment | | **Runner groups** | Use groups to restrict which repos/workflows can access which runners | | **Labels** | Use custom labels (e.g., `gpu`, `high-memory`) for precise targeting | | **Security** | Disable fork PR access to self-hosted runners in Settings | ```bash # Actions Runner Controller (Kubernetes) — recommended for ephemeral runners helm install arc \ --namespace arc-systems \ --create-namespace \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller ``` --- ## Conditional Execution & Flow Control ```yaml # Condition on branch + event - run: ./scripts/deploy.sh if: github.ref == 'refs/heads/main' && github.event_name == 'push' # Continue on error (non-blocking steps) - run: ./scripts/lint.sh continue-on-error: true # Job dependency and conditional execution jobs: test: runs-on: ubuntu-24.04 outputs: result: ${{ steps.run-tests.outcome }} deploy: needs: [test, build] if: | needs.test.result == 'success' && needs.build.result == 'success' && github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 notify-failure: needs: [test, deploy] if: failure() # Runs even if earlier jobs fail runs-on: ubuntu-24.04 steps: - run: ./scripts/notify-slack.sh "Pipeline failed!" ``` ### Passing Data Between Jobs ```yaml jobs: prepare: runs-on: ubuntu-24.04 outputs: version: ${{ steps.get-version.outputs.version }} should-deploy: ${{ steps.check.outputs.deploy }} steps: - id: get-version run: | VERSION=$(tr -d '\r\n' < VERSION) case "$VERSION" in ""|*[!0-9A-Za-z._-]*) echo "Invalid VERSION" >&2; exit 1 ;; esac printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT" - id: check run: | if git log -1 --pretty=%B | grep -q '\[deploy\]'; then echo "deploy=true" >> $GITHUB_OUTPUT else echo "deploy=false" >> $GITHUB_OUTPUT fi build: needs: prepare if: needs.prepare.outputs.should-deploy == 'true' runs-on: ubuntu-24.04 steps: - env: VERSION: ${{ needs.prepare.outputs.version }} run: echo "Building version $VERSION" ``` --- ## Debugging Techniques ```yaml # Enable runner diagnostic logging via repo secrets: # ACTIONS_RUNNER_DEBUG = true # ACTIONS_STEP_DEBUG = true # Log only necessary non-secret diagnostics through env (no complete contexts). - name: Debug event if: runner.debug == '1' env: EVENT_NAME: ${{ github.event_name }} RUN_ID: ${{ github.run_id }} run: printf 'Event=%s Run=%s\n' "$EVENT_NAME" "$RUN_ID" # SSH into a failing runner for interactive debugging - uses: mxschmitt/action-tmate@7b04f3521e6b0a9fc56fa8f9f50da4bcfb5fc7b5 # v3.19.0 if: failure() && runner.debug == '1' with: limit-access-to-actor: true # Only the workflow triggerer can SSH in timeout-minutes: 30 # Check what's pre-installed on GitHub-hosted runners - run: | echo "=== Tool Versions ===" node --version python3 --version go version docker --version echo "=== Disk Space ===" df -h echo "=== Memory ===" free -h ``` --- ## Complete Pipeline Patterns ### Pattern 1: Build → Test → Push → Deploy ```yaml name: CI/CD Pipeline on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: contents: read jobs: # ── Build & Test ────────────────────────────────────── build-test: runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: read checks: write # For test result reporting steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: '20' cache: 'npm' - run: npm ci - run: npm run lint - run: npm run test -- --coverage - run: npm run build - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: build-artifacts path: dist/ retention-days: 7 # ── Push Image (main branch only) ───────────────────── push-image: needs: build-test if: github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: read packages: write id-token: write # For OIDC outputs: image-digest: ${{ steps.push.outputs.digest }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/metadata-action@70b2cdc6480c1a8b86edf1777157f8f437de2166 # v5.5.1 id: meta with: images: ghcr.io/${{ github.repository }} tags: | type=sha,format=long type=raw,value=latest - id: push uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true # generate provenance metadata; does not itself establish a SLSA level sbom: true # Software Bill of Materials # ── Deploy Staging ──────────────────────────────────── deploy-staging: needs: push-image runs-on: ubuntu-24.04 timeout-minutes: 30 environment: name: staging url: https://staging.myapp.com steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - env: IMAGE_DIGEST: ${{ needs.push-image.outputs.image-digest }} run: ./scripts/deploy.sh staging "$IMAGE_DIGEST" # ── Deploy Production (manual approval required) ────── deploy-production: needs: [deploy-staging, push-image] runs-on: ubuntu-24.04 timeout-minutes: 30 environment: name: production url: https://myapp.com steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - env: IMAGE_DIGEST: ${{ needs.push-image.outputs.image-digest }} run: ./scripts/deploy.sh production "$IMAGE_DIGEST" ``` ### Pattern 2: Automated Release with Changelog ```yaml name: Release on: push: tags: ['v[0-9]+.[0-9]+.[0-9]+'] permissions: contents: write jobs: release: runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Full history needed for changelog generation - uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9 with: generate_release_notes: true # Auto-generates from PR titles and commits make_latest: true fail_on_unmatched_files: true files: | dist/**/*.tar.gz dist/**/*.zip ``` ### Pattern 3: Dependency Auto-Update with PR ```yaml name: Dependency Updates on: schedule: - cron: '0 9 * * 1' # Every Monday at 9am UTC workflow_dispatch: permissions: contents: write pull-requests: write jobs: update-deps: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: '20' - run: npx npm-check-updates -u - run: npm install - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 with: commit-message: 'chore: update npm dependencies' title: 'chore: update npm dependencies' branch: 'chore/npm-updates' delete-branch: true body: | Automated dependency updates generated by the dependency update workflow. Please review and test before merging. ``` ### Pattern 4: Security Scanning Pipeline ```yaml name: Security Scan on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 6 * * *' # Daily at 6am UTC permissions: contents: read security-events: write # For uploading SARIF results jobs: codeql: runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: security-events: write actions: read contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: languages: javascript-typescript - uses: github/codeql-action/autobuild@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 - uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 container-scan: runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.28.0 with: scan-type: 'fs' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: sarif_file: 'trivy-results.sarif' ``` --- ## Common Pitfalls & Fixes | Problem | Cause | Fix | |---|---|---| | Workflow doesn't trigger on PR from fork | Fork PRs use restricted `GITHUB_TOKEN` | Use `pull_request` not `pull_request_target`; avoid repo secrets in fork context | | Secret is `***` in logs but exposed | Dynamic value not masked | Use `echo "::add-mask::$VALUE"` before using it | | Cache never hits across branches | Cache key too specific | Add `restore-keys` fallback without branch or hash segment | | Matrix job fails silently | `fail-fast: true` (default) cancels siblings | Set `fail-fast: false` during debugging | | Job hangs indefinitely | No `timeout-minutes` set | Always set `timeout-minutes` on every job | | `$GITHUB_OUTPUT` not set | Old `set-output` command used | Use `echo "key=value" >> $GITHUB_OUTPUT` | | OIDC token request fails | Missing `id-token: write` permission | Add to job-level `permissions` block | | Reusable workflow can't access caller secrets | No `secrets: inherit` | Add `secrets: inherit` or explicitly pass secrets | --- ## GitHub Actions Expressions Reference ```yaml # Context objects available in expressions ${{ github.sha }} # Commit SHA ${{ github.ref }} # Branch/tag ref ${{ github.ref_name }} # Short branch/tag name ${{ github.event_name }} # Event name (push, pull_request, etc.) ${{ github.actor }} # Username who triggered the run ${{ github.repository }} # org/repo ${{ github.run_id }} # Unique run ID ${{ runner.os }} # Linux, Windows, macOS # Built-in functions ${{ toJson(github) }} # Serialize context to JSON ${{ fromJson(needs.job.outputs.matrix) }} # Parse JSON string ${{ hashFiles('**/package-lock.json') }} # Hash file(s) for cache keys ${{ format('{0}/{1}', var1, var2) }} # String formatting ${{ join(matrix.items, ',') }} # Join array # Status functions (use in if: conditions) ${{ success() }} # All previous steps succeeded ${{ failure() }} # Any previous step failed ${{ cancelled() }} # Workflow was cancelled ${{ always() }} # Always runs (success OR failure OR cancelled) ``` --- ## Production Readiness Checklist Before merging any workflow to `main`, verify: ### Security - [ ] All third-party actions pinned to full commit SHA - [ ] `permissions:` declared at workflow and job level (least privilege) - [ ] No `${{ }}` expressions directly in `run:` blocks (use env vars) - [ ] OIDC used for cloud credentials (no long-lived secrets stored) - [ ] `pull_request_target` gated with label check + author_association guard - [ ] Secrets never echoed or logged ### Reliability - [ ] `timeout-minutes` set on every job - [ ] `fail-fast: false` set for matrix builds used for debugging - [ ] `concurrency` configured to cancel stale runs - [ ] Retry logic for flaky external calls - [ ] Artifact retention policy set appropriately ### Performance - [ ] Dependency caching configured (setup-* cache or actions/cache) - [ ] Docker layer caching enabled (`type=gha`) - [ ] Path filters on `push`/`pull_request` to skip unrelated changes - [ ] Matrix parallelism appropriate (not exhausting runner pool) ### Maintainability - [ ] Reusable workflows used for repeated patterns - [ ] Composite actions used for repeated step sequences - [ ] Workflow names and step names are human-readable - [ ] `_` prefix on internal/reusable workflow files - [ ] Environment protection rules configured for `production` --- ## Related Skills - `gha-security-review` — Deep security audit of existing workflow files - `github-actions-templates` — Copy-paste ready workflow templates - `docker-expert` — Container build optimization and Dockerfile best practices - `kubernetes-architect` — Deploying to Kubernetes from GitHub Actions - `gitlab-ci-patterns` — GitLab CI/CD equivalent patterns