Skip to content
113 linesCodeBlameRaw
1// What a job looks like from inside, by shape, for comparing a run on
2// GitHub with the same run on g1t (scripts/conformance-diff.mjs). Values
3// that differ between hosts by nature (ids, times, hosts, paths) are kept
4// as their shape: which variables are set, which keys the event and the
5// contexts have and of what JSON type, and how paths sit relative to one
6// another. Run by .g1t/workflows/conformance.yml and its copy in
7// .github/workflows/; see CONTRIBUTING.md.
8//
9// node conformance/report.mjs <out.json>
10// with CTX_<NAME> set to toJSON of each context to compare by shape
11// (github, runner, job, strategy, matrix, steps, needs), and CHECK_<name>
12// to each value the steps and expressions should give alike everywhere.
13
14import { readFileSync, writeFileSync } from "node:fs";
15import { relative } from "node:path";
16
17/** A JSON value's shape: every key path with its type, arrays by their first item. */
18export function shape(value, prefix = "", into = {}) {
19 const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
20 if (prefix) into[prefix] = type;
21 if (type === "object") {
22 for (const [key, inner] of Object.entries(value)) shape(inner, prefix ? `${prefix}.${key}` : key, into);
23 } else if (type === "array" && value.length > 0) {
24 shape(value[0], `${prefix}[]`, into);
25 }
26 return into;
27}
28
29/** What kind of text a variable holds, without its value. */
30export function kind(text) {
31 if (text === "") return "empty";
32 if (/^(true|false)$/.test(text)) return "boolean";
33 if (/^\d+$/.test(text)) return "integer";
34 if (/^[0-9a-f]{40}$/.test(text)) return "sha";
35 if (/^https?:\/\//.test(text)) return "url";
36 if (/^(\/|[A-Za-z]:\\)/.test(text)) return "path";
37 if (/^refs\//.test(text)) return "ref";
38 return "text";
39}
40
41/** The variables GitHub's runner sets, by kind; others are left out. */
42export function variables(env) {
43 const out = {};
44 for (const [name, value] of Object.entries(env)) {
45 if (/^(GITHUB_|RUNNER_|ACTIONS_)/.test(name) && !name.startsWith("CTX_") || ["CI", "ImageOS", "ImageVersion", "HOME"].includes(name)) {
46 out[name] = kind(value ?? "");
47 }
48 }
49 return out;
50}
51
52/** How the job's folders sit relative to one another. */
53export function layout(env) {
54 const at = (name) => env[name] ?? "";
55 return {
56 workspace_under_runner_workspace: at("GITHUB_WORKSPACE") !== "" && !relative(at("RUNNER_WORKSPACE"), at("GITHUB_WORKSPACE")).startsWith(".."),
57 workspace_is_repo_twice: (() => {
58 const parts = at("GITHUB_WORKSPACE").split(/[\\/]/).filter(Boolean);
59 return parts.length >= 2 && parts[parts.length - 1] === parts[parts.length - 2];
60 })(),
61 temp_under_runner: at("RUNNER_TEMP") !== "" && at("RUNNER_TEMP").includes("runner"),
62 event_file_exists: (() => {
63 try {
64 readFileSync(at("GITHUB_EVENT_PATH"));
65 return true;
66 } catch {
67 return false;
68 }
69 })(),
70 };
71}
72
73/** The variables starting with `prefix`, by the rest of their names, lowercased. */
74function prefixed(env, prefix) {
75 return Object.fromEntries(
76 Object.entries(env)
77 .filter(([name]) => name.startsWith(prefix))
78 .map(([name, value]) => [name.slice(prefix.length).toLowerCase(), value ?? ""]),
79 );
80}
81
82export function report(env) {
83 const contexts = Object.fromEntries(
84 Object.entries(prefixed(env, "CTX_")).map(([name, text]) => {
85 try {
86 return [name, JSON.parse(text)];
87 } catch {
88 return [name, text];
89 }
90 }),
91 );
92 const event = (() => {
93 try {
94 return JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, "utf8"));
95 } catch {
96 return null;
97 }
98 })();
99 return {
100 variables: variables(env),
101 layout: layout(env),
102 contexts: Object.fromEntries(Object.entries(contexts).map(([name, value]) => [name, shape(value)])),
103 event: event ? shape(event) : null,
104 // What earlier steps saw, compared by value: these should be the same everywhere.
105 checks: prefixed(env, "CHECK_"),
106 };
107}
108
109if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
110 const out = process.argv[2] ?? "conformance.json";
111 writeFileSync(out, `${JSON.stringify(report(process.env), null, 2)}\n`);
112 console.log(`Wrote ${out}`);
113}