@the-open-engine/zeroshot 6.7.1 → 6.7.2
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/cli/index.js +114 -16
- package/lib/git-remote-utils.js +90 -7
- package/package.json +6 -15
- package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
- package/scripts/assert-release-published.js +128 -0
- package/scripts/release-dry-run.js +83 -0
- package/scripts/release-preflight.js +24 -12
- package/scripts/release-recovery.js +149 -0
- package/scripts/semantic-release-notes.js +44 -0
- package/scripts/setup-merge-queue.sh +104 -118
- package/src/agents/git-pusher-template.js +94 -19
- package/src/attach/socket-discovery.js +9 -19
- package/src/attach/socket-paths.js +85 -0
- package/src/isolation-manager.js +164 -74
- package/src/orchestrator.js +83 -34
- package/task-lib/attachable-watcher.js +3 -9
- package/task-lib/commands/run.js +17 -2
- package/task-lib/runner.js +21 -3
package/cli/index.js
CHANGED
|
@@ -1597,21 +1597,87 @@ function reportNoActiveAgents(status, id) {
|
|
|
1597
1597
|
}
|
|
1598
1598
|
}
|
|
1599
1599
|
|
|
1600
|
-
function
|
|
1601
|
-
|
|
1602
|
-
|
|
1600
|
+
async function inspectAgentAttachment(agent, socketDiscovery, readTask = readTaskFromDisk) {
|
|
1601
|
+
if (!agent.currentTaskId) {
|
|
1602
|
+
return { agent, state: 'starting', task: null, socketPath: null };
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const task = await readTask(agent.currentTaskId);
|
|
1606
|
+
if (task?.pid && !task.attachable) {
|
|
1607
|
+
return { agent, state: 'non_attachable', task, socketPath: null };
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const socketPath = task?.socketPath || socketDiscovery.getTaskSocketPath(agent.currentTaskId);
|
|
1611
|
+
if (await socketDiscovery.isSocketAlive(socketPath)) {
|
|
1612
|
+
return { agent, state: 'attachable', task, socketPath };
|
|
1613
|
+
}
|
|
1614
|
+
return { agent, state: 'unavailable', task, socketPath };
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function printLiveAttachableAgents(attachments) {
|
|
1618
|
+
for (const { agent } of attachments) {
|
|
1603
1619
|
const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1620
|
+
console.log(
|
|
1621
|
+
` ${chalk.cyan(agent.id)}${modelLabel} → task ${chalk.green(agent.currentTaskId)}`
|
|
1622
|
+
);
|
|
1623
|
+
console.log(chalk.dim(` zeroshot attach ${agent.currentTaskId}`));
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function printNonAttachableAgents(id, attachments) {
|
|
1628
|
+
if (attachments.length === 0) {
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
console.log(chalk.yellow('\nRunning without PTY attach support:'));
|
|
1633
|
+
for (const { agent, task } of attachments) {
|
|
1634
|
+
const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
|
|
1635
|
+
const providerLabel = task?.provider ? chalk.dim(` [provider: ${task.provider}]`) : '';
|
|
1636
|
+
console.log(
|
|
1637
|
+
` ${chalk.cyan(agent.id)}${modelLabel} → task ${chalk.green(agent.currentTaskId)}${providerLabel}`
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
console.log(chalk.dim(` Follow output instead: zeroshot logs ${id} -f`));
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function printUnavailableAgents(attachments) {
|
|
1644
|
+
if (attachments.length === 0) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
console.log(chalk.yellow('\nActive agents without a live attach socket:'));
|
|
1649
|
+
for (const { agent, state } of attachments) {
|
|
1650
|
+
const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
|
|
1651
|
+
const detail =
|
|
1652
|
+
state === 'starting'
|
|
1653
|
+
? 'task ID not yet assigned'
|
|
1654
|
+
: `task ${agent.currentTaskId} socket not ready`;
|
|
1655
|
+
console.log(` ${chalk.cyan(agent.id)}${modelLabel} → ${chalk.yellow(detail)}`);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
async function printAttachableAgentList(
|
|
1660
|
+
id,
|
|
1661
|
+
activeAgents,
|
|
1662
|
+
socketDiscovery,
|
|
1663
|
+
readTask = readTaskFromDisk
|
|
1664
|
+
) {
|
|
1665
|
+
const inspected = await Promise.all(
|
|
1666
|
+
activeAgents.map((agent) => inspectAgentAttachment(agent, socketDiscovery, readTask))
|
|
1667
|
+
);
|
|
1668
|
+
const attachable = inspected.filter(({ state }) => state === 'attachable');
|
|
1669
|
+
const nonAttachable = inspected.filter(({ state }) => state === 'non_attachable');
|
|
1670
|
+
const unavailable = inspected.filter(({ state }) => ['starting', 'unavailable'].includes(state));
|
|
1671
|
+
|
|
1672
|
+
console.log(chalk.yellow(`\nCluster ${id} - attachable agents:\n`));
|
|
1673
|
+
if (attachable.length === 0) {
|
|
1674
|
+
console.log(chalk.dim(' No live attach sockets.'));
|
|
1675
|
+
} else {
|
|
1676
|
+
printLiveAttachableAgents(attachable);
|
|
1677
|
+
console.log(chalk.dim('\nAttach to an agent by running: zeroshot attach <taskId>'));
|
|
1613
1678
|
}
|
|
1614
|
-
|
|
1679
|
+
printNonAttachableAgents(id, nonAttachable);
|
|
1680
|
+
printUnavailableAgents(unavailable);
|
|
1615
1681
|
}
|
|
1616
1682
|
|
|
1617
1683
|
function reportMissingAgent(agentName, status) {
|
|
@@ -1648,6 +1714,27 @@ async function reportClusterStatusUnavailable(err, socketDiscovery) {
|
|
|
1648
1714
|
}
|
|
1649
1715
|
}
|
|
1650
1716
|
|
|
1717
|
+
async function readTaskFromDisk(taskId) {
|
|
1718
|
+
try {
|
|
1719
|
+
const { getTask } = await import('../task-lib/store.js');
|
|
1720
|
+
return getTask(taskId);
|
|
1721
|
+
} catch {
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function reportAgentTaskNotAttachable(id, agent, task) {
|
|
1727
|
+
console.error(
|
|
1728
|
+
chalk.yellow(
|
|
1729
|
+
`Agent '${agent.id}' is running task ${agent.currentTaskId} without PTY attach support`
|
|
1730
|
+
)
|
|
1731
|
+
);
|
|
1732
|
+
if (task.provider) {
|
|
1733
|
+
console.error(chalk.dim(`Provider: ${task.provider}`));
|
|
1734
|
+
}
|
|
1735
|
+
console.error(chalk.dim(`Follow output instead: zeroshot logs ${id} -f`));
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1651
1738
|
async function resolveClusterSocketPath(id, options, socketDiscovery) {
|
|
1652
1739
|
const cluster = readClusterFromDisk(id);
|
|
1653
1740
|
ensureClusterRunning(cluster, id);
|
|
@@ -1662,7 +1749,7 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
|
|
|
1662
1749
|
}
|
|
1663
1750
|
|
|
1664
1751
|
if (!options.agent) {
|
|
1665
|
-
printAttachableAgentList(id, activeAgents);
|
|
1752
|
+
await printAttachableAgentList(id, activeAgents, socketDiscovery);
|
|
1666
1753
|
return null;
|
|
1667
1754
|
}
|
|
1668
1755
|
|
|
@@ -1680,7 +1767,12 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
|
|
|
1680
1767
|
console.log(
|
|
1681
1768
|
chalk.dim(`Attaching to agent ${options.agent} via task ${agent.currentTaskId}...`)
|
|
1682
1769
|
);
|
|
1683
|
-
|
|
1770
|
+
const task = await readTaskFromDisk(agent.currentTaskId);
|
|
1771
|
+
if (task?.pid && !task.attachable) {
|
|
1772
|
+
reportAgentTaskNotAttachable(id, agent, task);
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
return task?.socketPath || socketDiscovery.getTaskSocketPath(agent.currentTaskId);
|
|
1684
1776
|
} catch (err) {
|
|
1685
1777
|
await reportClusterStatusUnavailable(err, socketDiscovery);
|
|
1686
1778
|
return null;
|
|
@@ -5860,4 +5952,10 @@ if (require.main === module) {
|
|
|
5860
5952
|
});
|
|
5861
5953
|
}
|
|
5862
5954
|
|
|
5863
|
-
module.exports = {
|
|
5955
|
+
module.exports = {
|
|
5956
|
+
applyModelOverrideToConfig,
|
|
5957
|
+
inspectAgentAttachment,
|
|
5958
|
+
printAttachableAgentList,
|
|
5959
|
+
renderRecentMessagesToTerminal,
|
|
5960
|
+
resolveRunMode,
|
|
5961
|
+
};
|
package/lib/git-remote-utils.js
CHANGED
|
@@ -5,6 +5,63 @@
|
|
|
5
5
|
|
|
6
6
|
const { execSync } = require('../src/lib/safe-exec');
|
|
7
7
|
|
|
8
|
+
function hasInvalidGitRefCharacter(value) {
|
|
9
|
+
for (const character of value) {
|
|
10
|
+
const codePoint = character.codePointAt(0);
|
|
11
|
+
if (codePoint <= 0x20 || codePoint === 0x7f || '~^:?*[\\'.includes(character)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a Git remote name using the same ref-format rules Git applies to
|
|
20
|
+
* refs/remotes/<name>. Keeping this next to detection prevents a discovered
|
|
21
|
+
* remote from being rejected later by a narrower consumer-specific allowlist.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown} value - Candidate remote name
|
|
24
|
+
* @returns {string|null} Normalized remote name, or null when invalid
|
|
25
|
+
*/
|
|
26
|
+
function normalizeGitRemoteName(value) {
|
|
27
|
+
if (typeof value !== 'string') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const name = value.trim();
|
|
32
|
+
if (
|
|
33
|
+
name.length === 0 ||
|
|
34
|
+
name.endsWith('.') ||
|
|
35
|
+
name.includes('..') ||
|
|
36
|
+
name.includes('@{') ||
|
|
37
|
+
hasInvalidGitRefCharacter(name)
|
|
38
|
+
) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const components = name.split('/');
|
|
43
|
+
if (
|
|
44
|
+
components.some(
|
|
45
|
+
(component) =>
|
|
46
|
+
component.length === 0 || component.startsWith('.') || component.endsWith('.lock')
|
|
47
|
+
)
|
|
48
|
+
) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Quote one argument for the POSIX shell snippets embedded in agent prompts.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} value - Argument to quote
|
|
59
|
+
* @returns {string} Single-quoted shell argument
|
|
60
|
+
*/
|
|
61
|
+
function quoteShellArgument(value) {
|
|
62
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
63
|
+
}
|
|
64
|
+
|
|
8
65
|
/**
|
|
9
66
|
* Parse git remote URL into structured provider context.
|
|
10
67
|
* Supports GitHub, GitLab, and Azure DevOps (cloud + self-hosted).
|
|
@@ -140,19 +197,43 @@ function detectGitContext(cwd = process.cwd()) {
|
|
|
140
197
|
}
|
|
141
198
|
|
|
142
199
|
try {
|
|
143
|
-
|
|
144
|
-
const remoteUrl = execSync('git remote get-url origin', {
|
|
200
|
+
const remoteOutput = execSync('git remote -v', {
|
|
145
201
|
cwd,
|
|
146
202
|
stdio: 'pipe',
|
|
147
203
|
encoding: 'utf8',
|
|
148
|
-
})
|
|
204
|
+
});
|
|
149
205
|
|
|
150
|
-
|
|
151
|
-
|
|
206
|
+
const supportedRemotes = new Map();
|
|
207
|
+
for (const line of remoteOutput.split(/\r?\n/)) {
|
|
208
|
+
const match = line.match(/^(\S+)\s+(.+)\s+\(fetch\)$/);
|
|
209
|
+
if (!match) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const [, remoteCandidate, remoteUrl] = match;
|
|
214
|
+
const remote = normalizeGitRemoteName(remoteCandidate);
|
|
215
|
+
if (!remote) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const context = parseGitRemoteUrl(remoteUrl);
|
|
219
|
+
if (context && !supportedRemotes.has(remote)) {
|
|
220
|
+
supportedRemotes.set(remote, { ...context, remote });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Preserve existing behavior whenever origin is usable. Without origin,
|
|
225
|
+
// select a remote only when there is exactly one supported target. Guessing
|
|
226
|
+
// between multiple repositories could push code or create a PR in the wrong
|
|
227
|
+
// place, so ambiguous configurations deliberately fail closed.
|
|
228
|
+
if (supportedRemotes.has('origin')) {
|
|
229
|
+
return supportedRemotes.get('origin');
|
|
152
230
|
}
|
|
153
231
|
|
|
154
|
-
|
|
155
|
-
|
|
232
|
+
if (supportedRemotes.size === 1) {
|
|
233
|
+
return supportedRemotes.values().next().value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
156
237
|
} catch {
|
|
157
238
|
// No remote configured or command failed
|
|
158
239
|
return null;
|
|
@@ -160,6 +241,8 @@ function detectGitContext(cwd = process.cwd()) {
|
|
|
160
241
|
}
|
|
161
242
|
|
|
162
243
|
module.exports = {
|
|
244
|
+
normalizeGitRemoteName,
|
|
245
|
+
quoteShellArgument,
|
|
163
246
|
parseGitRemoteUrl,
|
|
164
247
|
detectGitContext,
|
|
165
248
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-open-engine/zeroshot",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.2",
|
|
4
4
|
"description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
|
|
5
5
|
"main": "src/orchestrator.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,18 +17,8 @@
|
|
|
17
17
|
"main"
|
|
18
18
|
],
|
|
19
19
|
"plugins": [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
"releaseRules": [
|
|
24
|
-
{
|
|
25
|
-
"type": "release",
|
|
26
|
-
"release": "minor"
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"@semantic-release/release-notes-generator",
|
|
20
|
+
"@semantic-release/commit-analyzer",
|
|
21
|
+
"./scripts/semantic-release-notes.js",
|
|
32
22
|
[
|
|
33
23
|
"@semantic-release/npm",
|
|
34
24
|
{
|
|
@@ -75,6 +65,7 @@
|
|
|
75
65
|
"check:all": "npm run check && npm run deadcode:all",
|
|
76
66
|
"release": "semantic-release",
|
|
77
67
|
"release:preflight": "node scripts/release-preflight.js",
|
|
68
|
+
"release:recover": "node scripts/release-recovery.js",
|
|
78
69
|
"release:assert-published": "node scripts/assert-release-published.js",
|
|
79
70
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run check:agent-cli-provider:ci",
|
|
80
71
|
"prepare": "npm run build:agent-cli-provider && husky"
|
|
@@ -151,8 +142,8 @@
|
|
|
151
142
|
"proper-lockfile": "^4.1.2"
|
|
152
143
|
},
|
|
153
144
|
"devDependencies": {
|
|
154
|
-
"@
|
|
155
|
-
"@
|
|
145
|
+
"@commitlint/cli": "^19.8.1",
|
|
146
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
156
147
|
"@semantic-release/github": "^11.0.6",
|
|
157
148
|
"@types/better-sqlite3": "^7.6.13",
|
|
158
149
|
"@types/node": "^25.0.3",
|
|
@@ -821,6 +821,26 @@
|
|
|
821
821
|
"profile"
|
|
822
822
|
],
|
|
823
823
|
"type": "object"
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
"additionalProperties": false,
|
|
827
|
+
"properties": {
|
|
828
|
+
"profile": {
|
|
829
|
+
"const": "openengine.worker.builtin/v1"
|
|
830
|
+
},
|
|
831
|
+
"protocol": {
|
|
832
|
+
"const": "builtin"
|
|
833
|
+
},
|
|
834
|
+
"version": {
|
|
835
|
+
"const": "1"
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
"required": [
|
|
839
|
+
"protocol",
|
|
840
|
+
"version",
|
|
841
|
+
"profile"
|
|
842
|
+
],
|
|
843
|
+
"type": "object"
|
|
824
844
|
}
|
|
825
845
|
]
|
|
826
846
|
}
|
|
@@ -1166,6 +1186,33 @@
|
|
|
1166
1186
|
"worker",
|
|
1167
1187
|
"binding"
|
|
1168
1188
|
]
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"properties": {
|
|
1192
|
+
"binding": {
|
|
1193
|
+
"properties": {
|
|
1194
|
+
"protocol": {
|
|
1195
|
+
"const": "builtin"
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
"required": [
|
|
1199
|
+
"protocol"
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
"credentialRequirements": {
|
|
1203
|
+
"maxItems": 0
|
|
1204
|
+
},
|
|
1205
|
+
"worker": {
|
|
1206
|
+
"not": {
|
|
1207
|
+
"const": "legacy.zeroshot.ship@1"
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
"required": [
|
|
1212
|
+
"worker",
|
|
1213
|
+
"binding",
|
|
1214
|
+
"credentialRequirements"
|
|
1215
|
+
]
|
|
1169
1216
|
}
|
|
1170
1217
|
]
|
|
1171
1218
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const path = require('path');
|
|
3
6
|
const { execFileSync } = require('child_process');
|
|
7
|
+
const { releaseTypeForMessages } = require('./release-preflight');
|
|
4
8
|
|
|
5
9
|
const DEFAULT_ATTEMPTS = 24;
|
|
6
10
|
const DEFAULT_DELAY_MS = 5000;
|
|
@@ -17,6 +21,87 @@ function npmLatest(name) {
|
|
|
17
21
|
return JSON.parse(run('npm', ['view', name, 'dist-tags.latest', '--json']));
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
function npmReleaseMetadata(name, version) {
|
|
25
|
+
return JSON.parse(
|
|
26
|
+
run('npm', ['view', `${name}@${version}`, 'version', 'gitHead', 'dist.attestations', '--json'])
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function httpsJson(url) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const request = https.get(url, { headers: { Accept: 'application/json' } }, (response) => {
|
|
33
|
+
let body = '';
|
|
34
|
+
response.setEncoding('utf8');
|
|
35
|
+
response.on('data', (chunk) => {
|
|
36
|
+
body += chunk;
|
|
37
|
+
});
|
|
38
|
+
response.on('end', () => {
|
|
39
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
40
|
+
reject(new Error(`HTTP ${response.statusCode} from ${url}: ${body}`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
resolve(JSON.parse(body));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
request.on('error', reject);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function provenanceStatement(attestations) {
|
|
51
|
+
const provenance = attestations.attestations?.find(
|
|
52
|
+
(attestation) => attestation.predicateType === 'https://slsa.dev/provenance/v1'
|
|
53
|
+
);
|
|
54
|
+
const payload = provenance?.bundle?.dsseEnvelope?.payload;
|
|
55
|
+
if (!payload) throw new Error('npm provenance statement is missing');
|
|
56
|
+
return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function verifyProvenance(statement, expectedCommit) {
|
|
60
|
+
const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow;
|
|
61
|
+
if (workflow?.repository !== 'https://github.com/the-open-engine/zeroshot') {
|
|
62
|
+
throw new Error(`unexpected provenance repository: ${workflow?.repository || '(missing)'}`);
|
|
63
|
+
}
|
|
64
|
+
if (workflow?.path !== '.github/workflows/release.yml') {
|
|
65
|
+
throw new Error(`unexpected provenance workflow: ${workflow?.path || '(missing)'}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolved = statement.predicate?.buildDefinition?.resolvedDependencies || [];
|
|
69
|
+
if (!resolved.some((dependency) => dependency.digest?.gitCommit === expectedCommit)) {
|
|
70
|
+
throw new Error(`provenance does not resolve to release commit ${expectedCommit}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function githubRelease(tag) {
|
|
75
|
+
return JSON.parse(run('gh', ['release', 'view', tag, '--json', 'tagName,body,url,publishedAt']));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function verifyCuratedNotes(tag, release) {
|
|
79
|
+
const notesPath = path.join(process.cwd(), 'docs', 'releases', `${tag}.md`);
|
|
80
|
+
if (!fs.existsSync(notesPath)) return;
|
|
81
|
+
const expected = fs.readFileSync(notesPath, 'utf8').trim();
|
|
82
|
+
const actual = String(release.body || '').trim();
|
|
83
|
+
if (actual !== expected) {
|
|
84
|
+
throw new Error(`GitHub Release ${tag} does not match ${notesPath}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verifyInstalledCli(name, version) {
|
|
89
|
+
const packageSpec = `${name}@${version}`;
|
|
90
|
+
const reported = run('npm', [
|
|
91
|
+
'exec',
|
|
92
|
+
'--yes',
|
|
93
|
+
`--package=${packageSpec}`,
|
|
94
|
+
'--',
|
|
95
|
+
'zeroshot',
|
|
96
|
+
'--version',
|
|
97
|
+
]);
|
|
98
|
+
if (!reported.split(/\s+/).includes(version)) {
|
|
99
|
+
throw new Error(`installed CLI reported ${reported}; expected ${version}`);
|
|
100
|
+
}
|
|
101
|
+
run('npm', ['exec', '--yes', `--package=${packageSpec}`, '--', 'zeroshot', '--help']);
|
|
102
|
+
run('npm', ['exec', '--yes', `--package=${packageSpec}`, '--', 'zeroshot', 'list']);
|
|
103
|
+
}
|
|
104
|
+
|
|
20
105
|
function tagsPointingAtHead() {
|
|
21
106
|
run('git', ['fetch', '--tags', '--force']);
|
|
22
107
|
return run('git', ['tag', '--points-at', 'HEAD', '--list', 'v[0-9]*'])
|
|
@@ -25,6 +110,18 @@ function tagsPointingAtHead() {
|
|
|
25
110
|
.filter(Boolean);
|
|
26
111
|
}
|
|
27
112
|
|
|
113
|
+
function latestReachableReleaseTag() {
|
|
114
|
+
return run('git', ['describe', '--tags', '--abbrev=0', '--match', 'v[0-9]*']);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function commitMessagesSince(tag) {
|
|
118
|
+
const output = run('git', ['log', '--format=%B%x1e', `${tag}..HEAD`]);
|
|
119
|
+
return output
|
|
120
|
+
.split('\x1e')
|
|
121
|
+
.map((message) => message.trim())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
28
125
|
function releaseTagParts(tag) {
|
|
29
126
|
const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
|
30
127
|
if (!match) return null;
|
|
@@ -80,6 +177,12 @@ async function main() {
|
|
|
80
177
|
const expectedTag = latestReleaseTag(headTags);
|
|
81
178
|
|
|
82
179
|
if (!expectedTag) {
|
|
180
|
+
const previousTag = latestReachableReleaseTag();
|
|
181
|
+
const releaseType = releaseTypeForMessages(commitMessagesSince(previousTag));
|
|
182
|
+
if (!releaseType) {
|
|
183
|
+
console.log('No release-worthy commits since the latest tag; no publication expected');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
83
186
|
throw new Error('expected a vX.Y.Z release tag to point at HEAD after release');
|
|
84
187
|
}
|
|
85
188
|
|
|
@@ -89,6 +192,27 @@ async function main() {
|
|
|
89
192
|
|
|
90
193
|
console.log(`npm latest for ${name}: ${latest}`);
|
|
91
194
|
|
|
195
|
+
const expectedCommit = run('git', ['rev-parse', 'HEAD']);
|
|
196
|
+
const metadata = npmReleaseMetadata(name, expectedVersion);
|
|
197
|
+
if (metadata.version !== expectedVersion) {
|
|
198
|
+
throw new Error(`npm metadata returned ${metadata.version}; expected ${expectedVersion}`);
|
|
199
|
+
}
|
|
200
|
+
if (metadata.gitHead !== expectedCommit) {
|
|
201
|
+
throw new Error(`npm gitHead ${metadata.gitHead || '(missing)'} does not match HEAD`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const attestationUrl = metadata['dist.attestations']?.url;
|
|
205
|
+
if (!attestationUrl) throw new Error('npm attestation URL is missing');
|
|
206
|
+
const attestations = await httpsJson(attestationUrl);
|
|
207
|
+
verifyProvenance(provenanceStatement(attestations), expectedCommit);
|
|
208
|
+
|
|
209
|
+
const release = githubRelease(expectedTag);
|
|
210
|
+
if (release.tagName !== expectedTag) {
|
|
211
|
+
throw new Error(`GitHub Release tag ${release.tagName} does not match ${expectedTag}`);
|
|
212
|
+
}
|
|
213
|
+
verifyCuratedNotes(expectedTag, release);
|
|
214
|
+
verifyInstalledCli(name, expectedVersion);
|
|
215
|
+
|
|
92
216
|
console.log(`Release publication verified: ${name}@${latest}`);
|
|
93
217
|
}
|
|
94
218
|
|
|
@@ -101,7 +225,11 @@ if (require.main === module) {
|
|
|
101
225
|
|
|
102
226
|
module.exports = {
|
|
103
227
|
latestReleaseTag,
|
|
228
|
+
latestReachableReleaseTag,
|
|
229
|
+
npmReleaseMetadata,
|
|
104
230
|
npmLatest,
|
|
231
|
+
provenanceStatement,
|
|
105
232
|
tagsPointingAtHead,
|
|
233
|
+
verifyProvenance,
|
|
106
234
|
waitForNpmLatest,
|
|
107
235
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
function runGit(args) {
|
|
12
|
+
execFileSync('git', args, {
|
|
13
|
+
cwd: projectRoot,
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pluginName(plugin) {
|
|
20
|
+
return Array.isArray(plugin) ? plugin[0] : plugin;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validationPlugins(releaseConfig) {
|
|
24
|
+
const allowed = new Set([
|
|
25
|
+
'@semantic-release/commit-analyzer',
|
|
26
|
+
'./scripts/semantic-release-notes.js',
|
|
27
|
+
]);
|
|
28
|
+
return releaseConfig.plugins.filter((plugin) => allowed.has(pluginName(plugin)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const branch = process.env.GITHUB_REF_NAME?.trim();
|
|
33
|
+
if (!branch) throw new Error('GITHUB_REF_NAME is required for a release dry run');
|
|
34
|
+
runGit(['check-ref-format', `refs/heads/${branch}`]);
|
|
35
|
+
|
|
36
|
+
const packageJson = require('../package.json');
|
|
37
|
+
const releaseConfig = packageJson.release;
|
|
38
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-release-dry-run-'));
|
|
39
|
+
const mirrorPath = path.join(tempRoot, 'candidate.git');
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
runGit(['init', '--bare', mirrorPath]);
|
|
43
|
+
runGit([
|
|
44
|
+
'--git-dir',
|
|
45
|
+
mirrorPath,
|
|
46
|
+
'fetch',
|
|
47
|
+
projectRoot,
|
|
48
|
+
`+HEAD:refs/heads/${branch}`,
|
|
49
|
+
'+refs/tags/*:refs/tags/*',
|
|
50
|
+
]);
|
|
51
|
+
runGit(['--git-dir', mirrorPath, 'symbolic-ref', 'HEAD', `refs/heads/${branch}`]);
|
|
52
|
+
|
|
53
|
+
const semanticRelease = (await import('semantic-release')).default;
|
|
54
|
+
const result = await semanticRelease(
|
|
55
|
+
{
|
|
56
|
+
...releaseConfig,
|
|
57
|
+
branches: [branch],
|
|
58
|
+
repositoryUrl: mirrorPath,
|
|
59
|
+
plugins: validationPlugins(releaseConfig),
|
|
60
|
+
dryRun: true,
|
|
61
|
+
ci: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
cwd: projectRoot,
|
|
65
|
+
env: process.env,
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!result) {
|
|
70
|
+
console.log('RELEASE_DRY_RUN_RESULT=no-release');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`RELEASE_DRY_RUN_RESULT=${result.nextRelease.version}`);
|
|
75
|
+
} finally {
|
|
76
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch((error) => {
|
|
81
|
+
console.error(error);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
});
|