Skip to content

Commit 90b8620

Browse files
authored
fix(ci): harden weekly-update — allowedTools, two-phase update, diff validation (#1159)
* fix(ci): harden weekly-update — allowedTools, two-phase update, diff validation * fix(ci): use curl+tar for npm upgrade (no npm/npx self-install) * fix(ci): use npm config get prefix for reliable npm dir detection * fix(ci): use env vars instead of template expressions in run blocks (zizmor) * fix(deps): override defu >=6.1.5 (prototype pollution CVE)
1 parent 4fd0d7c commit 90b8620

File tree

4 files changed

+157
-17
lines changed

4 files changed

+157
-17
lines changed

.github/workflows/provenance.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,12 @@ jobs:
166166

167167
- uses: SocketDev/socket-registry/.github/actions/install@6096b06b1790f411714c89c40f72aade2eeaab7c # main
168168

169-
- run: npm install -g npm@11.12.1
169+
- name: Upgrade npm for trusted publishing
170+
run: |
171+
# Avoid npm self-upgrade (corrupts deps mid-install on Node 22).
172+
NPM_DIR="$(npm config get prefix)/lib/node_modules/npm"
173+
curl -sL https://registry.npmjs.org/npm/-/npm-11.12.1.tgz | tar xz -C "$NPM_DIR" --strip-components=1
174+
echo "npm version: $(npm --version)"
170175
171176
# Get versions for lock-stepped and independent packages.
172177
- name: Get versions

.github/workflows/weekly-update.yml

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,31 @@ jobs:
9494
with:
9595
gpg-private-key: ${{ secrets.BOT_GPG_PRIVATE_KEY }}
9696

97-
- name: Run updating skill with Claude Code
98-
id: claude
99-
timeout-minutes: 30
97+
- name: Update dependencies (haiku — fast, cheap)
98+
id: update
99+
timeout-minutes: 10
100100
env:
101101
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
102102
GITHUB_ACTIONS: 'true'
103103
run: |
104+
if [ -n "$SFW_BIN" ]; then
105+
mkdir -p /tmp/sfw-bin
106+
printf '#!/bin/bash\nexec "%s" pnpm "$@"\n' "$SFW_BIN" > /tmp/sfw-bin/pnpm
107+
chmod +x /tmp/sfw-bin/pnpm
108+
export PATH="/tmp/sfw-bin:$PATH"
109+
fi
110+
104111
if [ -z "$ANTHROPIC_API_KEY" ]; then
105112
echo "ANTHROPIC_API_KEY not set - skipping automated update"
106113
echo "success=false" >> $GITHUB_OUTPUT
107114
exit 0
108115
fi
109116
110117
set +e
111-
pnpm exec claude --print --dangerously-skip-permissions \
112-
--model sonnet \
118+
pnpm exec claude --print \
119+
--allowedTools "Bash(pnpm:*)" "Bash(git add:*)" "Bash(git commit:*)" "Bash(git status:*)" "Bash(git diff:*)" "Bash(git log:*)" "Bash(git rev-parse:*)" "Read" "Write" "Edit" "Glob" "Grep" \
120+
--model haiku \
121+
--max-turns 15 \
113122
"$(cat <<'PROMPT'
114123
/updating
115124
@@ -122,7 +131,7 @@ jobs:
122131
Update all dependencies to their latest versions.
123132
Create one atomic commit per dependency update with a conventional commit message.
124133
Leave all changes local — the workflow handles pushing and PR creation.
125-
Skip running builds, tests, and type checks — CI runs those separately.
134+
Do not run builds or tests — the next step handles that.
126135
</instructions>
127136
128137
<success_criteria>
@@ -132,7 +141,7 @@ jobs:
132141
</success_criteria>
133142
PROMPT
134143
)" \
135-
2>&1 | tee claude-output.log
144+
2>&1 | tee claude-update.log
136145
CLAUDE_EXIT=${PIPESTATUS[0]}
137146
set -e
138147
@@ -142,6 +151,126 @@ jobs:
142151
echo "success=false" >> $GITHUB_OUTPUT
143152
fi
144153
154+
- name: Run tests
155+
id: tests
156+
if: steps.update.outputs.success == 'true'
157+
run: |
158+
if [ -n "$SFW_BIN" ]; then
159+
mkdir -p /tmp/sfw-bin
160+
printf '#!/bin/bash\nexec "%s" pnpm "$@"\n' "$SFW_BIN" > /tmp/sfw-bin/pnpm
161+
chmod +x /tmp/sfw-bin/pnpm
162+
export PATH="/tmp/sfw-bin:$PATH"
163+
fi
164+
165+
set +e
166+
pnpm build 2>&1 | tee build.log
167+
BUILD_EXIT=${PIPESTATUS[0]}
168+
169+
pnpm test 2>&1 | tee test.log
170+
TEST_EXIT=${PIPESTATUS[0]}
171+
set -e
172+
173+
if [ "$BUILD_EXIT" -eq 0 ] && [ "$TEST_EXIT" -eq 0 ]; then
174+
echo "tests-passed=true" >> $GITHUB_OUTPUT
175+
else
176+
echo "tests-passed=false" >> $GITHUB_OUTPUT
177+
fi
178+
179+
- name: Fix test failures (sonnet — smarter, escalated)
180+
id: claude
181+
if: steps.update.outputs.success == 'true' && steps.tests.outputs.tests-passed == 'false'
182+
timeout-minutes: 15
183+
env:
184+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
185+
GITHUB_ACTIONS: 'true'
186+
run: |
187+
if [ -n "$SFW_BIN" ]; then
188+
mkdir -p /tmp/sfw-bin
189+
printf '#!/bin/bash\nexec "%s" pnpm "$@"\n' "$SFW_BIN" > /tmp/sfw-bin/pnpm
190+
chmod +x /tmp/sfw-bin/pnpm
191+
export PATH="/tmp/sfw-bin:$PATH"
192+
fi
193+
194+
FAILURE_LOG="$(cat build.log test.log 2>/dev/null)"
195+
196+
set +e
197+
pnpm exec claude --print \
198+
--allowedTools "Bash(pnpm:*)" "Bash(git add:*)" "Bash(git commit:*)" "Bash(git status:*)" "Bash(git diff:*)" "Bash(git log:*)" "Bash(git rev-parse:*)" "Read" "Write" "Edit" "Glob" "Grep" \
199+
--model sonnet \
200+
--max-turns 25 \
201+
"$(cat <<PROMPT
202+
<context>
203+
You are an automated CI agent in a weekly dependency update workflow.
204+
Git is configured with GPG signing. A branch has been created for you.
205+
Dependencies were updated in the previous step but build/tests failed.
206+
</context>
207+
208+
<failure_log>
209+
$FAILURE_LOG
210+
</failure_log>
211+
212+
<instructions>
213+
The dependency updates above caused build or test failures.
214+
Diagnose the failures from the logs and fix the code so it builds and tests pass.
215+
Create one atomic commit per fix with a conventional commit message.
216+
Run pnpm build && pnpm test to verify your fixes.
217+
Leave all changes local — the workflow handles pushing and PR creation.
218+
</instructions>
219+
220+
<success_criteria>
221+
pnpm build succeeds.
222+
pnpm test succeeds.
223+
Each fix has its own commit.
224+
No uncommitted changes remain in the working tree.
225+
</success_criteria>
226+
PROMPT
227+
)" \
228+
2>&1 | tee claude-fix.log
229+
CLAUDE_EXIT=${PIPESTATUS[0]}
230+
set -e
231+
232+
if [ "$CLAUDE_EXIT" -eq 0 ]; then
233+
echo "success=true" >> $GITHUB_OUTPUT
234+
else
235+
echo "success=false" >> $GITHUB_OUTPUT
236+
fi
237+
238+
- name: Set final status
239+
id: final
240+
if: always()
241+
env:
242+
UPDATE_SUCCESS: ${{ steps.update.outputs.success }}
243+
TESTS_PASSED: ${{ steps.tests.outputs.tests-passed }}
244+
FIX_SUCCESS: ${{ steps.claude.outputs.success }}
245+
run: |
246+
if [ "$UPDATE_SUCCESS" = "true" ] && [ "$TESTS_PASSED" = "true" ]; then
247+
echo "success=true" >> $GITHUB_OUTPUT
248+
elif [ "$UPDATE_SUCCESS" = "true" ] && [ "$FIX_SUCCESS" = "true" ]; then
249+
echo "success=true" >> $GITHUB_OUTPUT
250+
else
251+
echo "success=false" >> $GITHUB_OUTPUT
252+
fi
253+
254+
- name: Validate changes
255+
id: validate
256+
if: steps.final.outputs.success == 'true'
257+
run: |
258+
UNEXPECTED=""
259+
for file in $(git diff --name-only origin/main..HEAD); do
260+
case "$file" in
261+
package.json|*/package.json|pnpm-lock.yaml|*/pnpm-lock.yaml|.npmrc|pnpm-workspace.yaml) ;;
262+
src/*|test/*) ;;
263+
*.ts|*.mts|*.js|*.mjs) ;;
264+
*) UNEXPECTED="$UNEXPECTED $file" ;;
265+
esac
266+
done
267+
if [ -n "$UNEXPECTED" ]; then
268+
echo "::error::Unexpected files modified by Claude:$UNEXPECTED"
269+
echo "valid=false" >> $GITHUB_OUTPUT
270+
else
271+
echo "valid=true" >> $GITHUB_OUTPUT
272+
fi
273+
145274
- name: Check for changes
146275
id: changes
147276
run: |
@@ -152,13 +281,13 @@ jobs:
152281
fi
153282
154283
- name: Push branch
155-
if: steps.claude.outputs.success == 'true' && steps.changes.outputs.has-changes == 'true'
284+
if: steps.final.outputs.success == 'true' && steps.validate.outputs.valid == 'true' && steps.changes.outputs.has-changes == 'true'
156285
env:
157286
BRANCH_NAME: ${{ steps.branch.outputs.branch }}
158287
run: git push origin "$BRANCH_NAME"
159288

160289
- name: Create Pull Request
161-
if: steps.claude.outputs.success == 'true' && steps.changes.outputs.has-changes == 'true'
290+
if: steps.final.outputs.success == 'true' && steps.validate.outputs.valid == 'true' && steps.changes.outputs.has-changes == 'true'
162291
env:
163292
GH_TOKEN: ${{ github.token }}
164293
BRANCH_NAME: ${{ steps.branch.outputs.branch }}
@@ -187,7 +316,7 @@ jobs:
187316
--base main
188317
189318
- name: Add job summary
190-
if: steps.claude.outputs.success == 'true' && steps.changes.outputs.has-changes == 'true'
319+
if: steps.final.outputs.success == 'true' && steps.validate.outputs.valid == 'true' && steps.changes.outputs.has-changes == 'true'
191320
env:
192321
BRANCH_NAME: ${{ steps.branch.outputs.branch }}
193322
run: |
@@ -202,7 +331,11 @@ jobs:
202331
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
203332
with:
204333
name: claude-output-${{ github.run_id }}
205-
path: claude-output.log
334+
path: |
335+
claude-update.log
336+
claude-fix.log
337+
build.log
338+
test.log
206339
retention-days: 7
207340

208341
- uses: SocketDev/socket-registry/.github/actions/cleanup-git-signing@6096b06b1790f411714c89c40f72aade2eeaab7c # main

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"aggregate-error": "catalog:",
146146
"ansi-regex": "catalog:",
147147
"brace-expansion": "catalog:",
148+
"defu": ">=6.1.5",
148149
"emoji-regex": "catalog:",
149150
"es-define-property": "catalog:",
150151
"es-set-tostringtag": "catalog:",

pnpm-lock.yaml

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)