Skip to content

PR-G 7a: deprecate Tier 1 third-party actions + zizmor provider safelist#1221

Merged
lmeyerov merged 9 commits intomasterfrom
chore/1130-pr-g-7a-safelist
Apr 27, 2026
Merged

PR-G 7a: deprecate Tier 1 third-party actions + zizmor provider safelist#1221
lmeyerov merged 9 commits intomasterfrom
chore/1130-pr-g-7a-safelist

Conversation

@lmeyerov
Copy link
Copy Markdown
Contributor

@lmeyerov lmeyerov commented Apr 26, 2026

Closes #1215. Part of umbrella #1130.

Summary

  • Deprecate Tier 1 third-party actions (no equivalent replacement needed):
    • styfle/cancel-workflow-action@0.11.0 → workflow-level concurrency: block in ci-gpu.yml
    • dorny/paths-filter@v3 → inline git diff --name-only shell filter in ci.yml (same outputs, merge-base semantics)
  • Enable zizmor unpinned-uses provider safelist in .github/zizmor.yml:
    • actions/*, github/*, pypa/* → ref-pin allowed (floating tags + branches OK)
    • everything else → must hash-pin
  • Lower zizmor gate from --min-severity high to medium in workflow-security.yml so policy violations actually fail CI.
  • Close 5 PR-B coverage gaps: add persist-credentials: false to LFS / fetch-depth checkouts in ci.yml (×3), codeql-analysis.yml, publish-pypi.yml. Surfaced when enabling the medium gate; in scope for the security-hardening theme.
  • Fix silent fail-closed in the changes job (caught by multi-wave review): when git merge-base cannot resolve (force-pushed branch with orphaned event.before, or PR base rebased mid-flight), the job previously emitted false for every filter output and silently skipped all gated tests. Now emits true for every output (conservative "couldn't compute, run everything") with a ::warning:: annotation in the runner log. Test harness covering 9 scenarios (incl. the orphaned-base BLOCKER repro) under plans/1130-pr-g-7a/waves/wave-2/test-filter.sh.

After this PR: 0 third-party uses: references in .github/workflows/. Trusted-org actions remain on floating refs by explicit team decision (see below).

Why we are NOT SHA-pinning trusted orgs

Durable rationale — please do not relitigate:

The team explicitly chose not to SHA-pin actions/*, github/*, or pypa/*. The tradeoff:

  • Trust: pypa and GitHub have strong account security; the historical mutable-tag attack pattern (tj-actions, reviewdog) is on smaller third-party orgs with weaker hygiene — exactly the Tier 1 we deprecate here.
  • Patch latency: floating @v4 / release/v1 gets CVE patches automatically. SHA-pinning 78 first-party call sites generates dependabot churn that nobody on a small team will triage carefully — leading to stale-pin-with-CVE, which is worse than floating-with-fast-patch.
  • Residual risk accepted: a compromised first-party action could ship a malicious wheel that still verifies via PR-F3 attestations (OIDC token minted before the malicious step). Narrow scenario; mitigated by trusting GitHub/PyPA account security.

Full discussion: #1130 (PR-G section + descope comment) and #1215 body.

Validation

  • actionlint clean on all touched workflows.
  • zizmor --min-severity medium .github/workflows/*.yml against the new config: 0 findings.
  • Inline shell filter preserves all 6 outputs (infra, python, gfql, cypher_frontend_ci, benchmarks, docs); downstream needs.changes.outputs.* references unchanged.
  • concurrency: block in ci-gpu.yml honors the existing skip-cancel PR label semantics via expression in cancel-in-progress.

Out of scope (follow-ups)

  • PR-G 7b: enable repo-level Settings → Actions → General → Allow specified actions allowlist matching this safelist (admin action; defense-in-depth at runner level).
  • PR-G 7c: agent-credential exposure audit (parallelizable; separate issue to be filed).

Test plan

  • CI passes on this PR (workflow-security.yml zizmor + actionlint gates green)
  • changes job emits expected outputs for: docs-only PR, python-code PR, gfql PR (verify by inspecting the job summary on this PR's run)
  • No regressions in jobs that gate on needs.changes.outputs.*

🤖 Generated with Claude Code

lmeyerov and others added 5 commits April 25, 2026 20:20
Drops the third-party `styfle/cancel-workflow-action@0.11.0` and the
`cancel_outstanding` job that wrapped it. Replaced by a workflow-level
`concurrency:` block which is GitHub's modern equivalent (the styfle
action's own README recommends switching).

Behavior preserved:
- Cancels in-progress runs of the same workflow on the same PR/ref
- Honors the `skip-cancel` PR label via expression in `cancel-in-progress`

The `cancel_outstanding` job was already gated by
`GRAPHISTRY_ENABLE_GPU_PUBLIC == 'true'` (disabled by default per PR-D
lockdown) so its removal does not affect any currently-running path.

Part of PR-G 7a (#1215) — Tier 1 third-party action deprecation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the third-party `dorny/paths-filter@v3` and replaces the
`changes` job's filter step with an inline `git diff --name-only`
shell that emits the same set of outputs (`infra`, `python`, `gfql`,
`cypher_frontend_ci`, `benchmarks`, `docs`).

Behavior preserved:
- Uses `git merge-base` to match dorny's default base resolution
- Each filter dimension keeps its full pattern set (translated from
  YAML globs to anchored regex)
- Same `$GITHUB_OUTPUT` keys, so all downstream `if:` conditions
  resolve unchanged
- Non-diff events (workflow_dispatch / schedule) emit `false`; the
  existing OR-condition `event_name == 'workflow_dispatch'` etc. in
  consumers handles those cases independently
- New-branch/zero-SHA pushes conservatively emit `true` (run all)

Tradeoff: lose YAML-style filter expressions in exchange for zero
third-party dep + consistency with the existing inline `docs_only_latest`
detection step in the same job.

Requires `fetch-depth: 0` on checkout for full history.

Part of PR-G 7a (#1215) — Tier 1 third-party action deprecation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ckouts

Five `actions/checkout@v4` call sites with `lfs: true` or
`fetch-depth: 0` did not get the `persist-credentials: false` treatment
during PR-B (#1130). Closes those gaps:

- ci.yml: cypher-frontend-strict-typing job (line 401)
- ci.yml: cypher-frontend-differential-parity job (line 451)
- ci.yml: 3.14 compat job (line 1248)
- codeql-analysis.yml: CodeQL scan checkout (line 43)
- publish-pypi.yml: versioneer fetch-depth checkout (line 39)

None of these jobs push back to the repo; the persisted token is unused
and removing it shrinks the credential window. Surfaced by enabling the
medium-severity zizmor gate in the next commit.

Part of PR-G 7a (#1215); also completes PR-B (#1130) coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the blanket `unpinned-uses: disable: true` with a policy
configuration that requires hash-pinning for any third-party action
outside the safelist of trusted providers:

  - actions/*  (GitHub-owned core)
  - github/*   (GitHub-owned extended, e.g., codeql-action)
  - pypa/*     (PyPA-maintained, including gh-action-pypi-publish)

These three providers may use ref-pin (tags or branches like
release/v1) without triggering a finding. Anything else must pin to
a full commit SHA.

Workflow-security gate dropped from `--min-severity high` to
`--min-severity medium` so `unpinned-uses` policy violations actually
fail CI (zizmor emits the rule at medium).

Decision context (durable; do not relitigate):
The team explicitly chose not to SHA-pin trusted-org actions. SHA-
pinning trades fast CVE patching for marginal mutable-tag protection
that the orgs' account security already provides; with 78 first-party
call sites the dependabot churn would not be triaged carefully on a
small team, leading to stale-pin-with-CVE which is worse than
floating-with-fast-patch. Full rationale in #1215 body.

Part of PR-G 7a (#1215). Follow-up: 7b enables a matching repo-level
Actions allowlist as defense-in-depth at the runner level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-review findings F1 + F6 from plans/1130-pr-g-7a/review/findings.md:

- ci-gpu.yml: explain why `!contains(null, …)` works for non-PR events
  (the cancel-in-progress expression looks surprising in isolation).
- ci.yml: rephrase the merge-base comment so it explains the *behavior*
  (diff diverging commits, not unrelated ones picked up on base) rather
  than naming the now-removed dorny dependency.

Cosmetic only; no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov marked this pull request as ready for review April 26, 2026 03:37
lmeyerov and others added 4 commits April 26, 2026 12:46
… diffs

Wave-1 review findings (plans/1130-pr-g-7a/waves/wave-1/):

- C1 [BLOCKER] force-push to a branch silently skipped all gated jobs.
  `github.event.before` after a force-push is an orphaned SHA; with
  `fetch-depth: 0` the orphan is not in the local clone, so
  `git merge-base "$base" "$head"` exits 128. The prior fallback
  `|| echo "$base"` re-assigned the unreachable SHA back to
  `merge_base`, and `git diff --name-only ... 2>/dev/null || true`
  swallowed the subsequent failure to produce empty `$changed`, so
  every `emit()` returned `false` and every gated job (python tests,
  gfql tests, etc.) was skipped on a code-bearing commit. PR showed
  green CI without running tests.

- C2 [BLOCKER] same root cause when a PR base ref is rebased between
  PR open and CI re-run. Same code path, same fix.

  Both BLOCKERs reproduced live in an isolated git repo; see
  plans/1130-pr-g-7a/waves/wave-1/adversarial/report.md.

  Fix: treat merge-base / diff failure as "couldn't compute, run
  everything" via emit_all true — same conservative stance already
  applied to zero-SHA new-branch pushes.

- C3 [IMPORTANT] `printf '%s\n' "$changed" | grep -qE "$pattern"`
  races SIGPIPE on diffs exceeding the Linux pipe buffer (~64 KB,
  thousands of files). With `set -o pipefail`, a SIGPIPE'd printf
  flips the pipeline non-zero and silently inverts the match.
  Replaced pipeline with here-string `<<<` which is fully buffered
  before grep starts; structurally eliminates the race.

- Q1 [SUGGESTION] Unicode `→` in ci-gpu.yml concurrency comment
  replaced with ASCII `->` for consistency with sibling workflow
  comment style.

Verification:
- Local actionlint + zizmor (--min-severity medium): 0 findings.
- Test harness in plans/1130-pr-g-7a/waves/wave-2/test-filter.sh
  drives the post-fix shell across 9 scenarios (PR/python, PR/docs,
  PR/gfql, PR/orphaned-base, push-zero-SHA, schedule, empty-diff,
  push-valid, ~234 KB SIGPIPE stress) — 28/28 PASS. Evidence in
  plans/1130-pr-g-7a/waves/wave-2/test-evidence.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave-2 review (full ceremony) findings (plans/1130-pr-g-7a/waves/wave-2/):

- W2-S2 [SUGGESTION] Operability: when the conservative `emit_all true;
  exit 0` fallback fires (force-pushed branch with orphaned event.before,
  or rebased PR base), no log signal was emitted. Operators had to infer
  the fail-open from the all-true outputs. Added a `::warning::` GHA
  annotation that names the failure and the underlying SHA pair.

- W2-S3 [SUGGESTION] Observability: dropped the `2>/dev/null` on the
  failing `git merge-base` and `git diff` calls so the underlying git
  error message (e.g., "fatal: Not a valid object name") shows up in
  the runner log alongside the warning.

- W2-S4 [SUGGESTION] Quality: expanded the `emit` SIGPIPE comment from
  one dense clause into a step-by-step explanation of how `set -o
  pipefail` + early `grep -q` exit + SIGPIPE'd printf produces a wrong
  `false` result. Self-evident now without requiring the reader to
  recall the pipefail/SIGPIPE interaction from memory.

All cosmetic / observability; no behavior change to the success paths.
Test harness re-run: 28/28 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the merged scope: Tier 1 third-party action deprecation,
zizmor unpinned-uses provider safelist, gate threshold lowered to
medium, 5 PR-B credential-persistence gaps closed, BLOCKER fix on
the changes job's fail-open behavior for force-push / rebased-base
edge cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 6 `emit` call sites previously squeezed each filter dimension into
a single regex blob — `'\.py$|^graphistry/|^setup\.py$|...'` — which
was hard to scan and didn't diff cleanly when adding/removing one
pattern. Refactored `emit` to take variadic regex args and join them
with `|` internally (`local IFS='|'; local pat="$*"`). Each call site
now lists patterns one per line, matching the original dorny YAML
shape:

    emit python \
      '\.py$' \
      '^graphistry/' \
      ...

Same regex semantics, no behavior change. Test harness updated to
match; 28/28 PASS unchanged.

Suggested by review: the joined-blob form was hard to read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov merged commit 2ef8736 into master Apr 27, 2026
135 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR-G 7a: deprecate Tier 1 third-party Actions + SHA-pin remaining

1 participant