opencode-skills-collection 3.0.31 → 3.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1075 @@
1
+ ---
2
+ name: github-actions-advanced
3
+ description: >
4
+ Design, debug, and harden GitHub Actions CI/CD workflows, including reusable
5
+ workflows, matrix builds, self-hosted runners, OIDC authentication, caching,
6
+ environments, secrets, and release automation.
7
+ category: devops
8
+ risk: safe
9
+ source: community
10
+ date_added: "2026-05-30"
11
+ ---
12
+
13
+ # GitHub Actions Advanced Skill
14
+
15
+ Expert guidance for designing, writing, debugging, and securing **production-grade** GitHub Actions workflows.
16
+
17
+ ---
18
+
19
+ ## When to Use This Skill
20
+
21
+ - User mentions GitHub Actions, `.github/workflows`, CI/CD pipelines, runners, jobs, steps, or actions
22
+ - User wants to automate builds, tests, deployments, or releases via GitHub
23
+ - User asks about matrix builds, reusable workflows, composite actions, or self-hosted runners
24
+ - User needs help with OIDC authentication, caching strategies, or secrets management
25
+ - User says "my GitHub pipeline is failing" or "set up CI for my repo"
26
+ - User asks about workflow security, hardening, or environment protection rules
27
+
28
+ ## When NOT to Use This Skill
29
+
30
+ - The user is working with GitLab CI/CD → recommend `gitlab-ci-patterns`
31
+ - The user is working with CircleCI, Jenkins, or other CI platforms
32
+ - The task is purely about Docker image building without GitHub context → recommend `docker-expert`
33
+ - The task is about Kubernetes deployment configuration → recommend `kubernetes-architect`
34
+
35
+ ---
36
+
37
+ ## Step 1: Understand Context Before Responding
38
+
39
+ When invoked, first gather context:
40
+
41
+ ```bash
42
+ # Discover existing workflows in the repo
43
+ find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20
44
+
45
+ # Check for composite actions
46
+ find .github/actions -name "action.yml" 2>/dev/null
47
+
48
+ # Detect tech stack (influences runner OS, language setup actions)
49
+ ls package.json requirements.txt Gemfile go.mod Cargo.toml pom.xml 2>/dev/null
50
+ ```
51
+
52
+ Then adapt recommendations to:
53
+ - Existing workflow patterns in the repo
54
+ - The tech stack and language runtime
55
+ - Whether this is a monorepo or single-project repo
56
+ - Whether self-hosted or GitHub-hosted runners are in use
57
+
58
+ ---
59
+
60
+ ## Workflow Structure Reference
61
+
62
+ ```yaml
63
+ name: Workflow Name
64
+
65
+ on: # Triggers (see Triggers section)
66
+ push:
67
+ branches: [main]
68
+
69
+ permissions: # Always declare — principle of least privilege
70
+ contents: read
71
+
72
+ env: # Workflow-level env vars
73
+ NODE_VERSION: '20'
74
+
75
+ concurrency: # Prevent duplicate runs
76
+ group: ${{ github.workflow }}-${{ github.ref }}
77
+ cancel-in-progress: true # Cancel older runs for same branch
78
+
79
+ jobs:
80
+ job-id:
81
+ name: Human-readable name
82
+ runs-on: ubuntu-24.04 # Pin OS version — never use -latest in prod
83
+ timeout-minutes: 15 # Always set — prevents runaway jobs
84
+ environment: production # Links to GitHub Environment (approvals/secrets)
85
+
86
+ steps:
87
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
88
+ - name: Step name
89
+ run: echo "hello"
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Triggers (`on:`)
95
+
96
+ ### Common Patterns
97
+
98
+ ```yaml
99
+ on:
100
+ push:
101
+ branches: [main, 'release/**']
102
+ paths-ignore: ['**.md', 'docs/**'] # Skip docs-only changes
103
+
104
+ pull_request:
105
+ types: [opened, synchronize, reopened]
106
+ branches: [main]
107
+
108
+ workflow_dispatch: # Manual trigger with inputs
109
+ inputs:
110
+ environment:
111
+ description: 'Deploy target'
112
+ required: true
113
+ type: choice
114
+ options: [staging, production]
115
+ dry-run:
116
+ description: 'Dry run only?'
117
+ type: boolean
118
+ default: false
119
+
120
+ schedule:
121
+ - cron: '0 2 * * 1' # Monday 2am UTC
122
+
123
+ workflow_call: # Called by other workflows (reusable)
124
+ inputs:
125
+ image-tag:
126
+ type: string
127
+ required: true
128
+ secrets:
129
+ deploy-token:
130
+ required: true
131
+
132
+ release:
133
+ types: [published] # Trigger only on published releases
134
+
135
+ pull_request_target: # Runs with repo secrets — use with care!
136
+ types: [labeled] # Gate with label + author_association check
137
+ ```
138
+
139
+ > **Security Warning:** `pull_request_target` runs with repo secrets. Only use after a maintainer labels the PR. Never check out fork code without explicit sandboxing.
140
+
141
+ ---
142
+
143
+ ## Reusable Workflows
144
+
145
+ Split large pipelines into composable units stored in `.github/workflows/`.
146
+
147
+ **Convention:** Prefix internal/reusable workflows with `_` (e.g., `_build.yml`).
148
+
149
+ ### Caller (`.github/workflows/deploy.yml`)
150
+
151
+ ```yaml
152
+ jobs:
153
+ call-build:
154
+ uses: ./.github/workflows/_build.yml # Same-repo reusable
155
+ # uses: org/repo/.github/workflows/build.yml@main # Cross-repo
156
+ with:
157
+ image-tag: ${{ github.sha }}
158
+ secrets: inherit # Pass all caller secrets down
159
+
160
+ call-test:
161
+ uses: ./.github/workflows/_test.yml
162
+ with:
163
+ node-version: '20'
164
+ secrets:
165
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Explicit secret passing
166
+ ```
167
+
168
+ ### Reusable Workflow (`.github/workflows/_build.yml`)
169
+
170
+ ```yaml
171
+ on:
172
+ workflow_call:
173
+ inputs:
174
+ image-tag:
175
+ type: string
176
+ required: true
177
+ push:
178
+ type: boolean
179
+ default: false
180
+ secrets:
181
+ registry-token:
182
+ required: false
183
+ outputs:
184
+ digest:
185
+ description: "Image digest"
186
+ value: ${{ jobs.build.outputs.digest }}
187
+
188
+ jobs:
189
+ build:
190
+ runs-on: ubuntu-24.04
191
+ timeout-minutes: 20
192
+ outputs:
193
+ digest: ${{ steps.build.outputs.digest }}
194
+ steps:
195
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
196
+ - id: build
197
+ uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
198
+ with:
199
+ push: ${{ inputs.push }}
200
+ tags: myapp:${{ inputs.image-tag }}
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Matrix Builds
206
+
207
+ ```yaml
208
+ jobs:
209
+ test:
210
+ strategy:
211
+ fail-fast: false # Don't cancel others if one fails
212
+ max-parallel: 4 # Limit concurrent runners
213
+ matrix:
214
+ os: [ubuntu-24.04, windows-2022, macos-14]
215
+ node: ['18', '20', '22']
216
+ exclude:
217
+ - os: windows-2022
218
+ node: '18'
219
+ include:
220
+ - os: ubuntu-24.04
221
+ node: '22'
222
+ experimental: true # Custom matrix variable
223
+
224
+ runs-on: ${{ matrix.os }}
225
+ timeout-minutes: 20
226
+
227
+ steps:
228
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
229
+ - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
230
+ with:
231
+ node-version: ${{ matrix.node }}
232
+ cache: 'npm'
233
+ - run: npm ci
234
+ - run: npm test
235
+ continue-on-error: ${{ matrix.experimental == true }}
236
+ ```
237
+
238
+ ### Dynamic Matrix via Script
239
+
240
+ ```yaml
241
+ jobs:
242
+ generate-matrix:
243
+ runs-on: ubuntu-24.04
244
+ outputs:
245
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
246
+ steps:
247
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
248
+ - id: set-matrix
249
+ run: |
250
+ SERVICES=$(ls services/ | jq -R -s -c 'split("\n")[:-1]')
251
+ echo "matrix={\"service\":$SERVICES}" >> $GITHUB_OUTPUT
252
+
253
+ build:
254
+ needs: generate-matrix
255
+ strategy:
256
+ matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
257
+ runs-on: ubuntu-24.04
258
+ steps:
259
+ - run: echo "Building ${{ matrix.service }}"
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Caching Strategies
265
+
266
+ ### Language Setup Actions (Preferred — No Extra Step Needed)
267
+
268
+ ```yaml
269
+ # Node.js
270
+ - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
271
+ with:
272
+ node-version: '20'
273
+ cache: 'npm' # or 'yarn' or 'pnpm'
274
+
275
+ # Python
276
+ - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
277
+ with:
278
+ python-version: '3.12'
279
+ cache: 'pip'
280
+
281
+ # Go
282
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
283
+ with:
284
+ go-version: '1.23'
285
+ cache: true
286
+
287
+ # Java / Gradle / Maven
288
+ - uses: actions/setup-java@7a6d8a8234af8eb26422e24052f73b12b0e46a27 # v4.6.0
289
+ with:
290
+ distribution: 'temurin'
291
+ java-version: '21'
292
+ cache: 'maven' # or 'gradle'
293
+ ```
294
+
295
+ ### Manual Cache (Any Tool)
296
+
297
+ ```yaml
298
+ - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
299
+ id: cache-deps
300
+ with:
301
+ path: |
302
+ ~/.cache/pip
303
+ .venv
304
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
305
+ restore-keys: |
306
+ ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
307
+ ${{ runner.os }}-pip-
308
+
309
+ - name: Install deps (only on cache miss)
310
+ if: steps.cache-deps.outputs.cache-hit != 'true'
311
+ run: pip install -r requirements.txt
312
+ ```
313
+
314
+ ### Docker Layer Caching
315
+
316
+ ```yaml
317
+ - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
318
+ with:
319
+ cache-from: type=gha
320
+ cache-to: type=gha,mode=max
321
+ # For registry-backed cache (cross-branch):
322
+ # cache-from: type=registry,ref=ghcr.io/myorg/myapp:buildcache
323
+ # cache-to: type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max
324
+ ```
325
+
326
+ ---
327
+
328
+ ## OIDC Authentication (Keyless Cloud Auth)
329
+
330
+ **Never store long-lived cloud credentials as secrets.** Use OIDC to get short-lived tokens that expire automatically.
331
+
332
+ ### AWS
333
+
334
+ ```yaml
335
+ permissions:
336
+ id-token: write
337
+ contents: read
338
+
339
+ steps:
340
+ - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
341
+ with:
342
+ role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
343
+ aws-region: us-east-1
344
+ role-session-name: GitHubActions-${{ github.run_id }}
345
+
346
+ # Trust policy on the IAM role must include:
347
+ # "token.actions.githubusercontent.com" as OIDC provider
348
+ # Condition: "repo:org/repo:ref:refs/heads/main" (restrict to branch)
349
+ ```
350
+
351
+ ### GCP (Workload Identity Federation)
352
+
353
+ ```yaml
354
+ permissions:
355
+ id-token: write
356
+ contents: read
357
+
358
+ steps:
359
+ - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7
360
+ with:
361
+ workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider
362
+ service_account: github-actions@my-project.iam.gserviceaccount.com
363
+ token_format: access_token # or 'id_token'
364
+ ```
365
+
366
+ ### Azure (Federated Identity)
367
+
368
+ ```yaml
369
+ permissions:
370
+ id-token: write
371
+ contents: read
372
+
373
+ steps:
374
+ - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
375
+ with:
376
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
377
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
378
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
379
+ # No client secret needed! Uses OIDC federated credentials
380
+ ```
381
+
382
+ ---
383
+
384
+ ## Environments & Deployment Protection
385
+
386
+ ```yaml
387
+ jobs:
388
+ deploy-staging:
389
+ environment:
390
+ name: staging
391
+ url: https://staging.myapp.com
392
+ runs-on: ubuntu-24.04
393
+ timeout-minutes: 30
394
+ steps:
395
+ - run: ./scripts/deploy.sh staging
396
+
397
+ deploy-production:
398
+ needs: deploy-staging
399
+ environment:
400
+ name: production
401
+ url: https://myapp.com # Shown in the GitHub UI deployment panel
402
+ runs-on: ubuntu-24.04
403
+ timeout-minutes: 30
404
+ steps:
405
+ - run: ./scripts/deploy.sh production
406
+ ```
407
+
408
+ **Configure in Settings → Environments:**
409
+ - **Required reviewers** — manual approval gate before run
410
+ - **Wait timer** — delay after approval (e.g., 10-minute buffer)
411
+ - **Branch/tag restrictions** — only `main` or `v*` tags can deploy to prod
412
+ - **Environment-specific secrets** — override repo-level secrets per environment
413
+ - **Deployment branches** — whitelist which branches can target this environment
414
+
415
+ ---
416
+
417
+ ## Secrets Management
418
+
419
+ ```yaml
420
+ # Access repo/org/environment secrets
421
+ env:
422
+ DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
423
+
424
+ # Auto-provided token — no setup needed
425
+ - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
426
+ with:
427
+ github-token: ${{ secrets.GITHUB_TOKEN }}
428
+
429
+ # Hierarchy (most specific wins):
430
+ # environment secret > repo secret > org secret
431
+ ```
432
+
433
+ ### Masking Dynamic Values
434
+
435
+ ```yaml
436
+ - name: Generate and mask dynamic token
437
+ run: |
438
+ TOKEN=$(./scripts/generate-token.sh)
439
+ echo "::add-mask::$TOKEN" # Mask in all subsequent logs
440
+ echo "DEPLOY_TOKEN=$TOKEN" >> $GITHUB_ENV
441
+ ```
442
+
443
+ ### Secrets in Composite Actions
444
+
445
+ ```yaml
446
+ # Secrets cannot be passed as inputs to composite actions
447
+ # Pass them as env vars instead:
448
+ - uses: ./.github/actions/my-action
449
+ env:
450
+ SECRET_VALUE: ${{ secrets.MY_SECRET }}
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Composite Actions
456
+
457
+ Package reusable step sequences into local actions. No container spin-up, no separate workflow file needed.
458
+
459
+ ### Action Definition (`.github/actions/setup-app/action.yml`)
460
+
461
+ ```yaml
462
+ name: Setup App
463
+ description: Install and configure application dependencies
464
+
465
+ inputs:
466
+ node-version:
467
+ description: 'Node.js version'
468
+ required: false
469
+ default: '20'
470
+ install-flags:
471
+ description: 'Additional npm install flags'
472
+ required: false
473
+ default: ''
474
+
475
+ outputs:
476
+ cache-hit:
477
+ description: 'Whether the dependency cache was hit'
478
+ value: ${{ steps.cache.outputs.cache-hit }}
479
+
480
+ runs:
481
+ using: composite
482
+ steps:
483
+ - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
484
+ with:
485
+ node-version: ${{ inputs.node-version }}
486
+ cache: npm
487
+
488
+ - id: cache
489
+ uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
490
+ with:
491
+ path: node_modules
492
+ key: ${{ runner.os }}-node-${{ inputs.node-version }}-${{ hashFiles('package-lock.json') }}
493
+
494
+ - name: Install dependencies
495
+ if: steps.cache.outputs.cache-hit != 'true'
496
+ shell: bash
497
+ run: npm ci ${{ inputs.install-flags }}
498
+
499
+ - name: Build
500
+ shell: bash
501
+ run: npm run build
502
+ ```
503
+
504
+ ### Usage in a Workflow
505
+
506
+ ```yaml
507
+ steps:
508
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
509
+ - uses: ./.github/actions/setup-app
510
+ with:
511
+ node-version: '22'
512
+ install-flags: '--ignore-scripts'
513
+ ```
514
+
515
+ ---
516
+
517
+ ## Self-Hosted Runners
518
+
519
+ ```yaml
520
+ jobs:
521
+ build-gpu:
522
+ runs-on: [self-hosted, linux, x64, gpu] # Label matching
523
+ timeout-minutes: 60
524
+
525
+ build-arm:
526
+ runs-on: [self-hosted, linux, arm64]
527
+ ```
528
+
529
+ ### Runner Best Practices
530
+
531
+ | Practice | Details |
532
+ |---|---|
533
+ | **Ephemeral runners** | Use Actions Runner Controller (ARC) on Kubernetes for fresh runners per job |
534
+ | **Isolation** | Never share prod runners with untrusted/fork PR workflows |
535
+ | **Cleanup hooks** | Set `ACTIONS_RUNNER_HOOK_JOB_COMPLETED` to reset environment |
536
+ | **Runner groups** | Use groups to restrict which repos/workflows can access which runners |
537
+ | **Labels** | Use custom labels (e.g., `gpu`, `high-memory`) for precise targeting |
538
+ | **Security** | Disable fork PR access to self-hosted runners in Settings |
539
+
540
+ ```bash
541
+ # Actions Runner Controller (Kubernetes) — recommended for ephemeral runners
542
+ helm install arc \
543
+ --namespace arc-systems \
544
+ --create-namespace \
545
+ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
546
+ ```
547
+
548
+ ---
549
+
550
+ ## Conditional Execution & Flow Control
551
+
552
+ ```yaml
553
+ # Condition on branch + event
554
+ - run: ./scripts/deploy.sh
555
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
556
+
557
+ # Continue on error (non-blocking steps)
558
+ - run: ./scripts/lint.sh
559
+ continue-on-error: true
560
+
561
+ # Job dependency and conditional execution
562
+ jobs:
563
+ test:
564
+ runs-on: ubuntu-24.04
565
+ outputs:
566
+ result: ${{ steps.run-tests.outcome }}
567
+
568
+ deploy:
569
+ needs: [test, build]
570
+ if: |
571
+ needs.test.result == 'success' &&
572
+ needs.build.result == 'success' &&
573
+ github.ref == 'refs/heads/main'
574
+ runs-on: ubuntu-24.04
575
+
576
+ notify-failure:
577
+ needs: [test, deploy]
578
+ if: failure() # Runs even if earlier jobs fail
579
+ runs-on: ubuntu-24.04
580
+ steps:
581
+ - run: ./scripts/notify-slack.sh "Pipeline failed!"
582
+ ```
583
+
584
+ ### Passing Data Between Jobs
585
+
586
+ ```yaml
587
+ jobs:
588
+ prepare:
589
+ runs-on: ubuntu-24.04
590
+ outputs:
591
+ version: ${{ steps.get-version.outputs.version }}
592
+ should-deploy: ${{ steps.check.outputs.deploy }}
593
+
594
+ steps:
595
+ - id: get-version
596
+ run: |
597
+ VERSION=$(cat VERSION)
598
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
599
+
600
+ - id: check
601
+ run: |
602
+ if git log -1 --pretty=%B | grep -q '\[deploy\]'; then
603
+ echo "deploy=true" >> $GITHUB_OUTPUT
604
+ else
605
+ echo "deploy=false" >> $GITHUB_OUTPUT
606
+ fi
607
+
608
+ build:
609
+ needs: prepare
610
+ if: needs.prepare.outputs.should-deploy == 'true'
611
+ runs-on: ubuntu-24.04
612
+ steps:
613
+ - run: echo "Building version ${{ needs.prepare.outputs.version }}"
614
+ ```
615
+
616
+ ---
617
+
618
+ ## Security Hardening
619
+
620
+ ### 1. Always Declare Permissions (Least Privilege)
621
+
622
+ ```yaml
623
+ # Workflow-level default — restrict everything
624
+ permissions:
625
+ contents: read
626
+
627
+ jobs:
628
+ publish:
629
+ # Job-level override — only expand what's needed
630
+ permissions:
631
+ contents: write # Only for release/publish jobs
632
+ packages: write # Only for container push jobs
633
+ pull-requests: write # Only for PR comment jobs
634
+ id-token: write # Only for OIDC auth jobs
635
+ ```
636
+
637
+ ### 2. Pin Third-Party Actions to Full Commit SHA
638
+
639
+ ```yaml
640
+ # ❌ UNSAFE — tag can be mutated or hijacked
641
+ - uses: actions/checkout@v4
642
+
643
+ # ✅ SAFE — commit SHA is immutable
644
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
645
+
646
+ # Tool to automate SHA pinning:
647
+ # npx pin-github-action .github/workflows/*.yml
648
+ # or: pip install ratchet && ratchet pin .github/workflows/
649
+ ```
650
+
651
+ ### 3. Prevent Script Injection
652
+
653
+ ```yaml
654
+ # ❌ UNSAFE — attacker controls PR title, which gets expanded in shell
655
+ - run: echo "${{ github.event.pull_request.title }}"
656
+
657
+ # ✅ SAFE — pass through environment variable (shell doesn't evaluate it)
658
+ - env:
659
+ PR_TITLE: ${{ github.event.pull_request.title }}
660
+ run: echo "$PR_TITLE"
661
+
662
+ # ✅ SAFE — expressions in if: conditions are evaluated by Actions, not shell
663
+ - if: github.event.pull_request.draft == false
664
+ run: echo "Not a draft"
665
+ ```
666
+
667
+ ### 4. Restrict `pull_request_target` Usage
668
+
669
+ ```yaml
670
+ # Only run when a maintainer adds a specific label — prevents untrusted execution
671
+ on:
672
+ pull_request_target:
673
+ types: [labeled]
674
+
675
+ jobs:
676
+ validate:
677
+ # Double-guard: check label name AND author_association
678
+ if: |
679
+ github.event.label.name == 'safe-to-test' &&
680
+ (github.event.pull_request.author_association == 'COLLABORATOR' ||
681
+ github.event.pull_request.author_association == 'MEMBER' ||
682
+ github.event.pull_request.author_association == 'OWNER')
683
+ ```
684
+
685
+ ### 5. Harden with StepSecurity
686
+
687
+ ```yaml
688
+ # Add to every workflow — hardens runner, monitors outbound traffic
689
+ - uses: step-security/harden-runner@4d991eb9995541a0b71d1b66f1f98a5f1bef422c # v2.11.0
690
+ with:
691
+ egress-policy: audit # Start with 'audit', move to 'block' after confirming allowlist
692
+ allowed-endpoints: >
693
+ api.github.com:443
694
+ registry.npmjs.org:443
695
+ objects.githubusercontent.com:443
696
+ ```
697
+
698
+ ---
699
+
700
+ ## Debugging Techniques
701
+
702
+ ```yaml
703
+ # Enable runner diagnostic logging via repo secrets:
704
+ # ACTIONS_RUNNER_DEBUG = true
705
+ # ACTIONS_STEP_DEBUG = true
706
+
707
+ # Dump full GitHub context for inspection
708
+ - name: Debug — dump github context
709
+ if: runner.debug == '1'
710
+ env:
711
+ GITHUB_CONTEXT: ${{ toJson(github) }}
712
+ run: echo "$GITHUB_CONTEXT" | jq '.'
713
+
714
+ # Dump all available contexts
715
+ - name: Debug — dump all contexts
716
+ if: runner.debug == '1'
717
+ run: |
718
+ echo "github: ${{ toJson(github) }}"
719
+ echo "env: ${{ toJson(env) }}"
720
+ echo "vars: ${{ toJson(vars) }}"
721
+ echo "runner: ${{ toJson(runner) }}"
722
+
723
+ # SSH into a failing runner for interactive debugging
724
+ - uses: mxschmitt/action-tmate@7b04f3521e6b0a9fc56fa8f9f50da4bcfb5fc7b5 # v3.19.0
725
+ if: failure() && runner.debug == '1'
726
+ with:
727
+ limit-access-to-actor: true # Only the workflow triggerer can SSH in
728
+ timeout-minutes: 30
729
+
730
+ # Check what's pre-installed on GitHub-hosted runners
731
+ - run: |
732
+ echo "=== Tool Versions ==="
733
+ node --version
734
+ python3 --version
735
+ go version
736
+ docker --version
737
+ echo "=== Disk Space ==="
738
+ df -h
739
+ echo "=== Memory ==="
740
+ free -h
741
+ ```
742
+
743
+ ---
744
+
745
+ ## Complete Pipeline Patterns
746
+
747
+ ### Pattern 1: Build → Test → Push → Deploy
748
+
749
+ ```yaml
750
+ name: CI/CD Pipeline
751
+
752
+ on:
753
+ push:
754
+ branches: [main]
755
+ pull_request:
756
+ branches: [main]
757
+
758
+ concurrency:
759
+ group: ${{ github.workflow }}-${{ github.ref }}
760
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
761
+
762
+ permissions:
763
+ contents: read
764
+
765
+ jobs:
766
+ # ── Build & Test ──────────────────────────────────────
767
+ build-test:
768
+ runs-on: ubuntu-24.04
769
+ timeout-minutes: 20
770
+ permissions:
771
+ contents: read
772
+ checks: write # For test result reporting
773
+
774
+ steps:
775
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
776
+
777
+ - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
778
+ with:
779
+ node-version: '20'
780
+ cache: 'npm'
781
+
782
+ - run: npm ci
783
+ - run: npm run lint
784
+ - run: npm run test -- --coverage
785
+ - run: npm run build
786
+
787
+ - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
788
+ with:
789
+ name: build-artifacts
790
+ path: dist/
791
+ retention-days: 7
792
+
793
+ # ── Push Image (main branch only) ─────────────────────
794
+ push-image:
795
+ needs: build-test
796
+ if: github.ref == 'refs/heads/main'
797
+ runs-on: ubuntu-24.04
798
+ timeout-minutes: 20
799
+ permissions:
800
+ contents: read
801
+ packages: write
802
+ id-token: write # For OIDC
803
+ outputs:
804
+ image-digest: ${{ steps.push.outputs.digest }}
805
+
806
+ steps:
807
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
808
+
809
+ - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
810
+
811
+ - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
812
+ with:
813
+ registry: ghcr.io
814
+ username: ${{ github.actor }}
815
+ password: ${{ secrets.GITHUB_TOKEN }}
816
+
817
+ - uses: docker/metadata-action@70b2cdc6480c1a8b86edf1777157f8f437de2166 # v5.5.1
818
+ id: meta
819
+ with:
820
+ images: ghcr.io/${{ github.repository }}
821
+ tags: |
822
+ type=sha,format=long
823
+ type=raw,value=latest
824
+
825
+ - id: push
826
+ uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
827
+ with:
828
+ context: .
829
+ push: true
830
+ tags: ${{ steps.meta.outputs.tags }}
831
+ labels: ${{ steps.meta.outputs.labels }}
832
+ cache-from: type=gha
833
+ cache-to: type=gha,mode=max
834
+ provenance: true # SLSA provenance attestation
835
+ sbom: true # Software Bill of Materials
836
+
837
+ # ── Deploy Staging ────────────────────────────────────
838
+ deploy-staging:
839
+ needs: push-image
840
+ runs-on: ubuntu-24.04
841
+ timeout-minutes: 30
842
+ environment:
843
+ name: staging
844
+ url: https://staging.myapp.com
845
+ steps:
846
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
847
+ - run: ./scripts/deploy.sh staging ${{ needs.push-image.outputs.image-digest }}
848
+
849
+ # ── Deploy Production (manual approval required) ──────
850
+ deploy-production:
851
+ needs: deploy-staging
852
+ runs-on: ubuntu-24.04
853
+ timeout-minutes: 30
854
+ environment:
855
+ name: production
856
+ url: https://myapp.com
857
+ steps:
858
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
859
+ - run: ./scripts/deploy.sh production ${{ needs.push-image.outputs.image-digest }}
860
+ ```
861
+
862
+ ### Pattern 2: Automated Release with Changelog
863
+
864
+ ```yaml
865
+ name: Release
866
+
867
+ on:
868
+ push:
869
+ tags: ['v[0-9]+.[0-9]+.[0-9]+']
870
+
871
+ permissions:
872
+ contents: write
873
+
874
+ jobs:
875
+ release:
876
+ runs-on: ubuntu-24.04
877
+ timeout-minutes: 15
878
+
879
+ steps:
880
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
881
+ with:
882
+ fetch-depth: 0 # Full history needed for changelog generation
883
+
884
+ - uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
885
+ with:
886
+ generate_release_notes: true # Auto-generates from PR titles and commits
887
+ make_latest: true
888
+ fail_on_unmatched_files: true
889
+ files: |
890
+ dist/**/*.tar.gz
891
+ dist/**/*.zip
892
+ ```
893
+
894
+ ### Pattern 3: Dependency Auto-Update with PR
895
+
896
+ ```yaml
897
+ name: Dependency Updates
898
+
899
+ on:
900
+ schedule:
901
+ - cron: '0 9 * * 1' # Every Monday at 9am UTC
902
+ workflow_dispatch:
903
+
904
+ permissions:
905
+ contents: write
906
+ pull-requests: write
907
+
908
+ jobs:
909
+ update-deps:
910
+ runs-on: ubuntu-24.04
911
+ timeout-minutes: 20
912
+ steps:
913
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
914
+
915
+ - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
916
+ with:
917
+ node-version: '20'
918
+
919
+ - run: npx npm-check-updates -u
920
+ - run: npm install
921
+
922
+ - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
923
+ with:
924
+ commit-message: 'chore: update npm dependencies'
925
+ title: 'chore: update npm dependencies'
926
+ branch: 'chore/npm-updates'
927
+ delete-branch: true
928
+ body: |
929
+ Automated dependency updates generated by the dependency update workflow.
930
+ Please review and test before merging.
931
+ ```
932
+
933
+ ### Pattern 4: Security Scanning Pipeline
934
+
935
+ ```yaml
936
+ name: Security Scan
937
+
938
+ on:
939
+ push:
940
+ branches: [main]
941
+ pull_request:
942
+ branches: [main]
943
+ schedule:
944
+ - cron: '0 6 * * *' # Daily at 6am UTC
945
+
946
+ permissions:
947
+ contents: read
948
+ security-events: write # For uploading SARIF results
949
+
950
+ jobs:
951
+ codeql:
952
+ runs-on: ubuntu-24.04
953
+ timeout-minutes: 30
954
+ permissions:
955
+ security-events: write
956
+ actions: read
957
+ contents: read
958
+ steps:
959
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
960
+ - uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1
961
+ with:
962
+ languages: javascript-typescript
963
+ - uses: github/codeql-action/autobuild@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1
964
+ - uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1
965
+
966
+ container-scan:
967
+ runs-on: ubuntu-24.04
968
+ timeout-minutes: 15
969
+ steps:
970
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
971
+ - uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.28.0
972
+ with:
973
+ scan-type: 'fs'
974
+ format: 'sarif'
975
+ output: 'trivy-results.sarif'
976
+ severity: 'CRITICAL,HIGH'
977
+ - uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1
978
+ with:
979
+ sarif_file: 'trivy-results.sarif'
980
+ ```
981
+
982
+ ---
983
+
984
+ ## Common Pitfalls & Fixes
985
+
986
+ | Problem | Cause | Fix |
987
+ |---|---|---|
988
+ | 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 |
989
+ | Secret is `***` in logs but exposed | Dynamic value not masked | Use `echo "::add-mask::$VALUE"` before using it |
990
+ | Cache never hits across branches | Cache key too specific | Add `restore-keys` fallback without branch or hash segment |
991
+ | Matrix job fails silently | `fail-fast: true` (default) cancels siblings | Set `fail-fast: false` during debugging |
992
+ | Job hangs indefinitely | No `timeout-minutes` set | Always set `timeout-minutes` on every job |
993
+ | `$GITHUB_OUTPUT` not set | Old `set-output` command used | Use `echo "key=value" >> $GITHUB_OUTPUT` |
994
+ | OIDC token request fails | Missing `id-token: write` permission | Add to job-level `permissions` block |
995
+ | Reusable workflow can't access caller secrets | No `secrets: inherit` | Add `secrets: inherit` or explicitly pass secrets |
996
+
997
+ ---
998
+
999
+ ## GitHub Actions Expressions Reference
1000
+
1001
+ ```yaml
1002
+ # Context objects available in expressions
1003
+ ${{ github.sha }} # Commit SHA
1004
+ ${{ github.ref }} # Branch/tag ref
1005
+ ${{ github.ref_name }} # Short branch/tag name
1006
+ ${{ github.event_name }} # Event name (push, pull_request, etc.)
1007
+ ${{ github.actor }} # Username who triggered the run
1008
+ ${{ github.repository }} # org/repo
1009
+ ${{ github.run_id }} # Unique run ID
1010
+ ${{ runner.os }} # Linux, Windows, macOS
1011
+
1012
+ # Built-in functions
1013
+ ${{ toJson(github) }} # Serialize context to JSON
1014
+ ${{ fromJson(needs.job.outputs.matrix) }} # Parse JSON string
1015
+ ${{ hashFiles('**/package-lock.json') }} # Hash file(s) for cache keys
1016
+ ${{ format('{0}/{1}', var1, var2) }} # String formatting
1017
+ ${{ join(matrix.items, ',') }} # Join array
1018
+
1019
+ # Status functions (use in if: conditions)
1020
+ ${{ success() }} # All previous steps succeeded
1021
+ ${{ failure() }} # Any previous step failed
1022
+ ${{ cancelled() }} # Workflow was cancelled
1023
+ ${{ always() }} # Always runs (success OR failure OR cancelled)
1024
+ ```
1025
+
1026
+ ---
1027
+
1028
+ ## Production Readiness Checklist
1029
+
1030
+ Before merging any workflow to `main`, verify:
1031
+
1032
+ ### Security
1033
+ - [ ] All third-party actions pinned to full commit SHA
1034
+ - [ ] `permissions:` declared at workflow and job level (least privilege)
1035
+ - [ ] No `${{ }}` expressions directly in `run:` blocks (use env vars)
1036
+ - [ ] OIDC used for cloud credentials (no long-lived secrets stored)
1037
+ - [ ] `pull_request_target` gated with label check + author_association guard
1038
+ - [ ] Secrets never echoed or logged
1039
+
1040
+ ### Reliability
1041
+ - [ ] `timeout-minutes` set on every job
1042
+ - [ ] `fail-fast: false` set for matrix builds used for debugging
1043
+ - [ ] `concurrency` configured to cancel stale runs
1044
+ - [ ] Retry logic for flaky external calls
1045
+ - [ ] Artifact retention policy set appropriately
1046
+
1047
+ ### Performance
1048
+ - [ ] Dependency caching configured (setup-* cache or actions/cache)
1049
+ - [ ] Docker layer caching enabled (`type=gha`)
1050
+ - [ ] Path filters on `push`/`pull_request` to skip unrelated changes
1051
+ - [ ] Matrix parallelism appropriate (not exhausting runner pool)
1052
+
1053
+ ### Maintainability
1054
+ - [ ] Reusable workflows used for repeated patterns
1055
+ - [ ] Composite actions used for repeated step sequences
1056
+ - [ ] Workflow names and step names are human-readable
1057
+ - [ ] `_` prefix on internal/reusable workflow files
1058
+ - [ ] Environment protection rules configured for `production`
1059
+
1060
+ ---
1061
+
1062
+ ## Related Skills
1063
+
1064
+ - `gha-security-review` — Deep security audit of existing workflow files
1065
+ - `github-actions-templates` — Copy-paste ready workflow templates
1066
+ - `docker-expert` — Container build optimization and Dockerfile best practices
1067
+ - `kubernetes-architect` — Deploying to Kubernetes from GitHub Actions
1068
+ - `gitlab-ci-patterns` — GitLab CI/CD equivalent patterns
1069
+
1070
+ ## Limitations
1071
+
1072
+ - Use this skill only when the task clearly matches the scope described above.
1073
+ - Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
1074
+ - Always test reusable workflows in a feature branch before merging to main.
1075
+ - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.