From 12e36187dda52da9a1204d8186aa3c89e1f7455c Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sat, 25 Apr 2026 20:20:03 -0700 Subject: [PATCH 1/9] ci(gpu): replace styfle/cancel-workflow-action with native concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/ci-gpu.yml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-gpu.yml b/.github/workflows/ci-gpu.yml index f09edd85c6..ca5fa7eb94 100644 --- a/.github/workflows/ci-gpu.yml +++ b/.github/workflows/ci-gpu.yml @@ -11,6 +11,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-cancel') }} + env: UV_EXCLUDE_NEWER: "6 days" @@ -48,22 +52,6 @@ jobs: - name: Do nothing run: echo "Do nothing" - cancel_outstanding: - name: Detect and cancel outstanding runs of this workflow - # Temporarily disabled while gpu_public is unavailable and PR-D hardening is pending. - # Re-enable only after issue #1130 criteria are met. - if: ${{ vars.GRAPHISTRY_ENABLE_GPU_PUBLIC == 'true' }} # see #1130; disabled by default - runs-on: ubuntu-latest - permissions: - actions: write - timeout-minutes: 10 - steps: - - name: Cancel Previous Runs - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-cancel') }} - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ github.token }} - test-full-ai: # Temporarily disabled while gpu_public is unavailable and PR-D hardening is pending. # Re-enable only after issue #1130 criteria are met. From 646f6de17d850780cd5ee4bc4d6444b816a4863e Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sat, 25 Apr 2026 20:21:00 -0700 Subject: [PATCH 2/9] ci: replace dorny/paths-filter with inline shell filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/ci.yml | 114 +++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4bd2b737a..2648b1222f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,60 +45,68 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: dorny/paths-filter@v3 + # full history needed so `git diff ..` resolves on PRs / non-trivial pushes + fetch-depth: 0 + - name: Compute path filters id: filter - with: - filters: | - # Infrastructure changes that affect all tests - infra: - - '.github/workflows/ci.yml' - - 'docker/**' - - 'bin/**' - - 'setup.py' - - 'setup.cfg' - - 'MANIFEST.in' - - # Python code changes - python: - - '**.py' - - 'graphistry/**' - - 'setup.py' - - 'setup.cfg' - - 'pytest.ini' - - 'mypy.ini' - - 'bin/lint.sh' - - 'bin/typecheck.sh' - - # GFQL-specific changes - gfql: - - 'graphistry/gfql/**' - - 'graphistry/compute/gfql/**' - - 'graphistry/compute/gfql_unified.py' - - 'graphistry/models/gfql/**' - - 'graphistry/Plottable.py' - - 'tests/gfql/**' - - # Cypher frontend gate relevant changes - cypher_frontend_ci: - - '.github/workflows/ci.yml' - - '.github/workflows/ci-gpu.yml' - - 'graphistry/compute/gfql/ir/**' - - 'graphistry/compute/gfql/cypher/**' - - 'graphistry/compute/gfql/frontends/cypher/**' - - 'graphistry/tests/compute/gfql/cypher/**' - - 'tests/gfql/ref/**' - - # Benchmark-specific changes - benchmarks: - - 'benchmarks/**' - - # Documentation changes - docs: - - 'docs/**' - - '**.md' - - '**.rst' - - 'demos/**' - - 'notebooks/**' + env: + EVENT_NAME: ${{ github.event_name }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PUSH_BEFORE: ${{ github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: | + set -euo pipefail + keys=(infra python gfql cypher_frontend_ci benchmarks docs) + + emit_all() { + # $1 = "true" or "false" — emit the same value for every key + for k in "${keys[@]}"; do + echo "${k}=$1" >> "$GITHUB_OUTPUT" + done + } + + # Non diff-bearing events: rely on downstream `event_name == 'workflow_dispatch'` + # / `'schedule'` OR-conditions in job `if:` expressions. Emit false safely. + if [[ "$EVENT_NAME" != "pull_request" && "$EVENT_NAME" != "push" ]]; then + emit_all false + exit 0 + fi + + if [[ "$EVENT_NAME" == "pull_request" ]]; then + base="$PR_BASE_SHA" + head="$PR_HEAD_SHA" + else + base="$PUSH_BEFORE" + head="$HEAD_SHA" + fi + + # New branch / first push: zero-SHA base. Conservative: run everything. + if [[ -z "$base" || "$base" == "0000000000000000000000000000000000000000" ]]; then + emit_all true + exit 0 + fi + + # Use merge-base to match dorny/paths-filter default semantics + merge_base=$(git merge-base "$base" "$head" 2>/dev/null || echo "$base") + changed=$(git diff --name-only "$merge_base" "$head" 2>/dev/null || true) + + emit() { + local key="$1" + local pattern="$2" + if [[ -n "$changed" ]] && printf '%s\n' "$changed" | grep -qE "$pattern"; then + echo "${key}=true" >> "$GITHUB_OUTPUT" + else + echo "${key}=false" >> "$GITHUB_OUTPUT" + fi + } + + emit infra '^\.github/workflows/ci\.yml$|^docker/|^bin/|^setup\.py$|^setup\.cfg$|^MANIFEST\.in$' + emit python '\.py$|^graphistry/|^setup\.py$|^setup\.cfg$|^pytest\.ini$|^mypy\.ini$|^bin/lint\.sh$|^bin/typecheck\.sh$' + emit gfql '^graphistry/gfql/|^graphistry/compute/gfql/|^graphistry/compute/gfql_unified\.py$|^graphistry/models/gfql/|^graphistry/Plottable\.py$|^tests/gfql/' + emit cypher_frontend_ci '^\.github/workflows/ci\.yml$|^\.github/workflows/ci-gpu\.yml$|^graphistry/compute/gfql/ir/|^graphistry/compute/gfql/cypher/|^graphistry/compute/gfql/frontends/cypher/|^graphistry/tests/compute/gfql/cypher/|^tests/gfql/ref/' + emit benchmarks '^benchmarks/' + emit docs '^docs/|\.md$|\.rst$|^demos/|^notebooks/' - name: Detect docs-only change on tip id: docs_only_latest From ff5c4588f38596424a27f921abb0da4a97180470 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sat, 25 Apr 2026 20:21:10 -0700 Subject: [PATCH 3/9] ci: add persist-credentials: false to remaining LFS / fetch-depth checkouts 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) --- .github/workflows/ci.yml | 3 +++ .github/workflows/codeql-analysis.yml | 1 + .github/workflows/publish-pypi.yml | 1 + 3 files changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2648b1222f..b860a6536f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -399,6 +399,7 @@ jobs: uses: actions/checkout@v4 with: lfs: true + persist-credentials: false - name: Download lockfiles uses: actions/download-artifact@v4 @@ -449,6 +450,7 @@ jobs: uses: actions/checkout@v4 with: lfs: true + persist-credentials: false - name: Download lockfiles uses: actions/download-artifact@v4 @@ -1245,6 +1247,7 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@v5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e1581e0158..a7b2ea7ee6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,6 +41,7 @@ jobs: uses: actions/checkout@v4 with: lfs: true + persist-credentials: false - name: Checkout LFS objects run: git lfs pull diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d273ea7aa6..42d825e0c4 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -37,6 +37,7 @@ jobs: - uses: actions/checkout@v4 with: # fetch tag for versioneer fetch-depth: 0 + persist-credentials: false - name: Verify trusted release trigger run: | From 14f8ea77d5d618f09d8b833e63fd9809c109372e Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sat, 25 Apr 2026 20:21:28 -0700 Subject: [PATCH 4/9] ci(security): enable zizmor unpinned-uses with provider safelist 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) --- .github/workflows/workflow-security.yml | 7 +++++-- .github/zizmor.yml | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-security.yml b/.github/workflows/workflow-security.yml index ef21b6b616..4d92ecf0ff 100644 --- a/.github/workflows/workflow-security.yml +++ b/.github/workflows/workflow-security.yml @@ -68,6 +68,9 @@ jobs: python3 -m venv .venv-zizmor source .venv-zizmor/bin/activate uv pip install --upgrade zizmor - - name: Run zizmor (high severity gate) + - name: Run zizmor (medium severity gate) + # Lowered from `high` to `medium` to enable enforcement of the + # `unpinned-uses` provider safelist (see #1130 / #1215). zizmor's + # `unpinned-uses` rule emits at medium severity for policy violations. run: | - .venv-zizmor/bin/zizmor --format=github --no-progress --config .github/zizmor.yml --min-severity high .github/workflows/*.yml + .venv-zizmor/bin/zizmor --format=github --no-progress --config .github/zizmor.yml --min-severity medium .github/workflows/*.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 2fe494c600..ae00adcbd2 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,3 +1,20 @@ rules: unpinned-uses: - disable: true + config: + # Provider safelist for `uses:` references. See umbrella #1130 + #1215. + # + # Trusted orgs (allowed on floating refs): + # - `actions/*` — GitHub-owned core + # - `github/*` — GitHub-owned extended (codeql-action etc.) + # - `pypa/*` — PyPA-maintained, including `gh-action-pypi-publish` + # + # Anything outside the safelist must pin to a full commit SHA. + # + # Decision context: SHA-pinning trusted-org actions trades fast CVE patching + # for marginal mutable-tag protection that the orgs' account security already + # provides. See #1215 body for the full rationale. + policies: + "actions/*": ref-pin + "github/*": ref-pin + "pypa/*": ref-pin + "*": hash-pin From bf4b5b6a1b21dbcd23d3a91172b56635fa8b9735 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sat, 25 Apr 2026 20:37:21 -0700 Subject: [PATCH 5/9] ci: clarify concurrency null-coercion + drop ephemeral dorny reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/ci-gpu.yml | 4 ++++ .github/workflows/ci.yml | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-gpu.yml b/.github/workflows/ci-gpu.yml index ca5fa7eb94..c6b33f7b77 100644 --- a/.github/workflows/ci-gpu.yml +++ b/.github/workflows/ci-gpu.yml @@ -12,6 +12,10 @@ permissions: contents: read concurrency: + # Cancel previous runs of the same workflow/PR/ref. On non-PR events + # (workflow_dispatch, repository_dispatch) `pull_request.labels` is null; + # `contains(null, _)` returns false → `!contains(…)` evaluates true, + # so cancellation defaults on. The `skip-cancel` PR label opts out on PRs. group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-cancel') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b860a6536f..b6d432d3bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,8 @@ jobs: exit 0 fi - # Use merge-base to match dorny/paths-filter default semantics + # Diff vs the merge-base so PR runs see only commits that diverge from base + # (avoids picking up unrelated commits the base ref has gained meanwhile). merge_base=$(git merge-base "$base" "$head" 2>/dev/null || echo "$base") changed=$(git diff --name-only "$merge_base" "$head" 2>/dev/null || true) From d7eccbdf3474bef2898d7a40c5d82d6dd1a7a618 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sun, 26 Apr 2026 12:46:52 -0700 Subject: [PATCH 6/9] ci: fix path-filter fail-closed on unreachable base + SIGPIPE on huge diffs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/ci-gpu.yml | 2 +- .github/workflows/ci.yml | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-gpu.yml b/.github/workflows/ci-gpu.yml index c6b33f7b77..69c9a58aab 100644 --- a/.github/workflows/ci-gpu.yml +++ b/.github/workflows/ci-gpu.yml @@ -14,7 +14,7 @@ permissions: concurrency: # Cancel previous runs of the same workflow/PR/ref. On non-PR events # (workflow_dispatch, repository_dispatch) `pull_request.labels` is null; - # `contains(null, _)` returns false → `!contains(…)` evaluates true, + # `contains(null, _)` returns false -> `!contains(...)` evaluates true, # so cancellation defaults on. The `skip-cancel` PR label opts out on PRs. group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-cancel') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6d432d3bd..63cd323ba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,13 +89,25 @@ jobs: # Diff vs the merge-base so PR runs see only commits that diverge from base # (avoids picking up unrelated commits the base ref has gained meanwhile). - merge_base=$(git merge-base "$base" "$head" 2>/dev/null || echo "$base") - changed=$(git diff --name-only "$merge_base" "$head" 2>/dev/null || true) + # If either resolution fails (e.g., force-pushed branch with orphaned + # event.before, or rebased PR base ref), fall back to the conservative + # "couldn't compute, run everything" stance. + if ! merge_base=$(git merge-base "$base" "$head" 2>/dev/null); then + emit_all true + exit 0 + fi + if ! changed=$(git diff --name-only "$merge_base" "$head" 2>/dev/null); then + emit_all true + exit 0 + fi emit() { local key="$1" local pattern="$2" - if [[ -n "$changed" ]] && printf '%s\n' "$changed" | grep -qE "$pattern"; then + # Use here-string instead of `printf | grep` to avoid SIGPIPE on huge + # diffs: with `set -o pipefail`, a SIGPIPE'd printf would flip the + # pipeline to non-zero and silently invert the match. + if [[ -n "$changed" ]] && grep -qE "$pattern" <<< "$changed"; then echo "${key}=true" >> "$GITHUB_OUTPUT" else echo "${key}=false" >> "$GITHUB_OUTPUT" From 7dff1738a12c70d6568232feedfe4126e3769960 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 27 Apr 2026 00:44:20 -0700 Subject: [PATCH 7/9] ci: surface fail-open path-filter warnings + expand SIGPIPE comment 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) --- .github/workflows/ci.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63cd323ba2..c5c68f7583 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,12 +91,17 @@ jobs: # (avoids picking up unrelated commits the base ref has gained meanwhile). # If either resolution fails (e.g., force-pushed branch with orphaned # event.before, or rebased PR base ref), fall back to the conservative - # "couldn't compute, run everything" stance. - if ! merge_base=$(git merge-base "$base" "$head" 2>/dev/null); then + # "couldn't compute, run everything" stance and surface a GHA warning + # so operators can see why every job ran. Note: stderr is left + # un-redirected so the underlying git error (e.g., "Not a valid object + # name") shows up in the runner log alongside the warning. + if ! merge_base=$(git merge-base "$base" "$head"); then + echo "::warning::path-filter: git merge-base failed for $base..$head; running all gated jobs conservatively" emit_all true exit 0 fi - if ! changed=$(git diff --name-only "$merge_base" "$head" 2>/dev/null); then + if ! changed=$(git diff --name-only "$merge_base" "$head"); then + echo "::warning::path-filter: git diff failed for $merge_base..$head; running all gated jobs conservatively" emit_all true exit 0 fi @@ -104,9 +109,14 @@ jobs: emit() { local key="$1" local pattern="$2" - # Use here-string instead of `printf | grep` to avoid SIGPIPE on huge - # diffs: with `set -o pipefail`, a SIGPIPE'd printf would flip the - # pipeline to non-zero and silently invert the match. + # Use a here-string instead of `printf "%s\n" "$changed" | grep -qE`. + # Why: with `set -o pipefail`, if `grep -q` matches early on a $changed + # larger than the pipe buffer (~64 KB; thousands of files), `grep` exits + # before `printf` finishes flushing. `printf` then receives SIGPIPE and + # exits 141. `pipefail` propagates that 141 to the pipeline, the `if` + # sees non-zero, and we'd silently emit `false` for a key that should + # be `true`. A here-string is a pure redirection (not a pipeline), so + # the data is fully buffered before grep reads, eliminating the race. if [[ -n "$changed" ]] && grep -qE "$pattern" <<< "$changed"; then echo "${key}=true" >> "$GITHUB_OUTPUT" else From dde2dc616d6bc56833eb1f3539dbf9c7ef0700a9 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 27 Apr 2026 00:44:27 -0700 Subject: [PATCH 8/9] docs(changelog): add PR-G 7a entry to Development / Infrastructure 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) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeafad4eba..d6e9760c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Infrastructure +- **CI / supply-chain hardening (PR-G 7a)**: Deprecated all third-party GitHub Actions in favor of native primitives — `styfle/cancel-workflow-action` replaced by a workflow-level `concurrency:` block in `ci-gpu.yml`; `dorny/paths-filter` replaced by an inline `git diff --name-only` shell filter in `ci.yml` (preserves all 6 outputs: `infra`, `python`, `gfql`, `cypher_frontend_ci`, `benchmarks`, `docs`). Configured zizmor `unpinned-uses` rule with a provider safelist (`actions/*`, `github/*`, `pypa/*` permitted on floating refs; everything else must hash-pin) and lowered the workflow-security gate from `--min-severity high` to `medium` so policy violations actually fail CI. Closed 5 PR-B follow-up gaps by adding `persist-credentials: false` to LFS / fetch-depth checkouts in `ci.yml` (×3), `codeql-analysis.yml`, and `publish-pypi.yml`. Also fixed a silent fail-closed bug surfaced during multi-wave review: when `git merge-base` cannot resolve (force-pushed branch with orphaned `event.before`, or PR base rebased mid-flight), the `changes` job now conservatively emits `true` for every output and downstream jobs run, instead of silently emitting `false` and skipping all gated tests (#1221, #1215, #1130). - **CI / docs**: `test-readme` no longer runs `actions/setup-python` with an EOL Python 3.8 pin. The job now runs markdown lint directly via its Docker image, removing an unnecessary setup step and avoiding intermittent Python toolcache fetch timeouts. - **CI / build lane**: `test-build` now runs on Python 3.14 with `build-py3.14.lock` instead of a fixed Python 3.8 runner, reducing reliance on EOL interpreter setup while preserving explicit 3.8 compatibility test lanes elsewhere in CI. - **CI / token hardening**: CI workflows now declare explicit least-privilege default token scope (`permissions: contents: read`) and set `persist-credentials: false` on all checkout steps in `ci.yml` and `ci-gpu.yml`; GPU cancel job keeps a scoped `actions: write` override for run cancellation (#1130). From ac94abcf26f1b6fdcf197ae5fdfaae121b9824c4 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 27 Apr 2026 09:11:50 -0700 Subject: [PATCH 9/9] ci: pattern-per-line filter call sites for readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/ci.yml | 70 +++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5c68f7583..b6e43a45f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,9 +106,14 @@ jobs: exit 0 fi + # emit ... — joins regex args with `|` and emits true/false. + # Each pattern goes on its own line at the call site for readability; + # this is the reason emit takes variadic args instead of a pre-joined string. emit() { local key="$1" - local pattern="$2" + shift + local IFS='|' + local pat="$*" # Use a here-string instead of `printf "%s\n" "$changed" | grep -qE`. # Why: with `set -o pipefail`, if `grep -q` matches early on a $changed # larger than the pipe buffer (~64 KB; thousands of files), `grep` exits @@ -117,19 +122,68 @@ jobs: # sees non-zero, and we'd silently emit `false` for a key that should # be `true`. A here-string is a pure redirection (not a pipeline), so # the data is fully buffered before grep reads, eliminating the race. - if [[ -n "$changed" ]] && grep -qE "$pattern" <<< "$changed"; then + if [[ -n "$changed" ]] && grep -qE "$pat" <<< "$changed"; then echo "${key}=true" >> "$GITHUB_OUTPUT" else echo "${key}=false" >> "$GITHUB_OUTPUT" fi } - emit infra '^\.github/workflows/ci\.yml$|^docker/|^bin/|^setup\.py$|^setup\.cfg$|^MANIFEST\.in$' - emit python '\.py$|^graphistry/|^setup\.py$|^setup\.cfg$|^pytest\.ini$|^mypy\.ini$|^bin/lint\.sh$|^bin/typecheck\.sh$' - emit gfql '^graphistry/gfql/|^graphistry/compute/gfql/|^graphistry/compute/gfql_unified\.py$|^graphistry/models/gfql/|^graphistry/Plottable\.py$|^tests/gfql/' - emit cypher_frontend_ci '^\.github/workflows/ci\.yml$|^\.github/workflows/ci-gpu\.yml$|^graphistry/compute/gfql/ir/|^graphistry/compute/gfql/cypher/|^graphistry/compute/gfql/frontends/cypher/|^graphistry/tests/compute/gfql/cypher/|^tests/gfql/ref/' - emit benchmarks '^benchmarks/' - emit docs '^docs/|\.md$|\.rst$|^demos/|^notebooks/' + # Filter dimensions — patterns mirror the prior dorny/paths-filter YAML + # globs converted to anchored regex. Keep one pattern per line so adds + # / removes diff cleanly and the conversion stays auditable. + + # Infrastructure: workflow defs, docker, bin scripts, root build config. + emit infra \ + '^\.github/workflows/ci\.yml$' \ + '^docker/' \ + '^bin/' \ + '^setup\.py$' \ + '^setup\.cfg$' \ + '^MANIFEST\.in$' + + # Python code + lint/type config. + emit python \ + '\.py$' \ + '^graphistry/' \ + '^setup\.py$' \ + '^setup\.cfg$' \ + '^pytest\.ini$' \ + '^mypy\.ini$' \ + '^bin/lint\.sh$' \ + '^bin/typecheck\.sh$' + + # GFQL core + tests. + emit gfql \ + '^graphistry/gfql/' \ + '^graphistry/compute/gfql/' \ + '^graphistry/compute/gfql_unified\.py$' \ + '^graphistry/models/gfql/' \ + '^graphistry/Plottable\.py$' \ + '^tests/gfql/' + + # Cypher frontend gate-relevant paths (workflow defs included so a CI + # change here re-triggers the cypher gate alongside gfql/infra). + emit cypher_frontend_ci \ + '^\.github/workflows/ci\.yml$' \ + '^\.github/workflows/ci-gpu\.yml$' \ + '^graphistry/compute/gfql/ir/' \ + '^graphistry/compute/gfql/cypher/' \ + '^graphistry/compute/gfql/frontends/cypher/' \ + '^graphistry/tests/compute/gfql/cypher/' \ + '^tests/gfql/ref/' + + # Benchmarks suite. + emit benchmarks \ + '^benchmarks/' + + # Documentation: docs tree + any md/rst at any depth + demos + notebooks. + emit docs \ + '^docs/' \ + '\.md$' \ + '\.rst$' \ + '^demos/' \ + '^notebooks/' - name: Detect docs-only change on tip id: docs_only_latest