GitHub Actions Skill
Guides the creation, modification, and review of GitHub Actions workflows in the Skill System Foundry repository. Codifies the conventions established across the repository's existing workflows.
Repository Workflows
| Workflow | File | Trigger | Jobs |
|---|---|---|---|
| Tests + coverage | python-tests.yaml |
Push to main, PRs, workflow_call |
test (matrix), validate-examples, reference-conformance, bundle-extract-smoke |
| Coverage badge | coverage-badge.yaml |
workflow_run after Python tests |
badge |
| CI helper tests | ci-helper-tests.yaml |
Push/PR (path-filtered to CI helpers) | helper-tests (matrix) |
| Shell lint | shellcheck.yaml |
Push/PR (path-filtered to .sh) |
shellcheck |
| Action-pin verify | verify-action-pins.yaml |
Push to main, PRs |
verify |
| Workflow lint | actionlint.yaml |
Push to main, PRs |
actionlint |
| Codex code review | codex-code-review.yaml |
PR events (non-draft) | review (read-only), publish (write) |
| Release-label check | verify-pr-release-label.yaml |
PR events | verify-release-label (required) |
| Supply-chain scorecard | scorecard.yaml |
Schedule, push to main, branch-protection change |
analysis |
| CodeQL | codeql.yaml |
Push to main, PRs, schedule |
analyze |
| Tool-catalog drift | tool-catalog-drift.yaml |
Schedule, workflow_dispatch |
detect-and-pr |
| Release prep | release-prep.yaml |
workflow_dispatch |
prepare, test, open-pr |
| Release tag | release-on-merge.yaml |
Release PR merged to main |
tag |
| Release bundle | release.yaml |
v*.*.* tag push |
release |
Hard Requirements
Actions Pinned to Commit SHAs
Every action must be pinned to a full 40-character commit SHA, not a tag. Include a comment with the tag and semver for readability:
# Correct
- uses: actions/checkout@de0fac2e5ef641dbfe0fef2a1de4a5c3a0d70dce # @v6 as 6.0.2
# Wrong — tag-only reference
- uses: actions/checkout@v4
When updating an action version:
- Find the new tag's commit SHA on the action's repository releases page
- Replace the SHA in the workflow
- Update the comment to reflect the new tag and version
This rule is enforced by .github/scripts/verify-action-pins.py, which the verify-action-pins.yaml workflow runs on every PR and push to main. Any uses: line that is not a 40-character lowercase commit SHA (or ./local-path / docker://...) fails CI. Run the script locally with python .github/scripts/verify-action-pins.py to check before pushing.
App Token Identity
When minting a GitHub App token with actions/create-github-app-token, use the client-id input — app-id is deprecated (the action carries a deprecationMessage pointing to client-id). Across these OSS repos each automation identity wires a <PREFIX>_CLIENT_ID repository variable for the App's client ID plus a repository secret holding its PEM private key; the private-key secret name is not uniform (AUTOMATION_PRIVATE_KEY for the automation App, RELEASE_APP_PRIVATE_KEY for the release App), so take the exact names from the list below:
- oss-automation-bot —
vars.AUTOMATION_CLIENT_ID+secrets.AUTOMATION_PRIVATE_KEY(the workhorse: dependency automation, opening/approving PRs). Wired in this repo'stool-catalog-drift.yamland the release-PR approval inrelease-prep.yaml. - oss-release-bot —
vars.RELEASE_CLIENT_ID+secrets.RELEASE_APP_PRIVATE_KEY(the release identity: opens the release PR, tags, and publishes). Wired inrelease-prep.yaml,release-on-merge.yaml, andrelease.yaml.
# Correct
- uses: actions/create-github-app-token@<sha> # @v3 as 3.x
with:
client-id: ${{ vars.AUTOMATION_CLIENT_ID }}
private-key: ${{ secrets.AUTOMATION_PRIVATE_KEY }}
# Wrong — deprecated app-id input
- uses: actions/create-github-app-token@<sha> # @v3 as 3.x
with:
app-id: ${{ vars.AUTOMATION_APP_ID }}
private-key: ${{ secrets.AUTOMATION_PRIVATE_KEY }}
Least-Privilege Permissions
Scope permissions as narrowly as possible. Default to contents: read. Only add write permissions where required, and prefer job-level over workflow-level permissions:
# Preferred: job-level permissions
jobs:
test:
permissions:
contents: read
deploy:
permissions:
contents: write
# Acceptable: workflow-level when all jobs need the same
permissions:
contents: read
The Codex code review workflow demonstrates the ideal pattern: a read-only review job followed by a write-capable publish job, isolating the permission boundary.
No Hardcoded Secrets
Secrets are accessed via ${{ secrets.VARIABLE_NAME }}, variables via ${{ vars.VARIABLE_NAME }}. Never echo secrets to logs. Shell scripts called by workflows validate required environment variables at the top with ${VAR:?}.
Conventions
Trigger Configuration
- Push to
main— for CI that should run on every merge (tests, badge updates) pull_request— for validation that gates merges (tests, lint, code review)- Path filtering — use
paths:to avoid triggering on unrelated changes.shellcheck.yamlonly runs when.shfiles change - Activity types — specify explicit types for PR workflows when not all events are relevant:
types: [opened, reopened, synchronize, ready_for_review]
Runner Selection
ubuntu-latestis the default for all jobs- Add
windows-latestvia matrix only when cross-platform testing is needed (currently onlypython-tests.yaml) - Do not add macOS unless explicitly required — it consumes more CI minutes
Matrix Strategy
Use matrix for cross-platform or multi-version testing. Set fail-fast: false so all combinations run even if one fails:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.12"]
Concurrency Control
Use concurrency groups with cancel-in-progress: true to avoid redundant runs on rapid pushes:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
The Codex review workflow uses PR-number-based concurrency: codex-review-${{ github.event.pull_request.number }}.
Artifact Passing Between Jobs
When jobs with different permission scopes need to share data, use upload/download artifact pairs:
- Job A (read-only) uploads an artifact
- Job B (write) downloads and processes it
This pattern is used by both python-tests.yaml (coverage total) and codex-code-review.yaml (review output). Never pass data through workflow outputs for sensitive content.
Timeout Discipline
Set explicit timeout-minutes on jobs that call external services or could hang:
jobs:
review:
timeout-minutes: 30
publish:
timeout-minutes: 5
Jobs that only run shell commands or Python scripts can rely on the GitHub default (6 hours) but consider setting a reasonable limit.
Adding a New Workflow
- Create
.github/workflows/<name>.yaml - Define the trigger (push, pull_request, release, workflow_dispatch)
- Set the minimum required permissions at job level
- Pin all actions to commit SHAs with version comments
- Add path filtering if the workflow only applies to specific file types
- Add concurrency control if the workflow could run redundantly
- Test the workflow on a branch before merging to
main
Modifying an Existing Workflow
- Identify which workflow file to change
- Check if the change affects permissions — if adding a write operation, it may need to move to a separate job with elevated permissions
- If adding a new action, pin it to a commit SHA immediately
- If changing triggers, verify the workflow still runs when expected and does not run unnecessarily
- Test on a branch — push a PR to verify the workflow triggers correctly
Reviewing Workflow Changes
Check these in order:
- Are all actions SHA-pinned? Enforced automatically by
verify-action-pins.yaml; rely on the gate instead of reviewing this by eye. - Are permissions minimal? Does the workflow request more access than it needs? Does a write permission belong at job level instead of workflow level?
- Is the permission boundary maintained? Read-only analysis and write-capable publishing should be in separate jobs
- Are new environment variables validated? Scripts called by the workflow should check
${VAR:?}at the top - Is path filtering appropriate? Does the workflow run on changes it does not need to process?
- Are concurrency groups set? Could rapid pushes cause redundant runs?
Common Mistakes
- Using a tag reference (
@v4) instead of a SHA — breaks reproducibility and is a security risk - Granting
contents: writeat workflow level when only one job needs it - Missing path filter — workflow runs on every PR even when irrelevant files changed
- Forgetting to update the version comment after changing a SHA
- Passing sensitive data through workflow outputs instead of artifacts
- Missing
timeout-minuteson jobs that call external APIs - Adding a new action without verifying its SHA from the official repository