38 steps:39 - name: π₯ Checkout code40 uses: actions/checkout@v641 with:42 fetch-depth: 143 ref: newjitsu4445 # setup-node MUST run before setup-pnpm: pnpm/action-setup@v4 binds its46 # CLI shim to whichever Node is active at install time, so if pnpm goes47 # first, every later `pnpm` call still runs on the runner's default48 # Node 20 (triggers "Unsupported engine: wanted {node:>=22}") even after49 # setup-node bumps the PATH to Node 22.50 - name: π§ Setup Node.js51 uses: actions/setup-node@v452 with:53 node-version: 225455 # No `version:` β pnpm/action-setup reads `packageManager` from56 # package.json (pnpm@10.22.0). Specifying both conflicts and fails the step.57 - name: π§ Setup pnpm58 uses: pnpm/action-setup@v45960 - name: π§ Setup Go61 uses: actions/setup-go@v662 with:63 go-version: "1.26"64 cache-dependency-path: "bulker/**/*.sum"6566 - name: π¦ Install dependencies67 if: false68 run: pnpm install --frozen-lockfile69 continue-on-error: true7071 - name: π Run npm audit72 id: npm-audit73 if: false74 run: |75 pnpm audit --json > .npm-audit.json 2>/dev/null || true76 echo "npm audit complete"77 # Summary for logs78 jq -r '.advisories // {} | length' .npm-audit.json 2>/dev/null || echo "0"7980 - name: π Run govulncheck81 id: go-vuln82 if: false83 run: |84 go install golang.org/x/vuln/cmd/govulncheck@latest85 mkdir -p .go-vulns8687 for moddir in bulker/*/; do88 if [ -f "${moddir}go.mod" ]; then89 modname=$(basename "${moddir}")90 echo "Scanning ${moddir}..."91 govulncheck -C "${{ github.workspace }}/${moddir}" -json ./... > ".go-vulns/${modname}.json" 2>/dev/null || true92 fi93 done94 jq -s 'flatten' .go-vulns/*.json > .go-vulns.json 2>/dev/null || echo '[]' > .go-vulns.json95 echo "govulncheck complete"9697 - name: π Fetch Dependabot alerts98 id: dependabot99 env:100 GH_TOKEN: ${{ secrets.DEPENDABOT_TOKEN }}101 run: |102 set -euo pipefail103 gh api repos/${{ github.repository }}/dependabot/alerts --paginate \104 --jq '[.[] | select(.state == "open") | {105 number: .number,106 ghsa: .security_advisory.ghsa_id,107 cve: .security_advisory.cve_id,108 severity: .security_advisory.severity,109 summary: .security_advisory.summary,110 published_at: .security_advisory.published_at,111 package: .security_vulnerability.package.name,112 ecosystem: .security_vulnerability.package.ecosystem,113 vulnerable_range: .security_vulnerability.vulnerable_version_range,114 fixed_version: .security_vulnerability.first_patched_version.identifier115 }]' > .dependabot-alerts.json116 echo "Dependabot alerts: $(jq length .dependabot-alerts.json)"117118 - name: π Write Codex prompt119 env:120 DRY_RUN: ${{ inputs.dry_run || 'false' }}121 MIN_SEVERITY: ${{ inputs.severity || 'moderate' }}122 REPO: ${{ github.repository }}123 run: |124 set -euo pipefail125 mkdir -p .github/codex126127 # Runtime context goes through an UNQUOTED heredoc so $REPO/$DRY_RUN/128 # $MIN_SEVERITY expand. The static prompt body below uses a QUOTED129 # heredoc ('PROMPT_EOF') so backticks, $vars, and glob chars inside130 # the markdown pass through literally instead of being interpreted.131 cat > .github/codex/security-fix.prompt.md <<EOF132 ## Runtime context133 - Repository: ${REPO}134 - Base branch: newjitsu135 - Dry run mode: ${DRY_RUN}136 - Minimum severity: ${MIN_SEVERITY}137138 EOF139140 cat >> .github/codex/security-fix.prompt.md << 'PROMPT_EOF'141 You are a security engineer fixing dependency vulnerabilities in a monorepo.142143 ## Operating mode (read first)144145 You are running non-interactively inside a GitHub Actions job. There is146 no human to answer questions. Do not ask for confirmation, do not stop147 to request clarification, do not print plans and wait β just execute.148 When a step is ambiguous, pick the option described in the Rules149 section below; if there is no matching rule, skip that vulnerability150 and move on. Finish by printing a short summary of what you did.151152 ## Repo layout153154 - Node.js/pnpm workspace rooted at repo root (libs/, services/, webapps/, cli/, types/).155 - Go workspace rooted at bulker/ (bulker/go.work). All Go modules are members of this workspace β do NOT treat them as independent repos. Workspace members are listed in bulker/go.work.156 - pnpm-workspace.yaml has an `overrides:` block for pinning transitive versions.157 (It used to live in root package.json under `pnpm.overrides`; pnpm >=10.22158 no longer reads that field and ignores it silently.)159160 ## Input161162 The primary source of work is .dependabot-alerts.json (already produced163 by an earlier step). It is a JSON array; each entry has: number, ghsa,164 cve, severity, summary, published_at, package, ecosystem,165 vulnerable_range, fixed_version.166167 Read it with: cat .dependabot-alerts.json | jq .168169 `published_at` is when the ADVISORY was published, not when the package170 was fixed. It matters because a patch released after that date may not171 be reflected in vulnerable_range / fixed_version β see "Major version172 bumps" below.173174 .github/security-fix-exceptions.yml records alerts a human has already175 resolved in a way the advisory database does not recognise. It is176 checked in and may be absent or empty.177178 ## Procedure179180 IMPORTANT: All examples below use PLACEHOLDER names in ALL_CAPS181 (ACTUAL_PKG_NAME, FIXED_VER, ACTUAL_MODULE). Substitute real values182 from the alerts before running any command. Never run a command that183 still contains a placeholder.184185 1. List existing security PRs to avoid duplicates:186 gh pr list --search "security" --state open187 Skip any alert whose fix is already covered by an open PR.188 2. Filter alerts by MIN_SEVERITY (severity order: critical > high > moderate > low).189 3. Drop alerts already settled by a human. If190 .github/security-fix-exceptions.yml exists, read its `exceptions:`191 list and look for an item matching the alert on ALL THREE of `ghsa`,192 `package` and `ecosystem`. One advisory can cover several packages,193 and a deviation accepted for one of them says nothing about the194 others β matching on ghsa alone would skip unrelated alerts that are195 still genuinely vulnerable. More than one item may share a ghsa;196 that is expected, so keep looking until all three fields match197 rather than taking the first ghsa hit.198 When an entry matches, judge every resolved copy of that package199 separately. A copy is settled if EITHER it satisfies the entry's200 `satisfied_by` semver range (the human vouched for it) OR it already201 falls outside the alert's vulnerable_range (it is fixed on the202 advisory's own terms, e.g. at or above fixed_version). SKIP the203 alert only if EVERY resolved copy is settled under one of those two;204 list it in the run summary under "Skipped (accepted deviation)" with205 the recorded reason. If even one copy is settled by neither, the206 exception does NOT apply β treat the alert normally.207 Both tests are needed because a pnpm lockfile routinely holds208 several copies of a package. A tree carrying a backport-fixed 7.x209 copy alongside an already-fixed 8.x one is fully patched, but210 `satisfied_by` alone would reject it and re-propose the very major211 bump the exception exists to prevent.212 Checking vulnerable_range rather than just widening the range keeps213 the bound meaningful. `satisfied_by` is bounded on both sides on214 purpose: a bare lower bound would keep matching after a major215 migration, so a repo that later moved to a still-vulnerable version216 on the next major line would have the alert silently suppressed by217 an exception written for the old one. Versions between the two β on218 the newer major but below its fix β satisfy neither test and are219 still reported, which is the point.220 Never edit this file yourself: it records decisions only a human can221 make. An entry does not expire, so an alert can stay open forever222 with the repo deliberately not on the advisory's fixed_version.223 4. For EACH remaining alert, determine ALL versions of the package224 actually resolved in this repo, then SKIP the alert only if every225 one of them is already fixed:226 - A package can be resolved at MULTIPLE versions at once: a pnpm227 lockfile routinely contains several copies of the same package,228 and each Go workspace member (bulker/*/go.mod) resolves the229 module independently. Collect every resolved version β e.g.230 `pnpm why -r ACTUAL_PKG_NAME` / grep pnpm-lock.yaml for npm, and231 `go -C bulker/ACTUAL_MODULE list -m ACTUAL_PKG` for each232 workspace member for Go.233 - Only if EVERY resolved version is >= the alert's fixed_version234 (or outside the vulnerable_range) is the repo already patched β235 skip it. If even one resolved copy is still vulnerable, the236 alert applies; fix that copy (without downgrading the others).237 Do NOT touch the manifest just to match fixed_version exactly;238 that is a no-op at best and a downgrade at worst.239 - Go major versions are distinct modules. An alert for240 `github.com/foo/bar` (the v0/v1 module path) does NOT apply to241 imports of `github.com/foo/bar/v2` (or higher) β that is a242 different, newer module. But both majors can be in use at once:243 check `grep -rE 'github.com/foo/bar[[:space:]]' bulker/*/go.mod`244 (the trailing whitespace class matches the space or tab `go mod245 tidy` uses, while still excluding /v2+ paths) before skipping. Skip246 the alert only when NO workspace member still requires the247 v0/v1 path; never "fix" a v0/v1 alert by pinning the older248 major-version path in a module that only uses v2+.249 - NEVER downgrade. Compare versions semantically: if applying the250 fix would move any package to a LOWER version than it currently251 resolves to (directly or transitively), skip the alert and note it252 in the PR body under "Skipped (already satisfied / would downgrade)".253 5. Group the remaining alerts into AT MOST TWO PRs β one per stack:254 - All npm / Node.js fixes β a single PR on branch security/fix-npm-YYYY-MM-DD255 - All Go fixes β a single PR on branch security/fix-go-YYYY-MM-DD256 This is mandatory: pnpm-lock.yaml, go.sum, and bulker/go.work.sum257 otherwise cause merge conflicts if two concurrent PRs touch them.258 If a fix is risky (major version bump, breaking change), still include259 it in the per-stack PR but flag it prominently in the PR body so a260 reviewer can split it out manually.261 6. Apply every ecosystem-specific step below for all alerts in that262 stack BEFORE creating the PR. Do NOT push incremental commits per263 alert β one branch, one commit per stack, one PR per stack.264 7. After applying fixes, verify nothing regressed before opening the PR:265 - npm: `pnpm install --no-frozen-lockfile` must succeed.266 - Go: `go -C bulker work sync` and `go -C bulker/ACTUAL_MODULE build ./...`267 for each touched module must succeed.268 If verification fails for an alert, revert that alert's change and269 move it to the "Skipped" list. Never open a PR that does not build.270 8. If after all of the above NO alerts remain to fix, do NOT create a271 branch or PR β just print a summary saying everything is already272 satisfied.273 9. If DRY_RUN is true, output a report of what would be done and do NOT274 create branches, commits, or PRs.275276 ### npm / Node.js packages277278 #### Major version bumps279280 Do this BEFORE writing any override or version bump whose fixed_version281 is on a HIGHER major than the version currently resolved.282283 An advisory is usually filed the day the first patch ships. If that284 patch was on the newest major and a backport to an older line landed285 later, the advisory frequently keeps the merged range it was created286 with (e.g. ">= 7.12.0, < 8.3.0", fixed 8.3.0) and is never amended.287 The backport is then invisible in vulnerable_range and fixed_version,288 and taking fixed_version literally forces a major bump that was never289 necessary. Do not assume the alert knows about every fix.290291 The upstream advisory is the authority, not the release dates. Work292 from it first and use dates only to explain what you find.293294 1. Read the upstream advisory's own patched-versions list:295 gh api /advisories/ACTUAL_GHSA_ID296 If it names a patched version on the SAME major as the currently297 resolved version, that is the backport β use it, and stop here.298 2. If it shows only the merged range, the entry may predate the299 backport. Widen the search: read the advisory page at300 https://github.com/OWNER/REPO/security/advisories/ACTUAL_GHSA_ID301 (its "Patched versions" field is often correct when the API entry is302 stale), then list candidate releases:303 npm view ACTUAL_PKG_NAME versions --json304 npm view ACTUAL_PKG_NAME time --json305 Consider EVERY release on the current major above the resolved306 version. Do not filter by date: `published_at` is a hint about why307 the advisory might be stale β a candidate published after it is a308 likely backport β but under coordinated disclosure a fix can ship309 before the advisory, so an earlier date does not rule a release out.310 3. Publication order is never proof of a fix. Use a candidate only if311 the advisory page, the release notes, or the changelog explicitly312 names it as containing this fix. If nothing says so, treat it as no313 backport rather than guessing.314 4. If a confirmed backport exists, use it as FIXED_VER instead of the315 alert's. The alert will NOT close, because the advisory range still316 covers the backport β that is expected, not a failure. Record it in317 the PR body under "Deviations" (see "Creating PRs") so a human can318 add it to .github/security-fix-exceptions.yml; do not add it319 yourself.320 5. If no backport exists, the major bump is the only fix. Apply it, and321 flag it under "Risks" in the PR body.322323 #### Applying the fix324325 - Determine if the package is a direct dependency:326 grep -r '"ACTUAL_PKG_NAME"' package.json */package.json libs/*/package.json webapps/*/package.json cli/*/package.json services/*/package.json types/*/package.json327 - Transitive-only: add an entry to the `overrides:` block in pnpm-workspace.yaml328 (NOT package.json - pnpm >=10.22 ignores `pnpm.overrides` there, so an329 override written to package.json silently does nothing).330 Pattern (YAML, quote both sides): "ACTUAL_PKG_NAME@<FIXED_VER": "^FIXED_VER"331 The override floor (^FIXED_VER) must never be lower than the version332 already resolved in pnpm-lock.yaml β an override that caps a package333 below its current version is a downgrade; skip it instead.334 - Companion packages that pin each other EXACTLY must move together.335 Sibling packages from one project (e.g. react-router / react-router-dom)336 routinely depend on each other at an exact version, so overriding one337 of the pair alone leaves the other demanding a version it will never338 get. Before installing, list every package that pins it, by tracking339 the enclosing snapshot header:340 awk -v dep="ACTUAL_PKG_NAME" '341 /^ [^ ].*:$/ { pkg = $0; gsub(/\047/, "", pkg); next }342 { line = $0; sub(/^ +/, "", line); gsub(/\047/, "", line)343 if (index(line, dep ": ") == 1) {344 ver = substr(line, length(dep) + 3)345 if (ver ~ /^[0-9]/) print pkg " -> " line346 } }' pnpm-lock.yaml347 Pass the name via -v and compare it as a plain string. Interpolating348 it into an awk regex literal breaks on scoped packages, because the349 slash in `@scope/pkg` closes the /.../ and awk dies with a syntax350 error β which would look like "no companion packages" and let a351 broken lockfile through. The gsub calls strip the single quotes that352 YAML puts around scoped keys ('@scope/pkg': 1.2.3); \047 is an353 apostrophe, written as an escape so the program can stay inside354 single quotes in the shell. The `ver ~ /^[0-9]/` test is what selects355 exact pins: range specifiers start with ^ or ~.356 Any package depending on ACTUAL_PKG_NAME at an exact version (no ^ or ~)357 needs its own override to a release that depends on FIXED_VER β add it358 in the same commit. If no such release exists, skip the alert rather359 than ship a lockfile whose two halves disagree.360 - Direct dependency: bump the version in the package.json that declares361 it β only ever UP. If the declared/resolved version is already >=362 FIXED_VER, leave it untouched (already satisfied).363 - MANDATORY after any change to package.json OR pnpm-workspace.yaml364 (root or any workspace member): run `pnpm install --no-frozen-lockfile`365 at the repo root. Omitting --no-frozen-lockfile breaks in CI because pnpm366 defaults to --frozen-lockfile when CI=true and refuses to update367 pnpm-lock.yaml when overrides / catalog / workspace config has changed.368 This step updates pnpm-lock.yaml. Commit the manifest changes369 (package.json and/or pnpm-workspace.yaml) together with pnpm-lock.yaml.370 - If pnpm install fails, revert the changes and skip that vulnerability.371372 ### Go packages (Go workspace at bulker/)373374 - Find every workspace member that depends on the package:375 grep -l 'ACTUAL_PKG' bulker/*/go.mod376 - Confirm the EXACT module path in go.mod matches the alert's package,377 including the major-version suffix (e.g. `/v2`). If the repo uses a378 higher major version than the alert references, the alert does not379 apply β skip it; never add or pin the older major-version path.380 - Check the current version in go.mod/go.sum. If it is already >= vFIXED,381 skip β `go get ACTUAL_PKG@vFIXED` would DOWNGRADE it, which is forbidden.382 - For EACH matching member whose version is below vFIXED, run inside its383 module directory:384 go -C bulker/ACTUAL_MODULE get ACTUAL_PKG@vFIXED385 - After updating all affected members, sync the workspace:386 go -C bulker work sync387 - Manually drop hashes for old vulnerable versions from the affected388 go.sum files and bulker/go.work.sum (leaving unrelated entries alone).389 - Commit the changed go.mod/go.sum of each affected member together390 with bulker/go.work.sum.391 - Do NOT modify bulker/go.work β the use(...) list must not change for392 a security fix.393 - If `go get` or `go work sync` fails, revert and skip.394395 ### Creating PRs396397 There are at most two PRs per run β one per stack β so the structure is:398399 - Branch name:400 security/fix-npm-YYYY-MM-DD (all npm fixes)401 security/fix-go-YYYY-MM-DD (all Go fixes)402 - Single commit per branch:403 fix(security): batch npm dependency security fixes (or batch go ...)404 - Push and open PR:405 git push -u origin HEAD406 gh pr create --base newjitsu --title "fix(security): <stack> security fixes (YYYY-MM-DD)" --body "..."407 - PR body must enumerate every included fix, one bullet per CVE:408 - CVE_ID (SEVERITY): short vuln description β ACTUAL_PKG FROM_VER β FIXED_VER409 Plus a final "Risks" section calling out any major version bumps or410 breaking changes that a reviewer should split into a separate follow-up PR.411 - If any fix used a backport instead of the alert's fixed_version, add a412 "Deviations" section naming the alert, the version used, why the413 advisory does not list it, and the evidence that it is patched. State414 plainly that the alert will stay open until a human either dismisses it415 or adds an entry to .github/security-fix-exceptions.yml β otherwise the416 next scheduled run reopens the same major bump this PR avoided. Suggest417 the exact entry to add.418 - After creating the first PR, reset before starting the other stack:419 git checkout newjitsu && git reset --hard origin/newjitsu420421 ## Rules422423 - NEVER modify the newjitsu branch directly β always use a new branch.424 - NEVER run a command that still contains an ALL_CAPS placeholder.425 - NEVER force-push.426 - NEVER downgrade a dependency. Every change must move a version up or427 leave it unchanged β directly and transitively. If a "fix" would lower428 any resolved version, it is wrong: skip it.429 - A fixed_version from an alert is a FLOOR, not a target. If the repo is430 already at or above it, there is nothing to do β skip the alert.431 - An alert describes the fixes known when it was written, not every fix432 that exists. Never cross a major version boundary without first433 checking for a backport on the current line.434 - Closing the alert is not the goal; removing the vulnerability is. Prefer435 the smallest upgrade that is genuinely patched, even when it leaves the436 alert open, over a major bump that merely satisfies the range.437 - Prefer opening NO PR over opening one that downgrades, is a no-op, or438 does not build.439 - If any command fails, revert the working tree to clean state and skip that vulnerability.440 - When in doubt between overriding vs. bumping a direct dependency, prefer the smaller change (override).441 - When in doubt between one PR or many, prefer many small PRs.442 PROMPT_EOF443444 - name: π§° Reinstall Codex CLI (workaround)445 # openai/codex-action@v1 relies on a globally-installed @openai/codex.446 # On GitHub-hosted runners the platform-specific optional dep447 # (@openai/codex-linux-x64) sometimes gets skipped, producing:448 # "Missing optional dependency @openai/codex-linux-x64"449 # Force a clean reinstall with --include=optional before the action.450 # Pinned to the same CLI release as `codex-version` below (and as451 # ai-review.yml in jitsucom/github-workflows) so the workaround can't452 # pull a newer CLI than the action itself installs.453 run: |454 set -euo pipefail455 npm uninstall -g @openai/codex >/dev/null 2>&1 || true456 npm install -g --include=optional @openai/codex@0.149.0457 codex --version458459 - name: π Fix vulnerabilities with Codex460 id: run-codex461 # Pinned to the v1.11 commit, not the floating v1 tag β same pin as462 # ai-review.yml in jitsucom/github-workflows: v1.12 (2026-08-20)463 # rewrote the Linux launch path and the step never returns after a464 # heavy turn (openai/codex-action#150). Bump both places together465 # once upstream ships a fix.