Skip to content

Commit 623f60f

Browse files
authored
fix: align release tag format and batch template with patchloom CLI (#98)
The patchloom CLI changed its release tag format from v{version} to patchloom-v{version} starting with v0.1.1 (cargo-dist convention). The managed installer was producing 404 URLs for all downloads. - Update normalizeReleaseVersion to strip patchloom-v prefix - Update buildManagedInstallReleaseAssets to use patchloom-v tag format - Change batch template from JSON to line-oriented format matching what patchloom batch actually expects - Update parseBatchOperationCount to count non-empty lines - Open batch template as plaintext instead of JSON - Update all affected tests Closes #97 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent 8803886 commit 623f60f

4 files changed

Lines changed: 62 additions & 70 deletions

File tree

src/commands/batchApply.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,19 @@ import { formatCliOutput } from "../util.js";
55
import { getPatchloomLog } from "../logging/outputChannel.js";
66
import { activeWorkspaceFolder } from "../workspace/readiness.js";
77

8-
export const BATCH_TEMPLATE = JSON.stringify({
9-
operations: [
10-
{ op: "replace", file: "", from: "", to: "" },
11-
{ op: "tidy", file: "", fixes: ["ensure-final-newline"] },
12-
{ op: "doc-set", file: "", selector: "", value: "" }
13-
]
14-
}, null, 2) + "\n";
8+
export const BATCH_TEMPLATE = [
9+
"replace src/example.ts \"old text\" \"new text\"",
10+
"doc.set package.json version \"2.0.0\"",
11+
"tidy.fix src/example.ts",
12+
""
13+
].join("\n");
1514

1615
export function buildBatchTemplate(): string {
1716
return BATCH_TEMPLATE;
1817
}
1918

2019
export function parseBatchOperationCount(plan: string): number {
21-
try {
22-
const parsed = JSON.parse(plan);
23-
if (Array.isArray(parsed?.operations)) {
24-
return parsed.operations.length;
25-
}
26-
} catch {
27-
// Invalid JSON
28-
}
29-
return 0;
20+
return plan.split("\n").filter((line) => line.trim().length > 0).length;
3021
}
3122

3223
export async function batchApply(): Promise<void> {
@@ -62,7 +53,7 @@ export async function batchApply(): Promise<void> {
6253

6354
const binaryPath = status.binaryPath;
6455
const doc = await vscode.workspace.openTextDocument({
65-
language: "json",
56+
language: "plaintext",
6657
content: BATCH_TEMPLATE
6758
});
6859
await vscode.window.showTextDocument(doc, { preview: false });

src/install/managed.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,11 @@ export function buildManagedInstallReleaseAssets(
300300
const normalizedVersion = normalizeReleaseVersion(version);
301301
const paths = resolveManagedInstallPaths(PATCHLOOM_MANAGED_INSTALL_DIR, normalizedVersion, target);
302302
return {
303-
tagName: `v${normalizedVersion}`,
303+
tagName: `patchloom-v${normalizedVersion}`,
304304
archiveFileName: paths.archiveFileName,
305305
checksumFileName: paths.checksumFileName,
306-
archiveDownloadUrl: `https://github.com/${repo}/releases/download/v${normalizedVersion}/${paths.archiveFileName}`,
307-
checksumDownloadUrl: `https://github.com/${repo}/releases/download/v${normalizedVersion}/${paths.checksumFileName}`
306+
archiveDownloadUrl: `https://github.com/${repo}/releases/download/patchloom-v${normalizedVersion}/${paths.archiveFileName}`,
307+
checksumDownloadUrl: `https://github.com/${repo}/releases/download/patchloom-v${normalizedVersion}/${paths.checksumFileName}`
308308
};
309309
}
310310

@@ -645,7 +645,7 @@ function classifyInstallFailureStage(error: unknown): { stage: ManagedInstallFai
645645
}
646646

647647
export function normalizeReleaseVersion(version: string): string {
648-
return version.replace(/^v/, "").trim();
648+
return version.replace(/^patchloom-v/, "").replace(/^v/, "").trim();
649649
}
650650

651651
function managedBinaryName(platform: NodeJS.Platform): string {

test/unit/batchApply.test.ts

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,75 +2,66 @@ import assert from "node:assert/strict";
22
import test from "node:test";
33
import { buildBatchTemplate, parseBatchOperationCount } from "../../src/commands/batchApply.js";
44

5-
test("buildBatchTemplate returns valid JSON with three operations", () => {
5+
test("buildBatchTemplate returns line-oriented format with three operations", () => {
66
const template = buildBatchTemplate();
7-
const parsed = JSON.parse(template);
7+
const lines = template.split("\n").filter((line) => line.trim().length > 0);
88

9-
assert.ok(Array.isArray(parsed.operations));
10-
assert.equal(parsed.operations.length, 3);
11-
assert.equal(parsed.operations[0].op, "replace");
12-
assert.equal(parsed.operations[1].op, "tidy");
13-
assert.equal(parsed.operations[2].op, "doc-set");
9+
assert.equal(lines.length, 3);
10+
assert.ok(lines[0].startsWith("replace "), "first line should be a replace operation");
11+
assert.ok(lines[1].startsWith("doc.set "), "second line should be a doc.set operation");
12+
assert.ok(lines[2].startsWith("tidy.fix "), "third line should be a tidy.fix operation");
1413
});
1514

1615
test("buildBatchTemplate ends with a newline", () => {
1716
const template = buildBatchTemplate();
1817
assert.ok(template.endsWith("\n"));
1918
});
2019

21-
test("parseBatchOperationCount counts operations in valid plan", () => {
22-
const plan = JSON.stringify({
23-
operations: [
24-
{ op: "replace", file: "a.txt", from: "x", to: "y" },
25-
{ op: "tidy", file: "b.txt", fixes: [] }
26-
]
27-
});
20+
test("parseBatchOperationCount counts non-empty lines", () => {
21+
const plan = [
22+
'replace a.txt "x" "y"',
23+
'doc.set b.json key "val"'
24+
].join("\n");
2825

2926
assert.equal(parseBatchOperationCount(plan), 2);
3027
});
3128

32-
test("parseBatchOperationCount returns 0 for invalid JSON", () => {
33-
assert.equal(parseBatchOperationCount("not json"), 0);
29+
test("parseBatchOperationCount returns 0 for empty input", () => {
30+
assert.equal(parseBatchOperationCount(""), 0);
3431
});
3532

36-
test("parseBatchOperationCount returns 0 for missing operations", () => {
37-
assert.equal(parseBatchOperationCount('{"other": "data"}'), 0);
33+
test("parseBatchOperationCount returns 0 for whitespace-only input", () => {
34+
assert.equal(parseBatchOperationCount(" \n \n"), 0);
3835
});
3936

40-
test("parseBatchOperationCount returns 0 for empty operations array", () => {
41-
assert.equal(parseBatchOperationCount('{"operations": []}'), 0);
37+
test("parseBatchOperationCount ignores blank lines between operations", () => {
38+
const plan = 'replace a.txt "x" "y"\n\ndoc.set b.json key "v"\n';
39+
assert.equal(parseBatchOperationCount(plan), 2);
4240
});
4341

44-
test("parseBatchOperationCount returns 0 when operations is not an array", () => {
45-
assert.equal(parseBatchOperationCount('{"operations": "not-an-array"}'), 0);
46-
assert.equal(parseBatchOperationCount('{"operations": 42}'), 0);
47-
assert.equal(parseBatchOperationCount('{"operations": null}'), 0);
42+
test("parseBatchOperationCount counts a single operation", () => {
43+
assert.equal(parseBatchOperationCount('tidy.fix src/main.ts'), 1);
4844
});
4945

5046
// --- #34: snapshot-style template tests ---
5147

52-
test("buildBatchTemplate replace operation has required fields", () => {
53-
const parsed = JSON.parse(buildBatchTemplate());
54-
const replace = parsed.operations[0];
55-
assert.equal(replace.op, "replace");
56-
assert.ok("file" in replace, "replace operation missing 'file'");
57-
assert.ok("from" in replace, "replace operation missing 'from'");
58-
assert.ok("to" in replace, "replace operation missing 'to'");
48+
test("buildBatchTemplate replace line has file and quoted arguments", () => {
49+
const lines = buildBatchTemplate().split("\n");
50+
const replaceLine = lines.find((l) => l.startsWith("replace "));
51+
assert.ok(replaceLine, "template should contain a replace line");
52+
assert.match(replaceLine, /replace \S+ ".+" ".+"/, "replace should have file and two quoted args");
5953
});
6054

61-
test("buildBatchTemplate tidy operation has required fields", () => {
62-
const parsed = JSON.parse(buildBatchTemplate());
63-
const tidy = parsed.operations[1];
64-
assert.equal(tidy.op, "tidy");
65-
assert.ok("file" in tidy, "tidy operation missing 'file'");
66-
assert.ok(Array.isArray(tidy.fixes), "tidy operation 'fixes' should be an array");
55+
test("buildBatchTemplate doc.set line has file, selector, and quoted value", () => {
56+
const lines = buildBatchTemplate().split("\n");
57+
const docSetLine = lines.find((l) => l.startsWith("doc.set "));
58+
assert.ok(docSetLine, "template should contain a doc.set line");
59+
assert.match(docSetLine, /doc\.set \S+ \S+ ".+"/, "doc.set should have file, selector, and quoted value");
6760
});
6861

69-
test("buildBatchTemplate doc-set operation has required fields", () => {
70-
const parsed = JSON.parse(buildBatchTemplate());
71-
const docSet = parsed.operations[2];
72-
assert.equal(docSet.op, "doc-set");
73-
assert.ok("file" in docSet, "doc-set operation missing 'file'");
74-
assert.ok("selector" in docSet, "doc-set operation missing 'selector'");
75-
assert.ok("value" in docSet, "doc-set operation missing 'value'");
62+
test("buildBatchTemplate tidy.fix line has a file path", () => {
63+
const lines = buildBatchTemplate().split("\n");
64+
const tidyLine = lines.find((l) => l.startsWith("tidy.fix "));
65+
assert.ok(tidyLine, "template should contain a tidy.fix line");
66+
assert.match(tidyLine, /tidy\.fix \S+/, "tidy.fix should have a file path");
7667
});

test/unit/binary.test.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,16 +224,16 @@ test("resolveManagedInstallPaths uses cargo-dist style archive names", () => {
224224
assert.equal(paths.binaryPath, path.join("/managed/install", "0.1.0", "managed-bin", "patchloom"));
225225
});
226226

227-
test("buildManagedInstallReleaseAssets builds archive and checksum urls", () => {
227+
test("buildManagedInstallReleaseAssets builds archive and checksum urls with patchloom-v tag", () => {
228228
const target = detectManagedInstallTarget("linux", "x64");
229229
assert.ok(target);
230230
const release = buildManagedInstallReleaseAssets("v0.1.0", target);
231231

232-
assert.equal(release.tagName, "v0.1.0");
232+
assert.equal(release.tagName, "patchloom-v0.1.0");
233233
assert.equal(release.archiveFileName, "patchloom-x86_64-unknown-linux-gnu.tar.xz");
234234
assert.equal(release.checksumFileName, "patchloom-x86_64-unknown-linux-gnu.tar.xz.sha256");
235-
assert.match(release.archiveDownloadUrl, /patchloom-x86_64-unknown-linux-gnu\.tar\.xz$/);
236-
assert.match(release.checksumDownloadUrl, /patchloom-x86_64-unknown-linux-gnu\.tar\.xz\.sha256$/);
235+
assert.match(release.archiveDownloadUrl, /\/patchloom-v0\.1\.0\/patchloom-x86_64-unknown-linux-gnu\.tar\.xz$/);
236+
assert.match(release.checksumDownloadUrl, /\/patchloom-v0\.1\.0\/patchloom-x86_64-unknown-linux-gnu\.tar\.xz\.sha256$/);
237237
});
238238

239239
test("parseManagedInstallChecksumFile accepts common sha256 sidecar formats", () => {
@@ -629,9 +629,19 @@ test("resolvePatchloomStatusWithInputs surfaces persisted managed install failur
629629
}
630630
});
631631

632-
test("normalizeReleaseVersion removes a leading v", () => {
632+
test("normalizeReleaseVersion strips v and patchloom-v prefixes", () => {
633633
assert.equal(normalizeReleaseVersion("v0.1.0"), "0.1.0");
634634
assert.equal(normalizeReleaseVersion("0.1.0"), "0.1.0");
635+
assert.equal(normalizeReleaseVersion("patchloom-v0.1.2"), "0.1.2");
636+
});
637+
638+
test("buildManagedInstallReleaseAssets normalizes patchloom-v prefixed versions", () => {
639+
const target = detectManagedInstallTarget("darwin", "arm64");
640+
assert.ok(target);
641+
const release = buildManagedInstallReleaseAssets("patchloom-v0.1.2", target);
642+
643+
assert.equal(release.tagName, "patchloom-v0.1.2");
644+
assert.match(release.archiveDownloadUrl, /\/patchloom-v0\.1\.2\/patchloom-aarch64-apple-darwin\.tar\.xz$/);
635645
});
636646

637647
test("resolvePatchloomStatusWithInputs falls back to a managed install when present", async () => {

0 commit comments

Comments
 (0)