Shell Scripts Skill
Enforces quality, safety, and consistency for all shell scripts under .github/scripts/ in the Skill System Foundry repository.
These scripts are CI helpers called by GitHub Actions workflows. They handle coverage badge updates and per-file coverage checks. They run in ephemeral CI environments and must be reliable, secure, and shellcheck-clean.
Directory Layout
.github/
├── scripts/
│ ├── build-skill-bundle.sh ← builds + verifies the release zip and checksum
│ ├── check-per-file-coverage.py ← enforces per-file branch coverage minimum
│ └── update-coverage-badge.sh ← pushes coverage.json to orphan badges branch
├── workflows/
│ ├── python-tests.yaml ← runs tests + coverage (ubuntu + windows)
│ ├── coverage-badge.yaml ← updates the coverage badge on push to main
│ ├── shellcheck.yaml ← lints all .sh files with shellcheck
│ ├── codex-code-review.yaml ← Codex PR review via codex-ai-code-review-action
│ ├── release-prep.yaml ← dispatch: bump, changelog, open/auto-merge release PR
│ ├── release-on-merge.yaml ← tags vX.Y.Z when a release PR merges
│ └── release.yaml ← builds the bundle + creates the GitHub Release on tag push
├── instructions/
│ ├── markdown.instructions.md ← Markdown review rules
│ └── scripts.instructions.md ← Python review rules
├── copilot-instructions.md ← top-level review guidance
└── CODEOWNERS ← requires code-owner approval
Hard Requirements
Strict Mode
Every script starts with:
#!/bin/bash
set -euo pipefail
set -e— exit immediately on non-zero returnset -u— treat unset variables as errorsset -o pipefail— propagate pipe failures
Environment Variable Validation
Validate all required environment variables at the top of the script, before any work:
# Validate required environment variables
: "${BASE_SHA:?Environment variable BASE_SHA is required}"
: "${HEAD_SHA:?Environment variable HEAD_SHA is required}"
: "${GITHUB_OUTPUT:?Environment variable GITHUB_OUTPUT is required}"
This pattern uses parameter expansion with :? — if the variable is unset or empty, the script exits immediately with the error message. Group all validations together at the top.
ShellCheck Compliance
All .sh files must pass shellcheck with no warnings. The shellcheck.yaml workflow runs automatically on PRs that modify .github/scripts/*.sh and on pushes to main.
Common shellcheck rules to watch:
- SC2086 — double-quote variable expansions:
"$VAR"not$VAR - SC2155 — separate declaration and assignment:
local var; var=$(...)notlocal var=$(cmd) - SC2046 — quote command substitutions:
"$(cmd)"not$(cmd) - SC2034 — unused variables (remove or export)
CODEOWNERS Protection
.github/scripts/ is protected by CODEOWNERS — changes require code-owner approval. This is a security boundary: these scripts run in CI with access to secrets and write permissions.
Script Header Convention
Every script follows this structure:
#!/bin/bash
set -euo pipefail
# Validate required environment variables
: "${VAR1:?Environment variable VAR1 is required}"
: "${VAR2:?Environment variable VAR2 is required}"
# <Brief description of what this script does.>
#
# Environment variables:
# VAR1 — description of what this variable provides
# VAR2 — description of what this variable provides
#
# Outputs:
# output-name=value → written to $GITHUB_OUTPUT
# path/to/file → created on disk
The header documents: purpose, required environment variables with descriptions, and outputs (both $GITHUB_OUTPUT values and files created).
GitHub Actions Integration
Writing to GITHUB_OUTPUT
Use >> append, not > overwrite:
echo "has-changes=true" >> "$GITHUB_OUTPUT"
Accessing Secrets Safely
Never echo secrets. Use environment variables set by the workflow:
env:
HAS_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY != '' }}
Check the boolean string, not the secret itself:
if [ "$HAS_OPENAI_API_KEY" != "true" ]; then
echo "OPENAI_API_KEY is not set. Skipping."
echo "allowed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
GitHub Actions Annotations
Use ::warning:: and ::error:: prefixes for structured log output:
echo "::warning::Base SHA ${BASE_SHA} not found locally — fetching from origin"
echo "::error::CODEX_REVIEW_USERS repository variable is not set."
Workflow Security Model
The repository uses permission isolation across workflow jobs:
- Read-only jobs —
contents: readonly. Cannot modify the repo. - Write jobs — scoped to the minimum permission needed (e.g.
pull-requests: write).
When adding new scripts:
- Determine which permission boundary the script belongs in
- Never combine read and write permissions in one job
- Pin all actions to full commit SHAs (not tags):
uses: actions/checkout@de0fac... - Fork PRs cannot access secrets — this is a GitHub security guarantee the pipeline relies on
Temporary Files and Cleanup
Use mktemp for temporary directories and trap for cleanup:
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
This ensures cleanup runs even on errors (because set -e triggers EXIT).
Adding a New Script
- Create
.github/scripts/<name>.shwith the standard header - Make it executable:
chmod +x .github/scripts/<name>.sh - Add environment variable validation at the top
- Document environment variables and outputs in the header comment
- Run
shellcheck .github/scripts/<name>.shlocally before pushing - Call from the workflow with
bash .github/scripts/<name>.sh(notsh— these are bash scripts) - Use explicit
env:blocks in the workflow step to pass variables
Common Patterns
Conditional Execution with Outputs
if [ "$CONDITION" = "true" ]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0 # Clean exit — not a failure
fi
JSON Validation with jq
if printf '%s' "$RAW_JSON" | jq . > output.json 2>/dev/null; then
handle_success
else
echo "::error::Invalid JSON in output"
exit 1
fi
Git Operations in CI
# Fetch with authentication fallback
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
git fetch --no-tags origin "${BASE_SHA}"
fi
Orphan Branch Push (Coverage Badge Pattern)
git init "$WORK_DIR/repo"
cp "$WORK_DIR/coverage.json" "$WORK_DIR/repo/coverage.json"
git -C "$WORK_DIR/repo" checkout --orphan badges
git -C "$WORK_DIR/repo" add coverage.json
git -C "$WORK_DIR/repo" \
-c user.name="github-actions[bot]" \
-c user.email="github-actions[bot]@users.noreply.github.com" \
commit -m "Update coverage badge [skip ci]"
Key details: use [skip ci] to prevent infinite loops, use github-actions[bot] as committer, propagate auth headers from actions/checkout.
Common Mistakes
- Missing
set -euo pipefailat the top - Unquoted variable expansion (
$VARinstead of"$VAR") - Using
>instead of>>for$GITHUB_OUTPUT(overwrites previous outputs) - Echoing secrets to logs
- Mixing
shandbashsyntax (these are bash scripts, call withbash) - Missing
trapcleanup for temporary files - Forgetting to validate environment variables before using them
- Using tag-based action pins (
@v6) instead of commit SHA pins - Combining read and write permissions in a single workflow job