vouchington-tooling 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/gha-claude-post-review/index.d.mts +3 -0
- package/dist/gha-claude-post-review/index.mjs +2 -0
- package/dist/gha-post-review/github.d.mts +7 -0
- package/dist/gha-post-review/github.mjs +20 -5
- package/dist/gha-post-review/index.d.mts +5 -1
- package/dist/gha-post-review/index.mjs +4 -1
- package/dist/gha-post-review/token.d.mts +1 -0
- package/dist/gha-post-review/token.mjs +1 -0
- package/dist/gha-workspace-policy/docker-workspace-user.d.mts +2 -0
- package/dist/gha-workspace-policy/docker-workspace-user.mjs +208 -0
- package/dist/gha-workspace-policy/index.d.mts +8 -0
- package/dist/gha-workspace-policy/index.mjs +17 -0
- package/dist/gha-workspace-policy/shared.d.mts +6 -0
- package/dist/gha-workspace-policy/shared.mjs +54 -0
- package/dist/gha-workspace-policy/sparse-checkout.d.mts +2 -0
- package/dist/gha-workspace-policy/sparse-checkout.mjs +28 -0
- package/dist/index.d.mts +8 -2
- package/dist/index.mjs +4 -1
- package/package.json +12 -2
- package/scripts/gha/clean-workspace.sh +48 -2
- package/skills/dependabot/SKILL.md +111 -0
- package/skills/github-actions-authoring/SKILL.md +10 -0
- package/skills/github-actions-checklist/SKILL.md +29 -2
- package/skills/manifest.json +24 -17
- package/skills/review-ci-logs/SKILL.md +8 -2
- package/skills/static-analysis-checklist/SKILL.md +8 -5
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ npm install vouchington-tooling
|
|
|
7
7
|
# optional, only if you import vouchington-tooling/sql-ast
|
|
8
8
|
npm install @libpg-query/parser
|
|
9
9
|
# optional, only for vouchington-tooling/agent-blackboard and agent-blackboard CLI commands
|
|
10
|
-
npm install agent-blackboard@^0.
|
|
10
|
+
npm install agent-blackboard@^0.5.0
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## CLI
|
|
@@ -88,7 +88,7 @@ GitHub reports a merged PR whose `baseRefName` is `main`; a merged PR into anoth
|
|
|
88
88
|
as not merged to main, and a missing base is unavailable.
|
|
89
89
|
|
|
90
90
|
Agent Blackboard support is optional: only the `agent-blackboard` subpath and its CLI commands
|
|
91
|
-
need `agent-blackboard@^0.
|
|
91
|
+
need `agent-blackboard@^0.5.0`. Snapshot cleanup accepts only package-generated temporary paths.
|
|
92
92
|
Programmatic callers launched from a different workspace directory pass their own module URL as
|
|
93
93
|
`dependencies: { resolveFrom: import.meta.url }`; the CLI defaults to the current package context.
|
|
94
94
|
It captures a target under a private tombstone, validates partition names, permissions, JSONL,
|
|
@@ -154,7 +154,8 @@ import { parseCsvRows, streamCsvRows } from 'vouchington-tooling/csv'
|
|
|
154
154
|
import { readResponseBody } from 'vouchington-tooling/http-body'
|
|
155
155
|
import { runAstGrepRule } from 'vouchington-tooling/ast-grep-rule'
|
|
156
156
|
import { parseReviewPayload, remapReviewComments } from 'vouchington-tooling/gha-review-payload'
|
|
157
|
-
import { runPostReview } from 'vouchington-tooling/gha-post-review'
|
|
157
|
+
import { postReviewWithTokenFromEnv, runPostReview } from 'vouchington-tooling/gha-post-review'
|
|
158
|
+
import { postClaudeReviewFromEnv } from 'vouchington-tooling/gha-claude-post-review'
|
|
158
159
|
import { nextPageUrlFromLinkHeader } from 'vouchington-tooling/http-link-pagination'
|
|
159
160
|
import {
|
|
160
161
|
cmdUpload,
|
|
@@ -168,6 +169,7 @@ import { runCiLocal } from 'vouchington-tooling/ci-local'
|
|
|
168
169
|
import { rateLimitDelay } from 'vouchington-tooling/gha-rate-limit'
|
|
169
170
|
import { parseCheckpoint } from 'vouchington-tooling/gha-pr-checkpoint'
|
|
170
171
|
import { checkWorkspaceGatesPolicy } from 'vouchington-tooling/workspace-gates'
|
|
172
|
+
import { checkGhaWorkspacePolicy } from 'vouchington-tooling/gha-workspace-policy'
|
|
171
173
|
import { validateNugetUpdate } from 'vouchington-tooling/nuget-central-version'
|
|
172
174
|
import { normalizeSwiftSource } from 'vouchington-tooling/swift-semantic-equal'
|
|
173
175
|
import { parseUniqueSwiftBinaryTargetChecksum } from 'vouchington-tooling/swift-source-offset'
|
|
@@ -211,8 +213,9 @@ touch. Recording and report log reads are synchronous: on contention they block
|
|
|
211
213
|
event loop for up to one second before failing explicitly. Avoid these APIs on hot request paths.
|
|
212
214
|
|
|
213
215
|
The artifact, review-payload, HTTP body, and pagination APIs validate untrusted inputs at their
|
|
214
|
-
boundaries.
|
|
215
|
-
|
|
216
|
+
boundaries. Neutral review posting lives in `gha-post-review` and requires a caller-supplied GitHub
|
|
217
|
+
token. Claude App OIDC is an explicitly selected `gha-claude-post-review` adapter; the legacy
|
|
218
|
+
token-source router remains deprecated for one release line.
|
|
216
219
|
|
|
217
220
|
`vitest-diagnostics` reads Node diagnostic report JSON from a caller-selected directory. It sorts
|
|
218
221
|
filenames, tolerates partial files, returns only a bounded field allowlist, and never emits raw
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { CLAUDE_APP_TOKEN_EXCHANGE_URL, CLAUDE_OIDC_AUDIENCE, CLAUDE_POSTER_PERMISSIONS, GITHUB_INSTALLATION_TOKEN_URL, createActionsClaudeTokenIo, mintClaudeAppToken, oidcTokenRequest, revokeClaudeAppToken, withClaudeAppToken, } from '../gha-post-review/claude-token.mts';
|
|
2
|
+
export type { ClaudeTokenIo, FetchLike } from '../gha-post-review/claude-token.mts';
|
|
3
|
+
export { postClaudeReviewFromEnv } from '../gha-post-review/github.mts';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { CLAUDE_APP_TOKEN_EXCHANGE_URL, CLAUDE_OIDC_AUDIENCE, CLAUDE_POSTER_PERMISSIONS, GITHUB_INSTALLATION_TOKEN_URL, createActionsClaudeTokenIo, mintClaudeAppToken, oidcTokenRequest, revokeClaudeAppToken, withClaudeAppToken, } from '../gha-post-review/claude-token.mjs';
|
|
2
|
+
export { postClaudeReviewFromEnv } from '../gha-post-review/github.mjs';
|
|
@@ -17,6 +17,13 @@ export declare function createGhPostReviewIo(options: {
|
|
|
17
17
|
expectedHeadSha?: string;
|
|
18
18
|
expectedBaseSha?: string;
|
|
19
19
|
}): PostReviewIo;
|
|
20
|
+
/** @deprecated Use postReviewWithTokenFromEnv or postClaudeReviewFromEnv explicitly. */
|
|
20
21
|
export declare function postReviewFromEnv(env?: NodeJS.ProcessEnv, exec?: GhExec, claudeIo?: import("./claude-token.mts").ClaudeTokenIo): Promise<{
|
|
21
22
|
posted: boolean;
|
|
22
23
|
}>;
|
|
24
|
+
export declare function postReviewWithTokenFromEnv(env?: NodeJS.ProcessEnv, exec?: GhExec, token?: string | undefined): {
|
|
25
|
+
posted: boolean;
|
|
26
|
+
};
|
|
27
|
+
export declare function postClaudeReviewFromEnv(env?: NodeJS.ProcessEnv, exec?: GhExec, claudeIo?: import("./claude-token.mts").ClaudeTokenIo): Promise<{
|
|
28
|
+
posted: boolean;
|
|
29
|
+
}>;
|
|
@@ -103,12 +103,19 @@ export function createGhPostReviewIo(options) {
|
|
|
103
103
|
},
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
/** @deprecated Use postReviewWithTokenFromEnv or postClaudeReviewFromEnv explicitly. */
|
|
106
107
|
export async function postReviewFromEnv(env = process.env, exec = createGhExec(), claudeIo = createActionsClaudeTokenIo(env)) {
|
|
108
|
+
const token = resolveReviewPostToken(env);
|
|
109
|
+
if (token.source === 'github-token')
|
|
110
|
+
return postReviewWithTokenFromEnv(env, exec, token.token);
|
|
111
|
+
return await postClaudeReviewFromEnv(env, exec, claudeIo);
|
|
112
|
+
}
|
|
113
|
+
function createPostWithToken(env, exec) {
|
|
107
114
|
const repository = requireEnv('GITHUB_REPOSITORY', env);
|
|
108
115
|
const prNumber = requireEnv('PR_NUMBER', env);
|
|
109
116
|
const payloadPath = requireEnv('CODE_REVIEW_PAYLOAD_PATH', env);
|
|
110
117
|
const payloadBytes = readRegularReviewPayload(payloadPath, 'required');
|
|
111
|
-
|
|
118
|
+
return (token) => runPostReview(payloadPath, createGhPostReviewIo({
|
|
112
119
|
repository,
|
|
113
120
|
prNumber,
|
|
114
121
|
payloadPath,
|
|
@@ -118,8 +125,16 @@ export async function postReviewFromEnv(env = process.env, exec = createGhExec()
|
|
|
118
125
|
expectedHeadSha: env.EXPECTED_HEAD_SHA ?? '',
|
|
119
126
|
expectedBaseSha: env.EXPECTED_BASE_SHA ?? '',
|
|
120
127
|
}));
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return
|
|
128
|
+
}
|
|
129
|
+
function withTokenEnv(exec, env, token) {
|
|
130
|
+
const tokenEnv = { ...env, GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
131
|
+
return (args, options) => exec(args, { ...options, env: { ...options?.env, ...tokenEnv } });
|
|
132
|
+
}
|
|
133
|
+
export function postReviewWithTokenFromEnv(env = process.env, exec = createGhExec(), token = env.GH_TOKEN || env.GITHUB_TOKEN) {
|
|
134
|
+
if (!token)
|
|
135
|
+
throw new ReviewPayloadError('GH_TOKEN or GITHUB_TOKEN is required.');
|
|
136
|
+
return createPostWithToken(env, withTokenEnv(exec, env, token))(token);
|
|
137
|
+
}
|
|
138
|
+
export async function postClaudeReviewFromEnv(env = process.env, exec = createGhExec(), claudeIo = createActionsClaudeTokenIo(env)) {
|
|
139
|
+
return await withClaudeAppToken(claudeIo, (token) => createPostWithToken(env, withTokenEnv(exec, env, token))(token));
|
|
125
140
|
}
|
|
@@ -2,8 +2,12 @@ export { MAX_COMMENTS, MAX_PAYLOAD_BYTES, PostReviewError, runPostReview } from
|
|
|
2
2
|
export type { PostResult, PostReviewIo, PullFile, ReviewComment, SanitizedReview } from './post.mts';
|
|
3
3
|
export { requireEnv, resolveReviewPostToken } from './token.mts';
|
|
4
4
|
export type { ReviewPostToken } from './token.mts';
|
|
5
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
5
6
|
export { CLAUDE_APP_TOKEN_EXCHANGE_URL, CLAUDE_OIDC_AUDIENCE, CLAUDE_POSTER_PERMISSIONS, GITHUB_INSTALLATION_TOKEN_URL, createActionsClaudeTokenIo, mintClaudeAppToken, oidcTokenRequest, revokeClaudeAppToken, withClaudeAppToken, } from './claude-token.mts';
|
|
7
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
6
8
|
export type { ClaudeTokenIo, FetchLike } from './claude-token.mts';
|
|
7
|
-
export { createGhExec, createGhPostReviewIo,
|
|
9
|
+
export { createGhExec, createGhPostReviewIo, postReviewWithTokenFromEnv, postWithGh, writePostedOutput, } from './github.mts';
|
|
10
|
+
/** @deprecated Select an explicit posting adapter. */
|
|
11
|
+
export { postClaudeReviewFromEnv, postReviewFromEnv } from './github.mts';
|
|
8
12
|
export type { GhExec } from './github.mts';
|
|
9
13
|
export { runPostReviewCli } from './cli.mts';
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { MAX_COMMENTS, MAX_PAYLOAD_BYTES, PostReviewError, runPostReview } from './post.mjs';
|
|
2
2
|
export { requireEnv, resolveReviewPostToken } from './token.mjs';
|
|
3
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
3
4
|
export { CLAUDE_APP_TOKEN_EXCHANGE_URL, CLAUDE_OIDC_AUDIENCE, CLAUDE_POSTER_PERMISSIONS, GITHUB_INSTALLATION_TOKEN_URL, createActionsClaudeTokenIo, mintClaudeAppToken, oidcTokenRequest, revokeClaudeAppToken, withClaudeAppToken, } from './claude-token.mjs';
|
|
4
|
-
export { createGhExec, createGhPostReviewIo,
|
|
5
|
+
export { createGhExec, createGhPostReviewIo, postReviewWithTokenFromEnv, postWithGh, writePostedOutput, } from './github.mjs';
|
|
6
|
+
/** @deprecated Select an explicit posting adapter. */
|
|
7
|
+
export { postClaudeReviewFromEnv, postReviewFromEnv } from './github.mjs';
|
|
5
8
|
export { runPostReviewCli } from './cli.mjs';
|
|
@@ -4,5 +4,6 @@ export type ReviewPostToken = {
|
|
|
4
4
|
} | {
|
|
5
5
|
source: 'claude-app';
|
|
6
6
|
};
|
|
7
|
+
/** @deprecated Select an explicit posting adapter instead of the legacy token-source router. */
|
|
7
8
|
export declare function resolveReviewPostToken(env?: NodeJS.ProcessEnv): ReviewPostToken;
|
|
8
9
|
export declare function requireEnv(name: string, env?: NodeJS.ProcessEnv): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReviewPayloadError } from '../gha-review-payload/index.mjs';
|
|
2
|
+
/** @deprecated Select an explicit posting adapter instead of the legacy token-source router. */
|
|
2
3
|
export function resolveReviewPostToken(env = process.env) {
|
|
3
4
|
const source = env.CODE_REVIEW_TOKEN_SOURCE || 'claude-app';
|
|
4
5
|
if (source === 'github-token') {
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { visitRunSteps } from './shared.mjs';
|
|
2
|
+
const WORKSPACE_RE = /(?:\$\{?GITHUB_WORKSPACE\}?|\$\{\{\s*github\.workspace\s*\}\})/iu;
|
|
3
|
+
const DOCKER_GLOBAL_FLAG_OPTIONS = new Set(['--debug', '-D', '--help', '--tls', '--tlsverify']);
|
|
4
|
+
const DOCKER_GLOBAL_VALUE_OPTIONS = new Set([
|
|
5
|
+
'--config',
|
|
6
|
+
'--context',
|
|
7
|
+
'-c',
|
|
8
|
+
'--host',
|
|
9
|
+
'-H',
|
|
10
|
+
'--log-level',
|
|
11
|
+
'-l',
|
|
12
|
+
'--tlscacert',
|
|
13
|
+
'--tlscert',
|
|
14
|
+
'--tlskey',
|
|
15
|
+
]);
|
|
16
|
+
const FLAG_OPTIONS = new Set([
|
|
17
|
+
'--detach',
|
|
18
|
+
'-d',
|
|
19
|
+
'--init',
|
|
20
|
+
'--interactive',
|
|
21
|
+
'-i',
|
|
22
|
+
'--privileged',
|
|
23
|
+
'--publish-all',
|
|
24
|
+
'-P',
|
|
25
|
+
'--read-only',
|
|
26
|
+
'--rm',
|
|
27
|
+
'--tty',
|
|
28
|
+
'-t',
|
|
29
|
+
]);
|
|
30
|
+
export function checkDockerWorkspaceUserDocument(file, document, kind, errors) {
|
|
31
|
+
visitRunSteps(document, kind === 'action', (scope, index, step) => {
|
|
32
|
+
if (typeof step.run !== 'string')
|
|
33
|
+
return;
|
|
34
|
+
for (const block of dockerRunBlocks(step.run)) {
|
|
35
|
+
const options = dockerOptions(block);
|
|
36
|
+
if (!options)
|
|
37
|
+
continue;
|
|
38
|
+
const writable = options.volumes.filter((volume) => WORKSPACE_RE.test(volume) && !isReadOnlyVolume(volume));
|
|
39
|
+
if (writable.length === 0 || hasHostUserMapping(options.user))
|
|
40
|
+
continue;
|
|
41
|
+
errors.push(`::error file=${file}::${file}: ${scope} step ${index} runs Docker with a writable ` +
|
|
42
|
+
`GITHUB_WORKSPACE mount (${writable.join(', ')}) but no --user host UID:GID mapping. ` +
|
|
43
|
+
'Use --user "$(id -u):$(id -g)" or make the workspace mount read-only.');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function shellTokens(input) {
|
|
48
|
+
const tokens = [];
|
|
49
|
+
let token = '';
|
|
50
|
+
let quote = '';
|
|
51
|
+
let substitutionDepth = 0;
|
|
52
|
+
let githubExpression = false;
|
|
53
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
54
|
+
const character = input[index];
|
|
55
|
+
if (githubExpression) {
|
|
56
|
+
token += character;
|
|
57
|
+
if (character === '}' && input[index + 1] === '}') {
|
|
58
|
+
token += '}';
|
|
59
|
+
index += 1;
|
|
60
|
+
githubExpression = false;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (substitutionDepth > 0) {
|
|
65
|
+
token += character;
|
|
66
|
+
if (character === '(')
|
|
67
|
+
substitutionDepth += 1;
|
|
68
|
+
if (character === ')')
|
|
69
|
+
substitutionDepth -= 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (character === '$' && input[index + 1] === '{' && input[index + 2] === '{') {
|
|
73
|
+
token += '${{';
|
|
74
|
+
index += 2;
|
|
75
|
+
githubExpression = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (character === '$' && input[index + 1] === '(') {
|
|
79
|
+
token += '$(';
|
|
80
|
+
substitutionDepth = 1;
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (quote) {
|
|
85
|
+
if (character === '\\' && input[index + 1] !== undefined)
|
|
86
|
+
token += input[++index];
|
|
87
|
+
else if (character === quote)
|
|
88
|
+
quote = '';
|
|
89
|
+
else
|
|
90
|
+
token += character;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (character === '"' || character === "'") {
|
|
94
|
+
quote = character;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (/\s/u.test(character)) {
|
|
98
|
+
if (token)
|
|
99
|
+
tokens.push(token);
|
|
100
|
+
token = '';
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (character === '#' && !token)
|
|
104
|
+
break;
|
|
105
|
+
if (character === ';' || character === '|' || character === '&') {
|
|
106
|
+
if (token)
|
|
107
|
+
tokens.push(token);
|
|
108
|
+
token = '';
|
|
109
|
+
if ((character === '&' || character === '|') && input[index + 1] === character)
|
|
110
|
+
index += 1;
|
|
111
|
+
tokens.push(character);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (character === '\\' && input[index + 1] !== undefined)
|
|
115
|
+
token += input[++index];
|
|
116
|
+
else
|
|
117
|
+
token += character;
|
|
118
|
+
}
|
|
119
|
+
if (token)
|
|
120
|
+
tokens.push(token);
|
|
121
|
+
return tokens;
|
|
122
|
+
}
|
|
123
|
+
function dockerRunBlocks(run) {
|
|
124
|
+
const blocks = [];
|
|
125
|
+
const lines = run.split('\n');
|
|
126
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
127
|
+
if (!/\bdocker\b/u.test(lines[index]))
|
|
128
|
+
continue;
|
|
129
|
+
let block = lines[index];
|
|
130
|
+
while (block.trimEnd().endsWith('\\') && index + 1 < lines.length) {
|
|
131
|
+
index += 1;
|
|
132
|
+
block = `${block.trimEnd().slice(0, -1)} ${lines[index]}`;
|
|
133
|
+
}
|
|
134
|
+
blocks.push(block);
|
|
135
|
+
}
|
|
136
|
+
return blocks;
|
|
137
|
+
}
|
|
138
|
+
function findDockerCommand(tokens) {
|
|
139
|
+
let commandStart = true;
|
|
140
|
+
for (const [index, token] of tokens.entries()) {
|
|
141
|
+
if (token === ';' || token === '|' || token === '&') {
|
|
142
|
+
commandStart = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!commandStart)
|
|
146
|
+
continue;
|
|
147
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(token))
|
|
148
|
+
continue;
|
|
149
|
+
if (token === 'docker')
|
|
150
|
+
return index;
|
|
151
|
+
commandStart = false;
|
|
152
|
+
}
|
|
153
|
+
return -1;
|
|
154
|
+
}
|
|
155
|
+
function dockerOptions(block) {
|
|
156
|
+
const tokens = shellTokens(block);
|
|
157
|
+
const dockerIndex = findDockerCommand(tokens);
|
|
158
|
+
if (dockerIndex < 0)
|
|
159
|
+
return undefined;
|
|
160
|
+
let start = dockerIndex + 1;
|
|
161
|
+
while (start < tokens.length && tokens[start].startsWith('-')) {
|
|
162
|
+
const option = tokens[start];
|
|
163
|
+
const name = option.includes('=') ? option.slice(0, option.indexOf('=')) : option;
|
|
164
|
+
if (DOCKER_GLOBAL_FLAG_OPTIONS.has(name) || option.includes('='))
|
|
165
|
+
start += 1;
|
|
166
|
+
else if (DOCKER_GLOBAL_VALUE_OPTIONS.has(name))
|
|
167
|
+
start += 2;
|
|
168
|
+
else
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (tokens[start] === 'container')
|
|
172
|
+
start += 1;
|
|
173
|
+
if (tokens[start] !== 'run')
|
|
174
|
+
return undefined;
|
|
175
|
+
start += 1;
|
|
176
|
+
const volumes = [];
|
|
177
|
+
let user;
|
|
178
|
+
for (let index = start; index < tokens.length; index += 1) {
|
|
179
|
+
const option = tokens[index];
|
|
180
|
+
if (!option.startsWith('-'))
|
|
181
|
+
break;
|
|
182
|
+
const equals = option.indexOf('=');
|
|
183
|
+
const compact = option.startsWith('-v') || option.startsWith('-u');
|
|
184
|
+
const name = equals > 0
|
|
185
|
+
? option.slice(0, equals)
|
|
186
|
+
: compact && option.length > 2
|
|
187
|
+
? option.slice(0, 2)
|
|
188
|
+
: option;
|
|
189
|
+
const inlineValue = equals > 0
|
|
190
|
+
? option.slice(equals + 1)
|
|
191
|
+
: compact && option.length > 2
|
|
192
|
+
? option.slice(2)
|
|
193
|
+
: undefined;
|
|
194
|
+
const bundledFlags = /^-[diPt]{2,}$/u.test(option);
|
|
195
|
+
const value = inlineValue ?? (FLAG_OPTIONS.has(name) || bundledFlags ? undefined : tokens[++index]);
|
|
196
|
+
if ((name === '-v' || name === '--volume' || name === '--mount') && value !== undefined)
|
|
197
|
+
volumes.push(value);
|
|
198
|
+
if ((name === '-u' || name === '--user') && value !== undefined)
|
|
199
|
+
user = value;
|
|
200
|
+
}
|
|
201
|
+
return { user, volumes };
|
|
202
|
+
}
|
|
203
|
+
function hasHostUserMapping(value) {
|
|
204
|
+
return value !== undefined && /^\$\(\s*id\s+-u\s*\):\$\(\s*id\s+-g\s*\)$/u.test(value);
|
|
205
|
+
}
|
|
206
|
+
function isReadOnlyVolume(value) {
|
|
207
|
+
return /(?:^|:)ro(?:$|,)/u.test(value) || /(?:^|,)readonly(?:$|,)/u.test(value);
|
|
208
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SharedContext } from '../shared-context/index.mts';
|
|
2
|
+
export interface GhaWorkspacePolicyOptions {
|
|
3
|
+
workflowDirectories?: readonly string[];
|
|
4
|
+
actionDirectories?: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function checkGhaWorkspacePolicy(ctx: SharedContext, options?: GhaWorkspacePolicyOptions): Promise<{
|
|
7
|
+
errors: string[];
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { checkDockerWorkspaceUserDocument } from './docker-workspace-user.mjs';
|
|
2
|
+
import { ghaFileKind, loadWorkflowDocument } from './shared.mjs';
|
|
3
|
+
import { checkNoSparseCheckoutDocument } from './sparse-checkout.mjs';
|
|
4
|
+
export async function checkGhaWorkspacePolicy(ctx, options = {}) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
if (!ctx.isInsideGitRepo)
|
|
7
|
+
return { errors };
|
|
8
|
+
for (const file of ctx.trackedFiles) {
|
|
9
|
+
const kind = ghaFileKind(file, options);
|
|
10
|
+
if (!kind)
|
|
11
|
+
continue;
|
|
12
|
+
const document = loadWorkflowDocument(ctx, file, errors);
|
|
13
|
+
checkNoSparseCheckoutDocument(file, document, kind, errors);
|
|
14
|
+
checkDockerWorkspaceUserDocument(file, document, kind, errors);
|
|
15
|
+
}
|
|
16
|
+
return { errors };
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SharedContext } from '../shared-context/index.mts';
|
|
2
|
+
import type { GhaWorkspacePolicyOptions } from './index.mts';
|
|
3
|
+
export type GhaFileKind = 'workflow' | 'action';
|
|
4
|
+
export declare function ghaFileKind(file: string, options: GhaWorkspacePolicyOptions): GhaFileKind | undefined;
|
|
5
|
+
export declare function loadWorkflowDocument(ctx: SharedContext, file: string, errors: string[]): unknown;
|
|
6
|
+
export declare function visitRunSteps(document: unknown, isAction: boolean, visitor: (scope: string, index: number, step: Record<string, unknown>) => void): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse as load } from 'yaml';
|
|
4
|
+
export function ghaFileKind(file, options) {
|
|
5
|
+
const workflowDirectories = options.workflowDirectories ?? ['.github/workflows'];
|
|
6
|
+
const actionDirectories = options.actionDirectories ?? ['.github/actions'];
|
|
7
|
+
if (workflowDirectories.some((directory) => file.startsWith(`${directory.replace(/\/$/u, '')}/`)) &&
|
|
8
|
+
/\.ya?ml$/u.test(file))
|
|
9
|
+
return 'workflow';
|
|
10
|
+
if (actionDirectories.some((directory) => file.startsWith(`${directory.replace(/\/$/u, '')}/`)) &&
|
|
11
|
+
/\/action\.ya?ml$/u.test(file))
|
|
12
|
+
return 'action';
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
export function loadWorkflowDocument(ctx, file, errors) {
|
|
16
|
+
try {
|
|
17
|
+
const content = ctx.readTrackedFile?.(file) ?? readFileSync(join(ctx.repoRoot, file), 'utf8');
|
|
18
|
+
return load(content);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
errors.push(`::error file=${file}::${file}: invalid YAML (${error.message})`);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function visitRunSteps(document, isAction, visitor) {
|
|
26
|
+
if (!document || typeof document !== 'object')
|
|
27
|
+
return;
|
|
28
|
+
if (isAction) {
|
|
29
|
+
const runs = document.runs;
|
|
30
|
+
if (!runs || typeof runs !== 'object')
|
|
31
|
+
return;
|
|
32
|
+
visitSteps('action', runs.steps, visitor);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const jobs = document.jobs;
|
|
36
|
+
if (!jobs || typeof jobs !== 'object')
|
|
37
|
+
return;
|
|
38
|
+
for (const [jobId, value] of Object.entries(jobs)) {
|
|
39
|
+
if (!value || typeof value !== 'object')
|
|
40
|
+
continue;
|
|
41
|
+
const job = value;
|
|
42
|
+
if (typeof job.uses === 'string')
|
|
43
|
+
continue;
|
|
44
|
+
visitSteps(`job "${jobId}"`, job.steps, visitor);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function visitSteps(scope, value, visitor) {
|
|
48
|
+
if (!Array.isArray(value))
|
|
49
|
+
return;
|
|
50
|
+
value.forEach((step, index) => {
|
|
51
|
+
if (step && typeof step === 'object')
|
|
52
|
+
visitor(scope, index, step);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { visitRunSteps } from './shared.mjs';
|
|
2
|
+
const SPARSE_CHECKOUT_KEYS = ['sparse-checkout', 'sparse-checkout-cone-mode'];
|
|
3
|
+
const SPARSE_CHECKOUT_COMMANDS = [
|
|
4
|
+
/\bgit\b[^\n;&|]*\bsparse-checkout\s+(?:init|set|add|reapply)\b/iu,
|
|
5
|
+
/\bgit\b[^\n;&|]*\bconfig\b[^\n;&|]*\bcore\.sparseCheckout(?:Cone)?(?:\s*=\s*|\s+)(?:"|')?(?:true|yes|on|1)(?:"|')?(?=\s|;|$)/iu,
|
|
6
|
+
];
|
|
7
|
+
export function checkNoSparseCheckoutDocument(file, document, kind, errors) {
|
|
8
|
+
visitRunSteps(document, kind === 'action', (scope, index, step) => {
|
|
9
|
+
const run = step.run;
|
|
10
|
+
const normalizedRun = typeof run === 'string' ? run.replace(/\\\r?\n\s*/gu, ' ') : '';
|
|
11
|
+
if (SPARSE_CHECKOUT_COMMANDS.some((pattern) => pattern.test(normalizedRun))) {
|
|
12
|
+
errors.push(`::error file=${file}::${file}: ${scope} step ${index} enables sparse checkout. ` +
|
|
13
|
+
'Use a full checkout; commands that disable or unset sparse-checkout state remain allowed.');
|
|
14
|
+
}
|
|
15
|
+
if (typeof step.uses !== 'string' || !step.uses.startsWith('actions/checkout@'))
|
|
16
|
+
return;
|
|
17
|
+
const withValue = step.with;
|
|
18
|
+
if (!withValue || typeof withValue !== 'object')
|
|
19
|
+
return;
|
|
20
|
+
for (const key of SPARSE_CHECKOUT_KEYS) {
|
|
21
|
+
if (!(key in withValue))
|
|
22
|
+
continue;
|
|
23
|
+
errors.push(`::error file=${file}::${file}: ${scope} step ${index} passes "${key}" to ` +
|
|
24
|
+
'actions/checkout. Persistent runners reuse workspace directories, so leaked ' +
|
|
25
|
+
'sparse-checkout state can silently narrow a later checkout. Check out the full tree.');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,8 @@ export type { ForeignKey, SqlCreateIndexMetadata, SqlCreateTableColumn, SqlCreat
|
|
|
15
15
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mts';
|
|
16
16
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mts';
|
|
17
17
|
export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAuditWorkflowFilter, RuntimeJobResult, RuntimeSample, } from './gha-runtime-audit/index.mts';
|
|
18
|
+
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mts';
|
|
19
|
+
export type { GhaWorkspacePolicyOptions } from './gha-workspace-policy/index.mts';
|
|
18
20
|
export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mts';
|
|
19
21
|
export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, VitestReportAttempt, VitestReportAttemptIdentity, } from './vitest-blob-manifest/index.mts';
|
|
20
22
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mts';
|
|
@@ -45,8 +47,12 @@ export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mts'
|
|
|
45
47
|
export type { AstGrepRuleInvocation, RunAstGrepRuleOptions } from './ast-grep-rule/index.mts';
|
|
46
48
|
export { indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mts';
|
|
47
49
|
export type { CommentableIndex, CommentableLine, LineKind, PayloadRequirement, ReviewComment, ReviewFile, ReviewSide, SanitizedReview, } from './gha-review-payload/index.mts';
|
|
48
|
-
export {
|
|
49
|
-
export type {
|
|
50
|
+
export { PostReviewError, requireEnv, runPostReview, runPostReviewCli, postReviewWithTokenFromEnv, } from './gha-post-review/index.mts';
|
|
51
|
+
export type { PostResult, PostReviewIo, PullFile } from './gha-post-review/index.mts';
|
|
52
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
53
|
+
export { CLAUDE_OIDC_AUDIENCE, createActionsClaudeTokenIo, mintClaudeAppToken, resolveReviewPostToken, revokeClaudeAppToken, withClaudeAppToken, } from './gha-post-review/index.mts';
|
|
54
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
55
|
+
export type { ClaudeTokenIo, ReviewPostToken } from './gha-post-review/index.mts';
|
|
50
56
|
export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mts';
|
|
51
57
|
export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mts';
|
|
52
58
|
export type { ExpectedTransportIdentity, ObjectSigner, PresignIdentity, TransportControl, } from './coverage-transport/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOn
|
|
|
8
8
|
export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mjs';
|
|
9
9
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
|
|
10
10
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mjs';
|
|
11
|
+
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mjs';
|
|
11
12
|
export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mjs';
|
|
12
13
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mjs';
|
|
13
14
|
export { findWorkspaceLinkMismatches, formatReleaseAgeFailure, INSTALL_TERMINATION_FAILED, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mjs';
|
|
@@ -25,7 +26,9 @@ export { escapeSpreadsheetFormula, parseCsvRows, streamCsvRows, stripCsvBom } fr
|
|
|
25
26
|
export { MissingResponseBodyError, readResponseBody, readResponseBodyAsBuffer, ResponseBodyTooLargeError, } from './http-body/index.mjs';
|
|
26
27
|
export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mjs';
|
|
27
28
|
export { indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mjs';
|
|
28
|
-
export {
|
|
29
|
+
export { PostReviewError, requireEnv, runPostReview, runPostReviewCli, postReviewWithTokenFromEnv, } from './gha-post-review/index.mjs';
|
|
30
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
31
|
+
export { CLAUDE_OIDC_AUDIENCE, createActionsClaudeTokenIo, mintClaudeAppToken, resolveReviewPostToken, revokeClaudeAppToken, withClaudeAppToken, } from './gha-post-review/index.mjs';
|
|
29
32
|
export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mjs';
|
|
30
33
|
export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mjs';
|
|
31
34
|
export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"import": "./dist/gha-runtime-audit/index.mjs",
|
|
77
77
|
"default": "./dist/gha-runtime-audit/index.mjs"
|
|
78
78
|
},
|
|
79
|
+
"./gha-workspace-policy": {
|
|
80
|
+
"types": "./dist/gha-workspace-policy/index.d.mts",
|
|
81
|
+
"import": "./dist/gha-workspace-policy/index.mjs",
|
|
82
|
+
"default": "./dist/gha-workspace-policy/index.mjs"
|
|
83
|
+
},
|
|
79
84
|
"./vitest-blob-manifest": {
|
|
80
85
|
"types": "./dist/vitest-blob-manifest/index.d.mts",
|
|
81
86
|
"import": "./dist/vitest-blob-manifest/index.mjs",
|
|
@@ -161,6 +166,11 @@
|
|
|
161
166
|
"import": "./dist/gha-post-review/index.mjs",
|
|
162
167
|
"default": "./dist/gha-post-review/index.mjs"
|
|
163
168
|
},
|
|
169
|
+
"./gha-claude-post-review": {
|
|
170
|
+
"types": "./dist/gha-claude-post-review/index.d.mts",
|
|
171
|
+
"import": "./dist/gha-claude-post-review/index.mjs",
|
|
172
|
+
"default": "./dist/gha-claude-post-review/index.mjs"
|
|
173
|
+
},
|
|
164
174
|
"./http-link-pagination": {
|
|
165
175
|
"types": "./dist/http-link-pagination/index.d.mts",
|
|
166
176
|
"import": "./dist/http-link-pagination/index.mjs",
|
|
@@ -250,7 +260,7 @@
|
|
|
250
260
|
},
|
|
251
261
|
"devDependencies": {
|
|
252
262
|
"@types/picomatch": "^4.0.3",
|
|
253
|
-
"agent-blackboard": "^0.
|
|
263
|
+
"agent-blackboard": "^0.5.0"
|
|
254
264
|
},
|
|
255
265
|
"optionalDependencies": {
|
|
256
266
|
"@libpg-query/parser": "^18.0.0"
|
|
@@ -65,11 +65,53 @@ workspace_sentinel_key() {
|
|
|
65
65
|
shasum -a 256 | awk '{print $1}'
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
clear_sparse_checkout_state() {
|
|
69
|
+
"${full_checkout_git[@]}" sparse-checkout disable >/dev/null 2>&1 || true
|
|
70
|
+
local key
|
|
71
|
+
for key in core.sparseCheckout core.sparseCheckoutCone; do
|
|
72
|
+
git config --worktree --unset-all "${key}" || true
|
|
73
|
+
git config --local --unset-all "${key}" || true
|
|
74
|
+
if git config --worktree --get "${key}" >/dev/null 2>&1 ||
|
|
75
|
+
git config --local --get "${key}" >/dev/null 2>&1; then
|
|
76
|
+
echo "::error::Sparse checkout config ${key} remains enabled after cleanup."
|
|
77
|
+
return 1
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
index_requires_recovery() {
|
|
83
|
+
"${full_checkout_git[@]}" update-index --refresh >/dev/null 2>&1 || return 0
|
|
84
|
+
if "${full_checkout_git[@]}" ls-files -v | grep -q '^S '; then
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
"${full_checkout_git[@]}" diff-index --quiet --ignore-submodules=all HEAD -- || return 0
|
|
88
|
+
return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
recover_full_index() {
|
|
92
|
+
local started_at=${SECONDS}
|
|
93
|
+
local tracked_paths
|
|
94
|
+
tracked_paths="$("${full_checkout_git[@]}" ls-tree -r --name-only HEAD | wc -l | tr -d ' ')"
|
|
95
|
+
rm -f -- "$(git rev-parse --git-path index)"
|
|
96
|
+
"${full_checkout_git[@]}" read-tree --empty
|
|
97
|
+
"${full_checkout_git[@]}" reset --hard HEAD
|
|
98
|
+
echo "::warning::Full workspace index recovery restored ${tracked_paths} tracked paths in $((SECONDS - started_at))s."
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
full_checkout_git=(git -c core.sparseCheckout=false -c core.sparseCheckoutCone=false)
|
|
102
|
+
clear_sparse_checkout_state
|
|
103
|
+
reset_failed=false
|
|
104
|
+
if ! "${full_checkout_git[@]}" reset --hard HEAD; then
|
|
69
105
|
echo "::warning::git reset --hard failed; reclaiming workspace write bits and retrying once."
|
|
70
106
|
restore_workspace_write_bits ||
|
|
71
107
|
echo "::warning::Could not restore workspace write bits completely; retrying git reset with the repaired subset."
|
|
72
|
-
|
|
108
|
+
if ! "${full_checkout_git[@]}" reset --hard HEAD; then
|
|
109
|
+
reset_failed=true
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
if [ "${reset_failed}" = "true" ] || index_requires_recovery; then
|
|
113
|
+
echo "::warning::Git index corruption remained after bounded cleanup; starting full recovery."
|
|
114
|
+
recover_full_index
|
|
73
115
|
fi
|
|
74
116
|
excludes=()
|
|
75
117
|
# Trust-gate: a fork pull request is the only untrusted case.
|
|
@@ -153,6 +195,10 @@ if ! git clean -ffdx "${excludes[@]+"${excludes[@]}"}"; then
|
|
|
153
195
|
echo "::warning::Could not restore workspace write bits completely; retrying git clean with the repaired subset."
|
|
154
196
|
git clean -ffdx "${excludes[@]+"${excludes[@]}"}"
|
|
155
197
|
fi
|
|
198
|
+
if index_requires_recovery; then
|
|
199
|
+
echo "::warning::Git index corruption detected after clean; starting full recovery."
|
|
200
|
+
recover_full_index
|
|
201
|
+
fi
|
|
156
202
|
# Runs that preserve node_modules retain them for install speed.
|
|
157
203
|
# Drop TypeScript incremental build info inside those preserved trees
|
|
158
204
|
# so stale virtual-store paths do not survive dependency graph changes.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dependabot
|
|
3
|
+
description: Configure or audit Dependabot coverage, cooldowns, package-family grouping, trusted CI, and conservative auto-merge policy for a repository.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dependabot policy
|
|
7
|
+
|
|
8
|
+
Read every applicable `AGENTS.md` and `CLAUDE.md`, then the repository's dependency, CI, release,
|
|
9
|
+
and branch-protection documentation. The consumer wrapper owns the accepted cooldown, package roots,
|
|
10
|
+
first-party publishers, manual-update exceptions, runner policy, required checks, and secret source.
|
|
11
|
+
|
|
12
|
+
## Cover the dependency graph
|
|
13
|
+
|
|
14
|
+
1. Inventory tracked manifests, lockfiles, Dockerfiles, workflow actions, infrastructure roots, and
|
|
15
|
+
any companion dependency bot. Add one Dependabot update entry for every supported ecosystem and
|
|
16
|
+
manifest root, without creating competing ownership for a dependency already assigned elsewhere.
|
|
17
|
+
2. Schedule version checks daily. Set `cooldown.default-days` to the consumer's release delay; use
|
|
18
|
+
seven days unless local policy explicitly chooses another value. Cooldown applies to version
|
|
19
|
+
updates, not security updates.
|
|
20
|
+
3. Set `open-pull-requests-limit: 5` on every update entry. This is a per-entry version-update
|
|
21
|
+
limit, not a repository-wide or security-update limit. A consumer may use a lower value when a
|
|
22
|
+
serialized repair or manual validation path requires it.
|
|
23
|
+
4. Keep security updates enabled and immediate. Do not use a custom age gate to delay vulnerability
|
|
24
|
+
fixes.
|
|
25
|
+
|
|
26
|
+
## Group package families
|
|
27
|
+
|
|
28
|
+
Build groups around packages that form one compatibility family or release train, not around version
|
|
29
|
+
types. Inspect manifests, peer dependencies, lockfiles, and upstream release practices to identify
|
|
30
|
+
packages that should move together. For example:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
cooldown:
|
|
34
|
+
default-days: 2
|
|
35
|
+
exclude: ['@acme/*', 'acme-cli']
|
|
36
|
+
groups:
|
|
37
|
+
first-party:
|
|
38
|
+
patterns: ['@acme/*', 'acme-cli']
|
|
39
|
+
oxc:
|
|
40
|
+
patterns: ['oxlint', 'oxfmt', 'oxlint-tsgolint']
|
|
41
|
+
vitest:
|
|
42
|
+
patterns: ['vitest', '@vitest/*', '@vitejs/*']
|
|
43
|
+
react:
|
|
44
|
+
patterns: ['react', 'react-dom']
|
|
45
|
+
react-security:
|
|
46
|
+
applies-to: 'security-updates'
|
|
47
|
+
patterns: ['react', 'react-dom']
|
|
48
|
+
react-email:
|
|
49
|
+
patterns: ['react-email', '@react-email/*']
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Name each group after the package, toolchain, framework, or verified release family it represents.
|
|
53
|
+
Prefer narrow namespace or prefix wildcards such as `@vitest/*` when that wildcard maps to one
|
|
54
|
+
compatibility family, and put more specific families before broader ones.
|
|
55
|
+
- Do not create generic groups such as `other-minor-and-patch`, `minor-and-patch`,
|
|
56
|
+
`security-minor-and-patch`, or `security-updates`. Do not use the bare catch-all
|
|
57
|
+
`patterns: ['*']` merely to ensure every update belongs to a group. Narrow family wildcards are
|
|
58
|
+
expected. Leave unrelated packages ungrouped so Dependabot opens independently reviewable pull
|
|
59
|
+
requests for them.
|
|
60
|
+
- Omit `update-types` from package-family groups so each family receives major, minor, and patch
|
|
61
|
+
updates. Major updates remain human-reviewed under the auto-merge policy; excluding them from a
|
|
62
|
+
family group only makes the release train harder to update coherently.
|
|
63
|
+
- Groups default to `applies-to: version-updates`. When the same package-family relationship also
|
|
64
|
+
makes a joint security upgrade safe, define a separately named family group such as
|
|
65
|
+
`react-security` with the same package patterns and `applies-to: security-updates`. Never combine
|
|
66
|
+
unrelated vulnerability fixes into a generic security group.
|
|
67
|
+
- Keep verified first-party packages in a dedicated `first-party` group. For a namespace wholly
|
|
68
|
+
controlled by the owner, prefer its scoped wildcard such as `@acme/*`; keep unscoped packages as
|
|
69
|
+
explicit names. Use the same patterns in `cooldown.exclude` so first-party updates have zero-day
|
|
70
|
+
eligibility while third-party packages retain the configured release delay. A grouped pull
|
|
71
|
+
request is auto-mergeable only when every included update is eligible.
|
|
72
|
+
|
|
73
|
+
## Exempt verified first-party releases
|
|
74
|
+
|
|
75
|
+
Use `cooldown.exclude` only for first-party packages whose owning repository and default-branch
|
|
76
|
+
release workflow have been verified. The workflow must publish that package through OIDC with
|
|
77
|
+
`id-token: write` and without a long-lived registry token. A namespace wildcard is appropriate only
|
|
78
|
+
when the owner controls the entire namespace and intends future packages to inherit the exemption;
|
|
79
|
+
otherwise use exact package names. Keep the consumer-owned registry, documentation, cooldown
|
|
80
|
+
exclusions, and first-party group synchronized.
|
|
81
|
+
|
|
82
|
+
## Preserve the trust boundary
|
|
83
|
+
|
|
84
|
+
Use a trusted default-branch `pull_request_target` workflow only for metadata inspection and the
|
|
85
|
+
minimal auto-merge mutation. Revalidate the live pull request, Dependabot identity, default base,
|
|
86
|
+
same-repository `dependabot/` head, and immutable base/head SHAs. Never check out or execute pull
|
|
87
|
+
request code in that privileged workflow, and pin external actions according to local policy.
|
|
88
|
+
|
|
89
|
+
Auto-merge only verified semantic-version patches and stable minor updates whose old and new major
|
|
90
|
+
versions are at least 1. Majors, pre-1.0 minors, prereleases, downgrades, malformed or inconsistent
|
|
91
|
+
metadata, and consumer-declared manual ecosystems require human action. Enable platform auto-merge;
|
|
92
|
+
do not auto-approve. Required checks and branch rules remain the merge gate.
|
|
93
|
+
|
|
94
|
+
Use a dedicated `DEPENDABOT_AUTOMERGE_TOKEN` stored as a Dependabot secret, not only as an Actions
|
|
95
|
+
secret. Grant only the repository Contents and Pull requests access needed to enable or disable
|
|
96
|
+
auto-merge, and fail an eligible mutation visibly when the token is absent or underprivileged.
|
|
97
|
+
|
|
98
|
+
Dependabot-triggered workflows cannot assume ordinary Actions secrets or trusted OIDC access. Copy
|
|
99
|
+
only narrowly scoped credentials required for mandatory tests into Dependabot secret scope. For
|
|
100
|
+
cloud, deployment, production, or otherwise privileged checks, keep credential-free validation
|
|
101
|
+
running and make the trusted portion explicitly skipped or not applicable for Dependabot. Required
|
|
102
|
+
fan-in jobs must distinguish that intentional state from an unexpected missing credential.
|
|
103
|
+
|
|
104
|
+
## Validate and roll out
|
|
105
|
+
|
|
106
|
+
Parse the final YAML, run the repository's workflow checker and policy tests, and inspect the diff
|
|
107
|
+
for uncovered manifests, overlapping groups, broad exemptions, secret exposure, or pull request code
|
|
108
|
+
crossing a privileged boundary. Verify repository settings for dependency security updates and
|
|
109
|
+
auto-merge. After the configuration reaches the default branch, inspect Dependabot logs for every
|
|
110
|
+
configured root and verify representative patch, minor, major, security, manual, and missing-secret
|
|
111
|
+
paths.
|
|
@@ -28,6 +28,16 @@ runner labels, permissions, action pins, concurrency, secrets, and required-chec
|
|
|
28
28
|
deployments and services. A scheduled reconciliation workflow may repair missed events, but it
|
|
29
29
|
must inspect a snapshot once and exit; it must not wait for convergence.
|
|
30
30
|
|
|
31
|
+
## Persistent workspaces
|
|
32
|
+
|
|
33
|
+
Check out the full tree and prevent sparse-checkout configuration with a YAML-aware policy over
|
|
34
|
+
tracked workflow and action files. Correct the producer of unsafe workspace state: writable workspace
|
|
35
|
+
bind mounts use a non-root identity whose ownership and write access are compatible with the runner.
|
|
36
|
+
Do not make each job traverse the whole workspace before checkout to repair permissions. Drain and
|
|
37
|
+
migrate an already contaminated runner once. At runtime, repair may cover bounded known generated
|
|
38
|
+
paths; a workspace-wide fallback must stay failure-gated, same-filesystem, directory-only, and batched,
|
|
39
|
+
with path-count and timing evidence in its diagnostics.
|
|
40
|
+
|
|
31
41
|
## Reject polling designs
|
|
32
42
|
|
|
33
43
|
Do not add a sleep-and-read loop, repeated run/check/deployment/lease/service queries, a CLI waiter,
|
|
@@ -16,6 +16,22 @@ Apply this portable baseline unless a stricter repository-local rule overrides i
|
|
|
16
16
|
pull-request content.
|
|
17
17
|
- Load [github-actions-authoring](../github-actions-authoring/SKILL.md) when changing orchestration.
|
|
18
18
|
Do not poll remote workflow, deployment, lease, service, or health state.
|
|
19
|
+
- Required checks must be actual workflow jobs that execute or aggregate the work they report.
|
|
20
|
+
Workflow code must not create or publish check runs or commit statuses merely to synthesize a
|
|
21
|
+
required context, copy another workflow's conclusion, or bypass the event graph. A purpose-built
|
|
22
|
+
external CI or analysis integration may report its own result; do not use its API as a relay for
|
|
23
|
+
work owned by GitHub Actions.
|
|
24
|
+
- On `main`, or the consumer's configured default branch, split test jobs by domain such as web and
|
|
25
|
+
backend instead of hiding unrelated suites in one monolithic test job. Keep domain job names stable
|
|
26
|
+
when they are required checks, and use a real bounded fan-in job only when the merge contract needs
|
|
27
|
+
one combined result.
|
|
28
|
+
- Serialize test runs for the same pull request or default branch with a stable concurrency group. For
|
|
29
|
+
pull requests, use `cancel-in-progress: true` so a superseded head replaces the active run. On
|
|
30
|
+
`main`, or the consumer's configured default branch, use `cancel-in-progress: false` so the active
|
|
31
|
+
run finishes before the newest pending revision starts. GitHub may replace an older pending main
|
|
32
|
+
run with the newest pending revision; preserving every intermediate queued revision is not required.
|
|
33
|
+
When one workflow handles both events, make `cancel-in-progress` conditional on the pull-request
|
|
34
|
+
event while keeping the PR number or branch ref in the concurrency group.
|
|
19
35
|
- Give every concrete job a timeout of no more than 30 minutes. A caller job that invokes a reusable
|
|
20
36
|
workflow through top-level `jobs.<job_id>.uses` cannot accept `timeout-minutes`; enforce the bound
|
|
21
37
|
on every concrete job inside the called workflow. If the underlying operation cannot terminate
|
|
@@ -27,6 +43,16 @@ Apply this portable baseline unless a stricter repository-local rule overrides i
|
|
|
27
43
|
operation in another service.
|
|
28
44
|
- Use GitHub-hosted runners only for public repositories. Private repositories use the consumer's
|
|
29
45
|
approved self-hosted or disposable runner labels.
|
|
46
|
+
- Persistent workspaces must check out the full tree. Do not configure sparse checkout; enforce that
|
|
47
|
+
prohibition with a YAML-aware check over intended tracked workflow and action files, with fixtures
|
|
48
|
+
for accepted and rejected shapes.
|
|
49
|
+
- Fix workspace ownership at the producer. A container with a writable workspace bind mount must use
|
|
50
|
+
a non-root identity whose ownership and write access are compatible with the runner workspace. Do not
|
|
51
|
+
add an unconditional, pre-checkout, workspace-wide permission or ownership traversal to recover
|
|
52
|
+
persistent runners. Migrate an already contaminated workspace once while the runner is drained.
|
|
53
|
+
Normal runtime repair may cover bounded known generated paths. A necessary workspace-wide fallback
|
|
54
|
+
must stay failure-gated, same-filesystem, directory-only, and batched, and record path count and timing
|
|
55
|
+
evidence.
|
|
30
56
|
- Pin every repository-backed external `uses:` reference—anything other than a local `./...`
|
|
31
57
|
action—to a full lowercase 40-character Git SHA followed immediately by its machine-maintainable
|
|
32
58
|
version comment, such as `# v4.2.0`, so Dependabot can update both. Pin `docker://...` actions to an
|
|
@@ -43,8 +69,9 @@ Apply this portable baseline unless a stricter repository-local rule overrides i
|
|
|
43
69
|
conflict. Identify trusted and untrusted inputs and every credential boundary.
|
|
44
70
|
2. Give each job the least permissions it needs. Keep untrusted pull-request content out of shell
|
|
45
71
|
interpolation, privileged tokens, and write-capable steps.
|
|
46
|
-
3. Apply the portable pinning, runner, trigger, and timeout
|
|
47
|
-
policy. Keep checkout refs, artifact boundaries, caches, and
|
|
72
|
+
3. Apply the portable check, test-topology, concurrency, pinning, runner, trigger, and timeout
|
|
73
|
+
baseline plus any stricter consumer policy. Keep checkout refs, artifact boundaries, caches, and
|
|
74
|
+
concurrency behavior explicit.
|
|
48
75
|
4. Validate changed YAML with the local workflow checker and run the affected workflow tests or
|
|
49
76
|
scripts. Update local CI documentation when behavior or operator expectations change.
|
|
50
77
|
5. Review the final diff for privilege escalation, accidental secret exposure, unsafe quoting,
|
package/skills/manifest.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"name": "agent-workflow",
|
|
6
6
|
"plugin": "vouchington-workflow",
|
|
7
|
-
"pluginVersion": "0.
|
|
7
|
+
"pluginVersion": "0.6.2",
|
|
8
8
|
"path": "agent-workflow/SKILL.md"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
@@ -17,9 +17,16 @@
|
|
|
17
17
|
{
|
|
18
18
|
"name": "blackboard",
|
|
19
19
|
"plugin": "vouchington-workflow",
|
|
20
|
-
"pluginVersion": "0.
|
|
20
|
+
"pluginVersion": "0.6.2",
|
|
21
21
|
"path": "blackboard/SKILL.md"
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
"name": "dependabot",
|
|
25
|
+
"plugin": "vouchington-workflow",
|
|
26
|
+
"pluginVersion": "0.6.2",
|
|
27
|
+
"path": "dependabot/SKILL.md",
|
|
28
|
+
"prerequisites": ["github-actions-checklist"]
|
|
29
|
+
},
|
|
23
30
|
{
|
|
24
31
|
"name": "dotnet-test-authoring",
|
|
25
32
|
"plugin": "vouchington-testing",
|
|
@@ -29,26 +36,26 @@
|
|
|
29
36
|
{
|
|
30
37
|
"name": "git-commit-checklist",
|
|
31
38
|
"plugin": "vouchington-workflow",
|
|
32
|
-
"pluginVersion": "0.
|
|
39
|
+
"pluginVersion": "0.6.2",
|
|
33
40
|
"path": "git-commit-checklist/SKILL.md"
|
|
34
41
|
},
|
|
35
42
|
{
|
|
36
43
|
"name": "github-actions-authoring",
|
|
37
44
|
"plugin": "vouchington-workflow",
|
|
38
|
-
"pluginVersion": "0.
|
|
45
|
+
"pluginVersion": "0.6.2",
|
|
39
46
|
"path": "github-actions-authoring/SKILL.md",
|
|
40
47
|
"prerequisites": ["github-actions-checklist"]
|
|
41
48
|
},
|
|
42
49
|
{
|
|
43
50
|
"name": "github-actions-checklist",
|
|
44
51
|
"plugin": "vouchington-workflow",
|
|
45
|
-
"pluginVersion": "0.
|
|
52
|
+
"pluginVersion": "0.6.2",
|
|
46
53
|
"path": "github-actions-checklist/SKILL.md"
|
|
47
54
|
},
|
|
48
55
|
{
|
|
49
56
|
"name": "github-issue",
|
|
50
57
|
"plugin": "vouchington-workflow",
|
|
51
|
-
"pluginVersion": "0.
|
|
58
|
+
"pluginVersion": "0.6.2",
|
|
52
59
|
"path": "github-issue/SKILL.md"
|
|
53
60
|
},
|
|
54
61
|
{
|
|
@@ -61,25 +68,25 @@
|
|
|
61
68
|
{
|
|
62
69
|
"name": "npm-publishing",
|
|
63
70
|
"plugin": "vouchington-workflow",
|
|
64
|
-
"pluginVersion": "0.
|
|
71
|
+
"pluginVersion": "0.6.2",
|
|
65
72
|
"path": "npm-publishing/SKILL.md"
|
|
66
73
|
},
|
|
67
74
|
{
|
|
68
75
|
"name": "organize-github-issues",
|
|
69
76
|
"plugin": "vouchington-workflow",
|
|
70
|
-
"pluginVersion": "0.
|
|
77
|
+
"pluginVersion": "0.6.2",
|
|
71
78
|
"path": "organize-github-issues/SKILL.md"
|
|
72
79
|
},
|
|
73
80
|
{
|
|
74
81
|
"name": "package-json-checklist",
|
|
75
82
|
"plugin": "vouchington-workflow",
|
|
76
|
-
"pluginVersion": "0.
|
|
83
|
+
"pluginVersion": "0.6.2",
|
|
77
84
|
"path": "package-json-checklist/SKILL.md"
|
|
78
85
|
},
|
|
79
86
|
{
|
|
80
87
|
"name": "planning",
|
|
81
88
|
"plugin": "vouchington-workflow",
|
|
82
|
-
"pluginVersion": "0.
|
|
89
|
+
"pluginVersion": "0.6.2",
|
|
83
90
|
"path": "planning/SKILL.md"
|
|
84
91
|
},
|
|
85
92
|
{
|
|
@@ -103,43 +110,43 @@
|
|
|
103
110
|
{
|
|
104
111
|
"name": "pr-description",
|
|
105
112
|
"plugin": "vouchington-workflow",
|
|
106
|
-
"pluginVersion": "0.
|
|
113
|
+
"pluginVersion": "0.6.2",
|
|
107
114
|
"path": "pr-description/SKILL.md"
|
|
108
115
|
},
|
|
109
116
|
{
|
|
110
117
|
"name": "retrospective",
|
|
111
118
|
"plugin": "vouchington-workflow",
|
|
112
|
-
"pluginVersion": "0.
|
|
119
|
+
"pluginVersion": "0.6.2",
|
|
113
120
|
"path": "retrospective/SKILL.md"
|
|
114
121
|
},
|
|
115
122
|
{
|
|
116
123
|
"name": "retrospective-distill",
|
|
117
124
|
"plugin": "vouchington-workflow",
|
|
118
|
-
"pluginVersion": "0.
|
|
125
|
+
"pluginVersion": "0.6.2",
|
|
119
126
|
"path": "retrospective-distill/SKILL.md"
|
|
120
127
|
},
|
|
121
128
|
{
|
|
122
129
|
"name": "review-ci-logs",
|
|
123
130
|
"plugin": "vouchington-workflow",
|
|
124
|
-
"pluginVersion": "0.
|
|
131
|
+
"pluginVersion": "0.6.2",
|
|
125
132
|
"path": "review-ci-logs/SKILL.md"
|
|
126
133
|
},
|
|
127
134
|
{
|
|
128
135
|
"name": "review-github-issue-taxonomy",
|
|
129
136
|
"plugin": "vouchington-workflow",
|
|
130
|
-
"pluginVersion": "0.
|
|
137
|
+
"pluginVersion": "0.6.2",
|
|
131
138
|
"path": "review-github-issue-taxonomy/SKILL.md"
|
|
132
139
|
},
|
|
133
140
|
{
|
|
134
141
|
"name": "revisit-followups",
|
|
135
142
|
"plugin": "vouchington-workflow",
|
|
136
|
-
"pluginVersion": "0.
|
|
143
|
+
"pluginVersion": "0.6.2",
|
|
137
144
|
"path": "revisit-followups/SKILL.md"
|
|
138
145
|
},
|
|
139
146
|
{
|
|
140
147
|
"name": "static-analysis-checklist",
|
|
141
148
|
"plugin": "vouchington-workflow",
|
|
142
|
-
"pluginVersion": "0.
|
|
149
|
+
"pluginVersion": "0.6.2",
|
|
143
150
|
"path": "static-analysis-checklist/SKILL.md"
|
|
144
151
|
},
|
|
145
152
|
{
|
|
@@ -14,9 +14,15 @@ Use when investigating CI failures, repeated workflow noise, or misleading diagn
|
|
|
14
14
|
Inspect failed steps and representative large entries, then remove temporary artifacts.
|
|
15
15
|
3. Classify findings as a real error, misleading output, downstream cascade, necessary diagnostic,
|
|
16
16
|
or volume-only concern. Identify the first repository-owned root cause.
|
|
17
|
-
4.
|
|
17
|
+
4. For persistent-workspace failures, identify the producer of sparse state or unsafe ownership before
|
|
18
|
+
proposing cleanup. Reject unconditional pre-checkout workspace traversals. Bounded known generated
|
|
19
|
+
paths may be repaired routinely; a workspace-wide fallback must be failure-gated, same-filesystem,
|
|
20
|
+
directory-only, and batched. Require path-count and timing evidence, plus a drained one-time migration
|
|
21
|
+
and producer-side fix such as a compatible non-root writable workspace mount when runner state is
|
|
22
|
+
contaminated.
|
|
23
|
+
5. Prefer one bounded fix that preserves non-zero exits, primary errors, artifacts, summaries, and
|
|
18
24
|
diagnostic evidence. Do not hide stderr, globally quiet output, or add retries to mask a cause.
|
|
19
|
-
|
|
25
|
+
6. Add focused regression evidence, run local workflow validation, compare before and after output
|
|
20
26
|
where meaningful, and report deferred findings without creating issues unless authorized.
|
|
21
27
|
|
|
22
28
|
This skill supplies no workflow names, log-retention policy, CI provider command, retry policy, or
|
|
@@ -10,13 +10,16 @@ documentation, rule inventory, and fixture conventions before selecting an imple
|
|
|
10
10
|
|
|
11
11
|
1. State the invariant and search for an existing analyzer, rule, or guard that owns it. Prefer the
|
|
12
12
|
narrowest established owner over a parallel scanner.
|
|
13
|
-
2.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
2. For workflow or action policy, parse YAML instead of text matching and inspect only intended tracked
|
|
14
|
+
configuration files. A persistent-workspace guard must reject sparse-checkout inputs and unsafe
|
|
15
|
+
writable workspace mounts while allowing unrelated YAML keys and ordinary read-only mounts.
|
|
16
|
+
3. Add meaningful accepted and rejected fixtures before the rule. Cover path routing, parser
|
|
17
|
+
boundaries, equivalent YAML forms, and every allowed exception.
|
|
18
|
+
4. Keep discovery limited to tracked, intended files. Make suppressions and allowlists narrow,
|
|
16
19
|
justified, and mechanically checked for freshness where practical.
|
|
17
|
-
|
|
20
|
+
5. Run the focused fixture test and analyzer, then the local aggregate checks. Keep diagnostics
|
|
18
21
|
actionable and deterministic.
|
|
19
|
-
|
|
22
|
+
6. Update the local inventory or documentation and delete superseded migration artifacts only when
|
|
20
23
|
the replacement invariant is demonstrably enforced.
|
|
21
24
|
|
|
22
25
|
This skill does not name analyzer roots, fixture locations, commands, suppression syntax, or
|