skilldrop-cli 0.16.31 → 0.16.32
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/bin/skilldrop.js +133 -18
- package/contracts/skill.schema.json +3 -3
- package/package.json +1 -1
- package/packs/converters/skills/mermaid-render/manifest.json +11 -1
- package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +2 -2
- package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +2 -2
- package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +52 -34
- package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +33 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/ci.yml +39 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/integration.yml +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/PR.md +39 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/go.mod +17 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/notify/client.go +35 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client.go +66 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_integration_test.go +38 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_test.go +53 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/settle.go +30 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/pr.diff +42 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/vendor-notes/httpkit-CHANGELOG.md +43 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.github/workflows/ci.yml +67 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.lintwright.json +7 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.nvmrc +1 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/PR.md +110 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/package.json +25 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/pr.diff +146 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/due-date.ts +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/totals.ts +11 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/due-date.test.ts +17 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/totals.test.ts +12 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +23 -14
- package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +17 -5
- package/packs/dev-team/skills/dependency-upgrade-review/reference.md +36 -1
- package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +22 -4
- package/packs/dev-team/skills/sonar-review/manifest.json +12 -0
- package/packs/infra-as-code/skills/terraform-module/manifest.json +11 -0
- package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +8 -1
- package/packs/solution-architect/skills/figma-diagrams/manifest.json +12 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
diff --git a/package.json b/package.json
|
|
2
|
+
index 1c2e8f0..5ad91b3 100644
|
|
3
|
+
--- a/package.json
|
|
4
|
+
+++ b/package.json
|
|
5
|
+
@@ -17,11 +17,11 @@
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
- "bundlecrate": "6.0.1",
|
|
8
|
+
- "lintwright": "4.2.0",
|
|
9
|
+
- "prettyquill": "3.1.2",
|
|
10
|
+
- "tsforge-types": "1.4.0",
|
|
11
|
+
- "vellum-test": "2.7.3"
|
|
12
|
+
+ "bundlecrate": "6.1.0",
|
|
13
|
+
+ "lintwright": "4.3.1",
|
|
14
|
+
+ "prettyquill": "3.1.4",
|
|
15
|
+
+ "tsforge-types": "1.4.2",
|
|
16
|
+
+ "vellum-test": "2.8.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
diff --git a/package-lock.json b/package-lock.json
|
|
20
|
+
index 9e04a1d..c3b7f52 100644
|
|
21
|
+
--- a/package-lock.json
|
|
22
|
+
+++ b/package-lock.json
|
|
23
|
+
@@ -12,11 +12,11 @@
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
- "bundlecrate": "6.0.1",
|
|
26
|
+
- "lintwright": "4.2.0",
|
|
27
|
+
- "prettyquill": "3.1.2",
|
|
28
|
+
- "tsforge-types": "1.4.0",
|
|
29
|
+
- "vellum-test": "2.7.3"
|
|
30
|
+
+ "bundlecrate": "6.1.0",
|
|
31
|
+
+ "lintwright": "4.3.1",
|
|
32
|
+
+ "prettyquill": "3.1.4",
|
|
33
|
+
+ "tsforge-types": "1.4.2",
|
|
34
|
+
+ "vellum-test": "2.8.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
+ "node_modules/@bundlecrate/resolver-core": {
|
|
41
|
+
+ "version": "1.0.0",
|
|
42
|
+
+ "resolved": "https://registry.npmjs.org/@bundlecrate/resolver-core/-/resolver-core-1.0.0.tgz",
|
|
43
|
+
+ "integrity": "sha512-Xk3bP9vQm2LrT7wZ1sNc4HfJ8eYd0GaU6oKiE5rBtWqV2nM7xL1pR9sT3vW8cZ4yH6jF0dK2gA5eN1bQ7uI3oP==",
|
|
44
|
+
+ "dev": true,
|
|
45
|
+
+ "license": "MIT",
|
|
46
|
+
+ "engines": {
|
|
47
|
+
+ "node": ">=18.18"
|
|
48
|
+
+ }
|
|
49
|
+
+ },
|
|
50
|
+
"node_modules/bundlecrate": {
|
|
51
|
+
- "version": "6.0.1",
|
|
52
|
+
- "resolved": "https://registry.npmjs.org/bundlecrate/-/bundlecrate-6.0.1.tgz",
|
|
53
|
+
- "integrity": "sha512-Lm7nQ2vKp9cRz4Wt1bXe8JdH3yGa0fUu6oNiE5rCtYqW2sM7xP1kR9tT3vV8bZ4yJ6hF0dL2gA5eN1cQ7uO3iP==",
|
|
54
|
+
+ "version": "6.1.0",
|
|
55
|
+
+ "resolved": "https://registry.npmjs.org/bundlecrate/-/bundlecrate-6.1.0.tgz",
|
|
56
|
+
+ "integrity": "sha512-Pq4rT8wXn1cLz6Vs2mBe9KdJ7hYf3aGu0oNiE5rDtUqX2nL7wM1pS9vT4bW8cZ3yK6jG0eH2fA5dN1aQ7iU3oR==",
|
|
57
|
+
"dev": true,
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"dependencies": {
|
|
60
|
+
- "bundlecrate-legacy-resolver": "^2.3.0",
|
|
61
|
+
+ "@bundlecrate/resolver-core": "^1.0.0",
|
|
62
|
+
"picolabel": "^0.9.0"
|
|
63
|
+
},
|
|
64
|
+
"bin": {
|
|
65
|
+
"bundlecrate": "bin/bundlecrate.js"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
- "node": ">=16"
|
|
69
|
+
+ "node": ">=18.18"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
- "node_modules/bundlecrate-legacy-resolver": {
|
|
73
|
+
- "version": "2.3.4",
|
|
74
|
+
- "resolved": "https://registry.npmjs.org/bundlecrate-legacy-resolver/-/bundlecrate-legacy-resolver-2.3.4.tgz",
|
|
75
|
+
- "integrity": "sha512-Ab2cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0eF2gH4iJ6kL8mN0oP2qR4sT6uV8wX0yZ2aB4cD6eF8==",
|
|
76
|
+
- "dev": true,
|
|
77
|
+
- "license": "MIT"
|
|
78
|
+
- },
|
|
79
|
+
"node_modules/lintwright": {
|
|
80
|
+
- "version": "4.2.0",
|
|
81
|
+
- "resolved": "https://registry.npmjs.org/lintwright/-/lintwright-4.2.0.tgz",
|
|
82
|
+
- "integrity": "sha512-Hn5mR2xLq8cTz3Wv1bYe7KdF4hJa9gUu0oNiE6rBtWpX3sM2wL1kQ8vT5cV9bZ7yG4hF0dJ2eA6nN1aR3uI7oS==",
|
|
83
|
+
+ "version": "4.3.1",
|
|
84
|
+
+ "resolved": "https://registry.npmjs.org/lintwright/-/lintwright-4.3.1.tgz",
|
|
85
|
+
+ "integrity": "sha512-Ty8kW3vNp2cQz5Ls1mXe6RdJ9hGa4fUu7oNiE0rBtVqY2sK3wM1pL8vS5cT9bZ6yH4jF0eG2dA7nN1bR3iU5oQ==",
|
|
86
|
+
"dev": true,
|
|
87
|
+
"license": "MIT",
|
|
88
|
+
"dependencies": {
|
|
89
|
+
- "picolabel": "0.9.1"
|
|
90
|
+
+ "picolabel": "0.9.2"
|
|
91
|
+
},
|
|
92
|
+
"bin": {
|
|
93
|
+
"lintwright": "bin/lintwright.js"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"node_modules/picolabel": {
|
|
97
|
+
- "version": "0.9.1",
|
|
98
|
+
- "resolved": "https://registry.npmjs.org/picolabel/-/picolabel-0.9.1.tgz",
|
|
99
|
+
- "integrity": "sha512-Cd3eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5aB7cD9eF1gH3iJ5kL7mN9oP1qR3sT5uV7wX9yZ1aB3cD5eF7gH==",
|
|
100
|
+
+ "version": "0.9.2",
|
|
101
|
+
+ "resolved": "https://registry.npmjs.org/picolabel/-/picolabel-0.9.2.tgz",
|
|
102
|
+
+ "integrity": "sha512-Ef4gH6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ==",
|
|
103
|
+
"dev": true,
|
|
104
|
+
"license": "MIT"
|
|
105
|
+
},
|
|
106
|
+
"node_modules/prettyquill": {
|
|
107
|
+
- "version": "3.1.2",
|
|
108
|
+
- "resolved": "https://registry.npmjs.org/prettyquill/-/prettyquill-3.1.2.tgz",
|
|
109
|
+
- "integrity": "sha512-Gh6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ0kL==",
|
|
110
|
+
+ "version": "3.1.4",
|
|
111
|
+
+ "resolved": "https://registry.npmjs.org/prettyquill/-/prettyquill-3.1.4.tgz",
|
|
112
|
+
+ "integrity": "sha512-Ij7kL9mN1oP3qR5sT7uV9wX1yZ3aB5cD7eF9gH1iJ3kL5mN7oP9qR1sT3uV5wX7yZ9aB1cD3eF5gH7iJ9kL1mN==",
|
|
113
|
+
"dev": true,
|
|
114
|
+
"license": "MIT",
|
|
115
|
+
"bin": {
|
|
116
|
+
"prettyquill": "bin/prettyquill.js"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"node_modules/tsforge-types": {
|
|
120
|
+
- "version": "1.4.0",
|
|
121
|
+
- "resolved": "https://registry.npmjs.org/tsforge-types/-/tsforge-types-1.4.0.tgz",
|
|
122
|
+
- "integrity": "sha512-Kl8mN0oP2qR4sT6uV8wX0yZ2aB4cD6eF8gH0iJ2kL4mN6oP8qR0sT2uV4wX6yZ8aB0cD2eF4gH6iJ8kL0mN2oP==",
|
|
123
|
+
+ "version": "1.4.2",
|
|
124
|
+
+ "resolved": "https://registry.npmjs.org/tsforge-types/-/tsforge-types-1.4.2.tgz",
|
|
125
|
+
+ "integrity": "sha512-Mn9oP1qR3sT5uV7wX9yZ1aB3cD5eF7gH9iJ1kL3mN5oP7qR9sT1uV3wX5yZ7aB9cD1eF3gH5iJ7kL9mN1oP3qR==",
|
|
126
|
+
"dev": true,
|
|
127
|
+
"license": "MIT"
|
|
128
|
+
},
|
|
129
|
+
"node_modules/vellum-test": {
|
|
130
|
+
- "version": "2.7.3",
|
|
131
|
+
- "resolved": "https://registry.npmjs.org/vellum-test/-/vellum-test-2.7.3.tgz",
|
|
132
|
+
- "integrity": "sha512-Op0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0eF2gH4iJ6kL8mN0oP2qR4sT==",
|
|
133
|
+
+ "version": "2.8.0",
|
|
134
|
+
+ "resolved": "https://registry.npmjs.org/vellum-test/-/vellum-test-2.8.0.tgz",
|
|
135
|
+
+ "integrity": "sha512-Qr1sT3uV5wX7yZ9aB1cD3eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5aB7cD9eF1gH3iJ5kL7mN9oP1qR3sT5uV==",
|
|
136
|
+
"dev": true,
|
|
137
|
+
"license": "MIT",
|
|
138
|
+
"bin": {
|
|
139
|
+
"vellum": "bin/vellum.js"
|
|
140
|
+
},
|
|
141
|
+
"engines": {
|
|
142
|
+
"node": ">=18"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Brand } from "tsforge-types";
|
|
2
|
+
|
|
3
|
+
export type IsoDate = Brand<string, "IsoDate">;
|
|
4
|
+
|
|
5
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
export function isOverdue(inv: { dueAt: string }): boolean {
|
|
8
|
+
return Date.now() > Date.parse(inv.dueAt);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatDue(inv: { dueAt: string }): string {
|
|
12
|
+
const days = Math.round((Date.parse(inv.dueAt) - Date.now()) / DAY_MS);
|
|
13
|
+
if (days < 0) return `overdue by ${-days} days`;
|
|
14
|
+
if (days === 0) return "due today";
|
|
15
|
+
return `due in ${days} days`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DeepReadonly } from "tsforge-types";
|
|
2
|
+
|
|
3
|
+
export type Line = DeepReadonly<{ qty: number; unitMinor: number }>;
|
|
4
|
+
|
|
5
|
+
export function lineTotal(l: Line): number {
|
|
6
|
+
return l.qty * l.unitMinor;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function invoiceTotal(lines: readonly Line[]): number {
|
|
10
|
+
return lines.reduce((sum, l) => sum + lineTotal(l), 0);
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect, vt } from "vellum-test";
|
|
2
|
+
import { isOverdue, formatDue } from "../src/due-date";
|
|
3
|
+
|
|
4
|
+
describe("due dates", () => {
|
|
5
|
+
it("flags invoices past their due date", () => {
|
|
6
|
+
vt.fakeTimers({ now: new Date("2026-03-15T09:00:00Z") });
|
|
7
|
+
expect(isOverdue({ dueAt: "2026-03-14T23:59:59Z" })).toBe(true);
|
|
8
|
+
expect(isOverdue({ dueAt: "2026-03-16T00:00:00Z" })).toBe(false);
|
|
9
|
+
vt.restoreTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("formats relative due text", () => {
|
|
13
|
+
vt.fakeTimers({ now: new Date("2026-03-15T09:00:00Z") });
|
|
14
|
+
expect(formatDue({ dueAt: "2026-03-18T09:00:00Z" })).toBe("due in 3 days");
|
|
15
|
+
vt.restoreTimers();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from "vellum-test";
|
|
2
|
+
import { lineTotal, invoiceTotal } from "../src/totals";
|
|
3
|
+
|
|
4
|
+
describe("totals", () => {
|
|
5
|
+
it("multiplies quantity by unit price in minor units", () => {
|
|
6
|
+
expect(lineTotal({ qty: 3, unitMinor: 1250 })).toBe(3750);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("sums lines", () => {
|
|
10
|
+
expect(invoiceTotal([{ qty: 1, unitMinor: 100 }, { qty: 2, unitMinor: 50 }])).toBe(200);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -67,25 +67,34 @@ No install scripts, sources, licences or pins appear. The floor says only that t
|
|
|
67
67
|
|
|
68
68
|
## Review (what the agent hands back)
|
|
69
69
|
|
|
70
|
-
**MERGE.**
|
|
70
|
+
**MERGE.** Two patch releases; the only behaviour change, 2.2.2's new 10 MB line limit, raises an error the import code already handles, and the PR's tests run that code.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
- Both releases read. 2.2.1 fixes quoted newlines splitting rows, covered by `test_quoted_newline_stays_one_row` (`tests/test_ingest.py:9`). 2.2.2 is a security fix: lines over 10 MB raise `RowTooLongError`, a subclass of `ParseError`.
|
|
73
|
+
- `git grep -n "tidyparse" -- '*.py'` finds one call, `app/ingest.py:14`, inside `except tidyparse.ParseError` (`app/ingest.py:15`), so the new error becomes `StatementRejected` like any malformed file.
|
|
74
|
+
- `ci.yml` runs `pytest -q` on the PR and `tests/test_ingest.py:3` imports `import_statement`, so the changed code ran.
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
- Search: `git grep -n "tidyparse" -- '*.py'` finds one call, `app/ingest.py:14`, inside `except tidyparse.ParseError` (`app/ingest.py:15`). The new error is caught there and becomes `StatementRejected`, the same path as any malformed file.
|
|
76
|
-
- CI: `ci.yml` runs `pytest -q` on the PR, and `tests/test_ingest.py:3` imports `import_statement`, so the changed code ran. No test feeds a 10 MB line; that is the library's limit, not a change in this repo's behaviour.
|
|
77
|
-
- Beyond the title: `charset-sniff` 1.4.0 → 1.4.1, a transitive patch, listed and not read.
|
|
78
|
-
- No new packages, install scripts, registry changes or pins.
|
|
76
|
+
Also in the lockfile: `charset-sniff` 1.4.0 → 1.4.1 (transitive patch). No new packages, install scripts, sources, runtime changes or pins.
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|---|---|---|---|
|
|
82
|
-
| tidyparse | 2.2.0 → 2.2.2 | patch | direct |
|
|
83
|
-
| charset-sniff | 1.4.0 → 1.4.1 | patch | transitive |
|
|
78
|
+
Rollback: `git revert` the merge commit, or `poetry add tidyparse@2.2.0` and commit `pyproject.toml` and `poetry.lock`; that reopens the 2.2.2 memory issue, so treat it as a stopgap.
|
|
84
79
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
To confirm: nothing.
|
|
80
|
+
About 150 words. No package table: two packages fit in a line. No "To confirm": nothing is open.
|
|
88
81
|
|
|
89
82
|
## Why this is MERGE and not MERGE AFTER CHECKS
|
|
90
83
|
|
|
91
84
|
Every check in the verdict rule is closed with evidence in the reply: both releases read, the one changed behaviour searched and found handled, the calling code run by a PR test, no pins, no install-time code, no new source. Remove any one of those (no repo to search, a release with no notes, a test that does not import `app/ingest.py`) and the verdict drops to MERGE AFTER CHECKS with that item as the check.
|
|
85
|
+
|
|
86
|
+
## The same shape when it blocks
|
|
87
|
+
|
|
88
|
+
Suppose instead the PR moved tidyparse to 3.0.0 and the notes (invented for this illustration) said 3.0.0 requires Python 3.11 and `parse_rows()` now returns a `RowSet` instead of a list. The reply keeps the order and stays short:
|
|
89
|
+
|
|
90
|
+
> **DO NOT MERGE.** 3.0.0 needs Python 3.11 and the production image runs 3.10, and the import code indexes a result that is no longer a list.
|
|
91
|
+
>
|
|
92
|
+
> 🟥 1. `Dockerfile:1` builds on `python:3.10-slim`; CI runs 3.12 (`ci.yml:12`), so the PR passed and the image build fails at install. Move the image to 3.11 in this PR.
|
|
93
|
+
> 🟥 2. `app/ingest.py:17` does `rows[0]` on what is now a `RowSet`; no test reads the first row, so CI missed it. Use `rows.first()`.
|
|
94
|
+
>
|
|
95
|
+
> Also in the lockfile: `charset-sniff` 1.4.0 → 1.4.1.
|
|
96
|
+
> Rollback: `git revert` the merge commit and rebuild the image.
|
|
97
|
+
>
|
|
98
|
+
> I can make both changes on this branch.
|
|
99
|
+
|
|
100
|
+
The lead names both blockers; each blocker says what breaks, where, why CI missed it and the fix; nothing is in the reply that does not change the decision.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-upgrade-review",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Give a merge verdict on a dependency-upgrade pull request (Dependabot, Renovate or a hand-made bump) across npm, pip, Go, Cargo and Maven lockfiles and GitHub Actions pins. Reads the release notes for every skipped version, lists what the lockfile changes beyond the PR title (transitive bumps, new and removed packages, install scripts, registry changes), checks
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Give a merge verdict on a dependency-upgrade pull request (Dependabot, Renovate or a hand-made bump) across npm, pip, Go, Cargo and Maven lockfiles and GitHub Actions pins. Reads the release notes for every skipped version, lists what the lockfile changes beyond the PR title (transitive bumps, new and removed packages, install scripts, registry changes), checks each SHA pin against its release tag and against the PR's other pins, checks raised runtime floors against the images and runners that install it, says whether CI ran the changed code, traces what each caller of a changed API now does and writes the rollback. The reply leads with MERGE, MERGE AFTER CHECKS or DO NOT MERGE and the reasons that decide it. Use when the user says \"review this Dependabot PR\", \"is this Renovate bump safe to merge\", \"can I merge this dependency upgrade\", \"check this lockfile diff\", or \"is this action SHA pin right\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
7
7
|
"npm": [],
|
|
@@ -38,10 +38,22 @@
|
|
|
38
38
|
"github-actions"
|
|
39
39
|
],
|
|
40
40
|
"permissions": {
|
|
41
|
-
"network": [
|
|
42
|
-
|
|
41
|
+
"network": [
|
|
42
|
+
"github.com",
|
|
43
|
+
"api.github.com",
|
|
44
|
+
"registry.npmjs.org",
|
|
45
|
+
"pypi.org",
|
|
46
|
+
"proxy.golang.org"
|
|
47
|
+
],
|
|
48
|
+
"commands": [
|
|
49
|
+
"git",
|
|
50
|
+
"gh",
|
|
51
|
+
"npm",
|
|
52
|
+
"pip",
|
|
53
|
+
"go"
|
|
54
|
+
],
|
|
43
55
|
"files": "named-paths",
|
|
44
|
-
"notes": "
|
|
56
|
+
"notes": "The script reads only the diff path it is given, or stdin, writes only the -o path when given, and makes no network calls. The instructions read the PR with gh or git, list a package's releases with npm, pip, go or gh, and verify a SHA pin with git ls-remote against github.com; gh reaches api.github.com and the package managers their registries; with no network those become commands in the reply for the reviewer to run."
|
|
45
57
|
},
|
|
46
58
|
"model": {
|
|
47
59
|
"tier": "standard",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dependency-upgrade-review reference
|
|
2
2
|
|
|
3
|
-
Long-form material for the steps in `SKILL.md`: what each ecosystem's diff shows, where the risky signals live, how to list the releases between two versions, how to verify a pin, and how to roll back.
|
|
3
|
+
Long-form material for the steps in `SKILL.md`: what each ecosystem's diff shows, where the risky signals live, how to list the releases between two versions, how to trace a behaviour change to its callers, where runtime floors bite, how to verify a pin, and how to roll back.
|
|
4
4
|
|
|
5
5
|
## Reading a lockfile diff, by ecosystem
|
|
6
6
|
|
|
@@ -44,6 +44,36 @@ What to pull from each release, in this order:
|
|
|
44
44
|
3. **Deprecations:** still works, removed later. A deprecated call the repo makes is a 🟧, not a 🟥.
|
|
45
45
|
4. **Licence:** a changed `license` field or a LICENSE file change. Moves between permissive licences (MIT, BSD, Apache-2.0, ISC) are a note. A move to a copyleft licence (GPL, AGPL, MPL at file level, LGPL) or a source-available licence (BUSL, SSPL, Elastic, "Commons Clause") needs a sign-off from whoever owns licence policy before merge.
|
|
46
46
|
|
|
47
|
+
## Tracing a behaviour change to its callers
|
|
48
|
+
|
|
49
|
+
A search hit is where the review starts. Read the calling function and write what it does under the new version.
|
|
50
|
+
|
|
51
|
+
| Change in the notes | What to look for in the caller | What breaks |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Returns `None` / `-1` / `false` → raises | `if result is None`, `== -1`, `if not ok` branches; retry loops and fallbacks built on them | The branch never runs; the first failure escapes as an exception and the retry or fallback is dead code |
|
|
54
|
+
| Raises → returns a sentinel | `try` / `except` around the call | The error is swallowed; the code carries on with the sentinel |
|
|
55
|
+
| Exception class renamed or re-parented | `except OldError`, `catch (e instanceof OldError)` | The handler stops catching it |
|
|
56
|
+
| Tuple or dict → object | `a, b = f(...)`, `result["key"]` | Unpacking or key access fails at runtime |
|
|
57
|
+
| Changed default | Every call that does not pass the option | Each one gets the new behaviour silently |
|
|
58
|
+
| Module or function removed | Imports at the top of a file, and imports inside functions | A top-level import fails at startup and in tests; an import inside a function fails only when that function runs |
|
|
59
|
+
|
|
60
|
+
Name the fix in the repo's code, and any migration aid an intermediate release ships (an opt-in flag that turns on the new behaviour early). Moving to that release first lets the code change land and be tested before the major.
|
|
61
|
+
|
|
62
|
+
## Runtime floors
|
|
63
|
+
|
|
64
|
+
A release that drops a runtime version breaks wherever the old runtime still runs, and CI usually runs a newer one.
|
|
65
|
+
|
|
66
|
+
| Floor raised in | Compare with |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Release notes ("requires Python 3.10"), `python_requires`, `python-versions` in `poetry.lock`, `requires-python` in `uv.lock` | Dockerfile `FROM`, `setup-python` version and matrix, `.python-version`, the Python the lockfile was compiled on |
|
|
69
|
+
| `engines.node` in `package-lock.json` or `package.json` | `.nvmrc`, `setup-node` / toolchain version, Dockerfile `FROM`, `engine-strict` in `.npmrc` |
|
|
70
|
+
| `go` directive in `go.mod`, `toolchain` line | `setup-go` version, Dockerfile builder image |
|
|
71
|
+
| `rust-version` (MSRV) in a crate | `rust-toolchain.toml`, CI toolchain |
|
|
72
|
+
| Wheels or prebuilt binaries only for some versions or platforms | Every image and runner that installs it; elsewhere it builds from source and needs a compiler or toolchain |
|
|
73
|
+
| An action's runtime (`node20` → `node24`) | Self-hosted runner versions; GitHub-hosted runners update themselves |
|
|
74
|
+
|
|
75
|
+
A lockfile generated on a newer runtime than production can resolve versions production cannot install. `pip install --require-hashes` and `npm ci` fail at build time, not at review time.
|
|
76
|
+
|
|
47
77
|
## Verifying a pin
|
|
48
78
|
|
|
49
79
|
```bash
|
|
@@ -57,6 +87,7 @@ git ls-remote https://github.com/<owner>/<repo> refs/tags/<tag> 'refs/tags/<tag>
|
|
|
57
87
|
- A SHA with no `# vX.Y.Z` comment: list the tags (`git ls-remote --tags https://github.com/<owner>/<repo>`) and find the SHA. No tag points to it means it is not a release.
|
|
58
88
|
- Tag-only pin (`uses: owner/repo@v3`): the maintainer, or anyone who takes over the account, can point `v3` at new code after review. Pin to the commit the `^{}` line returns and keep the tag as a comment.
|
|
59
89
|
- Branch pin (`@main`): every run gets whatever was pushed last. A PR that introduces one is DO NOT MERGE.
|
|
90
|
+
- Two SHAs for one tag: when the same `owner/repo` and `# vX.Y.Z` comment appear with different SHAs across workflow files, at most one is the tag's commit. Run the command once and compare both; until then, the PR is DO NOT MERGE. Read the PR's commit list: a bot writes every pin the same way, so a pin that differs from the bot's usually came from a hand-made commit.
|
|
60
91
|
|
|
61
92
|
Stakes come from the job, not the action: look at `permissions:` (`contents: write`, `id-token: write`, `packages: write`), the `secrets.*` it receives, and the `environment:` it runs in.
|
|
62
93
|
|
|
@@ -70,6 +101,10 @@ Stakes come from the job, not the action: look at `permissions:` (`contents: wri
|
|
|
70
101
|
| A build or publish step (bundler, packager, uploader) not in the PR workflow | No |
|
|
71
102
|
| A source file no test imports | No |
|
|
72
103
|
| A test that imports the file but never hits the changed behaviour (a default, an edge case the notes name) | Not the part that changed |
|
|
104
|
+
| A test that mocks the dependency with the old contract (a fake that returns a value where the new version raises) | No; it tests the old behaviour |
|
|
105
|
+
| An import inside a function no test calls | No; it never fails at import or collection |
|
|
106
|
+
| An install script, when the PR job installs with `--ignore-scripts` and the release job does not | No; it first runs in the release job, with that job's secrets and permissions |
|
|
107
|
+
| A runtime older than the one CI uses (production image, self-hosted runner) | No |
|
|
73
108
|
|
|
74
109
|
Ways to exercise an unrun path before merge: a `workflow_dispatch` dry run on the PR branch, a release to a test index (TestPyPI, a scratch npm scope), or a test that covers the changed behaviour, added in the same PR.
|
|
75
110
|
|
|
@@ -161,6 +161,7 @@ def host_of(url):
|
|
|
161
161
|
class Report:
|
|
162
162
|
def __init__(self):
|
|
163
163
|
self.changes, self.flags, self.files, self.unattributed = [], [], [], {}
|
|
164
|
+
self.sha_pins = {} # (repo, tag) -> {sha: [files]}, from added `uses:` lines
|
|
164
165
|
|
|
165
166
|
def change(self, path, eco, where, name, old, new, note=None):
|
|
166
167
|
if old == new:
|
|
@@ -256,6 +257,12 @@ def merge_blocks(rep, path, eco, hunks, key_fn, field_res, name_fn, reset_re=Non
|
|
|
256
257
|
rep.flag("source", path, f"{label}: {f} moved from {oh} to {nh}")
|
|
257
258
|
elif nh and not o and f != "index" and nh not in DEFAULT_HOSTS:
|
|
258
259
|
rep.flag("source", path, f"{label}: new package comes from {nh}, not the default registry")
|
|
260
|
+
for f in ("node", "python-versions"):
|
|
261
|
+
orv, nrv = of.get(f, (None,))[0], nf.get(f, (None,))[0]
|
|
262
|
+
if o and nrv and orv != nrv:
|
|
263
|
+
rt = "Node" if f == "node" else "Python"
|
|
264
|
+
rep.flag("runtime", path, f"{label}: requires {rt} {nrv} (was {orv or 'unstated'}); check every "
|
|
265
|
+
f"place it installs and runs (base images, CI versions, runners)")
|
|
259
266
|
ol, nl = of.get("license", (None,))[0], nf.get("license", (None,))[0]
|
|
260
267
|
if ol and nl and ol != nl:
|
|
261
268
|
rep.flag("licence", path, f"{label}: licence changed from {ol} to {nl}")
|
|
@@ -317,7 +324,7 @@ def npm_lock_name(key):
|
|
|
317
324
|
|
|
318
325
|
|
|
319
326
|
NPM_FIELDS = {f: re.compile(rf'^\s*"{f}":\s*"?([^",]+)"?,?\s*$') for f in
|
|
320
|
-
("version", "resolved", "license", "hasInstallScript", "dev", "optional", "gypfile")}
|
|
327
|
+
("version", "resolved", "license", "hasInstallScript", "dev", "optional", "gypfile", "node")}
|
|
321
328
|
|
|
322
329
|
|
|
323
330
|
def h_package_lock(rep, path, hunks):
|
|
@@ -360,7 +367,8 @@ def h_pnpm_lock(rep, path, hunks):
|
|
|
360
367
|
|
|
361
368
|
TOML_PKG_FIELDS = {"version": re.compile(r'^version\s*=\s*"([^"]+)"'),
|
|
362
369
|
"source": re.compile(r'^source\s*=\s*"([^"]+)"'),
|
|
363
|
-
"url": re.compile(r'^(?:url|source\s*=\s*\{\s*(?:registry|git|url))\s*=\s*"([^"]+)"')
|
|
370
|
+
"url": re.compile(r'^(?:url|source\s*=\s*\{\s*(?:registry|git|url))\s*=\s*"([^"]+)"'),
|
|
371
|
+
"python-versions": re.compile(r'^python-versions\s*=\s*"([^"]+)"')}
|
|
364
372
|
|
|
365
373
|
|
|
366
374
|
def h_toml_lock(eco):
|
|
@@ -527,6 +535,7 @@ def h_workflow(rep, path, hunks):
|
|
|
527
535
|
if n and SHA40.fullmatch(n[0]):
|
|
528
536
|
tag_ = re.match(r"^(v?[\w.\-]+)", n[1]).group(1) if n[1] else None
|
|
529
537
|
if tag_:
|
|
538
|
+
rep.sha_pins.setdefault((repo, tag_), {}).setdefault(n[0], []).append(path)
|
|
530
539
|
rep.flag("pin", path, f"{name}@{n[0]} claims {tag_}. UNVERIFIED until the tag resolves to this commit: "
|
|
531
540
|
f"`git ls-remote https://github.com/{repo} refs/tags/{tag_} 'refs/tags/{tag_}^{{}}'` "
|
|
532
541
|
f"(the `^{{}}` line is the commit for an annotated tag; it must equal {n[0]})")
|
|
@@ -591,9 +600,16 @@ def analyse(files, title):
|
|
|
591
600
|
for r in rep.changes:
|
|
592
601
|
short = r["name"].split("/")[-1].lower()
|
|
593
602
|
r["in_title"] = r["name"].lower() in t or (len(short) > 3 and re.search(rf"\b{re.escape(short)}\b", t) is not None)
|
|
603
|
+
for (repo, tag_), shas in rep.sha_pins.items():
|
|
604
|
+
if len(shas) > 1:
|
|
605
|
+
where = "; ".join(f"{s} in {', '.join(dict.fromkeys(ps))}" for s, ps in shas.items())
|
|
606
|
+
rep.flag("pin-conflict", next(iter(shas.values()))[0],
|
|
607
|
+
f"{repo} {tag_} is pinned to {len(shas)} different commits ({where}). A tag points to one "
|
|
608
|
+
f"commit, so at most one of these is {tag_}; find which with "
|
|
609
|
+
f"`git ls-remote https://github.com/{repo} refs/tags/{tag_} 'refs/tags/{tag_}^{{}}'`")
|
|
594
610
|
reasons_stop, reasons_check = [], []
|
|
595
611
|
for fl in rep.flags:
|
|
596
|
-
if fl["kind"] in ("install-script", "source"):
|
|
612
|
+
if fl["kind"] in ("install-script", "source", "pin-conflict"):
|
|
597
613
|
reasons_stop.append(f"{fl['kind']}: {fl['text']}")
|
|
598
614
|
elif fl["kind"] == "pin":
|
|
599
615
|
reasons_check.append("pin: " + fl["text"].split(". ")[0])
|
|
@@ -662,9 +678,11 @@ def render_md(rep, floor, stop, check, title):
|
|
|
662
678
|
out += ["", f"## Beyond the title ({len(beyond)})", ""]
|
|
663
679
|
out += [f"- `{r['name']}` {r['kind']}: {r['old'] or '—'} → {r['new'] or '—'} ({r['file']})" for r in beyond] or ["None."]
|
|
664
680
|
labels = (("install-script", "Install scripts"), ("native", "Native code"), ("source", "Sources and registries"),
|
|
665
|
-
("licence", "Licences"), ("pin", "Pins (verify every SHA; the script cannot)"))
|
|
681
|
+
("licence", "Licences"), ("runtime", "Runtime requirements"), ("pin", "Pins (verify every SHA; the script cannot)"))
|
|
666
682
|
for kind, heading in labels:
|
|
667
683
|
rows = [f for f in rep.flags if f["kind"] == kind]
|
|
684
|
+
if kind == "pin":
|
|
685
|
+
rows = [f for f in rep.flags if f["kind"] == "pin-conflict"] + rows
|
|
668
686
|
out += ["", f"## {heading} ({len(rows)})", ""]
|
|
669
687
|
out += [f"- `{f['file']}`: {f['text']}" for f in rows] or ["None seen in the diff."]
|
|
670
688
|
if rep.unattributed:
|
|
@@ -33,5 +33,17 @@
|
|
|
33
33
|
"model": {
|
|
34
34
|
"tier": "standard",
|
|
35
35
|
"rationale": "Formats Sonar API findings into a report. The hard analysis is Sonar's; this is synthesis. Escalates on very large diffs."
|
|
36
|
+
},
|
|
37
|
+
"permissions": {
|
|
38
|
+
"network": [
|
|
39
|
+
"sonarcloud.io"
|
|
40
|
+
],
|
|
41
|
+
"commands": [
|
|
42
|
+
"sonar-scanner",
|
|
43
|
+
"git",
|
|
44
|
+
"curl"
|
|
45
|
+
],
|
|
46
|
+
"files": "none",
|
|
47
|
+
"notes": "No scripts. Its instructions run sonar-scanner and query the Sonar API with curl, using SONAR_TOKEN from the environment, against sonarcloud.io or the self-hosted SonarQube URL the user configures."
|
|
36
48
|
}
|
|
37
49
|
}
|
|
@@ -37,5 +37,16 @@
|
|
|
37
37
|
"model": {
|
|
38
38
|
"tier": "standard",
|
|
39
39
|
"rationale": "Structured generation against fixed module conventions and a secure-defaults checklist; the judgment is bounded to choosing defaults and validation rules for a described need."
|
|
40
|
+
},
|
|
41
|
+
"permissions": {
|
|
42
|
+
"network": [
|
|
43
|
+
"registry.terraform.io",
|
|
44
|
+
"releases.hashicorp.com"
|
|
45
|
+
],
|
|
46
|
+
"commands": [
|
|
47
|
+
"terraform"
|
|
48
|
+
],
|
|
49
|
+
"files": "none",
|
|
50
|
+
"notes": "No scripts. Its instructions run `terraform fmt`, `terraform init -backend=false` and `terraform validate` on the module it writes. `init` downloads the providers from the Terraform registry; none of the commands contacts a cloud account."
|
|
40
51
|
}
|
|
41
52
|
}
|
|
@@ -40,7 +40,14 @@
|
|
|
40
40
|
"iam",
|
|
41
41
|
"drift"
|
|
42
42
|
],
|
|
43
|
-
"permissions": {
|
|
43
|
+
"permissions": {
|
|
44
|
+
"network": [],
|
|
45
|
+
"commands": [
|
|
46
|
+
"terraform"
|
|
47
|
+
],
|
|
48
|
+
"files": "named-paths",
|
|
49
|
+
"notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values. Its instructions run `terraform show -json` to produce that plan JSON."
|
|
50
|
+
},
|
|
44
51
|
"model": {
|
|
45
52
|
"tier": "heavy",
|
|
46
53
|
"rationale": "Adversarial review of a destructive change: judging which replacements lose data, which access changes widen exposure, and what is out of scope needs careful reasoning; a missed destroy costs far more than the model."
|
|
@@ -28,7 +28,18 @@
|
|
|
28
28
|
"design",
|
|
29
29
|
"api"
|
|
30
30
|
],
|
|
31
|
-
"permissions": {
|
|
31
|
+
"permissions": {
|
|
32
|
+
"network": [
|
|
33
|
+
"api.figma.com",
|
|
34
|
+
"pypi.org",
|
|
35
|
+
"files.pythonhosted.org"
|
|
36
|
+
],
|
|
37
|
+
"commands": [
|
|
38
|
+
"pip"
|
|
39
|
+
],
|
|
40
|
+
"files": "named-paths",
|
|
41
|
+
"notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API. Its instructions install the pinned requirements once with `python3 -m pip install -r requirements.txt`, which reaches PyPI."
|
|
42
|
+
},
|
|
32
43
|
"model": {
|
|
33
44
|
"tier": "standard",
|
|
34
45
|
"rationale": "API calls + spec generation. Bounded transformation."
|