CI/CD workflows should enforce state invariants across steps, runs, and runners: later steps must only execute when earlier phases truly produced a valid outcome, and all per-run state must be cleaned/flushed and permission-safe—especially on persistent self-hosted runners.
Apply these rules:
1) Gate downstream steps on all relevant phase outputs
gate -> verify -> push, the push step’s if: must include both gate success and verify’s final result (e.g., not no-fixes).# good: push requires gate=fixed AND verify != no-fixes
- name: Push and open PR
if: ${{ steps.gate.outputs.result == 'fixes' && steps.verify.outputs.result != 'no-fixes' }}
2) Keep published artifacts consistent after reverts/aborts
verify reverts commits, update the same data files/records you uploaded earlier (e.g., findings.json) so downstream consumers never see stale statuses.3) Flush per-run temp/state on persistent runners
rm -rf "$RUNNER_TEMP/verify-results" "$RUNNER_TEMP/verify-context" 2>/dev/null || true
mkdir -p "$RUNNER_TEMP/verify-results" "$RUNNER_TEMP/verify-context"
4) Restore both ownership and write permissions after hardening
chown without re-chmod can still break later cleanups/checkouts (EACCES). Restore mode to allow deletes/unlinks.chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" || true
chmod -R u+rwX "$GITHUB_WORKSPACE" || true
5) Make workflow behavior robust to cache misses and checkout edge-cases
chown when caches may miss.6) Prevent CI/resource hangs with explicit bounds
timeout-minutes.7) Keep concurrency predicates exact
concurrency.group logic matches the job’s runnable triggers; otherwise one command (e.g., /verify) can displace another (e.g., /triage).If your workflow cannot be proven under: (a) cancellations, (b) persistent runner reuse, (c) cache misses, and (d) permission/mode changes, it’s a CI reliability issue—fix it with the invariants above before merging.