Treat CI/workflow security like production security code: define explicit invariants, enforce them defensively, and lock them with executable regression tests—especially in workflows that run in privileged contexts (e.g., pull_request_target) or execute untrusted PR code.

Apply these rules: 1) Pin third-party actions by immutable SHA

2) Fail-closed authorization and principal resolution

3) Credential minimization and strict scoping

4) Prevent control/data-plane poisoning

5) Filesystem hardening for self-hosted/persistent workspaces

6) Add/extend executable regression tests for invariants

Minimal examples (adapt patterns):

Action pinning:

- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.x.y pinned

Avoid embedding step outputs directly into shell:

- name: Record receipt
  env:
    POOL_RUN_ID: '${{ steps.dispatch.outputs.run_id }}'
  run: |
    printf '%s\n' "Pool run: $POOL_RUN_ID" >> "$GITHUB_STEP_SUMMARY"

Fail-closed principal handling (pattern):

# deny on missing/empty principal or permission API failures
if [ -z "$PR_AUTHOR" ] || ! permission="$permission_api_result"; then
  echo "decision=skip" >> "$GITHUB_OUTPUT"
  exit 0
fi
case "$permission" in
  admin|maintain|write) : ;;
  *) echo "decision=skip" >> "$GITHUB_OUTPUT"; exit 0 ;;
esac

Run the above as a checklist for every security-critical workflow change: if you can’t test the invariant, you don’t truly have one.