Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

g1t can be checked against GitHub. The Conformance workflow, the same file in `.g1t/workflows/` and `.github/workflows/` (a test keeps them identical), runs by hand on either; each run's report records which runner variables were set and of what kind, every key of the contexts and the event payload with its type, how the workspace is laid out, and the values outputs, `GITHUB_ENV` and `GITHUB_PATH`, multi-line outputs, `working-directory`, job outputs, the matrix, a cache round trip, an artifact round trip, `hashFiles` and the expression functions give. `node scripts/conformance-diff.mjs github.json g1t.json` lists what GitHub has that g1t lacks or has otherwise, and what only g1t has. It reads no secrets. CONTRIBUTING says how to run it, and CI runs its tests.

Claude-Session:
https://claude.ai/code/session_01PnwTRSNaC7vEHTAuBzjhrn
Claudeagent and Claudeagentcommitted Parentfdde3acBrowse files
9 files+487−00/9 viewed
+2−0
8989 run: npm test --workspaces --if-present
9090 - name: The deploy tool's tests
9191 run: npm run test:deploy
92+ - name: The conformance report's tests
93+ run: npm run test:conformance
9294 - name: The ops scripts' tests
9395 run: npm run test:ops
9496 - name: The manifest matches every wrangler.jsonc
+118−0
1+# The same workflow on GitHub and on g1t: this file and
2+# .github/workflows/conformance.yml are kept identical (a test says so).
3+# Run it by hand on both, then compare what each job saw with
4+# `node scripts/conformance-diff.mjs github.json g1t.json`; see
5+# CONTRIBUTING.md, "Checking g1t against GitHub". It changes nothing,
6+# reads no secrets and never runs on its own.
7+name: Conformance
8+
9+on:
10+ workflow_dispatch:
11+
12+permissions:
13+ contents: read
14+
15+jobs:
16+ first:
17+ name: First
18+ runs-on: ubuntu-latest
19+ timeout-minutes: 10
20+ outputs:
21+ value: ${{ steps.out.outputs.value }}
22+ steps:
23+ - id: out
24+ run: echo "value=hello" >> "$GITHUB_OUTPUT"
25+ - name: Something to cache and to pass on
26+ run: |
27+ mkdir -p conformance-cache conformance-artifact
28+ echo "cached" > conformance-cache/file.txt
29+ echo "passed on" > conformance-artifact/file.txt
30+ - uses: actions/cache/save@v4
31+ with:
32+ path: conformance-cache
33+ key: conformance-${{ github.run_id }}-${{ github.run_attempt }}
34+ - uses: actions/upload-artifact@v4
35+ with:
36+ name: conformance-artifact
37+ path: conformance-artifact
38+ retention-days: 1
39+
40+ report:
41+ name: Report (${{ matrix.n }})
42+ needs: first
43+ runs-on: ubuntu-latest
44+ timeout-minutes: 10
45+ strategy:
46+ matrix:
47+ n: [1]
48+ steps:
49+ - uses: actions/checkout@v5
50+ - name: Environment and path for later steps
51+ run: |
52+ echo "FROM_ENV=set" >> "$GITHUB_ENV"
53+ mkdir -p "$RUNNER_TEMP/conformance-bin"
54+ printf '#!/bin/sh\necho on-path\n' > "$RUNNER_TEMP/conformance-bin/conformance-tool"
55+ chmod +x "$RUNNER_TEMP/conformance-bin/conformance-tool"
56+ echo "$RUNNER_TEMP/conformance-bin" >> "$GITHUB_PATH"
57+ - id: later
58+ name: What a later step sees
59+ run: |
60+ echo "tool=$(conformance-tool)" >> "$GITHUB_OUTPUT"
61+ {
62+ echo "text<<CONFORMANCE_EOF"
63+ echo "line one"
64+ echo "line two"
65+ echo "CONFORMANCE_EOF"
66+ } >> "$GITHUB_OUTPUT"
67+ - id: folder
68+ working-directory: conformance
69+ run: echo "name=$(basename "$PWD")" >> "$GITHUB_OUTPUT"
70+ - id: restore
71+ uses: actions/cache/restore@v4
72+ with:
73+ path: conformance-cache
74+ key: conformance-${{ github.run_id }}-${{ github.run_attempt }}
75+ - uses: actions/download-artifact@v4
76+ with:
77+ name: conformance-artifact
78+ path: downloaded
79+ - id: files
80+ run: |
81+ echo "cached=$(cat conformance-cache/file.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"
82+ echo "artifact=$(cat downloaded/file.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"
83+ - name: Report
84+ env:
85+ CTX_GITHUB: ${{ toJSON(github) }}
86+ CTX_RUNNER: ${{ toJSON(runner) }}
87+ CTX_JOB: ${{ toJSON(job) }}
88+ CTX_STRATEGY: ${{ toJSON(strategy) }}
89+ CTX_MATRIX: ${{ toJSON(matrix) }}
90+ CTX_STEPS: ${{ toJSON(steps) }}
91+ CTX_NEEDS: ${{ toJSON(needs) }}
92+ CHECK_NEEDS_OUTPUT: ${{ needs.first.outputs.value }}
93+ CHECK_ENV_FROM_FILE: ${{ env.FROM_ENV }}
94+ CHECK_PATH_FROM_FILE: ${{ steps.later.outputs.tool }}
95+ CHECK_MULTILINE_OUTPUT: ${{ steps.later.outputs.text }}
96+ CHECK_WORKING_DIRECTORY: ${{ steps.folder.outputs.name }}
97+ CHECK_CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
98+ CHECK_CACHE_CONTENT: ${{ steps.files.outputs.cached }}
99+ CHECK_ARTIFACT_CONTENT: ${{ steps.files.outputs.artifact }}
100+ CHECK_FORMAT: ${{ format('{0}-{1}-{{2}}', 'a', 1) }}
101+ CHECK_CONTAINS: ${{ contains(fromJSON('["a","b"]'), 'b') }}
102+ CHECK_STARTS_WITH: ${{ startsWith('Hello', 'he') }}
103+ CHECK_JOIN: ${{ join(fromJSON('["a","b","c"]'), '+') }}
104+ CHECK_TO_JSON: ${{ toJSON(fromJSON('{"a":[1,2],"b":null}')) }}
105+ CHECK_EQUALITY: ${{ 1 == '1' }} ${{ 'A' == 'a' }} ${{ null == '' }}
106+ CHECK_FALSY_OR: ${{ '' || 'fallback' }}
107+ CHECK_HASH_FILES: ${{ hashFiles('conformance/report.mjs') }}
108+ CHECK_SUCCESS: ${{ success() }}
109+ CHECK_MATRIX: ${{ matrix.n }}
110+ CHECK_JOB_TOTAL: ${{ strategy.job-total }}
111+ CHECK_EVENT_NAME: ${{ github.event_name }}
112+ CHECK_RUNNER_OS: ${{ runner.os }}
113+ run: node conformance/report.mjs conformance.json
114+ - uses: actions/upload-artifact@v4
115+ with:
116+ name: conformance-report
117+ path: conformance.json
118+ retention-days: 7
+118−0
1+# The same workflow on GitHub and on g1t: this file and
2+# .github/workflows/conformance.yml are kept identical (a test says so).
3+# Run it by hand on both, then compare what each job saw with
4+# `node scripts/conformance-diff.mjs github.json g1t.json`; see
5+# CONTRIBUTING.md, "Checking g1t against GitHub". It changes nothing,
6+# reads no secrets and never runs on its own.
7+name: Conformance
8+
9+on:
10+ workflow_dispatch:
11+
12+permissions:
13+ contents: read
14+
15+jobs:
16+ first:
17+ name: First
18+ runs-on: ubuntu-latest
19+ timeout-minutes: 10
20+ outputs:
21+ value: ${{ steps.out.outputs.value }}
22+ steps:
23+ - id: out
24+ run: echo "value=hello" >> "$GITHUB_OUTPUT"
25+ - name: Something to cache and to pass on
26+ run: |
27+ mkdir -p conformance-cache conformance-artifact
28+ echo "cached" > conformance-cache/file.txt
29+ echo "passed on" > conformance-artifact/file.txt
30+ - uses: actions/cache/save@v4
31+ with:
32+ path: conformance-cache
33+ key: conformance-${{ github.run_id }}-${{ github.run_attempt }}
34+ - uses: actions/upload-artifact@v4
35+ with:
36+ name: conformance-artifact
37+ path: conformance-artifact
38+ retention-days: 1
39+
40+ report:
41+ name: Report (${{ matrix.n }})
42+ needs: first
43+ runs-on: ubuntu-latest
44+ timeout-minutes: 10
45+ strategy:
46+ matrix:
47+ n: [1]
48+ steps:
49+ - uses: actions/checkout@v5
50+ - name: Environment and path for later steps
51+ run: |
52+ echo "FROM_ENV=set" >> "$GITHUB_ENV"
53+ mkdir -p "$RUNNER_TEMP/conformance-bin"
54+ printf '#!/bin/sh\necho on-path\n' > "$RUNNER_TEMP/conformance-bin/conformance-tool"
55+ chmod +x "$RUNNER_TEMP/conformance-bin/conformance-tool"
56+ echo "$RUNNER_TEMP/conformance-bin" >> "$GITHUB_PATH"
57+ - id: later
58+ name: What a later step sees
59+ run: |
60+ echo "tool=$(conformance-tool)" >> "$GITHUB_OUTPUT"
61+ {
62+ echo "text<<CONFORMANCE_EOF"
63+ echo "line one"
64+ echo "line two"
65+ echo "CONFORMANCE_EOF"
66+ } >> "$GITHUB_OUTPUT"
67+ - id: folder
68+ working-directory: conformance
69+ run: echo "name=$(basename "$PWD")" >> "$GITHUB_OUTPUT"
70+ - id: restore
71+ uses: actions/cache/restore@v4
72+ with:
73+ path: conformance-cache
74+ key: conformance-${{ github.run_id }}-${{ github.run_attempt }}
75+ - uses: actions/download-artifact@v4
76+ with:
77+ name: conformance-artifact
78+ path: downloaded
79+ - id: files
80+ run: |
81+ echo "cached=$(cat conformance-cache/file.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"
82+ echo "artifact=$(cat downloaded/file.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"
83+ - name: Report
84+ env:
85+ CTX_GITHUB: ${{ toJSON(github) }}
86+ CTX_RUNNER: ${{ toJSON(runner) }}
87+ CTX_JOB: ${{ toJSON(job) }}
88+ CTX_STRATEGY: ${{ toJSON(strategy) }}
89+ CTX_MATRIX: ${{ toJSON(matrix) }}
90+ CTX_STEPS: ${{ toJSON(steps) }}
91+ CTX_NEEDS: ${{ toJSON(needs) }}
92+ CHECK_NEEDS_OUTPUT: ${{ needs.first.outputs.value }}
93+ CHECK_ENV_FROM_FILE: ${{ env.FROM_ENV }}
94+ CHECK_PATH_FROM_FILE: ${{ steps.later.outputs.tool }}
95+ CHECK_MULTILINE_OUTPUT: ${{ steps.later.outputs.text }}
96+ CHECK_WORKING_DIRECTORY: ${{ steps.folder.outputs.name }}
97+ CHECK_CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
98+ CHECK_CACHE_CONTENT: ${{ steps.files.outputs.cached }}
99+ CHECK_ARTIFACT_CONTENT: ${{ steps.files.outputs.artifact }}
100+ CHECK_FORMAT: ${{ format('{0}-{1}-{{2}}', 'a', 1) }}
101+ CHECK_CONTAINS: ${{ contains(fromJSON('["a","b"]'), 'b') }}
102+ CHECK_STARTS_WITH: ${{ startsWith('Hello', 'he') }}
103+ CHECK_JOIN: ${{ join(fromJSON('["a","b","c"]'), '+') }}
104+ CHECK_TO_JSON: ${{ toJSON(fromJSON('{"a":[1,2],"b":null}')) }}
105+ CHECK_EQUALITY: ${{ 1 == '1' }} ${{ 'A' == 'a' }} ${{ null == '' }}
106+ CHECK_FALSY_OR: ${{ '' || 'fallback' }}
107+ CHECK_HASH_FILES: ${{ hashFiles('conformance/report.mjs') }}
108+ CHECK_SUCCESS: ${{ success() }}
109+ CHECK_MATRIX: ${{ matrix.n }}
110+ CHECK_JOB_TOTAL: ${{ strategy.job-total }}
111+ CHECK_EVENT_NAME: ${{ github.event_name }}
112+ CHECK_RUNNER_OS: ${{ runner.os }}
113+ run: node conformance/report.mjs conformance.json
114+ - uses: actions/upload-artifact@v4
115+ with:
116+ name: conformance-report
117+ path: conformance.json
118+ retention-days: 7
+28−0
7979 - Look at what you changed in a browser. Screenshots catch what type
8080 checks do not.
8181
82+## Checking g1t against GitHub
83+
84+g1t runs GitHub Actions workflows as GitHub does. The Conformance
85+workflow is how that is checked: `.g1t/workflows/conformance.yml` and
86+`.github/workflows/conformance.yml` are the same file (a test in
87+`crates/actions` fails when they differ), and each run's
88+`conformance-report` artifact records what its jobs saw: which `GITHUB_*`
89+and `RUNNER_*` variables were set and of what kind, every key of the
90+contexts and the event and its type, how the workspace is laid out, and
91+the values of outputs, files, caches, artifacts and expressions that
92+should be the same everywhere. It reads no secrets and runs only by hand.
93+
94+1. Run it on both, at the same commit: **Actions → Conformance → Run
95+ workflow** on GitHub and on g1t.sh.
96+2. Download each run's `conformance-report` artifact, and name the two
97+ files `github.json` and `g1t.json`.
98+3. Compare them:
99+
100+ ```sh
101+ node scripts/conformance-diff.mjs github.json g1t.json
102+ ```
103+
104+It lists what GitHub has that g1t lacks or has otherwise, then what only
105+g1t has, and exits 1 while there are differences. A difference is a bug in
106+g1t, or belongs in [What g1t can't do yet](https://docs.g1t.sh/about/limitations/).
107+Add a step and a `CHECK_` value to the workflow for anything new g1t
108+should match.
109+
82110 ## Deploying
83111
84112 Pushes to `main` deploy themselves: `.g1t/workflows/deploy.yml` runs
+113−0
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+
14+import { readFileSync, writeFileSync } from "node:fs";
15+import { relative } from "node:path";
16+
17+/** A JSON value's shape: every key path with its type, arrays by their first item. */
18+export 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. */
30+export 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. */
42+export 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. */
53+export 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. */
74+function 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+
82+export 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+
109+if (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+}
+42−0
1+import assert from "node:assert/strict";
2+import { test } from "node:test";
3+
4+import { kind, layout, shape, variables } from "./report.mjs";
5+
6+test("a value's shape is its key paths and types, not its values", () => {
7+ assert.deepEqual(shape({ a: 1, b: { c: "x", d: [ { e: true } ] }, f: null }), {
8+ a: "number",
9+ b: "object",
10+ "b.c": "string",
11+ "b.d": "array",
12+ "b.d[]": "object",
13+ "b.d[].e": "boolean",
14+ f: "null",
15+ });
16+});
17+
18+test("variables are kept by kind, and only GitHub's", () => {
19+ assert.deepEqual(
20+ variables({ GITHUB_SHA: "a".repeat(40), GITHUB_RUN_ID: "42", GITHUB_SERVER_URL: "https://g1t.sh", RUNNER_TEMP: "/home/runner/work/_temp", CI: "true", PATH: "/bin", SECRET: "x" }),
21+ { GITHUB_SHA: "sha", GITHUB_RUN_ID: "integer", GITHUB_SERVER_URL: "url", RUNNER_TEMP: "path", CI: "boolean" },
22+ );
23+ assert.equal(kind("refs/heads/main"), "ref");
24+ assert.equal(kind(""), "empty");
25+});
26+
27+test("the layout says where the workspace is, as GitHub lays it out", () => {
28+ const on = layout({ RUNNER_WORKSPACE: "/home/runner/work/web", GITHUB_WORKSPACE: "/home/runner/work/web/web", RUNNER_TEMP: "/home/runner/work/_temp" });
29+ assert.equal(on.workspace_under_runner_workspace, true);
30+ assert.equal(on.workspace_is_repo_twice, true);
31+ assert.equal(layout({ GITHUB_WORKSPACE: "/home/runner/work/repo" }).workspace_is_repo_twice, false);
32+});
33+
34+test("the diff names what g1t lacks or has differently, and what it adds", async () => {
35+ const { compare } = await import("../scripts/conformance-diff.mjs");
36+ const github = { variables: { GITHUB_SHA: "sha", ImageOS: "text" }, layout: { a: true }, contexts: { github: { sha: "string", "event.ref": "string" } }, event: { ref: "string" }, checks: { format: "a-1-{2}" } };
37+ const g1t = { variables: { GITHUB_SHA: "sha", G1T: "boolean" }, layout: { a: false }, contexts: { github: { sha: "string", "event.ref": "number" } }, event: { ref: "string" }, checks: { format: "a-1-{2}" } };
38+ const found = compare(github, g1t);
39+ assert.deepEqual(found.missing, ["variable ImageOS (text)"]);
40+ assert.deepEqual(found.different, ["layout a: GitHub true, g1t false", 'github. event.ref: GitHub "string", g1t "number"']);
41+ assert.deepEqual(found.extra, ["variable G1T (boolean)"]);
42+});
+12−0
337337 let job = base.jobs.iter().find(|j| j.id == "build").unwrap();
338338 assert_eq!(job.runs_on, json!(["self-hosted", "docker"]));
339339 }
340+
341+#[test]
342+fn the_conformance_workflow_is_the_same_on_github_and_on_g1t() {
343+ let root = workflows_dir().parent().unwrap().parent().unwrap().to_path_buf();
344+ let ours = std::fs::read_to_string(root.join(".g1t/workflows/conformance.yml")).unwrap();
345+ let theirs = std::fs::read_to_string(root.join(".github/workflows/conformance.yml")).unwrap();
346+ assert_eq!(ours, theirs, "copy .g1t/workflows/conformance.yml over .github/workflows/conformance.yml");
347+ // It runs only by hand, and reads nothing it could change.
348+ let workflow = read("conformance.yml");
349+ assert_eq!(workflow.triggers.iter().map(|t| t.event.as_str()).collect::<Vec<_>>(), ["workflow_dispatch"]);
350+ assert!(!ours.contains("secrets."));
351+}
+1−0
1515 "deploy": "npm run deploy -w @g1t/web",
1616 "test:deploy": "node --test \"scripts/deploy/*.test.mjs\"",
1717 "test:ops": "node --test \"scripts/ops/*.test.mjs\"",
18+ "test:conformance": "node --test \"conformance/*.test.mjs\"",
1819 "test:release": "node --test \"scripts/desktop-release.test.mjs\" \"scripts/mobile-release.test.mjs\""
1920 },
2021 "devDependencies": {
+53−0
1+#!/usr/bin/env node
2+// Compares what the Conformance workflow (.g1t/workflows/conformance.yml)
3+// saw on GitHub with what it saw on g1t: each run's `conformance-report`
4+// artifact, conformance.json. What GitHub has and g1t lacks, or has of
5+// another type, and every check whose value differs, is a difference, and
6+// the exit code is 1. What g1t has beyond GitHub is listed, as g1t's own.
7+//
8+// node scripts/conformance-diff.mjs github.json g1t.json
9+
10+import { readFileSync } from "node:fs";
11+
12+/** The differences between `github`'s and `g1t`'s reports, by section. */
13+export function compare(github, g1t) {
14+ const found = { missing: [], different: [], extra: [] };
15+ const mapping = (section, theirs = {}, ours = {}) => {
16+ for (const [key, value] of Object.entries(theirs)) {
17+ if (!(key in ours)) found.missing.push(`${section} ${key} (${value})`);
18+ else if (ours[key] !== value) found.different.push(`${section} ${key}: GitHub ${JSON.stringify(value)}, g1t ${JSON.stringify(ours[key])}`);
19+ }
20+ for (const [key, value] of Object.entries(ours)) {
21+ if (!(key in theirs)) found.extra.push(`${section} ${key} (${value})`);
22+ }
23+ };
24+ mapping("variable", github.variables, g1t.variables);
25+ mapping("layout", github.layout, g1t.layout);
26+ for (const name of new Set([...Object.keys(github.contexts ?? {}), ...Object.keys(g1t.contexts ?? {})])) {
27+ mapping(`${name}.`, github.contexts?.[name], g1t.contexts?.[name]);
28+ }
29+ mapping("event", github.event ?? {}, g1t.event ?? {});
30+ mapping("check", github.checks, g1t.checks);
31+ return found;
32+}
33+
34+if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
35+ const [githubFile, g1tFile] = process.argv.slice(2);
36+ if (!githubFile || !g1tFile) {
37+ console.error("usage: node scripts/conformance-diff.mjs github.json g1t.json");
38+ process.exit(2);
39+ }
40+ const read = (file) => JSON.parse(readFileSync(file, "utf8"));
41+ const found = compare(read(githubFile), read(g1tFile));
42+ const section = (title, items) => {
43+ if (items.length === 0) return;
44+ console.log(`${title} (${items.length})`);
45+ for (const item of items) console.log(` ${item}`);
46+ };
47+ section("On GitHub, not on g1t", found.missing);
48+ section("Different on g1t", found.different);
49+ section("Only on g1t", found.extra);
50+ const failing = found.missing.length + found.different.length;
51+ console.log(failing === 0 ? "g1t matches GitHub." : `${failing} differences from GitHub.`);
52+ process.exit(failing === 0 ? 0 : 1);
53+}