Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions .claude/skills/soak/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ against it:
- `pnpm run soak` — parity-check every surface (CI-gated in docs-links)
- `pnpm run soak:fix` — rewrite drifted windows, prune expired exclusions
- `pnpm run deps:update` — bump npm (taze) + cargo deps through the window
- `pnpm run tools:check` / `tools:install` — validate / install the
SRI-pinned external tools (`external-tools.json`)
- `pnpm run tools:check` / `tools:fix` / `tools:install` — validate /
prune-expired-bypasses / install the SRI-pinned external tools
(`external-tools.json`)
- `pnpm run test:scripts` — the scripts' own unit tests

The gates fail closed on invalid states (missing, malformed, or
wrong-arithmetic annotations) and WARN on expired ones — stale is not
unsafe, and nobody has to watch for it: the scheduled `soak-autofix`
workflow runs `soak:fix` + `tools:fix` daily and commits the pruning
as a bot PR.

A soak change is done when `pnpm run soak` and `pnpm run test:scripts`
both exit 0 — the same gates CI runs. Re-run them after every fix.

Expand All @@ -53,14 +60,15 @@ annotation on the line above (block list only — flow `[..]` is rejected
because a comment line can't attach to an inline entry):

```yaml
# published: 2026-07-08 | removable: 2026-07-15
# published: YYYY-MM-DD | removable: YYYY-MM-DD
- 'name@1.2.3'
```

`removable` = `published + SOAK_DAYS` (this example assumes a 7-day
window). `published` must be the real registry publish date. Once
`removable` passes, `pnpm run soak` fails until the pin is pruned
(`soak:fix` does it). Bare names / `@scope/*` globs are standing trust and
`removable` = `published + SOAK_DAYS`; `published` must be the real
registry publish date (the placeholders above are schematic — copying
them verbatim is rejected). Once `removable` passes, `pnpm run soak`
warns until the pin is pruned (`soak:fix` or the soak-autofix workflow
does it). Bare names / `@scope/*` globs are standing trust and
need no annotation. External tools use the same shape via a `soakBypass`
object in `external-tools.json`.

Expand All @@ -78,6 +86,17 @@ update`. Everything else — every CI job, every shipped binary — builds on
stable. If you need the cargo soak somewhere new, call `cargo +nightly`
there; do not add a toolchain file.

**Keep the nightly current — a merely-old one silently disables the
window.** Cargo treats an `[unstable]` key it does not implement as a
warning and exits 0, so an old nightly resolves with NO window while
looking successful. Measured both sides: nightly 2026-03-21 (cargo
1.96.0-nightly) has no such `-Z` and skips the window silently; nightly
2026-07-27 (cargo 1.99.0-nightly) supports `-Z min-publish-age` and
visibly holds a too-fresh release back (`available: v0.2.189, published
7 days ago`). `deps:update` detects the warning and fails with the fix
(`rustup update nightly`) — if you see it, the lockfile changes it just
made are unsoaked.

## Maintaining this skill

`scripts/soak/` is the law; this file only documents it — when they
Expand Down
83 changes: 67 additions & 16 deletions .github/renovate.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"dependencyDashboard": true,
"suppressNotifications": ["prEditedNotification"],
"labels": ["dependencies"],
"schedule": ["before 4am on Monday"],
"suppressNotifications": [
"prEditedNotification"
],
"labels": [
"dependencies"
],
"schedule": [
"before 4am on Monday"
],
"semanticCommits": "disabled",
"separateMajorMinor": true,
"minimumReleaseAge": "7 days",
"internalChecksFilter": "strict",
"enabledManagers": ["github-actions", "cargo", "npm"],
"enabledManagers": [
"github-actions",
"cargo",
"npm"
],
"cargo": {
"rangeStrategy": "update-lockfile",
"managerFilePatterns": ["/^Cargo\\.toml$/", "/^crates/[^/]+/Cargo\\.toml$/", "/^vendor/"]
"managerFilePatterns": [
"/^Cargo\\.toml$/",
"/^crates/[^/]+/Cargo\\.toml$/",
"/^vendor/"
]
},
"npm": {
"managerFilePatterns": [
Expand All @@ -22,41 +36,78 @@
"packageRules": [
{
"description": "Pin GitHub Actions to immutable commit SHAs",
"matchDepTypes": ["action"],
"matchDepTypes": [
"action"
],
"pinDigests": true
},
{
"description": "Annotate pinned GitHub Actions SHAs with a version comment",
"extends": ["helpers:pinGitHubActionDigests"],
"extends": [
"helpers:pinGitHubActionDigests"
],
"extractVersion": "^(?<version>v?\\d+\\.\\d+\\.\\d+)$",
"versioning": "regex:^v?(?<major>\\d+)(\\.(?<minor>\\d+)\\.(?<patch>\\d+))?$"
},
{
"description": "Group upload/download artifact updates (versions are dependent)",
"groupName": "artifact GitHub Actions",
"matchManagers": ["github-actions"],
"matchPackageNames": ["actions/upload-artifact", "actions/download-artifact"]
"matchManagers": [
"github-actions"
],
"matchPackageNames": [
"actions/upload-artifact",
"actions/download-artifact"
]
},
{
"description": "Disable PRs updating GitHub-hosted runner versions (runs-on: ubuntu-latest, etc.)",
"matchManagers": ["github-actions"],
"matchDatasources": ["github-runners"],
"matchManagers": [
"github-actions"
],
"matchDatasources": [
"github-runners"
],
"enabled": false
},
{
"description": "Group Cargo minor+patch updates into one PR; majors get individual PRs",
"groupName": "Cargo dependencies (minor+patch)",
"matchManagers": ["cargo"],
"matchUpdateTypes": ["minor", "patch", "digest"]
"matchManagers": [
"cargo"
],
"matchUpdateTypes": [
"minor",
"patch",
"digest"
]
},
{
"description": "Group npm minor+patch updates; majors get individual PRs",
"groupName": "npm dependencies (minor+patch)",
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"]
"matchManagers": [
"npm"
],
"matchUpdateTypes": [
"minor",
"patch"
]
},
{
"description": "decmpfs 0.1.2 fails to compile for musl targets (FICLONE ioctl request typed c_ulong, which is c_int on musl), and nub builds musl (docker-smoke alpine leg, release musl binaries). Hold at 0.1.0 until upstream fixes it \u2014 mirrors the rule in upstream aube.",
"matchManagers": [
"cargo"
],
"matchPackageNames": [
"decmpfs"
],
"allowedVersions": "<=0.1.0"
}
],
"vulnerabilityAlerts": {
"labels": ["security", "dependencies"]
"labels": [
"security",
"dependencies"
]
}
}
14 changes: 8 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -610,12 +610,10 @@ jobs:
# upgrades itself to enterprise when the secret appears.
env:
SOCKET_SECURITY_KEY: ${{ secrets.SOCKET_SECURITY_KEY }}
# cargo test spawns nub test binaries that talk to LOCAL registry
# mocks; routing `cargo` through the sfw shim would wrap that traffic
# in the firewall proxy and mangle it (upstream ECONNRESET, 405s on
# registry writes). The sentinel makes the cargo shim exec the real
# cargo directly — pnpm/npm installs stay firewalled.
SFW_SHIM_ACTIVE_CARGO: "1"
# The old SFW_SHIM_ACTIVE_CARGO=1 escape hatch is gone: sfw >= 1.14.0
# exempts loopback in the child's NO_PROXY (localhost,127.0.0.1,::1),
# so nub test binaries talking to LOCAL registry mocks are never
# proxied — cargo can ride the shim like everything else.
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.matrix-plan.outputs.test) }}
Expand Down Expand Up @@ -734,6 +732,10 @@ jobs:
shell: bash
env:
MATRIX_NODE: ${{ matrix.node }}
# download() attaches this to github.com fetches only (release
# assets: pnpm/sfw binaries) — avoids unauthenticated rate
# limits and covers private assets (sfw-enterprise).
GITHUB_TOKEN: ${{ github.token }}
run: |
# The soak scripts are erasable-TS .mts and need a type-stripping
# Node; the compat legs run them with the side-installed 26.5
Expand Down
125 changes: 125 additions & 0 deletions .github/workflows/soak-autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: soak-autofix

# An expired soakBypass annotation or a cleared minimumReleaseAgeExclude pin
# is STALE, not unsafe: the version has soaked, so the gates warn rather than
# fail. Nobody should have to watch for that warning either — this workflow
# runs the fixers daily and, when they change anything, commits to a bot
# branch and opens (or updates) a PR, so the ledger converges to clean on its
# own. Invalid annotations (missing, malformed, wrong arithmetic) still fail
# the gate; those need a human.

on:
schedule:
# Daily, shortly after midnight UTC — annotations expire on date
# boundaries, so the fix lands the morning a window clears.
- cron: '23 0 * * *'
workflow_dispatch:
Comment thread
jdalton marked this conversation as resolved.

permissions: {}

# One run at a time: overlapping runs would race on the bot branch
# force-push and the open-PR check.
concurrency:
group: soak-autofix
cancel-in-progress: false

jobs:
autofix:
# Guard the dispatch path: run only from main, so a workflow_dispatch
# on a topic branch can't force-push bot/soak-autofix from arbitrary
# HEAD state.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
# persist-credentials stays on: this job pushes its own fix branch.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # zizmor: ignore[artipacked]
with:
# Optional PAT: PRs opened with the default github.token do NOT
# trigger CI on the PR (GitHub drops workflow events from
# token-created refs), so checks stay pending until a human
# closes/reopens the PR. Set the SOAK_AUTOFIX_TOKEN repo secret
# (a fine-grained PAT with contents+pull-requests write) to make
# the autofix PRs run CI like any other PR.
token: ${{ secrets.SOAK_AUTOFIX_TOKEN || github.token }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
- name: Run the soak fixers
id: fixers
# Fixers write mechanical repairs first, then exit by post-fix
# check status. Capture that status instead of masking it: the
# commit step below still lands whatever WAS fixable, and the
# final step re-raises the failure so a crashed fixer or a
# human-only finding can never ride a green run.
run: |
status=0
node scripts/soak/soak.mts --fix || status=$?
node scripts/soak/external-tools.mts --fix || status=$?
echo "status=${status}" >> "$GITHUB_OUTPUT"
- name: Commit + PR when something was fixed
env:
GH_TOKEN: ${{ secrets.SOAK_AUTOFIX_TOKEN || github.token }}
run: |
set -euo pipefail
if git diff --quiet; then
echo "soak surfaces clean — nothing to fix"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
BRANCH="bot/soak-autofix"
# NO force push. The previous fetch-then---force-with-lease shape
# was security theater: `git fetch origin $BRANCH` updates the
# remote-tracking ref to the branch's CURRENT tip (checkout leaves
# the default wildcard refspec in place), so the lease is taken
# against whatever another actor just pushed and the force
# succeeds — silently discarding, for example, a human's review
# fixes committed onto the open autofix PR.
#
# Instead: stash the mechanical fixes, BASE the work on the
# existing bot branch when there is one, and fast-forward push.
# Human commits on the branch are preserved by construction, and
# a genuine conflict fails the run instead of being resolved by
# deletion.
git stash push --include-untracked -m soak-autofix
if git fetch origin "$BRANCH"; then
git checkout -B "$BRANCH" "origin/$BRANCH"
else
echo "no existing $BRANCH on origin — creating it"
git checkout -B "$BRANCH"
fi
if ! git stash pop; then
echo "::error::soak fixes conflict with the existing $BRANCH; resolve that branch (or close its PR) and re-run"
exit 1
fi
git add -A
# Re-running the fixers on top of an already-fixed branch is a
# no-op; say so and stop rather than pushing an empty commit.
if git diff --cached --quiet; then
echo "$BRANCH already carries these fixes — nothing to push"
exit 0
fi
git commit -m "chore(soak): prune expired soak annotations (automated)

Generated by the soak-autofix workflow: soak.mts --fix +
external-tools.mts --fix. Windows that cleared have soaked;
their bypass annotations are dead weight the gates would
otherwise fail on."
git push origin "$BRANCH"
if [ -z "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')" ]; then
gh pr create --head "$BRANCH" \
--title "chore(soak): prune expired soak annotations (automated)" \
--body "Automated by the soak-autofix workflow. The listed soak windows have cleared, so their bypass annotations are stale. Diff is the full review: only annotation/window lines are touched."
fi
- name: Re-raise fixer findings
# After the mechanical repairs are committed, a nonzero fixer
# status means findings remain that need a human (or the fixer
# itself crashed) — fail the run so it can't read as clean.
if: steps.fixers.outputs.status != '0'
run: |
echo "soak fixers exited nonzero — human-actionable findings remain (see fixer step log)"
exit 1
Loading
Loading