triflux 10.41.0 → 10.41.1
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-doctor.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tfx-doctor — triflux doctor 바로가기
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
3
5
|
process.argv = [
|
|
4
6
|
process.argv[0],
|
|
5
|
-
|
|
7
|
+
// triflux.mjs's own isMainModule() guard compares this against its real
|
|
8
|
+
// path, not this shim's — point argv[1] at triflux.mjs so it recognizes
|
|
9
|
+
// itself as the entry point instead of silently no-op'ing.
|
|
10
|
+
fileURLToPath(new URL("./triflux.mjs", import.meta.url)),
|
|
6
11
|
"doctor",
|
|
7
12
|
...process.argv.slice(2),
|
|
8
13
|
];
|
package/bin/tfx-live.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { homedir, tmpdir } from "node:os";
|
|
5
6
|
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
@@ -2368,11 +2369,19 @@ export {
|
|
|
2368
2369
|
resolveAskTransport,
|
|
2369
2370
|
};
|
|
2370
2371
|
|
|
2371
|
-
|
|
2372
|
-
process.argv[1]
|
|
2373
|
-
fileURLToPath(import.meta.url)
|
|
2372
|
+
function isMainModule() {
|
|
2373
|
+
if (!process.argv[1]) return false;
|
|
2374
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
2375
|
+
try {
|
|
2376
|
+
return realpathSync(process.argv[1]) === modulePath;
|
|
2377
|
+
} catch {
|
|
2378
|
+
// process.argv[1] doesn't resolve on disk (e.g. `node -e`) — fall back
|
|
2379
|
+
// to a non-symlink-aware comparison instead of treating it as not-main.
|
|
2380
|
+
return pathResolve(process.argv[1]) === modulePath;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2374
2383
|
|
|
2375
|
-
if (
|
|
2384
|
+
if (isMainModule()) {
|
|
2376
2385
|
main().catch((error) => {
|
|
2377
2386
|
printJson({
|
|
2378
2387
|
ok: false,
|
package/bin/tfx-setup.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tfx-setup — triflux setup 바로가기
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
3
5
|
process.argv = [
|
|
4
6
|
process.argv[0],
|
|
5
|
-
|
|
7
|
+
// triflux.mjs's own isMainModule() guard compares this against its real
|
|
8
|
+
// path, not this shim's — point argv[1] at triflux.mjs so it recognizes
|
|
9
|
+
// itself as the entry point instead of silently no-op'ing.
|
|
10
|
+
fileURLToPath(new URL("./triflux.mjs", import.meta.url)),
|
|
6
11
|
"setup",
|
|
7
12
|
...process.argv.slice(2),
|
|
8
13
|
];
|
package/package.json
CHANGED
|
@@ -165,7 +165,13 @@ describe("release governance scripts", () => {
|
|
|
165
165
|
);
|
|
166
166
|
assert.deepEqual(
|
|
167
167
|
tagOnlyPublish.steps.map((step) => step.label),
|
|
168
|
-
[
|
|
168
|
+
[
|
|
169
|
+
"git tag",
|
|
170
|
+
"git push branch",
|
|
171
|
+
"git push tag",
|
|
172
|
+
"wait for tag",
|
|
173
|
+
"gh release create",
|
|
174
|
+
],
|
|
169
175
|
);
|
|
170
176
|
|
|
171
177
|
const executed = [];
|
|
@@ -178,6 +184,9 @@ describe("release governance scripts", () => {
|
|
|
178
184
|
execFileSyncFn: (command, args) => {
|
|
179
185
|
executed.push([command, ...args].join(" "));
|
|
180
186
|
if (command === "git" && args[0] === "rev-parse") return "tag\n";
|
|
187
|
+
if (command === "git" && args[0] === "ls-remote") {
|
|
188
|
+
return "abc123\trefs/tags/v1.2.3\n";
|
|
189
|
+
}
|
|
181
190
|
if (command === "gh" && args[0] === "release") {
|
|
182
191
|
return '{"tagName":"v1.2.3"}\n';
|
|
183
192
|
}
|
|
@@ -187,7 +196,9 @@ describe("release governance scripts", () => {
|
|
|
187
196
|
assert.equal(resumedPublish.allowExistingArtifacts, true);
|
|
188
197
|
assert.deepEqual(executed, [
|
|
189
198
|
"git rev-parse --verify v1.2.3",
|
|
190
|
-
"git push origin HEAD
|
|
199
|
+
"git push origin HEAD",
|
|
200
|
+
"git push origin v1.2.3",
|
|
201
|
+
"git ls-remote --tags origin v1.2.3",
|
|
191
202
|
"gh release view v1.2.3 --json tagName",
|
|
192
203
|
]);
|
|
193
204
|
|
|
@@ -222,6 +233,41 @@ describe("release governance scripts", () => {
|
|
|
222
233
|
}
|
|
223
234
|
});
|
|
224
235
|
|
|
236
|
+
it("tag-only(CI) 모드는 preflight로 HEAD==origin/main을 검증하고 branch push를 생략한다", async () => {
|
|
237
|
+
const root = makeRepo();
|
|
238
|
+
const prevRef = process.env.GITHUB_REF_NAME;
|
|
239
|
+
process.env.GITHUB_REF_NAME = "main";
|
|
240
|
+
try {
|
|
241
|
+
assertVersionSync({ rootDir: root, fix: true });
|
|
242
|
+
const executed = [];
|
|
243
|
+
await publishRelease({
|
|
244
|
+
rootDir: root,
|
|
245
|
+
version: "1.2.3",
|
|
246
|
+
dryRun: false,
|
|
247
|
+
publishNpm: false,
|
|
248
|
+
pushBranch: false,
|
|
249
|
+
createGithubRelease: false,
|
|
250
|
+
tagPoll: { attempts: 1, sleepFn: async () => {} },
|
|
251
|
+
execFileSyncFn: (command, args) => {
|
|
252
|
+
executed.push([command, ...args].join(" "));
|
|
253
|
+
if (command === "git" && args[0] === "rev-parse") {
|
|
254
|
+
return "deadbeef\n";
|
|
255
|
+
}
|
|
256
|
+
return "";
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
assert.ok(executed.includes("git fetch origin main"));
|
|
260
|
+
assert.ok(executed.includes("git rev-parse HEAD"));
|
|
261
|
+
assert.ok(executed.includes("git rev-parse origin/main"));
|
|
262
|
+
assert.ok(executed.includes("git push origin v1.2.3"));
|
|
263
|
+
assert.ok(!executed.includes("git push origin HEAD"));
|
|
264
|
+
} finally {
|
|
265
|
+
if (prevRef === undefined) delete process.env.GITHUB_REF_NAME;
|
|
266
|
+
else process.env.GITHUB_REF_NAME = prevRef;
|
|
267
|
+
rmSync(root, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
225
271
|
it("release workflows use trusted publishing and skip duplicate tag publishes", () => {
|
|
226
272
|
const releaseWorkflow = readFileSync(
|
|
227
273
|
new URL("../../.github/workflows/release.yml", import.meta.url),
|
|
@@ -230,6 +276,7 @@ describe("release governance scripts", () => {
|
|
|
230
276
|
assert.match(releaseWorkflow, /actions:\s*write/);
|
|
231
277
|
assert.match(releaseWorkflow, /node-version:\s*24/);
|
|
232
278
|
assert.match(releaseWorkflow, /publish\.mjs.*--skip-npm.*--allow-existing/);
|
|
279
|
+
assert.match(releaseWorkflow, /publish\.mjs.*--tag-only/);
|
|
233
280
|
assert.match(
|
|
234
281
|
releaseWorkflow,
|
|
235
282
|
/gh workflow run npm-publish\.yml --ref \$\{\{ github\.ref_name \}\}/,
|
|
@@ -3,6 +3,40 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { assertVersionSync, parseArgs, ROOT, runCommand } from "./lib.mjs";
|
|
5
5
|
|
|
6
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
|
|
8
|
+
export async function waitForRemoteTag(
|
|
9
|
+
tagName,
|
|
10
|
+
{
|
|
11
|
+
rootDir = ROOT,
|
|
12
|
+
execFileSyncFn,
|
|
13
|
+
attempts = 30,
|
|
14
|
+
intervalMs = 2000,
|
|
15
|
+
sleepFn = defaultSleep,
|
|
16
|
+
} = {},
|
|
17
|
+
) {
|
|
18
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
const out = runCommand(
|
|
21
|
+
"git",
|
|
22
|
+
["ls-remote", "--tags", "origin", tagName],
|
|
23
|
+
{
|
|
24
|
+
cwd: rootDir,
|
|
25
|
+
execFileSyncFn,
|
|
26
|
+
stdio: "pipe",
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
if (String(out).includes(`refs/tags/${tagName}`)) return true;
|
|
30
|
+
} catch {
|
|
31
|
+
// Transient network/auth blips are retried below.
|
|
32
|
+
}
|
|
33
|
+
if (attempt < attempts - 1) await sleepFn(intervalMs);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Tag ${tagName} not visible on origin after ${attempts} attempts`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
export async function publishRelease({
|
|
7
41
|
version,
|
|
8
42
|
rootDir = ROOT,
|
|
@@ -12,6 +46,8 @@ export async function publishRelease({
|
|
|
12
46
|
publishNpm = true,
|
|
13
47
|
provenance = false,
|
|
14
48
|
allowExistingArtifacts = false,
|
|
49
|
+
pushBranch = true,
|
|
50
|
+
tagPoll = {},
|
|
15
51
|
execFileSyncFn,
|
|
16
52
|
} = {}) {
|
|
17
53
|
const sync = assertVersionSync({ rootDir });
|
|
@@ -73,14 +109,27 @@ export async function publishRelease({
|
|
|
73
109
|
}))
|
|
74
110
|
: []),
|
|
75
111
|
{ label: "git tag", command: "git", args: ["tag", `v${releaseVersion}`] },
|
|
76
|
-
{
|
|
77
|
-
label: "git push",
|
|
78
|
-
command: "git",
|
|
79
|
-
args: ["push", "origin", "HEAD", "--tags"],
|
|
80
|
-
},
|
|
81
112
|
];
|
|
113
|
+
if (pushBranch) {
|
|
114
|
+
steps.push({
|
|
115
|
+
label: "git push branch",
|
|
116
|
+
command: "git",
|
|
117
|
+
args: ["push", "origin", "HEAD"],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
steps.push({
|
|
121
|
+
label: "git push tag",
|
|
122
|
+
command: "git",
|
|
123
|
+
args: ["push", "origin", `v${releaseVersion}`],
|
|
124
|
+
});
|
|
82
125
|
|
|
83
126
|
if (createGithubRelease) {
|
|
127
|
+
steps.push({
|
|
128
|
+
label: "wait for tag",
|
|
129
|
+
kind: "wait-tag",
|
|
130
|
+
command: "git",
|
|
131
|
+
args: ["ls-remote", "--tags", "origin", `v${releaseVersion}`],
|
|
132
|
+
});
|
|
84
133
|
steps.push({
|
|
85
134
|
label: "gh release create",
|
|
86
135
|
command: "gh",
|
|
@@ -125,11 +174,45 @@ export async function publishRelease({
|
|
|
125
174
|
};
|
|
126
175
|
|
|
127
176
|
if (!dryRun) {
|
|
177
|
+
if (!pushBranch) {
|
|
178
|
+
const branch = process.env.GITHUB_REF_NAME || "main";
|
|
179
|
+
runCommand("git", ["fetch", "origin", branch], {
|
|
180
|
+
cwd: rootDir,
|
|
181
|
+
execFileSyncFn,
|
|
182
|
+
});
|
|
183
|
+
const head = String(
|
|
184
|
+
runCommand("git", ["rev-parse", "HEAD"], {
|
|
185
|
+
cwd: rootDir,
|
|
186
|
+
execFileSyncFn,
|
|
187
|
+
stdio: "pipe",
|
|
188
|
+
}),
|
|
189
|
+
).trim();
|
|
190
|
+
const remote = String(
|
|
191
|
+
runCommand("git", ["rev-parse", `origin/${branch}`], {
|
|
192
|
+
cwd: rootDir,
|
|
193
|
+
execFileSyncFn,
|
|
194
|
+
stdio: "pipe",
|
|
195
|
+
}),
|
|
196
|
+
).trim();
|
|
197
|
+
if (head !== remote) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Refusing tag-only publish: HEAD ${head} != origin/${branch} ${remote} (stale checkout or unpushed ${branch})`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
128
203
|
for (const step of steps) {
|
|
129
204
|
const tagName = `v${releaseVersion}`;
|
|
130
205
|
if (step.label === "git tag" && shouldSkipExistingTag(tagName)) {
|
|
131
206
|
continue;
|
|
132
207
|
}
|
|
208
|
+
if (step.kind === "wait-tag") {
|
|
209
|
+
await waitForRemoteTag(tagName, {
|
|
210
|
+
rootDir,
|
|
211
|
+
execFileSyncFn,
|
|
212
|
+
...tagPoll,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
133
216
|
if (
|
|
134
217
|
step.label === "gh release create" &&
|
|
135
218
|
shouldSkipExistingGithubRelease(tagName)
|
|
@@ -151,6 +234,7 @@ export async function publishRelease({
|
|
|
151
234
|
publishNpm,
|
|
152
235
|
provenance,
|
|
153
236
|
allowExistingArtifacts,
|
|
237
|
+
pushBranch,
|
|
154
238
|
dryRun,
|
|
155
239
|
notesPath,
|
|
156
240
|
steps: steps.map((step) => ({
|
|
@@ -175,6 +259,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
175
259
|
publishNpm: !args["skip-npm"],
|
|
176
260
|
provenance: Boolean(args.provenance || envProvenance),
|
|
177
261
|
allowExistingArtifacts: Boolean(args["allow-existing"]),
|
|
262
|
+
pushBranch: !args["tag-only"],
|
|
178
263
|
});
|
|
179
264
|
console.log(JSON.stringify(result, null, 2));
|
|
180
265
|
}
|