diff --git a/.github/workflows/cloudflare-pages-deploy.yml b/.github/workflows/cloudflare-pages-deploy.yml index 60e27e9..959c38f 100644 --- a/.github/workflows/cloudflare-pages-deploy.yml +++ b/.github/workflows/cloudflare-pages-deploy.yml @@ -192,7 +192,7 @@ jobs: ref: ${{ github.event.workflow_run.head_sha || github.ref }} - name: Setup pnpm + Node (+ install) - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: ${{ inputs.package-json-file }} node-version: ${{ inputs.node-version }} @@ -213,7 +213,7 @@ jobs: run: bash --noprofile --norc -euo pipefail -c "$TRUSTED_COMMAND" - name: Deploy to Cloudflare Pages - uses: hseshadr/ci/.github/actions/pages-deploy-dist@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/pages-deploy-dist@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: project-name: ${{ inputs.project-name }} dist-dir: ${{ inputs.dist-dir }} diff --git a/.github/workflows/frontend-gate.yml b/.github/workflows/frontend-gate.yml index d7edc4f..eef864e 100644 --- a/.github/workflows/frontend-gate.yml +++ b/.github/workflows/frontend-gate.yml @@ -88,7 +88,7 @@ jobs: persist-credentials: false - name: Setup pnpm + Node (+ install) - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: ${{ inputs.package-json-file }} node-version: ${{ inputs.node-version }} @@ -99,7 +99,7 @@ jobs: - name: Setup Playwright (cached) if: ${{ inputs.install-playwright }} - uses: hseshadr/ci/.github/actions/setup-playwright@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-playwright@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: browsers: ${{ inputs.playwright-browsers }} working-directory: ${{ inputs.playwright-working-directory || inputs.working-directory }} diff --git a/.github/workflows/python-gate.yml b/.github/workflows/python-gate.yml index 2010ba8..d4b2cf1 100644 --- a/.github/workflows/python-gate.yml +++ b/.github/workflows/python-gate.yml @@ -79,7 +79,7 @@ jobs: persist-credentials: false - name: Setup Python + uv (+ sync) - uses: hseshadr/ci/.github/actions/setup-python-uv@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-python-uv@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: python-version: ${{ inputs.python-version }} sync-args: ${{ inputs.sync-args }} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index b2064c5..a5d4736 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -115,7 +115,7 @@ jobs: persist-credentials: false - name: Setup Python + uv (+ sync) - uses: hseshadr/ci/.github/actions/setup-python-uv@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-python-uv@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: python-version: ${{ inputs.python-version }} sync-args: ${{ inputs.sync-args }} @@ -189,15 +189,35 @@ jobs: echo "Verifying ${name}==${version} is served by PyPI..." # Registries have propagation delay, so retry — but a timeout is a FAILURE, - # never a pass. Bounded: 6 attempts, 10s apart (~60s). - for attempt in 1 2 3 4 5 6; do + # never a pass. + # + # The old bound was 6 attempts 10s apart (~60s) and it was too tight: it + # failed publishes that had genuinely succeeded. A red run on a live release + # is not harmless — it teaches the reader to wave off red publish runs, which + # is exactly how the six-green-while-404 defect comes back. Measured + # 2026-08-03, time from a green upload until the registry served the version: + # PyPI edgeproc-core 0.4.0 >60s (failed this check while genuinely live) + # PyPI edge-proc 0.3.0 ~120s + # npm @edgeproc/errors 0.1.0 ~200s (first publish of a NEW package name) + # npm @edgeproc/avow 0.3.0 ~60s + # The first publish of a new name is the slowest case AND the case a real + # trusted-publisher misconfiguration is indistinguishable from, so the budget + # must clear propagation by a wide margin before it is allowed to accuse. + # Bound: 600s of sleep, 3x the slowest measured case. Backoff (5, 10, 15, 30, + # then 60s) so the common case still verifies in ~15s instead of paying it. + delays=(5 10 15 30 60 60 60 60 60 60 60 60 60) # 13 sleeps, exactly 600s + attempts=$((${#delays[@]} + 1)) + + for ((i = 0; i < attempts; i++)); do if curl -fsS --max-time 15 -o /dev/null "$url"; then echo "Verified: ${name}==${version} is live on PyPI." exit 0 fi - echo "Attempt ${attempt}/6: not served yet; retrying in 10s..." - sleep 10 + if ((i < ${#delays[@]})); then + echo "Attempt $((i + 1))/${attempts}: not served yet; retrying in ${delays[i]}s..." + sleep "${delays[i]}" + fi done - echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~60s. A green upload is not proof of a release. Most likely cause: an incomplete PyPI trusted-publisher bootstrap — the OIDC exchange can look fine while no project is actually registered for this repo+workflow. Check the project's Publishing settings, or the account-level pending publisher, on PyPI." + echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~10 minutes of retrying. A green upload is not proof of a release. Two very different causes land here, so establish which one before touching this run: (1) STILL PROPAGATING — open ${url} yourself; the slowest propagation measured for this portfolio was ~200s, so 10 minutes should be ample, but if it loads now then the release DID happen and this is a false alarm worth reporting. (2) THE RELEASE NEVER HAPPENED — an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch; the OIDC exchange can look fine while no project is actually registered for this repo+workflow. Check the project's Publishing settings, or the account-level pending publisher, on PyPI. Do not re-run this job merely to make it green." exit 1 diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 00c7e5e..3db0afd 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -107,7 +107,7 @@ jobs: # run-sync: false — we `uv export` the lock, not build a venv. - name: Setup Python + uv - uses: hseshadr/ci/.github/actions/setup-python-uv@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-python-uv@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: working-directory: ${{ inputs.python-working-directory }} run-sync: "false" @@ -163,7 +163,7 @@ jobs: # install: false — `pnpm audit` reads the lockfile + queries the advisory DB # directly; no node_modules needed. - name: Setup pnpm + Node - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: ${{ inputs.frontend-working-directory }}/package.json node-version: ${{ inputs.node-version }} diff --git a/.github/workflows/ts-publish.yml b/.github/workflows/ts-publish.yml index b0e3d48..a9c604c 100644 --- a/.github/workflows/ts-publish.yml +++ b/.github/workflows/ts-publish.yml @@ -141,7 +141,7 @@ jobs: registry-url: ${{ inputs.registry-url }} - name: Setup pnpm + Node (+ install) - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: ${{ inputs.package-json-file }} node-version: ${{ inputs.node-version }} @@ -201,15 +201,34 @@ jobs: echo "Verifying ${name}@${version} is served by ${NPM_REGISTRY_URL}..." # Retry for propagation delay, but a timeout is a FAILURE, never a pass. - # Bounded: 6 attempts, 10s apart (~60s). - for attempt in 1 2 3 4 5 6; do + # + # The old bound was 6 attempts 10s apart (~60s) and it was too tight: it + # failed publishes that had genuinely succeeded. A red run on a live release + # is not harmless — it teaches the reader to wave off red publish runs, which + # is exactly how the six-green-while-404 defect comes back. Measured + # 2026-08-03, time from a green publish until the registry served the version: + # PyPI edgeproc-core 0.4.0 >60s (failed this check while genuinely live) + # PyPI edge-proc 0.3.0 ~120s + # npm @edgeproc/errors 0.1.0 ~200s (first publish of a NEW package name) + # npm @edgeproc/avow 0.3.0 ~60s + # The first publish of a new name is the slowest case AND the case a real + # trusted-publisher misconfiguration is indistinguishable from, so the budget + # must clear propagation by a wide margin before it is allowed to accuse. + # Bound: 600s of sleep, 3x the slowest measured case. Backoff (5, 10, 15, 30, + # then 60s) so the common case still verifies in ~15s instead of paying it. + delays=(5 10 15 30 60 60 60 60 60 60 60 60 60) # 13 sleeps, exactly 600s + attempts=$((${#delays[@]} + 1)) + + for ((i = 0; i < attempts; i++)); do if npm view "${name}@${version}" version --registry "$NPM_REGISTRY_URL" >/dev/null 2>&1; then echo "Verified: ${name}@${version} is live on ${NPM_REGISTRY_URL}." exit 0 fi - echo "Attempt ${attempt}/6: not served yet; retrying in 10s..." - sleep 10 + if ((i < ${#delays[@]})); then + echo "Attempt $((i + 1))/${attempts}: not served yet; retrying in ${delays[i]}s..." + sleep "${delays[i]}" + fi done - echo "::error::PUBLISH VERIFICATION FAILED — the publish step reported success, but ${NPM_REGISTRY_URL} does not serve ${name}@${version} after ~60s. A green publish is not proof of a release. Most likely cause: an incomplete npm trusted-publisher bootstrap — npm has no 'pending publisher', so the package must already exist before a trusted publisher can be attached, and the first publish of a brand-new name must be token/OTP-based. Confirm the trusted publisher is registered for this repo + workflow filename." + echo "::error::PUBLISH VERIFICATION FAILED — the publish step reported success, but ${NPM_REGISTRY_URL} does not serve ${name}@${version} after ~10 minutes of retrying. A green publish is not proof of a release. Two very different causes land here, so establish which one before touching this run: (1) STILL PROPAGATING — run 'npm view ${name}@${version} version' yourself; the slowest propagation measured for this portfolio was ~200s, on the first publish of a brand-new package name, so 10 minutes should be ample — but if it resolves now then the release DID happen and this is a false alarm worth reporting. (2) THE RELEASE NEVER HAPPENED — an incomplete npm trusted-publisher bootstrap; npm has no 'pending publisher', so the package must already exist before a trusted publisher can be attached, and the first publish of a brand-new name must be token/OTP-based. Confirm the trusted publisher is registered for this repo + workflow filename. Do not re-run this job merely to make it green." exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0d978..2b549e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,29 @@ included. ## Unreleased (on `main`, after ci-v3.0.0) -**No brick changed shape** — every entry here is a guard, a test, or a fix to the -copy-paste surface in `examples/`. Nothing below requires a re-pin; the `examples/` -changes require a re-copy. - +**No brick changed shape** — no input, output or permission moved, so no caller needs +editing. One entry below changes brick *behaviour*: the publish-verification retry bound +in `python-publish.yml` and `ts-publish.yml`. That one needs a re-pin to reach a consumer. +Everything else is a guard, a test, or a fix to the copy-paste surface in `examples/`, +which needs a re-copy. + +- **The publish-verification bound was too tight, and it failed a release that had + genuinely succeeded.** The check itself is right and stays: ask the registry whether the + version is served, never trust the uploader, and treat a timeout as a FAILURE. Its bound + was wrong — 6 attempts 10s apart (~60s) against measured propagation of ~120s + (PyPI `edge-proc` 0.3.0) and ~200s (npm `@edgeproc/errors` 0.1.0, the first publish of a + brand-new name). `edgeproc-core` 0.4.0 went live on PyPI and + [its publish run went red anyway](https://github.com/hseshadr/edgeproc-core/actions/runs/30842985605). + That false negative is not harmless: a red run on a live release teaches the reader to + wave off red publish runs, which is exactly how the six-green-while-404 defect returns. + New bound: 14 attempts with backoff (5, 10, 15, 30, then 60s) — 600s of sleep, 3x the + slowest case measured, while the common case still verifies in ~15s. The failure message + now separates "STILL PROPAGATING" from "THE RELEASE NEVER HAPPENED"; it previously listed + only the misconfiguration causes, which is misleading now that a timeout is more often + propagation. The guard keeps its teeth: run the step against a version PyPI/npm does not + serve and it still exits **1** after the full budget. The three `examples/*/publish.yml` + inline copies carry the same bound, so the surface consumers copy does not ship the + defect. - **The drift detector caught its first new control, and the cause was partly this repo.** On 2026-08-02 the scheduled sweep went red: `30 … 29 allowlisted; 1 new` ([run 30739082151](https://github.com/hseshadr/ci/actions/runs/30739082151)); the day diff --git a/examples/almamesh/security-audit.yml b/examples/almamesh/security-audit.yml index ae42dbe..c9cb1e8 100644 --- a/examples/almamesh/security-audit.yml +++ b/examples/almamesh/security-audit.yml @@ -12,7 +12,7 @@ permissions: contents: read jobs: audit: - uses: hseshadr/ci/.github/workflows/security-audit.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/security-audit.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: run-python-audit: true python-working-directory: backend diff --git a/examples/aml-filter/ci.yml b/examples/aml-filter/ci.yml index 173d43d..f7a6d1a 100644 --- a/examples/aml-filter/ci.yml +++ b/examples/aml-filter/ci.yml @@ -33,7 +33,7 @@ jobs: permissions: contents: read pull-requests: read - uses: hseshadr/ci/.github/workflows/secret-scan.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/secret-scan.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 frontend: runs-on: ubuntu-latest @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: frontend/package.json node-version-file: frontend/.nvmrc @@ -51,7 +51,7 @@ jobs: # Weights come from a GitHub RELEASE, not a package registry, so the fetch # needs GH_TOKEN. Job-level `env:` is what reaches a composite's steps — # the composite deliberately takes no secret input. - - uses: hseshadr/ci/.github/actions/restore-model-cache@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/restore-model-cache@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 env: GH_TOKEN: ${{ github.token }} with: @@ -67,7 +67,7 @@ jobs: gh release download model-weights-v1 --repo "$GITHUB_REPOSITORY" --dir "$dir/onnx" --clobber \ --pattern model_quantized.onnx node scripts/download-model.mjs - - uses: hseshadr/ci/.github/actions/setup-playwright@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/setup-playwright@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: working-directory: frontend cache-key: playwright-${{ runner.os }}-${{ hashFiles('frontend/pnpm-lock.yaml') }} diff --git a/examples/aml-filter/deploy.yml b/examples/aml-filter/deploy.yml index 2be664b..938ff2a 100644 --- a/examples/aml-filter/deploy.yml +++ b/examples/aml-filter/deploy.yml @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: frontend/package.json node-version-file: frontend/.nvmrc @@ -105,7 +105,7 @@ jobs: fi # --- shared deploy half: identical wrangler step used by every Pages repo --- - - uses: hseshadr/ci/.github/actions/pages-deploy-dist@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/pages-deploy-dist@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: project-name: aml-filter dist-dir: frontend/app/dist diff --git a/examples/aml-filter/security-audit.yml b/examples/aml-filter/security-audit.yml index 68e6f98..fbd1571 100644 --- a/examples/aml-filter/security-audit.yml +++ b/examples/aml-filter/security-audit.yml @@ -8,10 +8,10 @@ permissions: contents: read jobs: audit: - uses: hseshadr/ci/.github/workflows/security-audit.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/security-audit.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: run-pnpm-audit: true frontend-working-directory: frontend pnpm-audit-level: low gitleaks: - uses: hseshadr/ci/.github/workflows/secret-scan.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/secret-scan.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 diff --git a/examples/assay/publish.yml b/examples/assay/publish.yml index ad571e4..78cbf57 100644 --- a/examples/assay/publish.yml +++ b/examples/assay/publish.yml @@ -59,7 +59,7 @@ jobs: persist-credentials: false - name: Setup Python + uv (+ sync) - uses: hseshadr/ci/.github/actions/setup-python-uv@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-python-uv@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: sync-args: "--frozen --all-extras" @@ -102,17 +102,37 @@ jobs: echo "Verifying ${name}==${version} is served by PyPI..." # Registries have propagation delay, so retry — but a timeout is a FAILURE, - # never a pass. Bounded: 6 attempts, 10s apart (~60s). - for attempt in 1 2 3 4 5 6; do + # never a pass. + # + # The old bound was 6 attempts 10s apart (~60s) and it was too tight: it + # failed publishes that had genuinely succeeded. A red run on a live release + # is not harmless — it teaches the reader to wave off red publish runs, which + # is exactly how the six-green-while-404 defect comes back. Measured + # 2026-08-03, time from a green upload until the registry served the version: + # PyPI edgeproc-core 0.4.0 >60s (failed this check while genuinely live) + # PyPI edge-proc 0.3.0 ~120s + # npm @edgeproc/errors 0.1.0 ~200s (first publish of a NEW package name) + # npm @edgeproc/avow 0.3.0 ~60s + # The first publish of a new name is the slowest case AND the case a real + # trusted-publisher misconfiguration is indistinguishable from, so the budget + # must clear propagation by a wide margin before it is allowed to accuse. + # Bound: 600s of sleep, 3x the slowest measured case. Backoff (5, 10, 15, 30, + # then 60s) so the common case still verifies in ~15s instead of paying it. + delays=(5 10 15 30 60 60 60 60 60 60 60 60 60) # 13 sleeps, exactly 600s + attempts=$((${#delays[@]} + 1)) + + for ((i = 0; i < attempts; i++)); do if curl -fsS --max-time 15 -o /dev/null "$url"; then echo "Verified: ${name}==${version} is live on PyPI." exit 0 fi - echo "Attempt ${attempt}/6: not served yet; retrying in 10s..." - sleep 10 + if ((i < ${#delays[@]})); then + echo "Attempt $((i + 1))/${attempts}: not served yet; retrying in ${delays[i]}s..." + sleep "${delays[i]}" + fi done - echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~60s. A green upload is not proof of a release. Most likely cause: an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch. Check the project's Publishing settings on PyPI." + echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~10 minutes of retrying. A green upload is not proof of a release. Two very different causes land here, so establish which one before touching this run: (1) STILL PROPAGATING — open ${url} yourself; the slowest propagation measured for this portfolio was ~200s, so 10 minutes should be ample, but if it loads now then the release DID happen and this is a false alarm worth reporting. (2) THE RELEASE NEVER HAPPENED — an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch; the OIDC exchange can look fine while no project is actually registered for this repo+workflow. Check the project's Publishing settings, or the account-level pending publisher, on PyPI. Do not re-run this job merely to make it green." exit 1 # TS `@edgeproc/avow` (ts/) -> npm. Re-runs `pnpm gate` (lint -> tsc -> vitest @@ -121,7 +141,7 @@ jobs: permissions: id-token: write # OIDC — npm verifies this against the registered trusted publisher. contents: read - uses: hseshadr/ci/.github/workflows/ts-publish.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/ts-publish.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: working-directory: ts # assay has no ROOT package.json/lockfile — the TS package lives wholly in ts/. diff --git a/examples/edge-proc/ci.yml b/examples/edge-proc/ci.yml index 4eed0a9..de289af 100644 --- a/examples/edge-proc/ci.yml +++ b/examples/edge-proc/ci.yml @@ -12,8 +12,8 @@ permissions: pull-requests: read jobs: gate: - uses: hseshadr/ci/.github/workflows/python-gate.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/python-gate.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: sync-args: "--frozen --all-extras" gitleaks: - uses: hseshadr/ci/.github/workflows/secret-scan.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/secret-scan.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 diff --git a/examples/edge-proc/publish.yml b/examples/edge-proc/publish.yml index 5f2b864..e60b2d9 100644 --- a/examples/edge-proc/publish.yml +++ b/examples/edge-proc/publish.yml @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: Setup Python + uv (+ sync) - uses: hseshadr/ci/.github/actions/setup-python-uv@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-python-uv@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: sync-args: "--frozen --all-extras" @@ -86,15 +86,35 @@ jobs: echo "Verifying ${name}==${version} is served by PyPI..." # Registries have propagation delay, so retry — but a timeout is a FAILURE, - # never a pass. Bounded: 6 attempts, 10s apart (~60s). - for attempt in 1 2 3 4 5 6; do + # never a pass. + # + # The old bound was 6 attempts 10s apart (~60s) and it was too tight: it + # failed publishes that had genuinely succeeded. A red run on a live release + # is not harmless — it teaches the reader to wave off red publish runs, which + # is exactly how the six-green-while-404 defect comes back. Measured + # 2026-08-03, time from a green upload until the registry served the version: + # PyPI edgeproc-core 0.4.0 >60s (failed this check while genuinely live) + # PyPI edge-proc 0.3.0 ~120s + # npm @edgeproc/errors 0.1.0 ~200s (first publish of a NEW package name) + # npm @edgeproc/avow 0.3.0 ~60s + # The first publish of a new name is the slowest case AND the case a real + # trusted-publisher misconfiguration is indistinguishable from, so the budget + # must clear propagation by a wide margin before it is allowed to accuse. + # Bound: 600s of sleep, 3x the slowest measured case. Backoff (5, 10, 15, 30, + # then 60s) so the common case still verifies in ~15s instead of paying it. + delays=(5 10 15 30 60 60 60 60 60 60 60 60 60) # 13 sleeps, exactly 600s + attempts=$((${#delays[@]} + 1)) + + for ((i = 0; i < attempts; i++)); do if curl -fsS --max-time 15 -o /dev/null "$url"; then echo "Verified: ${name}==${version} is live on PyPI." exit 0 fi - echo "Attempt ${attempt}/6: not served yet; retrying in 10s..." - sleep 10 + if ((i < ${#delays[@]})); then + echo "Attempt $((i + 1))/${attempts}: not served yet; retrying in ${delays[i]}s..." + sleep "${delays[i]}" + fi done - echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~60s. A green upload is not proof of a release. Most likely cause: an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch. Check the project's Publishing settings, or the account-level pending publisher, on PyPI." + echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~10 minutes of retrying. A green upload is not proof of a release. Two very different causes land here, so establish which one before touching this run: (1) STILL PROPAGATING — open ${url} yourself; the slowest propagation measured for this portfolio was ~200s, so 10 minutes should be ample, but if it loads now then the release DID happen and this is a false alarm worth reporting. (2) THE RELEASE NEVER HAPPENED — an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch; the OIDC exchange can look fine while no project is actually registered for this repo+workflow. Check the project's Publishing settings, or the account-level pending publisher, on PyPI. Do not re-run this job merely to make it green." exit 1 diff --git a/examples/edge-proc/security-audit.yml b/examples/edge-proc/security-audit.yml index e737512..92c0082 100644 --- a/examples/edge-proc/security-audit.yml +++ b/examples/edge-proc/security-audit.yml @@ -8,6 +8,6 @@ permissions: contents: read jobs: audit: - uses: hseshadr/ci/.github/workflows/security-audit.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/security-audit.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: run-python-audit: true diff --git a/examples/edge-reco/ci.yml b/examples/edge-reco/ci.yml index 6adb07f..71f46b6 100644 --- a/examples/edge-reco/ci.yml +++ b/examples/edge-reco/ci.yml @@ -17,10 +17,10 @@ permissions: pull-requests: read jobs: gitleaks: - uses: hseshadr/ci/.github/workflows/secret-scan.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/secret-scan.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 quality: - uses: hseshadr/ci/.github/workflows/python-gate.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/python-gate.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: working-directory: backend sync-args: "--locked --group dev" @@ -31,13 +31,13 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: frontend/package.json node-version-file: frontend/.nvmrc cache-dependency-path: frontend/pnpm-lock.yaml working-directory: frontend - - uses: hseshadr/ci/.github/actions/restore-model-cache@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/restore-model-cache@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: cache-path: frontend/app/public/models cache-key: model-weights-${{ runner.os }}-${{ hashFiles('frontend/app/scripts/download-model.mjs') }} @@ -53,19 +53,19 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: hseshadr/ci/.github/actions/setup-pnpm@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/setup-pnpm@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: package-json-file: frontend/package.json node-version-file: frontend/.nvmrc cache-dependency-path: frontend/pnpm-lock.yaml working-directory: frontend - - uses: hseshadr/ci/.github/actions/restore-model-cache@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/restore-model-cache@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: cache-path: frontend/app/public/models cache-key: model-weights-${{ runner.os }}-${{ hashFiles('frontend/app/scripts/download-model.mjs') }} fetch-command: node scripts/download-model.mjs working-directory: frontend/app - - uses: hseshadr/ci/.github/actions/setup-playwright@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + - uses: hseshadr/ci/.github/actions/setup-playwright@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: working-directory: frontend cache-key: playwright-${{ runner.os }}-${{ hashFiles('frontend/pnpm-lock.yaml') }} diff --git a/examples/edge-reco/deploy.yml b/examples/edge-reco/deploy.yml index 19272ad..5b2011e 100644 --- a/examples/edge-reco/deploy.yml +++ b/examples/edge-reco/deploy.yml @@ -32,7 +32,7 @@ jobs: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) }} - uses: hseshadr/ci/.github/workflows/cloudflare-pages-deploy.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/cloudflare-pages-deploy.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: project-name: edge-reco dist-dir: frontend/app/dist diff --git a/examples/edge-reco/security-audit.yml b/examples/edge-reco/security-audit.yml index 8a80e96..ff6cfd2 100644 --- a/examples/edge-reco/security-audit.yml +++ b/examples/edge-reco/security-audit.yml @@ -8,7 +8,7 @@ permissions: contents: read jobs: audit: - uses: hseshadr/ci/.github/workflows/security-audit.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/security-audit.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: run-python-audit: true run-pnpm-audit: true diff --git a/examples/edgeproc-core/ci.yml b/examples/edgeproc-core/ci.yml index de1992d..3e079ea 100644 --- a/examples/edgeproc-core/ci.yml +++ b/examples/edgeproc-core/ci.yml @@ -13,7 +13,7 @@ permissions: pull-requests: read jobs: gate: - uses: hseshadr/ci/.github/workflows/python-gate.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/python-gate.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: upload-coverage: true # Name the one secret the gate needs. `secrets: inherit` would forward every repo @@ -21,4 +21,4 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} gitleaks: - uses: hseshadr/ci/.github/workflows/secret-scan.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/secret-scan.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 diff --git a/examples/edgeproc-core/publish.yml b/examples/edgeproc-core/publish.yml index 0af9c39..f7d7ae3 100644 --- a/examples/edgeproc-core/publish.yml +++ b/examples/edgeproc-core/publish.yml @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: Setup Python + uv (+ sync) - uses: hseshadr/ci/.github/actions/setup-python-uv@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/actions/setup-python-uv@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: sync-args: "--frozen --all-extras" @@ -86,15 +86,35 @@ jobs: echo "Verifying ${name}==${version} is served by PyPI..." # Registries have propagation delay, so retry — but a timeout is a FAILURE, - # never a pass. Bounded: 6 attempts, 10s apart (~60s). - for attempt in 1 2 3 4 5 6; do + # never a pass. + # + # The old bound was 6 attempts 10s apart (~60s) and it was too tight: it + # failed publishes that had genuinely succeeded. A red run on a live release + # is not harmless — it teaches the reader to wave off red publish runs, which + # is exactly how the six-green-while-404 defect comes back. Measured + # 2026-08-03, time from a green upload until the registry served the version: + # PyPI edgeproc-core 0.4.0 >60s (failed this check while genuinely live) + # PyPI edge-proc 0.3.0 ~120s + # npm @edgeproc/errors 0.1.0 ~200s (first publish of a NEW package name) + # npm @edgeproc/avow 0.3.0 ~60s + # The first publish of a new name is the slowest case AND the case a real + # trusted-publisher misconfiguration is indistinguishable from, so the budget + # must clear propagation by a wide margin before it is allowed to accuse. + # Bound: 600s of sleep, 3x the slowest measured case. Backoff (5, 10, 15, 30, + # then 60s) so the common case still verifies in ~15s instead of paying it. + delays=(5 10 15 30 60 60 60 60 60 60 60 60 60) # 13 sleeps, exactly 600s + attempts=$((${#delays[@]} + 1)) + + for ((i = 0; i < attempts; i++)); do if curl -fsS --max-time 15 -o /dev/null "$url"; then echo "Verified: ${name}==${version} is live on PyPI." exit 0 fi - echo "Attempt ${attempt}/6: not served yet; retrying in 10s..." - sleep 10 + if ((i < ${#delays[@]})); then + echo "Attempt $((i + 1))/${attempts}: not served yet; retrying in ${delays[i]}s..." + sleep "${delays[i]}" + fi done - echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~60s. A green upload is not proof of a release. Most likely cause: an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch. Check the project's Publishing settings, or the account-level pending publisher, on PyPI." + echo "::error::PUBLISH VERIFICATION FAILED — the upload step reported success, but PyPI does not serve ${name}==${version} (${url}) after ~10 minutes of retrying. A green upload is not proof of a release. Two very different causes land here, so establish which one before touching this run: (1) STILL PROPAGATING — open ${url} yourself; the slowest propagation measured for this portfolio was ~200s, so 10 minutes should be ample, but if it loads now then the release DID happen and this is a false alarm worth reporting. (2) THE RELEASE NEVER HAPPENED — an incomplete PyPI trusted-publisher bootstrap, or a job_workflow_ref/publisher mismatch; the OIDC exchange can look fine while no project is actually registered for this repo+workflow. Check the project's Publishing settings, or the account-level pending publisher, on PyPI. Do not re-run this job merely to make it green." exit 1 diff --git a/examples/edgeproc-core/security-audit.yml b/examples/edgeproc-core/security-audit.yml index c0a1b57..d2a9cb4 100644 --- a/examples/edgeproc-core/security-audit.yml +++ b/examples/edgeproc-core/security-audit.yml @@ -8,6 +8,6 @@ permissions: contents: read jobs: audit: - uses: hseshadr/ci/.github/workflows/security-audit.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/security-audit.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: run-python-audit: true diff --git a/examples/privacy-core/ci.yml b/examples/privacy-core/ci.yml index dca9ac2..70c87d6 100644 --- a/examples/privacy-core/ci.yml +++ b/examples/privacy-core/ci.yml @@ -13,9 +13,9 @@ permissions: pull-requests: read jobs: gate: - uses: hseshadr/ci/.github/workflows/frontend-gate.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/frontend-gate.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: install-args: "--frozen-lockfile --config.dangerously-allow-all-builds=true" install-playwright: true gitleaks: - uses: hseshadr/ci/.github/workflows/secret-scan.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/secret-scan.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 diff --git a/examples/privacy-core/publish.yml b/examples/privacy-core/publish.yml index ef9c31e..bb063bc 100644 --- a/examples/privacy-core/publish.yml +++ b/examples/privacy-core/publish.yml @@ -37,7 +37,7 @@ jobs: permissions: id-token: write # OIDC — npm verifies this against the registered trusted publisher. contents: read - uses: hseshadr/ci/.github/workflows/ts-publish.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/ts-publish.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: working-directory: "." install-args: "--frozen-lockfile --config.dangerously-allow-all-builds=true" diff --git a/examples/privacy-core/security-audit.yml b/examples/privacy-core/security-audit.yml index 27a992a..3672856 100644 --- a/examples/privacy-core/security-audit.yml +++ b/examples/privacy-core/security-audit.yml @@ -8,7 +8,7 @@ permissions: contents: read jobs: audit: - uses: hseshadr/ci/.github/workflows/security-audit.yml@33c5e5fa421210e6cc91ea30cad708bce29a2407 # ci-v3.1.0 + uses: hseshadr/ci/.github/workflows/security-audit.yml@7226072bd02e7aecc5b065b3eaf0bfbf4b3e1790 # ci-v3.2.0 with: run-pnpm-audit: true frontend-working-directory: "."