-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathruntime-release-identity.ts
More file actions
213 lines (198 loc) · 6.97 KB
/
Copy pathruntime-release-identity.ts
File metadata and controls
213 lines (198 loc) · 6.97 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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import assert from "node:assert/strict";
import { appendFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import * as semver from "semver";
export type ReleaseDistTag = "canary" | "latest" | "prerelease" | "unstable";
export type ReleaseMode = "dry-run" | "publish";
export type ReleaseTestPolicy = "advisory" | "required" | "skipped";
export type RuntimeReleaseChannel = "canary" | "unstable";
export interface ReleaseDispatchInputs {
distTag: ReleaseDistTag;
mode: ReleaseMode;
runtimeJson: string;
testPolicy: ReleaseTestPolicy;
version: string;
}
export interface ReleaseDispatchPlan {
kind: "direct" | "runtime";
runtimeRunId: string;
runtimeSha: string;
runtimeVersion: string;
testPolicy: ReleaseTestPolicy;
}
interface RuntimeDescriptor {
run_id: string;
sha: string;
version: string;
}
export interface RuntimeReleaseIdentity {
channel: RuntimeReleaseChannel;
runId: string;
sha: string;
version: string;
}
export function validateRuntimeReleaseIdentity(identity: RuntimeReleaseIdentity): void {
const { channel, runId, sha, version } = identity;
assert(channel === "canary" || channel === "unstable", "Invalid channel");
assert.match(runId, /^[1-9][0-9]*$/, "Runtime run_id must be a positive canonical integer");
assert.match(sha, /^[0-9a-f]{40}$/, "Runtime sha must be a lowercase full SHA");
const parsed = semver.parse(version);
assert(parsed, "Runtime version must be exact SemVer");
assert.equal(parsed.build.length, 0, "Runtime version must not contain build metadata");
assert.equal(version, parsed.version, "Runtime version must be exact SemVer");
const channelIndex = typeof parsed.prerelease[0] === "number" ? 1 : 0;
assert.equal(
parsed.prerelease[channelIndex],
channel,
`Runtime version '${version}' does not use the '${channel}' channel identifier`
);
assert.equal(
parsed.prerelease[channelIndex + 1],
`r${runId}`,
`Runtime version '${version}' does not match run_id '${runId}'`
);
assert.equal(
parsed.prerelease[channelIndex + 2],
`g${sha.slice(0, 7)}`,
`Runtime version '${version}' does not match runtime sha '${sha}'`
);
if (channel === "canary") {
assert.equal(
parsed.prerelease.length,
channelIndex + 4,
"Canary runtime version must end with exactly one signing identifier"
);
assert(
parsed.prerelease[channelIndex + 3] === "signed" ||
parsed.prerelease[channelIndex + 3] === "unsigned",
"Canary runtime version must end with signed or unsigned"
);
} else {
assert.equal(
parsed.prerelease.length,
channelIndex + 3,
"Unstable runtime version must not contain a signing or additional suffix"
);
}
}
function parseRuntimeDescriptor(value: string): RuntimeDescriptor {
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new Error("Runtime input must be valid JSON.");
}
assert(
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed),
"Runtime input must be a JSON object"
);
assert.deepEqual(
Object.keys(parsed).sort(),
["run_id", "sha", "version"],
"Runtime input must contain exactly version, sha, and run_id"
);
const runtime = parsed as Partial<RuntimeDescriptor>;
for (const name of ["version", "sha", "run_id"] as const) {
assert.equal(typeof runtime[name], "string", `Runtime ${name} must be a string`);
}
return runtime as RuntimeDescriptor;
}
export function validateReleaseDispatch(inputs: ReleaseDispatchInputs): ReleaseDispatchPlan {
assert(
inputs.distTag === "latest" ||
inputs.distTag === "prerelease" ||
inputs.distTag === "unstable" ||
inputs.distTag === "canary",
"Invalid release dist-tag"
);
assert(inputs.mode === "publish" || inputs.mode === "dry-run", "Invalid release mode");
assert(
inputs.testPolicy === "required" ||
inputs.testPolicy === "advisory" ||
inputs.testPolicy === "skipped",
"Invalid runtime E2E test policy"
);
if (inputs.runtimeJson === "") {
assert.equal(
inputs.testPolicy,
"required",
"Direct releases require the default runtime E2E test policy"
);
assert(inputs.distTag !== "canary", "Canary releases require runtime JSON");
assert(
inputs.mode !== "dry-run" || inputs.distTag === "unstable",
"Dry-run mode is supported only for canary and unstable releases"
);
return {
kind: "direct",
runtimeRunId: "",
runtimeSha: "",
runtimeVersion: "",
testPolicy: inputs.testPolicy,
};
}
assert.equal(
inputs.runtimeJson,
inputs.runtimeJson.trim(),
"Runtime input must not contain surrounding whitespace"
);
assert(
inputs.distTag === "canary" || inputs.distTag === "unstable",
"Runtime JSON is supported only for canary and unstable releases"
);
assert.equal(
inputs.version,
"",
"The direct version input cannot be combined with runtime JSON"
);
const runtime = parseRuntimeDescriptor(inputs.runtimeJson);
validateRuntimeReleaseIdentity({
channel: inputs.distTag,
runId: runtime.run_id,
sha: runtime.sha,
version: runtime.version,
});
return {
kind: "runtime",
runtimeRunId: runtime.run_id,
runtimeSha: runtime.sha,
runtimeVersion: runtime.version,
testPolicy: inputs.testPolicy,
};
}
function requiredEnvironment(name: string): string {
const value = process.env[name];
if (value === undefined || value === "") {
throw new Error(`${name} is required.`);
}
return value;
}
function main(): void {
const plan = validateReleaseDispatch({
distTag: requiredEnvironment("DIST_TAG") as ReleaseDistTag,
mode: requiredEnvironment("MODE") as ReleaseMode,
runtimeJson: process.env.RUNTIME_JSON ?? "",
testPolicy: requiredEnvironment("TEST_POLICY") as ReleaseTestPolicy,
version: process.env.VERSION_OVERRIDE ?? "",
});
appendFileSync(
requiredEnvironment("GITHUB_OUTPUT"),
[
`kind=${plan.kind}`,
`runtime_run_id=${plan.runtimeRunId}`,
`runtime_sha=${plan.runtimeSha}`,
`runtime_version=${plan.runtimeVersion}`,
`test_policy=${plan.testPolicy}`,
"",
].join("\n")
);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
try {
main();
} catch (error) {
console.error(`::error::${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
}
}