triflux 10.45.0 → 10.46.0
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/tfx-live.mjs +576 -53
- package/config/routing-policy.json +1 -1
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +9 -1
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/workers/codex-mcp.mjs +1 -1
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +235 -11
- package/scripts/__tests__/release-publish-ancestor.test.mjs +67 -0
- package/scripts/__tests__/release-verify-wait.test.mjs +169 -0
- package/scripts/__tests__/resolve-auto-release.test.mjs +216 -0
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +4 -4
- package/scripts/lib/agent-route-policy.mjs +18 -17
- package/scripts/lib/codex-profile-config.mjs +19 -5
- package/scripts/release/publish.mjs +16 -1
- package/scripts/release/resolve-auto-release.mjs +136 -0
- package/scripts/release/verify.mjs +74 -25
- package/scripts/setup.mjs +13 -7
- package/scripts/tfx-route.sh +26 -23
- package/skills/tfx-auto/SKILL.md +5 -4
- package/skills/tfx-live/SKILL.md +112 -7
- package/skills/tfx-profile/SKILL.md +9 -7
- package/skills/tfx-setup/SKILL.md +1 -1
- package/skills/tfx-ship/SKILL.md +43 -36
- package/tui/codex-profile.mjs +2 -3
- package/tui/setup.mjs +4 -3
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import { resolveAutoRelease } from "../release/resolve-auto-release.mjs";
|
|
8
|
+
|
|
9
|
+
function resolveVersion(
|
|
10
|
+
version,
|
|
11
|
+
{
|
|
12
|
+
tags = [],
|
|
13
|
+
headSha = "current",
|
|
14
|
+
main = "current",
|
|
15
|
+
parentVersion = "1.2.2",
|
|
16
|
+
ancestorStatus = 0,
|
|
17
|
+
} = {},
|
|
18
|
+
) {
|
|
19
|
+
const rootDir = mkdtempSync(join(tmpdir(), "tfx-resolve-release-"));
|
|
20
|
+
// The checkout metadata must not override the exact CI commit.
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(rootDir, "package.json"),
|
|
23
|
+
JSON.stringify({ version: "99.0.0" }),
|
|
24
|
+
);
|
|
25
|
+
try {
|
|
26
|
+
return resolveAutoRelease({
|
|
27
|
+
rootDir,
|
|
28
|
+
eventName: "workflow_run",
|
|
29
|
+
headSha,
|
|
30
|
+
execFileSyncFn(command, args, options) {
|
|
31
|
+
assert.equal(command, "git");
|
|
32
|
+
assert.equal(options.cwd, rootDir);
|
|
33
|
+
if (args[0] === "show") {
|
|
34
|
+
if (args[1] === `${headSha}:package.json`)
|
|
35
|
+
return JSON.stringify({ version });
|
|
36
|
+
assert.equal(args[1], `${headSha}^1:package.json`);
|
|
37
|
+
return JSON.stringify({ version: parentVersion });
|
|
38
|
+
}
|
|
39
|
+
if (args[0] === "fetch") {
|
|
40
|
+
assert.deepEqual(args, ["fetch", "--no-tags", "origin", main]);
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (args[0] === "merge-base") {
|
|
44
|
+
assert.deepEqual(args, [
|
|
45
|
+
"merge-base",
|
|
46
|
+
"--is-ancestor",
|
|
47
|
+
headSha,
|
|
48
|
+
main,
|
|
49
|
+
]);
|
|
50
|
+
if (ancestorStatus !== 0)
|
|
51
|
+
throw Object.assign(new Error("ancestry check failed"), {
|
|
52
|
+
status: ancestorStatus,
|
|
53
|
+
});
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
assert.notEqual(
|
|
57
|
+
parentVersion,
|
|
58
|
+
version,
|
|
59
|
+
"unchanged commits must not query remote refs",
|
|
60
|
+
);
|
|
61
|
+
assert.deepEqual(args, [
|
|
62
|
+
"ls-remote",
|
|
63
|
+
"--refs",
|
|
64
|
+
"origin",
|
|
65
|
+
"refs/heads/main",
|
|
66
|
+
"refs/tags/v*",
|
|
67
|
+
]);
|
|
68
|
+
assert.equal(options.cwd, rootDir);
|
|
69
|
+
return [
|
|
70
|
+
...(main ? [`${main}\trefs/heads/main`] : []),
|
|
71
|
+
...tags.map((tag) => `tag-sha\trefs/tags/${tag}`),
|
|
72
|
+
].join("\n");
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(rootDir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("automatic release resolution", () => {
|
|
81
|
+
it("skips an already released version even when its CI is stale", () => {
|
|
82
|
+
const result = resolveVersion("1.2.3", {
|
|
83
|
+
tags: ["v1.2.3"],
|
|
84
|
+
headSha: "older",
|
|
85
|
+
});
|
|
86
|
+
assert.equal(result.release, false);
|
|
87
|
+
assert.match(result.reason, /already released.*v1\.2\.3/i);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("skips a commit whose first parent already has the same version", () => {
|
|
91
|
+
const result = resolveVersion("1.2.4", { parentVersion: "1.2.4" });
|
|
92
|
+
assert.equal(result.release, false);
|
|
93
|
+
assert.match(result.reason, /did not change.*version/i);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("releases a version bump even when main has advanced beyond the CI head", () => {
|
|
97
|
+
const result = resolveVersion("1.2.4", { headSha: "older" });
|
|
98
|
+
assert.equal(result.release, true);
|
|
99
|
+
assert.equal(result.sha, "older");
|
|
100
|
+
assert.equal(result.npm_tag, "latest");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("skips a CI head removed from main by a force push", () => {
|
|
104
|
+
const result = resolveVersion("1.2.4", {
|
|
105
|
+
headSha: "removed",
|
|
106
|
+
ancestorStatus: 1,
|
|
107
|
+
});
|
|
108
|
+
assert.equal(result.release, false);
|
|
109
|
+
assert.match(result.reason, /not.*ancestor.*main/i);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("fails closed on ancestry errors other than a non-ancestor result", () => {
|
|
113
|
+
assert.throws(
|
|
114
|
+
() => resolveVersion("1.2.4", { ancestorStatus: 128 }),
|
|
115
|
+
/ancestry check failed/,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("leaves prereleases to manual dispatch", () => {
|
|
120
|
+
const result = resolveVersion("1.3.0-rc.1");
|
|
121
|
+
assert.equal(result.release, false);
|
|
122
|
+
assert.match(result.reason, /prerelease.*dispatch/i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects a regression with both candidate and latest stable versions", () => {
|
|
126
|
+
assert.throws(
|
|
127
|
+
() =>
|
|
128
|
+
resolveVersion("1.2.4", { tags: ["v1.2.3", "v1.3.0", "v9.0.0-rc.1"] }),
|
|
129
|
+
/1\.2\.4.*1\.3\.0/,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("compares numeric version components, ignoring prerelease and unrelated tags", () => {
|
|
134
|
+
const result = resolveVersion("1.10.0", {
|
|
135
|
+
tags: ["v1.9.0", "v1.8.12", "v2.0.0-rc.1", "vnext"],
|
|
136
|
+
});
|
|
137
|
+
assert.equal(result.release, true);
|
|
138
|
+
assert.equal(result.version, "1.10.0");
|
|
139
|
+
assert.equal(result.channel, "stable");
|
|
140
|
+
assert.equal(result.mode, "auto");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("rejects a lower numeric version even when tag enumeration is unsorted", () => {
|
|
144
|
+
assert.throws(
|
|
145
|
+
() => resolveVersion("1.9.1", { tags: ["v1.10.0", "v1.9.0"] }),
|
|
146
|
+
/1\.9\.1.*1\.10\.0/,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("allows the first stable release when no stable tags exist", () => {
|
|
151
|
+
assert.equal(
|
|
152
|
+
resolveVersion("1.0.0", { tags: ["v1.0.0-rc.1"] }).release,
|
|
153
|
+
true,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("fails closed if the remote main ref cannot be found", () => {
|
|
158
|
+
assert.throws(
|
|
159
|
+
() => resolveVersion("1.2.3", { main: null }),
|
|
160
|
+
/origin\/main/,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("rejects malformed versions before querying the remote", () => {
|
|
165
|
+
assert.throws(() => resolveVersion("not-a-version"), /invalid.*version/i);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("passes manual version and channel through without git or metadata reads", () => {
|
|
169
|
+
assert.deepEqual(
|
|
170
|
+
resolveAutoRelease({
|
|
171
|
+
eventName: "workflow_dispatch",
|
|
172
|
+
version: "2.0.0-canary.4",
|
|
173
|
+
channel: "canary",
|
|
174
|
+
headSha: "dispatch-sha",
|
|
175
|
+
rootDir: "/nonexistent",
|
|
176
|
+
execFileSyncFn: () => assert.fail("dispatch must not query git"),
|
|
177
|
+
}),
|
|
178
|
+
{
|
|
179
|
+
release: true,
|
|
180
|
+
version: "2.0.0-canary.4",
|
|
181
|
+
channel: "canary",
|
|
182
|
+
mode: "dispatch",
|
|
183
|
+
npm_tag: "canary",
|
|
184
|
+
sha: "dispatch-sha",
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("writes all six workflow outputs for dispatch", () => {
|
|
190
|
+
const root = mkdtempSync(join(tmpdir(), "tfx-resolve-output-"));
|
|
191
|
+
const output = join(root, "output");
|
|
192
|
+
try {
|
|
193
|
+
execFileSync(
|
|
194
|
+
process.execPath,
|
|
195
|
+
["scripts/release/resolve-auto-release.mjs"],
|
|
196
|
+
{
|
|
197
|
+
cwd: new URL("../../", import.meta.url),
|
|
198
|
+
env: {
|
|
199
|
+
...process.env,
|
|
200
|
+
GITHUB_EVENT_NAME: "workflow_dispatch",
|
|
201
|
+
GITHUB_OUTPUT: output,
|
|
202
|
+
RELEASE_VERSION: "2.0.0-canary.4",
|
|
203
|
+
RELEASE_CHANNEL: "canary",
|
|
204
|
+
RELEASE_HEAD_SHA: "dispatch-sha",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
assert.equal(
|
|
209
|
+
readFileSync(output, "utf8"),
|
|
210
|
+
"release=true\nversion=2.0.0-canary.4\nchannel=canary\nmode=dispatch\nnpm_tag=canary\nsha=dispatch-sha\n",
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(root, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -87,7 +87,7 @@ describe("tfx-route bash/node parity — Phase 0 회귀가드", () => {
|
|
|
87
87
|
assert.equal(plan.agent, "executor");
|
|
88
88
|
assert.equal(plan.cliType, "codex");
|
|
89
89
|
assert.equal(plan.adapter, "codex");
|
|
90
|
-
assert.equal(plan.effort, "
|
|
90
|
+
assert.equal(plan.effort, "gpt6_sol_high");
|
|
91
91
|
assert.equal(plan.runMode, "fg");
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -234,14 +234,14 @@ describe("tfx-route.mjs — buildRoutePlan (agent-map.json 전수)", () => {
|
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
describe("cli-codex.mjs — 핵심 매핑", () => {
|
|
237
|
-
test("executor →
|
|
237
|
+
test("executor → gpt6_sol_high / 1080s / fg / mcp=implement", () => {
|
|
238
238
|
const p = codexAdapter.plan({
|
|
239
239
|
agent: "executor",
|
|
240
240
|
prompt: "x",
|
|
241
241
|
mcpProfile: "auto",
|
|
242
242
|
});
|
|
243
|
-
assert.equal(p.effort, "
|
|
244
|
-
assert.equal(p.profile, "
|
|
243
|
+
assert.equal(p.effort, "gpt6_sol_high");
|
|
244
|
+
assert.equal(p.profile, "gpt6_sol_high");
|
|
245
245
|
assert.equal(p.timeoutMs, 1_080_000);
|
|
246
246
|
assert.equal(p.runMode, "fg");
|
|
247
247
|
assert.equal(p.opusOversight, false);
|
|
@@ -334,7 +334,7 @@ describe("cli-codex.mjs — 핵심 매핑", () => {
|
|
|
334
334
|
prompt: "x",
|
|
335
335
|
mcpProfile: "auto",
|
|
336
336
|
});
|
|
337
|
-
assert.equal(p.effort, "
|
|
337
|
+
assert.equal(p.effort, "gpt6_sol_high");
|
|
338
338
|
assert.equal(p.timeoutMs, 1_080_000);
|
|
339
339
|
});
|
|
340
340
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
const policy = {
|
|
9
9
|
executor: {
|
|
10
|
-
profile: "
|
|
10
|
+
profile: "gpt6_sol_high",
|
|
11
11
|
timeoutSec: 1080,
|
|
12
12
|
runMode: "fg",
|
|
13
13
|
opusOversight: false,
|
|
@@ -15,7 +15,7 @@ const policy = {
|
|
|
15
15
|
subcommand: "exec",
|
|
16
16
|
},
|
|
17
17
|
codex: {
|
|
18
|
-
profile: "
|
|
18
|
+
profile: "gpt6_sol_high",
|
|
19
19
|
timeoutSec: 1080,
|
|
20
20
|
runMode: "fg",
|
|
21
21
|
opusOversight: false,
|
|
@@ -23,7 +23,7 @@ const policy = {
|
|
|
23
23
|
subcommand: "exec",
|
|
24
24
|
},
|
|
25
25
|
"build-fixer": {
|
|
26
|
-
profile: "
|
|
26
|
+
profile: "gpt6_luna_high",
|
|
27
27
|
timeoutSec: 540,
|
|
28
28
|
runMode: "fg",
|
|
29
29
|
opusOversight: false,
|
|
@@ -31,7 +31,7 @@ const policy = {
|
|
|
31
31
|
subcommand: "exec",
|
|
32
32
|
},
|
|
33
33
|
cleanup: {
|
|
34
|
-
profile: "
|
|
34
|
+
profile: "gpt6_sol_med",
|
|
35
35
|
timeoutSec: 540,
|
|
36
36
|
runMode: "fg",
|
|
37
37
|
opusOversight: false,
|
|
@@ -39,7 +39,7 @@ const policy = {
|
|
|
39
39
|
subcommand: "exec",
|
|
40
40
|
},
|
|
41
41
|
deslop: {
|
|
42
|
-
profile: "
|
|
42
|
+
profile: "gpt6_sol_med",
|
|
43
43
|
timeoutSec: 540,
|
|
44
44
|
runMode: "fg",
|
|
45
45
|
opusOversight: false,
|
|
@@ -95,7 +95,7 @@ const policy = {
|
|
|
95
95
|
subcommand: "exec",
|
|
96
96
|
},
|
|
97
97
|
"code-reviewer": {
|
|
98
|
-
profile: "
|
|
98
|
+
profile: "gpt6_sol_high",
|
|
99
99
|
timeoutSec: 1800,
|
|
100
100
|
runMode: "bg",
|
|
101
101
|
opusOversight: false,
|
|
@@ -103,7 +103,7 @@ const policy = {
|
|
|
103
103
|
subcommand: "review",
|
|
104
104
|
},
|
|
105
105
|
"quality-reviewer": {
|
|
106
|
-
profile: "
|
|
106
|
+
profile: "gpt6_sol_high",
|
|
107
107
|
timeoutSec: 1800,
|
|
108
108
|
runMode: "bg",
|
|
109
109
|
opusOversight: false,
|
|
@@ -119,7 +119,7 @@ const policy = {
|
|
|
119
119
|
subcommand: "review",
|
|
120
120
|
},
|
|
121
121
|
scientist: {
|
|
122
|
-
profile: "
|
|
122
|
+
profile: "gpt6_sol_high",
|
|
123
123
|
timeoutSec: 1440,
|
|
124
124
|
runMode: "bg",
|
|
125
125
|
opusOversight: false,
|
|
@@ -127,7 +127,7 @@ const policy = {
|
|
|
127
127
|
subcommand: "exec",
|
|
128
128
|
},
|
|
129
129
|
"document-specialist": {
|
|
130
|
-
profile: "
|
|
130
|
+
profile: "gpt6_sol_high",
|
|
131
131
|
timeoutSec: 1440,
|
|
132
132
|
runMode: "bg",
|
|
133
133
|
opusOversight: false,
|
|
@@ -143,7 +143,7 @@ const policy = {
|
|
|
143
143
|
subcommand: "exec",
|
|
144
144
|
},
|
|
145
145
|
verifier: {
|
|
146
|
-
profile: "
|
|
146
|
+
profile: "gpt6_sol_high",
|
|
147
147
|
timeoutSec: 1200,
|
|
148
148
|
runMode: "fg",
|
|
149
149
|
opusOversight: false,
|
|
@@ -151,7 +151,7 @@ const policy = {
|
|
|
151
151
|
subcommand: "review",
|
|
152
152
|
},
|
|
153
153
|
"test-engineer": {
|
|
154
|
-
profile: "
|
|
154
|
+
profile: "gpt6_sol_high",
|
|
155
155
|
timeoutSec: 1200,
|
|
156
156
|
runMode: "bg",
|
|
157
157
|
opusOversight: false,
|
|
@@ -159,7 +159,7 @@ const policy = {
|
|
|
159
159
|
subcommand: "exec",
|
|
160
160
|
},
|
|
161
161
|
"qa-tester": {
|
|
162
|
-
profile: "
|
|
162
|
+
profile: "gpt6_sol_high",
|
|
163
163
|
timeoutSec: 1200,
|
|
164
164
|
runMode: "bg",
|
|
165
165
|
opusOversight: false,
|
|
@@ -167,7 +167,7 @@ const policy = {
|
|
|
167
167
|
subcommand: "review",
|
|
168
168
|
},
|
|
169
169
|
spark: {
|
|
170
|
-
profile: "
|
|
170
|
+
profile: "gpt6_luna_low",
|
|
171
171
|
timeoutSec: 180,
|
|
172
172
|
runMode: "fg",
|
|
173
173
|
opusOversight: false,
|
|
@@ -185,7 +185,7 @@ const policy = {
|
|
|
185
185
|
subcommand: "exec",
|
|
186
186
|
},
|
|
187
187
|
writer: {
|
|
188
|
-
profile: "
|
|
188
|
+
profile: "gpt6_luna_high",
|
|
189
189
|
timeoutSec: 900,
|
|
190
190
|
runMode: "bg",
|
|
191
191
|
opusOversight: false,
|
|
@@ -210,9 +210,10 @@ export const DEFAULT_CODEX_AGENT = "executor";
|
|
|
210
210
|
|
|
211
211
|
export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
|
|
212
212
|
new Set([
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"
|
|
213
|
+
"gpt6_luna_low",
|
|
214
|
+
"gpt6_luna_high",
|
|
215
|
+
"gpt6_sol_med",
|
|
216
|
+
"gpt6_sol_high",
|
|
216
217
|
"gpt6_astra_xhigh",
|
|
217
218
|
"gpt6_astra_max",
|
|
218
219
|
"gpt6_astra_ultra",
|
|
@@ -19,18 +19,24 @@ const EFFORT_BY_SUFFIX = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
const CANONICAL_PROFILE_VALUES = Object.freeze({
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
gpt6_luna_low: { model: "gpt-6-luna", effort: "low" },
|
|
23
|
+
gpt6_luna_high: { model: "gpt-6-luna", effort: "high" },
|
|
24
|
+
gpt6_sol_med: { model: "gpt-6-sol", effort: "medium" },
|
|
25
|
+
gpt6_sol_high: { model: "gpt-6-sol", effort: "high" },
|
|
25
26
|
gpt6_astra_xhigh: { model: "gpt-6-astra", effort: "xhigh" },
|
|
26
27
|
gpt6_astra_max: { model: "gpt-6-astra", effort: "max" },
|
|
27
28
|
gpt6_astra_ultra: { model: "gpt-6-astra", effort: "ultra" },
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
// GPT-5.6 lane names map to the GPT-6 lane with the same effort. GPT-6 has no
|
|
32
|
+
// Terra tier, so Terra lanes move to Sol.
|
|
30
33
|
const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
|
|
31
34
|
gpt56_sol_xhigh: "gpt6_astra_xhigh",
|
|
32
35
|
gpt56_sol_max: "gpt6_astra_max",
|
|
33
36
|
gpt56_sol_ultra: "gpt6_astra_ultra",
|
|
37
|
+
gpt56_terra_high: "gpt6_sol_high",
|
|
38
|
+
gpt56_terra_med: "gpt6_sol_med",
|
|
39
|
+
gpt56_luna_low: "gpt6_luna_low",
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
export function normalizeCodexProfileName(profileName) {
|
|
@@ -38,6 +44,14 @@ export function normalizeCodexProfileName(profileName) {
|
|
|
38
44
|
return LEGACY_CANONICAL_PROFILE_ALIASES[normalized] || normalized;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
/** Model ID of a canonical profile (after legacy normalization), or null. */
|
|
48
|
+
export function canonicalCodexProfileModel(profileName) {
|
|
49
|
+
return (
|
|
50
|
+
CANONICAL_PROFILE_VALUES[normalizeCodexProfileName(profileName)]?.model ??
|
|
51
|
+
null
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
function readProfileScalar(raw, key) {
|
|
42
56
|
const match = String(raw).match(
|
|
43
57
|
new RegExp(
|
|
@@ -51,7 +65,7 @@ function readProfileScalar(raw, key) {
|
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
/**
|
|
54
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
68
|
+
* Resolve a Codex effort profile name (e.g. "gpt6_sol_high") to the `-c key=value`
|
|
55
69
|
* config overrides that `codex exec` accepts, so the headless CLI lanes can
|
|
56
70
|
* select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
|
|
57
71
|
* `--profile X` whenever config.toml still contains an inline `[profiles.X]`
|
|
@@ -69,7 +83,7 @@ function readProfileScalar(raw, key) {
|
|
|
69
83
|
*
|
|
70
84
|
* @param {string} profileName
|
|
71
85
|
* @param {{ codexHome?: string }} [opts]
|
|
72
|
-
* @returns {string[]} e.g. ['model="gpt-
|
|
86
|
+
* @returns {string[]} e.g. ['model="gpt-6-sol"', 'model_reasoning_effort="high"']
|
|
73
87
|
*/
|
|
74
88
|
export function resolveCodexProfileConfigValues(profileName, opts = {}) {
|
|
75
89
|
if (typeof profileName !== "string" || !profileName) {
|
|
@@ -47,6 +47,7 @@ export async function publishRelease({
|
|
|
47
47
|
provenance = false,
|
|
48
48
|
allowExistingArtifacts = false,
|
|
49
49
|
pushBranch = true,
|
|
50
|
+
allowAncestor = false,
|
|
50
51
|
tagPoll = {},
|
|
51
52
|
execFileSyncFn,
|
|
52
53
|
} = {}) {
|
|
@@ -194,7 +195,19 @@ export async function publishRelease({
|
|
|
194
195
|
stdio: "pipe",
|
|
195
196
|
}),
|
|
196
197
|
).trim();
|
|
197
|
-
if (
|
|
198
|
+
if (allowAncestor) {
|
|
199
|
+
try {
|
|
200
|
+
runCommand(
|
|
201
|
+
"git",
|
|
202
|
+
["merge-base", "--is-ancestor", "HEAD", `origin/${branch}`],
|
|
203
|
+
{ cwd: rootDir, execFileSyncFn, stdio: "pipe" },
|
|
204
|
+
);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Refusing tag-only publish: HEAD ${head} is not an ancestor of origin/${branch} ${remote}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} else if (head !== remote) {
|
|
198
211
|
throw new Error(
|
|
199
212
|
`Refusing tag-only publish: HEAD ${head} != origin/${branch} ${remote} (stale checkout or unpushed ${branch})`,
|
|
200
213
|
);
|
|
@@ -235,6 +248,7 @@ export async function publishRelease({
|
|
|
235
248
|
provenance,
|
|
236
249
|
allowExistingArtifacts,
|
|
237
250
|
pushBranch,
|
|
251
|
+
allowAncestor,
|
|
238
252
|
dryRun,
|
|
239
253
|
notesPath,
|
|
240
254
|
steps: steps.map((step) => ({
|
|
@@ -260,6 +274,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
260
274
|
provenance: Boolean(args.provenance || envProvenance),
|
|
261
275
|
allowExistingArtifacts: Boolean(args["allow-existing"]),
|
|
262
276
|
pushBranch: !args["tag-only"],
|
|
277
|
+
allowAncestor: Boolean(args["allow-ancestor"]),
|
|
263
278
|
});
|
|
264
279
|
console.log(JSON.stringify(result, null, 2));
|
|
265
280
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { isSemver, ROOT } from "./lib.mjs";
|
|
6
|
+
|
|
7
|
+
function compareStableVersions(left, right) {
|
|
8
|
+
const a = left.split(".").map(BigInt);
|
|
9
|
+
const b = right.split(".").map(BigInt);
|
|
10
|
+
for (let i = 0; i < 3; i++) {
|
|
11
|
+
if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveAutoRelease({
|
|
17
|
+
eventName,
|
|
18
|
+
version,
|
|
19
|
+
channel,
|
|
20
|
+
headSha,
|
|
21
|
+
rootDir = ROOT,
|
|
22
|
+
execFileSyncFn = execFileSync,
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (eventName === "workflow_dispatch") {
|
|
25
|
+
return {
|
|
26
|
+
release: true,
|
|
27
|
+
version,
|
|
28
|
+
channel,
|
|
29
|
+
mode: "dispatch",
|
|
30
|
+
npm_tag: channel === "canary" ? "canary" : "latest",
|
|
31
|
+
sha: headSha,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (eventName !== "workflow_run" || !headSha) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Automatic release requires workflow_run and its head SHA.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const git = (args) =>
|
|
41
|
+
execFileSyncFn("git", args, { cwd: rootDir, encoding: "utf8" });
|
|
42
|
+
const releaseVersion = JSON.parse(
|
|
43
|
+
git(["show", `${headSha}:package.json`]),
|
|
44
|
+
).version;
|
|
45
|
+
if (!isSemver(releaseVersion)) {
|
|
46
|
+
throw new Error(`Invalid package.json version: ${releaseVersion}`);
|
|
47
|
+
}
|
|
48
|
+
const result = {
|
|
49
|
+
release: false,
|
|
50
|
+
version: releaseVersion,
|
|
51
|
+
channel: "stable",
|
|
52
|
+
mode: "auto",
|
|
53
|
+
npm_tag: "latest",
|
|
54
|
+
sha: headSha,
|
|
55
|
+
};
|
|
56
|
+
const parentVersion = JSON.parse(
|
|
57
|
+
git(["show", `${headSha}^1:package.json`]),
|
|
58
|
+
).version;
|
|
59
|
+
if (releaseVersion === parentVersion) {
|
|
60
|
+
return {
|
|
61
|
+
...result,
|
|
62
|
+
reason: "This commit did not change the package.json version.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// A remote snapshot avoids stale local tags and origin/main tracking refs.
|
|
66
|
+
const refs = new Map(
|
|
67
|
+
execFileSyncFn(
|
|
68
|
+
"git",
|
|
69
|
+
["ls-remote", "--refs", "origin", "refs/heads/main", "refs/tags/v*"],
|
|
70
|
+
{ cwd: rootDir, encoding: "utf8" },
|
|
71
|
+
)
|
|
72
|
+
.trim()
|
|
73
|
+
.split(/\r?\n/)
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((line) => {
|
|
76
|
+
const [sha, ref] = line.split(/\s+/);
|
|
77
|
+
return [ref, sha];
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
if (refs.has(`refs/tags/v${releaseVersion}`)) {
|
|
81
|
+
return { ...result, reason: `Already released: v${releaseVersion}.` };
|
|
82
|
+
}
|
|
83
|
+
const mainSha = refs.get("refs/heads/main");
|
|
84
|
+
if (!mainSha) throw new Error("Cannot resolve origin/main from remote refs.");
|
|
85
|
+
// main may have advanced after checkout; fetch the exact remote snapshot
|
|
86
|
+
// before checking ancestry so a missing local object cannot look like a skip.
|
|
87
|
+
git(["fetch", "--no-tags", "origin", mainSha]);
|
|
88
|
+
try {
|
|
89
|
+
git(["merge-base", "--is-ancestor", headSha, mainSha]);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error.status !== 1) throw error;
|
|
92
|
+
return { ...result, reason: "CI head is not an ancestor of origin/main." };
|
|
93
|
+
}
|
|
94
|
+
if (releaseVersion.includes("-")) {
|
|
95
|
+
return {
|
|
96
|
+
...result,
|
|
97
|
+
reason: `Prerelease ${releaseVersion} requires manual dispatch.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const stableVersions = [...refs.keys()]
|
|
102
|
+
.map((ref) => /^refs\/tags\/v(\d+\.\d+\.\d+)$/.exec(ref)?.[1])
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.sort(compareStableVersions);
|
|
105
|
+
const latest = stableVersions.at(-1);
|
|
106
|
+
if (latest && compareStableVersions(releaseVersion, latest) <= 0) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Version ${releaseVersion} must be greater than latest stable tag v${latest}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return { ...result, release: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
115
|
+
try {
|
|
116
|
+
const result = resolveAutoRelease({
|
|
117
|
+
eventName: process.env.GITHUB_EVENT_NAME,
|
|
118
|
+
version: process.env.RELEASE_VERSION,
|
|
119
|
+
channel: process.env.RELEASE_CHANNEL,
|
|
120
|
+
headSha: process.env.RELEASE_HEAD_SHA,
|
|
121
|
+
});
|
|
122
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
123
|
+
appendFileSync(
|
|
124
|
+
process.env.GITHUB_OUTPUT,
|
|
125
|
+
["release", "version", "channel", "mode", "npm_tag", "sha"]
|
|
126
|
+
.map((key) => `${key}=${result[key]}\n`)
|
|
127
|
+
.join(""),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!result.release) console.log(`::notice::${result.reason}`);
|
|
131
|
+
console.log(JSON.stringify(result, null, 2));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(`::error::${error.message}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
}
|