-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathresolve-release-source.test.mjs
More file actions
156 lines (149 loc) · 4.93 KB
/
Copy pathresolve-release-source.test.mjs
File metadata and controls
156 lines (149 loc) · 4.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { resolveReleaseSource } from "./resolve-release-source.mjs";
test("release sources must be commits from main's history", async (t) => {
const parent = fileURLToPath(
new URL("../target/release-source-tests/", import.meta.url),
);
fs.mkdirSync(parent, { recursive: true });
const repositoryPath = fs.mkdtempSync(path.join(parent, "repository-"));
t.after(() =>
fs.rmSync(repositoryPath, { recursive: true, force: true, maxRetries: 3 }),
);
const git = (...args) =>
execFileSync("git", args, {
cwd: repositoryPath,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
git("init", "--initial-branch=main");
git("config", "user.name", "Release source fixture");
git("config", "user.email", "fixture@example.invalid");
git("config", "commit.gpgSign", "false");
git("config", "tag.gpgSign", "false");
const trailer =
"Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>";
git(
"commit",
"--allow-empty",
"-m",
"Previous release source",
"-m",
trailer,
);
const previous = git("rev-parse", "HEAD");
git("commit", "--allow-empty", "-m", "Current main source", "-m", trailer);
const current = git("rev-parse", "HEAD");
git("update-ref", "refs/remotes/origin/main", current);
git("switch", "--create", "unmerged", previous);
git("commit", "--allow-empty", "-m", "Unmerged source", "-m", trailer);
const unmerged = git("rev-parse", "HEAD");
git("switch", "main");
git(
"tag",
"--annotate",
"release-fixture",
previous,
"-m",
"Annotated fixture",
);
const annotatedTag = git("rev-parse", "refs/tags/release-fixture");
const resolve = (sourceSha, workflowRef = "refs/heads/main") =>
resolveReleaseSource({ sourceSha, workflowRef, repositoryPath });
await t.test("accepts the current main commit", () => {
assert.equal(resolve(current), current);
});
await t.test(
"preserves an earlier main commit for an independent retry",
() => {
assert.equal(resolve(previous), previous);
assert.equal(git("rev-parse", "HEAD"), current);
},
);
await t.test("normalizes a full uppercase SHA", () => {
assert.equal(resolve(previous.toUpperCase()), previous);
});
await t.test(
"rejects an unmerged commit even when it is present locally",
() => {
assert.throws(() => resolve(unmerged), /not in main's history/);
},
);
await t.test("rejects a dispatch from another branch", () => {
assert.throws(
() => resolve(previous, "refs/heads/unmerged"),
/dispatched from main/,
);
});
await t.test("rejects tag object IDs", () => {
assert.throws(
() => resolve(annotatedTag),
/must identify a commit, not a tag/,
);
});
await t.test("rejects tree object IDs", () => {
assert.throws(() => resolve(git("rev-parse", "HEAD^{tree}")), /not a tree/);
});
await t.test("rejects an unknown commit", () => {
assert.throws(() => resolve("f".repeat(40)));
});
for (const source of [
"main",
"HEAD",
"release-fixture",
previous.slice(0, 8),
`${previous}\n`,
`${previous}; echo unsafe`,
"g".repeat(40),
"",
]) {
await t.test(`rejects non-SHA input ${JSON.stringify(source)}`, () => {
assert.throws(() => resolve(source), /full 40-character commit SHA/);
});
}
const script = fileURLToPath(
new URL("./resolve-release-source.mjs", import.meta.url),
);
const run = (sourceSha) =>
spawnSync(process.execPath, [script], {
cwd: repositoryPath,
encoding: "utf8",
env: {
...process.env,
REQUESTED_SOURCE: sourceSha,
WORKFLOW_REF: "refs/heads/main",
},
});
await t.test("emits only a validated commit through the CLI", () => {
const result = run(previous);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout.trim(), previous);
});
await t.test(
"fails without emitting a checkout target for an unmerged commit",
() => {
const result = run(unmerged);
assert.equal(result.status, 1);
assert.equal(result.stdout, "");
assert.match(result.stderr, /not in main's history/);
assert.equal(git("rev-parse", "HEAD"), current);
},
);
await t.test(
"fails closed if the trusted main reference is unavailable",
() => {
git("update-ref", "-d", "refs/remotes/origin/main");
assert.throws(
() => resolve(previous),
/Could not validate main's history/,
);
},
);
});