diff --git a/.claude/hooks/check-new-deps/package.json b/.claude/hooks/check-new-deps/package.json index 4d8496b9..15db293c 100644 --- a/.claude/hooks/check-new-deps/package.json +++ b/.claude/hooks/check-new-deps/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@socketregistry/packageurl-js": "1.4.2", - "@socketsecurity/lib": "5.24.0", + "@socketsecurity/lib": "catalog:", "@socketsecurity/sdk": "4.0.1" }, "devDependencies": { diff --git a/.claude/hooks/release-workflow-guard/index.mts b/.claude/hooks/release-workflow-guard/index.mts index b824c3f5..d3d833f6 100644 --- a/.claude/hooks/release-workflow-guard/index.mts +++ b/.claude/hooks/release-workflow-guard/index.mts @@ -48,35 +48,103 @@ type ToolInput = { // The captured workflow argument is reported back so the user can // see what was blocked. const GH_WORKFLOW_DISPATCH_RE = - /\bgh\s+workflow\s+(?:run|dispatch)\b(?:\s+(?:--repo|--ref|-f|--field)\s+\S+)*\s+(['"]?)([^\s'"]+)\1/ + /\bgh\s+workflow\s+(?:run|dispatch)\b(?:\s+(?:--repo|--ref|-f|--field)\s+\S+)*\s+(['"]?)([^\s'"]+)\1/g // `gh api .../actions/workflows//dispatches` (POST/PUT). // The path component implies dispatch — no need to also match -X. const GH_API_WORKFLOW_DISPATCH_RE = - /\bgh\s+api\b[^|]*?\/actions\/workflows\/([^/\s]+)\/dispatches\b/ + /\bgh\s+api\b[^|]*?\/actions\/workflows\/([^/\s]+)\/dispatches\b/g + +// Walk the command and return a per-position boolean: true means the +// char at index i sits inside a single- or double-quoted string. We +// use this to skip matches that fall inside `git commit -m "..."` +// message bodies, heredocs, etc. — text that the shell will pass as +// a literal argument value, not execute. Without this, mentioning +// `gh workflow run` inside a commit message body trips the hook. +// +// Limitations: this is not a full POSIX shell parser. Heredocs +// (<(s.length).fill(false) + let inSingle = false + let inDouble = false + for (let i = 0; i < s.length; i += 1) { + const c = s[i] + if (!inSingle && !inDouble && c === "'") { + inSingle = true + mask[i] = true + continue + } + if (inSingle && c === "'") { + inSingle = false + mask[i] = true + continue + } + if (!inSingle && !inDouble && c === '"') { + inDouble = true + mask[i] = true + continue + } + if (inDouble && c === '"') { + inDouble = false + mask[i] = true + continue + } + if (inDouble && c === '\\' && i + 1 < s.length) { + mask[i] = true + mask[i + 1] = true + i += 1 + continue + } + mask[i] = inSingle || inDouble + } + return mask +} function detectDispatch(command: string): { blocked: boolean workflow?: string shape?: string } { - const normalized = command.replace(/\s+/g, ' ') + // We can't `replace(/\s+/g, ' ')` first because that would offset + // the quote mask from the original string. Match against the raw + // command and use the mask to filter false-positives. + const mask = buildQuoteMask(command) - const cliMatch = GH_WORKFLOW_DISPATCH_RE.exec(normalized) - if (cliMatch) { - return { - blocked: true, - workflow: cliMatch[2], - shape: 'gh workflow run/dispatch', + // The /g-flag regex is a module-scoped singleton; `.exec()` advances + // `lastIndex` and only resets when it returns null at end-of-input. + // If our previous call broke out of the loop early (because we found + // a quote-masked match), `lastIndex` is left mid-string and the next + // `detectDispatch` call would resume from there instead of scanning + // the whole command. Reset before each scan to make the regex + // stateless from the caller's perspective. + GH_WORKFLOW_DISPATCH_RE.lastIndex = 0 + let cliMatch: RegExpExecArray | null + while ((cliMatch = GH_WORKFLOW_DISPATCH_RE.exec(command))) { + if (!mask[cliMatch.index]) { + return { + blocked: true, + workflow: cliMatch[2], + shape: 'gh workflow run/dispatch', + } } } - const apiMatch = GH_API_WORKFLOW_DISPATCH_RE.exec(normalized) - if (apiMatch) { - return { - blocked: true, - workflow: apiMatch[1], - shape: 'gh api .../dispatches', + // Same /g-flag reset rationale as above — keep the regex stateless + // across calls. + GH_API_WORKFLOW_DISPATCH_RE.lastIndex = 0 + let apiMatch: RegExpExecArray | null + while ((apiMatch = GH_API_WORKFLOW_DISPATCH_RE.exec(command))) { + if (!mask[apiMatch.index]) { + return { + blocked: true, + workflow: apiMatch[1], + shape: 'gh api .../dispatches', + } } } diff --git a/.claude/hooks/release-workflow-guard/package.json b/.claude/hooks/release-workflow-guard/package.json index 7e19aa13..5c8d11c1 100644 --- a/.claude/hooks/release-workflow-guard/package.json +++ b/.claude/hooks/release-workflow-guard/package.json @@ -6,7 +6,11 @@ "exports": { ".": "./index.mts" }, + "scripts": { + "test": "node --test test/*.test.mts" + }, "devDependencies": { - "@types/node": "24.9.2" + "@socketsecurity/lib": "catalog:", + "@types/node": "catalog:" } } diff --git a/.claude/hooks/release-workflow-guard/test/release-workflow-guard.test.mts b/.claude/hooks/release-workflow-guard/test/release-workflow-guard.test.mts new file mode 100644 index 00000000..92ec5b7e --- /dev/null +++ b/.claude/hooks/release-workflow-guard/test/release-workflow-guard.test.mts @@ -0,0 +1,176 @@ +/** + * @fileoverview Tests for the release-workflow-guard hook. + * + * Runs the hook as a subprocess (node --test), piping a tool-use + * payload on stdin and asserting on the exit code + stderr. Exit 2 + * means the hook refused the command; exit 0 means it passed it + * through. + */ + +import { execPath } from 'node:process' +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' + +import { isSpawnError, spawn } from '@socketsecurity/lib/spawn' + +const hookScript = new URL('../index.mts', import.meta.url).pathname + +async function runHook( + command: string, + toolName = 'Bash', +): Promise<{ code: number | null; stdout: string; stderr: string }> { + const payload = JSON.stringify({ + tool_name: toolName, + tool_input: { command }, + }) + return runChild(payload) +} + +// Async @socketsecurity/lib/spawn — preferred over child_process +// spawnSync (see CLAUDE.md "Async spawn preferred"). Hooks are +// small, but async tests run in parallel under node --test, so +// even short subprocess waits compound when sync. spawn returns +// `{ stdin, stdout, stderr, process }` synchronously plus a thenable +// for the result; write the payload to stdin and await the result. +// On non-zero exit it throws a SpawnError — catch and lift fields +// back out so tests can assert on code (the hook's exit-2 path is +// the primary thing we test). +async function runChild( + payload: string, +): Promise<{ code: number | null; stdout: string; stderr: string }> { + const child = spawn(execPath, [hookScript], { + timeout: 5_000, + stdio: ['pipe', 'pipe', 'pipe'], + }) + child.stdin?.end(payload) + try { + const result = await child + return { + code: result.code, + stdout: (result.stdout || '').toString(), + stderr: (result.stderr || '').toString(), + } + } catch (e) { + if (isSpawnError(e)) { + return { + code: e.code, + stdout: (e.stdout || '').toString(), + stderr: (e.stderr || '').toString(), + } + } + throw e + } +} + +describe('release-workflow-guard hook', () => { + describe('blocks dispatching commands', () => { + it('gh workflow run', async () => { + const r = await runHook('gh workflow run release.yml') + assert.equal(r.code, 2) + assert.match(r.stderr, /BLOCKED/) + assert.match(r.stderr, /release\.yml/) + }) + + it('gh workflow dispatch', async () => { + const r = await runHook('gh workflow dispatch publish.yml') + assert.equal(r.code, 2) + assert.match(r.stderr, /publish\.yml/) + }) + + it('gh workflow run with -f flags', async () => { + const r = await runHook( + 'gh workflow run build.yml -f mode=prod -f arch=arm64', + ) + assert.equal(r.code, 2) + assert.match(r.stderr, /build\.yml/) + }) + + it('gh api .../dispatches', async () => { + const r = await runHook( + 'gh api repos/foo/bar/actions/workflows/42/dispatches -X POST', + ) + assert.equal(r.code, 2) + assert.match(r.stderr, /42/) + }) + + it('gh workflow run after a chained &&', async () => { + const r = await runHook('git fetch && gh workflow run release.yml') + assert.equal(r.code, 2) + }) + }) + + describe('allows benign commands', () => { + it('plain echo', async () => { + assert.equal((await runHook('echo hello')).code, 0) + }) + + it('git status', async () => { + assert.equal((await runHook('git status --short')).code, 0) + }) + + it('gh pr list (not a dispatch)', async () => { + assert.equal((await runHook('gh pr list --state open')).code, 0) + }) + + it('gh workflow list (read-only, no dispatch)', async () => { + assert.equal((await runHook('gh workflow list')).code, 0) + }) + + it('gh api repos/.../workflows (no /dispatches)', async () => { + assert.equal( + (await runHook('gh api repos/foo/bar/actions/workflows')).code, + 0, + ) + }) + }) + + describe('does not match inside quoted argument bodies', () => { + it('git commit -m with double-quoted body mentioning gh workflow run', async () => { + const r = await runHook( + 'git commit -m "chore: blocks dispatching gh workflow run jobs"', + ) + assert.equal(r.code, 0, `Expected 0 but got ${r.code}: ${r.stderr}`) + }) + + it('git commit -m with heredoc body mentioning gh workflow run', async () => { + const r = await runHook( + `git commit -m "$(cat <<'EOF'\nchore: never gh workflow run anything\nEOF\n)"`, + ) + assert.equal(r.code, 0, `Expected 0 but got ${r.code}: ${r.stderr}`) + }) + + it('echo of a doc string mentioning gh api .../dispatches', async () => { + const r = await runHook( + 'echo "see also: gh api repos/x/y/actions/workflows/1/dispatches"', + ) + assert.equal(r.code, 0, `Expected 0 but got ${r.code}: ${r.stderr}`) + }) + + it('single-quoted body protects against dispatch substring', async () => { + const r = await runHook( + "echo 'pretend command: gh workflow dispatch foo.yml'", + ) + assert.equal(r.code, 0, `Expected 0 but got ${r.code}: ${r.stderr}`) + }) + }) + + describe('payload edge cases', () => { + it('non-Bash tool is ignored', async () => { + assert.equal( + (await runHook('gh workflow run release.yml', 'Read')).code, + 0, + ) + }) + + it('empty command is ignored', async () => { + assert.equal((await runHook('')).code, 0) + }) + + it('invalid JSON on stdin returns 0 (silent)', async () => { + // Hook intentionally returns 0 on bad JSON (don't punish the + // model for unparseable payloads — pass them through). + const r = await runChild('not json') + assert.equal(r.code, 0) + }) + }) +}) diff --git a/.claude/skills/updating-xport/SKILL.md b/.claude/skills/updating-xport/SKILL.md new file mode 100644 index 00000000..aa1fecaf --- /dev/null +++ b/.claude/skills/updating-xport/SKILL.md @@ -0,0 +1,212 @@ +--- +name: updating-xport +description: Acts on `xport.json` drift for repos that carry the xport lock-step manifest. Reads `pnpm run xport --json`, then for each row acts per-kind — auto-bump `version-pin` rows (low-risk mechanical updates), advisory-only for `file-fork` / `feature-parity` / `spec-conformance` / `lang-parity` (upstream semantics need human judgment). Invoked by the `updating` umbrella skill; can also be invoked standalone. +user-invocable: true +allowed-tools: Bash(pnpm:*), Bash(npm:*), Bash(git:*), Bash(node:*), Bash(rg:*), Bash(grep:*), Bash(find:*), Bash(ls:*), Bash(cat:*), Bash(head:*), Bash(tail:*), Bash(wc:*), Bash(diff:*), Read, Edit, Grep, Glob--- + +# updating-xport + + +Act on drift findings in `xport.json`. Auto-apply mechanical version-pin bumps; surface everything else as advisory notes for human review. Commit each actioned row as its own atomic commit so the PR reviewer can accept/reject per-row. + + + +**xport** is a cross-project lock-step manifest. Not every repo has one; this skill exits cleanly when `xport.json` is absent. See `xport.schema.json` (deployed via `socket-repo-template/sync-scaffolding.mjs`) for the five row kinds. + +The harness at `scripts/xport.mts` emits JSON reports with `severity ∈ {ok, drift, error}` per row. This skill consumes that JSON. + +**Per-kind action policy:** + +| Kind | Drift signal | Action | +|------|--------------|--------| +| `version-pin` | Upstream commits on default ref since pinned SHA | **Auto-bump** per `upgrade_policy`: `track-latest` → advance to latest stable tag; `major-gate` → advance patch/minor only; `locked` → advisory only | +| `file-fork` | Upstream file changed since `forked_at_sha` | **Advisory** — note in PR body; do NOT auto-merge (forks carry local deltas that need human review) | +| `feature-parity` | Parity score below `criticality/10` floor | **Advisory** — note in PR body; human decides implement vs downgrade criticality | +| `spec-conformance` | Spec submodule moved | **Advisory** — note in PR body; human decides whether to bump `spec_version` | +| `lang-parity` | Port divergence / `rejected` anti-pattern reintroduced | **Advisory** — note in PR body; humans fix the port or update the manifest | + +The common rule: **version-pin is mechanical** (safe to auto-apply with `track-latest`/`major-gate` policies); everything else is **advisory** (upstream semantics and local deltas matter, humans decide). + + + +**Requirements:** +- Start with clean working directory (check via `git status --porcelain`) +- Run from repo root +- Exit 0 cleanly if `xport.json` is absent (the repo doesn't use xport) +- Conventional commit format: `chore(deps): bump to ` +- Update `.gitmodules` version comments when submodule tags change (pattern: `# -` on the line above the submodule block) +- Target stable releases only (filter `-rc`, `-alpha`, `-beta`, `-dev`, `-snapshot`, `-nightly`, `-preview`) + +**Forbidden:** +- Never auto-edit `file-fork`, `feature-parity`, `spec-conformance`, or `lang-parity` rows' tracked state +- Never bump a `locked` version-pin without human approval +- Never skip the tag-stability filter +- Never use `npx`, `pnpm dlx`, `yarn dlx` — use `pnpm exec` or `pnpm run` + +**CI mode** (`CI=true` or `GITHUB_ACTIONS`): skip per-row test validation (workflow validates at the end); emit advisory summary to `$GITHUB_OUTPUT` when present. + +**Interactive mode** (default): validate each auto-bump with `pnpm test` before committing the next. + + + + +## Phase 1 — Pre-flight + +```bash +test -f xport.json || { echo "no xport.json; skill n/a"; exit 0; } +test -f xport.schema.json || { echo "xport.schema.json missing — malformed scaffolding"; exit 1; } +test -f scripts/xport.mts || { echo "scripts/xport.mts missing — malformed scaffolding"; exit 1; } + +git status --porcelain | grep -v '^??' && { echo "dirty tree; aborting"; exit 1; } || true + +[ "$CI" = "true" ] || [ -n "$GITHUB_ACTIONS" ] && CI_MODE=true || CI_MODE=false +``` + +## Phase 2 — Collect drift + +```bash +pnpm run xport --json > /tmp/xport-report.json +``` + +Parse `reports[]` from the JSON. Split into: + +- **auto** — rows where `severity == "drift"` AND `kind == "version-pin"` AND `upgrade_policy` ∈ `{ "track-latest", "major-gate" }` +- **advisory** — everything else with `severity != "ok"` + +If both lists empty: exit 0 with "no xport drift". + +## Phase 3 — Auto-bump version-pin rows + +For each row in **auto** list, in manifest declaration order: + +**3a. Resolve the upstream submodule + fetch tags** + +```bash +SUBMODULE=$(jq -r --arg a "$UPSTREAM_ALIAS" '.upstreams[$a].submodule' xport.json) +cd "$SUBMODULE" +git fetch origin --tags --quiet +OLD_SHA=$(git rev-parse HEAD) +``` + +**3b. Find the target tag** + +Examine existing `pinned_tag` to identify the tag scheme, then match: + +- `v1.2.3` (v-prefixed semver) +- `1.2.3` (bare semver) +- `-1.2.3` (project-prefixed) +- `_1_2_3` (underscore style; curl, liburing) + +For `major-gate` policy: parse major version from `LATEST` vs current `pinned_tag`. If majors differ, skip — add to advisory with note "major bump needs human review". + +**3c. Check out + capture new SHA** + +```bash +NEW_SHA_FOR_CHECK=$(git rev-parse "$LATEST") +[ "$OLD_SHA" = "$NEW_SHA_FOR_CHECK" ] && { cd -; continue; } +git checkout "$LATEST" --quiet +NEW_SHA=$(git rev-parse HEAD) +cd - +``` + +**3d. Update `xport.json` + `.gitmodules`** + +Use `jq` for structured edit: + +```bash +jq --arg id "$ROW_ID" --arg sha "$NEW_SHA" --arg tag "$LATEST" \ + '(.rows[] | select(.id == $id) | .pinned_sha) = $sha + | (.rows[] | select(.id == $id) | .pinned_tag) = $tag' \ + xport.json > xport.json.tmp && mv xport.json.tmp xport.json +``` + +Update `.gitmodules` version comment via Edit tool (NOT sed per CLAUDE.md) — replace `# -` with `# -` on the comment line above the submodule block. + +**3e. Validate + commit** + +```bash +# Confirm xport harness accepts the new state +pnpm run xport --json > /tmp/xport-post.json +jq --arg id "$ROW_ID" '.reports[] | select(.id == $id) | .severity' /tmp/xport-post.json +# expect "ok" + +if [ "$CI_MODE" = "false" ]; then + pnpm test || { + echo "tests failed; rolling back $ROW_ID" + git checkout xport.json .gitmodules "$SUBMODULE" + continue + } +fi + +git add xport.json .gitmodules "$SUBMODULE" +git commit -m "chore(deps): bump $UPSTREAM_ALIAS to $LATEST" +``` + +Record bumped row in summary accumulator. + +## Phase 4 — Compose advisory notes + +For each row in **advisory**, accumulate a markdown line: + +``` +- **file-fork** ``: `` — upstream commit(s) since . Review diff, cherry-pick if applicable, bump forked_at_sha. +- **feature-parity** ``: parity score below floor . Implement or downgrade criticality with reason. +- **spec-conformance** ``: upstream spec repo moved. Review for breaking changes before bumping spec_version. +- **lang-parity** ``:
. +- **version-pin** ``: major bump to — policy=major-gate requires human review. +- **version-pin** ``: upgrade_policy=locked — skipped. +``` + +## Phase 5 — Report + emit + +Final human-readable report to stdout: + +``` +## updating-xport report + +**Auto-bumped:** row(s) + + +**Advisory (human review):** row(s) + +``` + +In CI mode, emit the advisory block to `$GITHUB_OUTPUT` (base64-encoded) under key `xport-advisory` so the weekly-update workflow can include it in the PR body: + +```bash +if [ -n "$GITHUB_OUTPUT" ]; then + echo "xport-advisory=$(printf '%s' "$ADVISORY" | base64 | tr -d '\n')" >> "$GITHUB_OUTPUT" +fi +``` + +Emit a HANDOFF block per `_shared/report-format.md`: + +``` +=== HANDOFF: updating-xport === +Status: {pass|fail} +Findings: {auto_bumped: N, advisory: M} +Summary: {one-line description} +=== END HANDOFF === +``` + + + +## Success Criteria + +- All actionable `version-pin` rows bumped atomically (one commit per row) +- Advisory rows collected for PR body / workflow output +- No edits to non-version-pin row state +- `pnpm run xport` exits 0 or 2 at end (never 1 — no schema errors introduced) +- `.gitmodules` version comments synchronized with `pinned_tag` + +## Commands + +- `pnpm run xport --json` — drift report (consumed by this skill) +- `jq` — parse + edit `xport.json` (structured JSON edits) +- `git submodule status` — verify submodule state after bumps + +## When to use + +- Invoked by the `updating` umbrella skill (weekly-update workflow) +- Standalone: `/updating-xport` when syncing just the xport manifest +- After manual submodule bumps, to refresh `xport.json` metadata diff --git a/.claude/skills/updating/SKILL.md b/.claude/skills/updating/SKILL.md index 159199ec..871cf9f5 100644 --- a/.claude/skills/updating/SKILL.md +++ b/.claude/skills/updating/SKILL.md @@ -1,22 +1,45 @@ --- name: updating -description: Updates all npm dependencies to their latest versions. Triggers when user asks to "update dependencies", "update packages", or prepare for a release. +description: Umbrella update skill for a Socket fleet repo. Runs `pnpm run update` (npm), validates `xport.json` via `pnpm run xport` (if present), optionally bumps submodules, and checks workflow SHA pins. Use when asked to update dependencies, sync upstreams, or prepare for a release. user-invocable: true -allowed-tools: Read, Edit, Bash(pnpm run:*), Bash(pnpm test:*), Bash(git status:*), Bash(git diff:*), Bash(git add:*), Bash(git commit:*), Bash(git log:*) +allowed-tools: Task, Skill, Read, Edit, Grep, Glob, Bash(pnpm run:*), Bash(pnpm test:*), Bash(pnpm install:*), Bash(git:*), Bash(claude --version) --- # updating -Your task is to update all npm dependencies to their latest versions, ensuring all builds and tests pass. +Update all dependencies for this repo: npm packages first, then the +xport-managed version pins (if `xport.json` exists), then any other +submodules tracked via `.gitmodules`, and finally verify workflow +SHA pins are current. Validate with the full check/test suite before +committing. The sub-skill delegation mirrors the canonical +socket-registry `updating` skill; uncomment the phases that apply to +this repo and delete those that don't. **What is this?** -This skill updates npm packages for security patches, bug fixes, and new features. +The umbrella update skill. Runs `pnpm run update` for npm deps, then +adapts to what the repo has: **Update Targets:** -- npm packages via `pnpm run update` +- **npm packages** — via `pnpm run update` (every Socket repo has this script) +- **xport-managed upstreams** — via `pnpm run xport` when `xport.json` exists + (manifest-managed submodule pins + advisory drift on file-fork / + feature-parity / spec-conformance / lang-parity rows) +- **Other submodules** — via repo-specific `updating-*` sub-skills + when `.gitmodules` has entries not claimed by xport version-pin rows +- **Workflow SHA pins** — check `_local-not-for-reuse-*.yml` against + `origin/main`; run the `updating-workflows` skill when stale + +**Key files this skill consults:** +- `xport.json` — if present, drives version-pin bumps and surfaces drift +- `.gitmodules` — listed submodules; xport's `version-pin` rows take precedence +- `.github/workflows/_local-not-for-reuse-*.yml` — SHA pin sources +- `package.json` — `pnpm run update` script + +Sub-skills are invoked only when applicable — this umbrella reads repo +state first to discover what to run. @@ -33,7 +56,9 @@ This skill updates npm packages for security patches, bug fixes, and new feature **Actions:** - Update npm packages -- Create atomic commits +- Apply xport-driven bumps (if `xport.json` present) +- Bump remaining submodules (if any) +- Create atomic commits per category - Report summary of changes @@ -43,96 +68,144 @@ This skill updates npm packages for security patches, bug fixes, and new feature ### Phase 1: Validate Environment - -Check working directory is clean and detect CI mode: - +Check clean working directory, detect CI mode (`CI=true` or +`GITHUB_ACTIONS`), verify submodules initialized (if any). + +--- + +### Phase 2: Update npm Packages ```bash -# Detect CI mode -if [ "$CI" = "true" ] || [ -n "$GITHUB_ACTIONS" ]; then - CI_MODE=true - echo "Running in CI mode - will skip build validation" +pnpm run update + +if [ -n "$(git status --porcelain)" ]; then + git add pnpm-lock.yaml package.json */package.json + git commit -m "chore: update npm dependencies + +Updated npm packages via pnpm run update." + echo "npm packages updated" else - CI_MODE=false - echo "Running in interactive mode - will validate builds" + echo "npm packages already up to date" fi +``` + +--- -# Check working directory is clean -git status --porcelain +### Phase 3: Validate xport manifest (if applicable) + +If `xport.json` exists at repo root, run the harness in read-only mode +to classify drift before acting on it: + +```bash +if [ -f xport.json ]; then + pnpm run xport + XPORT_EXIT=$? + + case $XPORT_EXIT in + 0) echo "✓ xport clean — manifest valid, no drift; skip Phase 4 xport step" ;; + 1) echo "✗ xport schema/structural error — stopping"; exit 1 ;; + 2) echo "⚠ xport drift — Phase 4 will invoke updating-xport to act" ;; + esac +fi ``` - -- Working directory must be clean -- CI_MODE detected for subsequent phases - +Exit code semantics: +- **0** — manifest valid, no drift; nothing for `updating-xport` to do. +- **1** — schema violation, missing file, or unreachable baseline. Stop + and investigate via `scripts/xport-schema.mts` and the failing row's + `local_*`/`upstream` fields. Do not auto-retry. +- **2** — drift detected. Phase 4 invokes the `updating-xport` skill, + which auto-bumps mechanical `version-pin` rows (per `upgrade_policy`) + and surfaces everything else (`file-fork` / `feature-parity` / + `spec-conformance` / `lang-parity` / `locked` version-pins) as + advisory notes for the PR body. Drift on `locked` rows never + auto-bumps — they need a coordinated upstream change first (e.g., + `temporal-rs` is `locked` because Node vendors it and bumping is + gated on a Node bump landing first). + +If `xport.json` does NOT exist, skip this phase. --- -### Phase 2: Update npm Packages +### Phase 4: Apply xport drift + update other submodules (if applicable) - -Run pnpm run update to update npm dependencies: - +**4a. xport drift** — if Phase 3 reported exit 2 (drift), invoke the +`updating-xport` skill. It auto-bumps `version-pin` rows whose +`upgrade_policy` is `track-latest` or `major-gate` (patch/minor only, +majors → advisory), and emits an advisory block for everything else. +Each auto-bumped row becomes its own atomic commit. ```bash -# Update npm packages -pnpm run update +if [ "$XPORT_EXIT" = "2" ]; then + # Invoke via the Skill tool / programmatic-claude flow used by the + # weekly-update workflow. Standalone runs can do `/updating-xport`. + echo "Invoking updating-xport for drift handling" +fi +``` -# Check if there are changes -if [ -n "$(git status --porcelain pnpm-lock.yaml package.json)" ]; then - git add pnpm-lock.yaml package.json - git commit -m "chore: update npm dependencies +**4b. Non-xport submodules** — invoke each `updating-*` sub-skill this +repo defines (e.g., `updating-node`, `updating-curl`) for submodules +NOT claimed by an xport `version-pin` row. These sub-skills know about +build inputs that aren't tracked in xport (cache-versions bumps, +patch regeneration, etc.). -Updated npm packages via pnpm run update." - echo "npm packages updated" +If no `.gitmodules` exists, skip 4b. + +--- + +### Phase 5: Check Workflow SHA Pins + +Inspect `_local-not-for-reuse-*.yml` files for their pinned SHA and +compare against `origin/main`: + +```bash +PINNED_SHA=$(grep -ohP '(?<=@)[0-9a-f]{40}' .github/workflows/_local-not-for-reuse-ci.yml 2>/dev/null | head -1) +MAIN_SHA=$(git rev-parse origin/main 2>/dev/null || echo "") + +if [ -n "$PINNED_SHA" ] && [ -n "$MAIN_SHA" ] && [ "$PINNED_SHA" != "$MAIN_SHA" ]; then + echo "Workflow SHA pins are stale: $PINNED_SHA → $MAIN_SHA" + echo "Run the updating-workflows skill to cascade." else - echo "npm packages already up to date" + echo "Workflow SHA pins are up to date (or no _local-not-for-reuse-*.yml pins in this repo)" fi ``` --- -### Phase 3: Final Validation - - -Run build and test suite (skip in CI mode): - +### Phase 6: Final Validation (skip in CI) ```bash -if [ "$CI_MODE" = "true" ]; then - echo "CI mode: Skipping final validation (CI will run builds/tests separately)" - echo "Commits created - ready for push by CI workflow" +if [ "$CI" = "true" ] || [ -n "$GITHUB_ACTIONS" ]; then + echo "CI mode: skipping validation" else - echo "Interactive mode: Running full validation..." - pnpm run fix --all pnpm run check --all pnpm test + pnpm run build # if this repo has a build step fi ``` --- -### Phase 4: Report Summary - - -Generate update report: - +### Phase 7: Report Summary ``` ## Update Complete ### Updates Applied: -| Category | Status | -|----------|--------| -| npm packages | Updated/Up to date | +| Category | Status | +|--------------------|--------------------------------------| +| npm packages | Updated / Up to date | +| xport manifest | / ok, drift, error (exit ) — or n/a | +| Other submodules | K bumped — or n/a | +| Workflow SHA pins | Up to date / Stale | ### Commits Created: -- [list commits if any] +- [list commits, if any] ### Validation: -- Build: SUCCESS/SKIPPED (CI mode) -- Tests: PASS/SKIPPED (CI mode) +- Build: SUCCESS / SKIPPED (CI mode) +- Tests: PASS / SKIPPED (CI mode) ### Next Steps: **Interactive mode:** @@ -150,6 +223,7 @@ Generate update report: ## Success Criteria - All npm packages checked for updates +- xport manifest validated (when present); schema/structural errors block - Full build and tests pass (interactive mode) - Summary report generated @@ -157,8 +231,10 @@ Generate update report: This skill is useful for: -- Weekly maintenance (automated via weekly-update.yml) +- Weekly maintenance (automated via `weekly-update.yml`) - Security patch rollout - Pre-release preparation -**Safety:** Updates are validated before committing. Failures stop the process. +**Safety:** Updates are validated before committing. Schema errors +(xport exit 1) stop the process; drift (xport exit 2) is advisory +and does not block. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b430cd4b..57912d3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,6 @@ concurrency: jobs: ci: name: Run CI Pipeline - uses: SocketDev/socket-registry/.github/workflows/ci.yml@85a2fc0d33af6304246620365de3e7f053035a8d # main + uses: SocketDev/socket-registry/.github/workflows/ci.yml@4c4b12cc32121314f56e2bc04e92eafa98e01104 # main with: test-script: 'pnpm run test --all --skip-build' diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index b4534c3e..974dbaf4 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -46,14 +46,14 @@ jobs: echo "Sleeping for $delay seconds..." sleep $delay - - uses: SocketDev/socket-registry/.github/actions/setup-and-install@85a2fc0d33af6304246620365de3e7f053035a8d # main + - uses: SocketDev/socket-registry/.github/actions/setup-and-install@4c4b12cc32121314f56e2bc04e92eafa98e01104 # main - name: Configure push credentials env: GH_TOKEN: ${{ github.token }} run: git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" - - uses: SocketDev/socket-registry/.github/actions/setup-git-signing@85a2fc0d33af6304246620365de3e7f053035a8d # main + - uses: SocketDev/socket-registry/.github/actions/setup-git-signing@4c4b12cc32121314f56e2bc04e92eafa98e01104 # main with: gpg-private-key: ${{ secrets.BOT_GPG_PRIVATE_KEY }} @@ -145,5 +145,5 @@ jobs: > \`\`\` EOF - - uses: SocketDev/socket-registry/.github/actions/cleanup-git-signing@85a2fc0d33af6304246620365de3e7f053035a8d # main + - uses: SocketDev/socket-registry/.github/actions/cleanup-git-signing@4c4b12cc32121314f56e2bc04e92eafa98e01104 # main if: always() diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index ecf5df13..7cacd283 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -25,7 +25,7 @@ jobs: permissions: contents: write # To create GitHub releases id-token: write # For npm trusted publishing via OIDC - uses: SocketDev/socket-registry/.github/workflows/provenance.yml@85a2fc0d33af6304246620365de3e7f053035a8d # main + uses: SocketDev/socket-registry/.github/workflows/provenance.yml@4c4b12cc32121314f56e2bc04e92eafa98e01104 # main with: debug: ${{ inputs.debug }} dist-tag: ${{ inputs.dist-tag }} diff --git a/.github/workflows/weekly-update.yml b/.github/workflows/weekly-update.yml index 1112eaed..9944f3bc 100644 --- a/.github/workflows/weekly-update.yml +++ b/.github/workflows/weekly-update.yml @@ -10,7 +10,7 @@ permissions: jobs: weekly-update: - uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@85a2fc0d33af6304246620365de3e7f053035a8d # main + uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@4c4b12cc32121314f56e2bc04e92eafa98e01104 # main with: test-setup-script: 'pnpm run build' test-script: 'pnpm test' diff --git a/package.json b/package.json index 586274d1..48bb8776 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,13 @@ }, "devDependencies": { "@anthropic-ai/claude-code": "2.1.92", - "@sinclair/typebox": "0.34.49", "@babel/generator": "7.28.5", "@babel/parser": "7.26.3", "@babel/traverse": "7.26.4", "@babel/types": "7.26.3", "@oxlint/migrate": "1.52.0", - "@socketsecurity/lib": "5.25.1", + "@sinclair/typebox": "0.34.49", + "@socketsecurity/lib": "catalog:", "@sveltejs/acorn-typescript": "1.0.8", "@types/babel__traverse": "7.28.0", "@types/node": "24.9.2", @@ -111,7 +111,7 @@ }, "engines": { "node": ">=18.20.8", - "pnpm": ">=11.0.0-rc.0" + "pnpm": ">=11.0.6" }, - "packageManager": "pnpm@11.0.0-rc.5" + "packageManager": "pnpm@11.0.6+sha512.97f906e1da2bedac3df83cadae04b4753a130092dd49d55cd36825ad3e623e9df3f97754f8f259e699172a360fac569acf2f908e7732bdae3eddb2dcf7e121fd" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d33e9e50..78f85492 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,15 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@socketsecurity/lib': + specifier: 5.27.0 + version: 5.27.0 + '@types/node': + specifier: 24.9.2 + version: 24.9.2 + overrides: defu: '>=6.1.7' vite: 7.3.2 @@ -82,8 +91,8 @@ importers: specifier: 0.34.49 version: 0.34.49 '@socketsecurity/lib': - specifier: 5.25.1 - version: 5.25.1(typescript@5.9.3) + specifier: 'catalog:' + version: 5.27.0(typescript@5.9.3) '@sveltejs/acorn-typescript': specifier: 1.0.8 version: 1.0.8(acorn@8.15.0) @@ -154,14 +163,24 @@ importers: specifier: 4.0.3 version: 4.0.3(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.3) + .claude/hooks/auth-rotation-reminder: + dependencies: + '@socketsecurity/lib': + specifier: 'catalog:' + version: 5.27.0(typescript@5.9.3) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.9.2 + .claude/hooks/check-new-deps: dependencies: '@socketregistry/packageurl-js': specifier: 1.4.2 version: 1.4.2 '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 'catalog:' + version: 5.27.0(typescript@5.9.3) '@socketsecurity/sdk': specifier: 4.0.1 version: 4.0.1 @@ -170,6 +189,12 @@ importers: specifier: 24.9.2 version: 24.9.2 + .claude/hooks/logger-guard: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.9.2 + .claude/hooks/path-guard: {} .claude/hooks/private-name-guard: @@ -186,10 +211,15 @@ importers: .claude/hooks/release-workflow-guard: devDependencies: + '@socketsecurity/lib': + specifier: 'catalog:' + version: 5.27.0(typescript@5.9.3) '@types/node': - specifier: 24.9.2 + specifier: 'catalog:' version: 24.9.2 + .claude/hooks/stale-process-sweeper: {} + .claude/hooks/token-guard: {} packages: @@ -1255,18 +1285,9 @@ packages: resolution: {integrity: sha512-yt9UfUzD02wZ7kwb67oe4jxG2D9JtgPqjrK/ans2BovFyeie0w8hvRR0MuOWM4mUt2371oFPp7NB6O5ZjYJmlw==} engines: {node: '>=18.20.8', pnpm: '>=11.0.0-rc.0'} - '@socketsecurity/lib@5.24.0': - resolution: {integrity: sha512-4Yar8oo4N12ESoNt/i2PNf08HRABUC0OcfUfwzIF3xjq89E5VMDN+aeOtnn6Oo4Y6u3TiuZRG7NgEBZ83LQ1Lw==} - engines: {node: '>=22', pnpm: '>=11.0.0-rc.0'} - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - - '@socketsecurity/lib@5.25.1': - resolution: {integrity: sha512-I/sXk5FDOF7FVstzYn8tKtCvRe97KU/hl4p0e3OI1O9gma2uYypDiJT/n3axvtkqOyNFxWICWuXfy8Hnzeaw6Q==} - engines: {node: '>=22', pnpm: '>=11.0.0-rc.0'} + '@socketsecurity/lib@5.27.0': + resolution: {integrity: sha512-RTplAVxQ6VUG3GXw2RcKRveEdkMneO6LbbNDTX1eXtvd6dazypFoXqyeN7KfbFN21/9A4EjgaMgamd0f9Y3cyQ==} + engines: {node: '>=22', pnpm: '>=11.0.6'} peerDependencies: typescript: '>=5.0.0' peerDependenciesMeta: @@ -2993,11 +3014,7 @@ snapshots: '@socketregistry/packageurl-js@1.4.2': {} - '@socketsecurity/lib@5.24.0(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 - - '@socketsecurity/lib@5.25.1(typescript@5.9.3)': + '@socketsecurity/lib@5.27.0(typescript@5.9.3)': optionalDependencies: typescript: 5.9.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b8a1c8ba..6f89b31e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,6 +25,10 @@ enablePrePostScripts: true # (which in rc.5 leaves a placeholder launcher that errors at runtime). pmOnFail: error +catalog: + '@socketsecurity/lib': 5.27.0 + '@types/node': 24.9.2 + overrides: defu: '>=6.1.7' vite: 7.3.2 @@ -38,6 +42,15 @@ minimumReleaseAgeExclude: - '@socketregistry/*' - '@socketsecurity/*' +# Refuse transitive dependencies declared via git/tarball/local-tarball +# specs — an npm package shouldn't be allowed to drag in a git URL we +# don't control (bypasses npm registry validation, no provenance, no +# soak window). Direct git deps are still allowed (the test suite at +# pnpm/pkg-manager/core/test/install/blockExoticSubdeps.ts confirms +# this). pnpm's current default is `false`; declared explicitly so a +# future flip can't silently change install behavior. +blockExoticSubdeps: true + # Pin exact versions on `pnpm add`. Catalog and overrides should # also be exact pins (5.24.0, not ^5.24.0). saveExact: true