@rightkit/git 0.2.23 → 0.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.mjs +334 -55
- package/manifest.schema.json +30 -2
- package/manifest.test.mjs +115 -1
- package/ownership-registration.mjs +33 -0
- package/ownership-registration.test.mjs +20 -0
- package/pack-smoke.test.mjs +8 -8
- package/package.json +1 -1
- package/render.test.mjs +436 -22
- package/sync.test.mjs +3 -0
- package/templates/ci.yml +1 -1
- package/templates/package-smoke.yml +1 -0
- package/templates/profiles/rust-hybrid/ci.yml +2 -0
- package/templates/profiles/rust-hybrid/release-candidate.yml +119 -7
- package/templates/publish-npm.yml +1 -0
- package/templates/publish-package-managers.yml +1 -0
- package/templates/qualification.yml +1 -0
- package/templates/release-candidate.yml +1 -0
package/manifest.schema.json
CHANGED
|
@@ -84,6 +84,19 @@
|
|
|
84
84
|
"type": "string",
|
|
85
85
|
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
|
86
86
|
}
|
|
87
|
+
,"ownership": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"required": ["roots"],
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"properties": {
|
|
92
|
+
"roots": {
|
|
93
|
+
"type": "array",
|
|
94
|
+
"minItems": 1,
|
|
95
|
+
"uniqueItems": true,
|
|
96
|
+
"items": { "type": "string", "pattern": "^(?:\\.|(?!.*\\.\\.)(?!.*\\\\)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*)$" }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
87
100
|
,"profile": { "enum": ["node", "rust-hybrid"] },
|
|
88
101
|
"rustCache": {
|
|
89
102
|
"type": "object",
|
|
@@ -105,11 +118,21 @@
|
|
|
105
118
|
},
|
|
106
119
|
"candidate": {
|
|
107
120
|
"type": "object",
|
|
108
|
-
"required": ["buildScript", "checkScript", "artifactRoot", "os"],
|
|
121
|
+
"required": ["buildScript", "checkScript", "admissionScript", "stageSummaryScript", "artifactRoot", "os"],
|
|
109
122
|
"additionalProperties": false,
|
|
110
123
|
"properties": {
|
|
111
124
|
"buildScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
112
125
|
"checkScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
126
|
+
"admissionScript": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$",
|
|
129
|
+
"description": "Runs in the unconditional `admission` job (workflow_dispatch envelope validation). Required for every release-candidate lane, signed or not."
|
|
130
|
+
},
|
|
131
|
+
"stageSummaryScript": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$",
|
|
134
|
+
"description": "Runs (init/finalize) in the `candidate` job and, when configured, the windows-sign/macos-sign jobs. Required for every release-candidate lane, signed or not."
|
|
135
|
+
},
|
|
113
136
|
"artifactRoot": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" },
|
|
114
137
|
"os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
|
|
115
138
|
"targets": {
|
|
@@ -130,9 +153,14 @@
|
|
|
130
153
|
"signMacos": { "type": "boolean" },
|
|
131
154
|
"releaseChain": {
|
|
132
155
|
"type": "object",
|
|
133
|
-
"required": ["windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
|
|
156
|
+
"required": ["evidenceVerificationScript", "windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
|
|
134
157
|
"additionalProperties": false,
|
|
135
158
|
"properties": {
|
|
159
|
+
"evidenceVerificationScript": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$",
|
|
162
|
+
"description": "Runs in the `publish` job before the publish step, verifying the aggregated same-run signed/qualification evidence."
|
|
163
|
+
},
|
|
136
164
|
"windowsFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
137
165
|
"macosFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
138
166
|
"installedQualificationScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
package/manifest.test.mjs
CHANGED
|
@@ -22,6 +22,13 @@ test("accepts a minimal valid manifest", () => {
|
|
|
22
22
|
assert.deepEqual(errors, []);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("ownership scan registration is explicit and supports nested package roots", () => {
|
|
26
|
+
assert.equal(validateManifest({ ...VALID_MINIMAL, ownership: { roots: ["engine", "apps/client"] } }).valid, true);
|
|
27
|
+
for (const roots of [[], ["../outside"], ["src\\escape"], [""], ["engine", "engine"]]) {
|
|
28
|
+
assert.equal(validateManifest({ ...VALID_MINIMAL, ownership: { roots } }).valid, false, JSON.stringify(roots));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
25
32
|
test("rust-hybrid supports ci plus a configured unsigned release-candidate lane", () => {
|
|
26
33
|
const valid = { ...VALID_MINIMAL, profile: "rust-hybrid", gate: { path: "scripts/gate.sh" } };
|
|
27
34
|
assert.equal(validateManifest(valid).valid, true);
|
|
@@ -32,7 +39,14 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
|
|
|
32
39
|
const candidate = {
|
|
33
40
|
...valid,
|
|
34
41
|
lanes: ["ci", "release-candidate"],
|
|
35
|
-
candidate: {
|
|
42
|
+
candidate: {
|
|
43
|
+
buildScript: "ci:candidate",
|
|
44
|
+
checkScript: "ci:candidate-check",
|
|
45
|
+
admissionScript: "release:admit",
|
|
46
|
+
stageSummaryScript: "release:stage-summary",
|
|
47
|
+
artifactRoot: "legion-unsigned",
|
|
48
|
+
os: ["ubuntu-latest"],
|
|
49
|
+
},
|
|
36
50
|
};
|
|
37
51
|
assert.equal(validateManifest(candidate).valid, true);
|
|
38
52
|
assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, signWindows: true } }).valid, false);
|
|
@@ -53,6 +67,7 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
|
|
|
53
67
|
candidate: {
|
|
54
68
|
...windowsCandidate.candidate,
|
|
55
69
|
releaseChain: {
|
|
70
|
+
evidenceVerificationScript: "release:verify-evidence",
|
|
56
71
|
windowsFinalizeScript: "release:finalize:win",
|
|
57
72
|
installedQualificationScript: "release:qualify:installed",
|
|
58
73
|
publishScript: "release:publish:qualified",
|
|
@@ -68,6 +83,103 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
|
|
|
68
83
|
assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, releaseChain: { ...releaseChain.candidate.releaseChain, publishSecrets: ["TOKEN", "TOKEN"] } } }).valid, false);
|
|
69
84
|
});
|
|
70
85
|
|
|
86
|
+
// Part B fix: admissionScript & stageSummaryScript were moved OUT of the
|
|
87
|
+
// optional candidate.releaseChain object and onto candidate directly,
|
|
88
|
+
// because the admission job and the candidate job's stage-summary
|
|
89
|
+
// init/finalize steps are unconditional parts of the rendered
|
|
90
|
+
// release-candidate template — present for every release-candidate lane,
|
|
91
|
+
// signed or not. This test proves each is independently required with a
|
|
92
|
+
// specific, actionable error, and that the full releaseChain contract
|
|
93
|
+
// (now minus those two promoted fields) is validated key-by-key.
|
|
94
|
+
test("rust-hybrid release-candidate requires admissionScript & stageSummaryScript independent of signing", () => {
|
|
95
|
+
const base = {
|
|
96
|
+
...VALID_MINIMAL,
|
|
97
|
+
profile: "rust-hybrid",
|
|
98
|
+
gate: { path: "scripts/gate.sh" },
|
|
99
|
+
lanes: ["ci", "release-candidate"],
|
|
100
|
+
candidate: {
|
|
101
|
+
buildScript: "ci:candidate",
|
|
102
|
+
checkScript: "ci:candidate-check",
|
|
103
|
+
admissionScript: "release:admit",
|
|
104
|
+
stageSummaryScript: "release:stage-summary",
|
|
105
|
+
artifactRoot: "widgets-unsigned",
|
|
106
|
+
os: ["ubuntu-latest"],
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
assert.equal(validateManifest(base).valid, true, JSON.stringify(validateManifest(base).errors));
|
|
110
|
+
|
|
111
|
+
const { admissionScript, ...withoutAdmission } = base.candidate;
|
|
112
|
+
const missingAdmission = validateManifest({ ...base, candidate: withoutAdmission });
|
|
113
|
+
assert.equal(missingAdmission.valid, false);
|
|
114
|
+
assert.ok(missingAdmission.errors.includes("candidate.admissionScript must be one safe package script name"));
|
|
115
|
+
|
|
116
|
+
const { stageSummaryScript, ...withoutStageSummary } = base.candidate;
|
|
117
|
+
const missingStageSummary = validateManifest({ ...base, candidate: withoutStageSummary });
|
|
118
|
+
assert.equal(missingStageSummary.valid, false);
|
|
119
|
+
assert.ok(missingStageSummary.errors.includes("candidate.stageSummaryScript must be one safe package script name"));
|
|
120
|
+
|
|
121
|
+
// Neither field is accepted inside releaseChain any more.
|
|
122
|
+
const windows = {
|
|
123
|
+
...base,
|
|
124
|
+
matrix: { ...base.matrix, os: ["ubuntu-latest", "windows-2025"] },
|
|
125
|
+
candidate: { ...base.candidate, os: ["windows-2025"], signWindows: true },
|
|
126
|
+
};
|
|
127
|
+
const misplaced = validateManifest({
|
|
128
|
+
...windows,
|
|
129
|
+
candidate: {
|
|
130
|
+
...windows.candidate,
|
|
131
|
+
releaseChain: {
|
|
132
|
+
admissionScript: "release:admit",
|
|
133
|
+
evidenceVerificationScript: "release:verify-evidence",
|
|
134
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
135
|
+
installedQualificationScript: "release:qualify:installed",
|
|
136
|
+
publishScript: "release:publish:qualified",
|
|
137
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
assert.equal(misplaced.valid, false);
|
|
142
|
+
assert.ok(misplaced.errors.includes("unknown candidate.releaseChain field: admissionScript"));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Part B fix: evidenceVerificationScript is the one new script that DOES
|
|
146
|
+
// stay inside releaseChain (it runs in the `publish` job, verifying
|
|
147
|
+
// aggregated same-run evidence before the publish step) — required
|
|
148
|
+
// whenever releaseChain is configured, with its own specific error.
|
|
149
|
+
test("candidate.releaseChain requires evidenceVerificationScript with a specific error", () => {
|
|
150
|
+
const windows = {
|
|
151
|
+
...VALID_MINIMAL,
|
|
152
|
+
profile: "rust-hybrid",
|
|
153
|
+
gate: { path: "scripts/gate.sh" },
|
|
154
|
+
lanes: ["ci", "release-candidate"],
|
|
155
|
+
matrix: { ...VALID_MINIMAL.matrix, os: ["ubuntu-latest", "windows-2025"] },
|
|
156
|
+
candidate: {
|
|
157
|
+
buildScript: "ci:candidate",
|
|
158
|
+
checkScript: "ci:candidate-check",
|
|
159
|
+
admissionScript: "release:admit",
|
|
160
|
+
stageSummaryScript: "release:stage-summary",
|
|
161
|
+
artifactRoot: "widgets-unsigned",
|
|
162
|
+
os: ["windows-2025"],
|
|
163
|
+
signWindows: true,
|
|
164
|
+
releaseChain: {
|
|
165
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
166
|
+
installedQualificationScript: "release:qualify:installed",
|
|
167
|
+
publishScript: "release:publish:qualified",
|
|
168
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const result = validateManifest(windows);
|
|
173
|
+
assert.equal(result.valid, false);
|
|
174
|
+
assert.ok(result.errors.includes("candidate.releaseChain.evidenceVerificationScript must be one safe package script name"));
|
|
175
|
+
|
|
176
|
+
const fixed = {
|
|
177
|
+
...windows,
|
|
178
|
+
candidate: { ...windows.candidate, releaseChain: { ...windows.candidate.releaseChain, evidenceVerificationScript: "release:verify-evidence" } },
|
|
179
|
+
};
|
|
180
|
+
assert.equal(validateManifest(fixed).valid, true, JSON.stringify(validateManifest(fixed).errors));
|
|
181
|
+
});
|
|
182
|
+
|
|
71
183
|
test("candidate targets permit native multi-architecture matrices independent of normal CI runners", () => {
|
|
72
184
|
const multiArchitecture = {
|
|
73
185
|
...VALID_MINIMAL,
|
|
@@ -77,6 +189,8 @@ test("candidate targets permit native multi-architecture matrices independent of
|
|
|
77
189
|
candidate: {
|
|
78
190
|
buildScript: "ci:candidate",
|
|
79
191
|
checkScript: "ci:candidate-check",
|
|
192
|
+
admissionScript: "release:admit",
|
|
193
|
+
stageSummaryScript: "release:stage-summary",
|
|
80
194
|
artifactRoot: "widgets-unsigned",
|
|
81
195
|
os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
|
|
82
196
|
targets: [
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/** Normalize HTTPS, SSH, and SCP GitHub remotes to one immutable repository key. */
|
|
4
|
+
export function normalizeRepositoryIdentity(value) {
|
|
5
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("ownership registration requires a repository identity");
|
|
6
|
+
let text = value.trim();
|
|
7
|
+
const scp = text.match(/^(?:[^@/]+@)?([^:]+):([^/]+\/[^/]+)$/);
|
|
8
|
+
if (scp) text = `https://${scp[1]}/${scp[2]}`;
|
|
9
|
+
let url;
|
|
10
|
+
try { url = new URL(text); } catch { throw new Error("ownership repository identity must be a Git remote URL"); }
|
|
11
|
+
if (!["https:", "ssh:"].includes(url.protocol) || (url.username && !(url.protocol === "ssh:" && url.username === "git")) || url.password || url.search || url.hash) throw new Error("unsupported or credential-bearing ownership repository identity");
|
|
12
|
+
const host = url.hostname.toLowerCase();
|
|
13
|
+
const parts = url.pathname.replace(/\.git$/i, "").split("/").filter(Boolean);
|
|
14
|
+
if (host !== "github.com" || parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9_.-]+$/.test(part))) throw new Error("ownership repository identity must be owner/repository on github.com");
|
|
15
|
+
return `https://${host}/${parts[0].toLowerCase()}/${parts[1].toLowerCase()}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Resolve immutable OS-owned roots for one checkout; repo declarations can only narrow reporting, never authority. */
|
|
19
|
+
export function protectedOwnershipRoots(trust, { repositoryIdentity, scanRoot, repoRoot }) {
|
|
20
|
+
if (!trust || !Array.isArray(trust.repositories)) throw new Error("protected ownership repository registry is missing");
|
|
21
|
+
const wanted = normalizeRepositoryIdentity(repositoryIdentity);
|
|
22
|
+
const matches = trust.repositories.filter((entry) => normalizeRepositoryIdentity(entry.repositoryIdentity) === wanted);
|
|
23
|
+
if (matches.length !== 1) throw new Error(matches.length ? "protected ownership registry has duplicate repository identities" : `repository is not registered for ownership enforcement: ${wanted}`);
|
|
24
|
+
const entry = matches[0];
|
|
25
|
+
if (!["public", "private"].includes(entry.visibility) || !Array.isArray(entry.roots) || !entry.roots.length) throw new Error("protected ownership registration is malformed");
|
|
26
|
+
const roots = entry.roots.map((root) => {
|
|
27
|
+
if (typeof root !== "string" || !root || root.includes("\\") || root.startsWith("/") || (root !== "." && root.split("/").some((p) => !p || p === "." || p === "..")) || /[*?]/.test(root)) throw new Error("protected ownership roots must be exact relative paths");
|
|
28
|
+
return root;
|
|
29
|
+
});
|
|
30
|
+
const relative = path.relative(repoRoot, scanRoot).replaceAll("\\", "/") || ".";
|
|
31
|
+
if (!roots.includes(relative)) throw new Error(`scan root is not registered for ${wanted}: ${relative}`);
|
|
32
|
+
return { repositoryIdentity: wanted, visibility: entry.visibility, roots };
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { normalizeRepositoryIdentity, protectedOwnershipRoots } from "./ownership-registration.mjs";
|
|
4
|
+
|
|
5
|
+
test("repository identity normalization collapses HTTPS, SSH, and SCP GitHub remotes", () => {
|
|
6
|
+
const expected = "https://github.com/bogusyogi/coderight";
|
|
7
|
+
for (const remote of ["https://github.com/bogusyogi/CodeRight.git", "ssh://git@github.com/bogusyogi/coderight.git", "git@github.com:bogusyogi/coderight.git"]) {
|
|
8
|
+
assert.equal(normalizeRepositoryIdentity(remote), expected);
|
|
9
|
+
}
|
|
10
|
+
assert.throws(() => normalizeRepositoryIdentity("https://user:secret@github.com/a/b.git"), /credential-bearing/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("protected registry is authoritative for exact nested roots and visibility", () => {
|
|
14
|
+
const trust = { repositories: [{ repositoryIdentity: "https://github.com/bogusyogi/coderight", roots: ["engine", "apps/ui"], visibility: "private" }] };
|
|
15
|
+
assert.deepEqual(protectedOwnershipRoots(trust, { repositoryIdentity: "git@github.com:bogusyogi/coderight.git", scanRoot: "C:/repo/engine", repoRoot: "C:/repo" }), {
|
|
16
|
+
repositoryIdentity: "https://github.com/bogusyogi/coderight", visibility: "private", roots: ["engine", "apps/ui"],
|
|
17
|
+
});
|
|
18
|
+
assert.throws(() => protectedOwnershipRoots(trust, { repositoryIdentity: "https://github.com/bogusyogi/coderight", scanRoot: "C:/repo/src", repoRoot: "C:/repo" }), /not registered/);
|
|
19
|
+
assert.throws(() => protectedOwnershipRoots({ repositories: [] }, { repositoryIdentity: "https://github.com/a/b", scanRoot: ".", repoRoot: "." }), /not registered/);
|
|
20
|
+
});
|
package/pack-smoke.test.mjs
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// publish and runs the extracted bin with `node` — `node --test` run from
|
|
4
4
|
// the source tree never notices a missing directory because the source
|
|
5
5
|
// tree has every file regardless of what `files` declares. This test packs
|
|
6
|
-
// for real (
|
|
6
|
+
// for real (pnpm pack), extracts to a temp dir, and executes the built bin's
|
|
7
7
|
// --help and --version from inside that extracted tree only.
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -24,17 +24,17 @@ function run(command, args, options) {
|
|
|
24
24
|
test("packaged tarball: right-git --help and --version run clean from the published files only", () => {
|
|
25
25
|
const workDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-git-pack-"));
|
|
26
26
|
try {
|
|
27
|
-
// shell:true
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// shell:true resolves pnpm.cmd on Windows; args are fixed literals.
|
|
28
|
+
const packResult = run("pnpm", ["pack", "--pack-destination", workDir], { cwd: packageRoot, shell: true });
|
|
29
|
+
assert.equal(packResult.status, 0, `pnpm pack failed: ${packResult.stderr}`);
|
|
30
|
+
const archive = readdirSync(workDir).find((name) => name.endsWith(".tgz"));
|
|
31
|
+
assert.ok(archive, `pnpm pack did not create a tarball: ${packResult.stdout}`);
|
|
32
32
|
|
|
33
33
|
// Run tar FROM the temp dir on the bare filename so no absolute "C:\..."
|
|
34
34
|
// path reaches tar — that colon is the only reason GNU tar's --force-local
|
|
35
35
|
// was ever needed, and avoiding it keeps this portable to bsdtar (the
|
|
36
36
|
// Windows built-in) and any CI host, not just a GNU-tar toolchain.
|
|
37
|
-
const extractResult = run("tar", ["-xzf",
|
|
37
|
+
const extractResult = run("tar", ["-xzf", archive], { cwd: workDir });
|
|
38
38
|
assert.equal(extractResult.status, 0, `tar extract failed: ${extractResult.stderr}`);
|
|
39
39
|
// npm tarballs always nest content under a top-level "package/" directory.
|
|
40
40
|
const extractedPackageDir = path.join(workDir, "package");
|
package/package.json
CHANGED