@rightkit/git 0.2.1 → 0.2.3
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/README.md +1 -1
- package/manifest.mjs +34 -2
- package/manifest.schema.json +10 -0
- package/manifest.test.mjs +9 -1
- package/package.json +1 -1
- package/render.test.mjs +23 -3
- package/templates/profiles/rust-hybrid/ci.yml +15 -6
- package/templates/profiles/rust-hybrid/release-candidate.yml +55 -0
package/README.md
CHANGED
|
@@ -38,4 +38,4 @@ Node builtins only; no runtime dependencies.
|
|
|
38
38
|
|
|
39
39
|
## Profiles
|
|
40
40
|
|
|
41
|
-
`profile` defaults to `node`. `rust-hybrid`
|
|
41
|
+
`profile` defaults to `node`. `rust-hybrid` requires `ci`, accepts optional `release-candidate`, installs pinned Node plus `rustup stable`, then runs product-owned package scripts. Candidate configuration is `{ buildScript, checkScript, artifactRoot }`; RightGit passes an isolated `RIGHT_GIT_ARTIFACT_ROOT`, checks it, & uploads it as an unsigned handoff. It never signs, finalizes, publishes releases, or receives signing credentials.
|
package/manifest.mjs
CHANGED
|
@@ -57,8 +57,10 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
57
57
|
"templateVersion",
|
|
58
58
|
"profile",
|
|
59
59
|
"gate",
|
|
60
|
+
"candidate",
|
|
60
61
|
]);
|
|
61
62
|
const GATE_PATH_RE = /^(?!.*\.\.)(?!.*\s)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
63
|
+
const PACKAGE_SCRIPT_RE = /^[A-Za-z0-9][A-Za-z0-9:_-]*$/;
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* Validate a parsed manifest object against the right-git manifest contract.
|
|
@@ -123,7 +125,9 @@ export function validateManifest(manifest) {
|
|
|
123
125
|
const profile = manifest.profile === undefined ? "node" : manifest.profile;
|
|
124
126
|
if (profile !== "node" && profile !== "rust-hybrid") errors.push("profile must be 'node' or 'rust-hybrid'");
|
|
125
127
|
if (profile === "rust-hybrid") {
|
|
126
|
-
if (lanes.
|
|
128
|
+
if (lanes.some((lane) => lane !== "ci" && lane !== "release-candidate") || !lanes.includes("ci")) {
|
|
129
|
+
errors.push("profile 'rust-hybrid' requires lane 'ci' and supports optional lane 'release-candidate'");
|
|
130
|
+
}
|
|
127
131
|
if (!manifest.gate || typeof manifest.gate !== "object" || Array.isArray(manifest.gate) || Object.keys(manifest.gate).some((key) => key !== "path") || typeof manifest.gate.path !== "string" || !GATE_PATH_RE.test(manifest.gate.path)) {
|
|
128
132
|
errors.push("profile 'rust-hybrid' requires gate.path with a safe relative path");
|
|
129
133
|
}
|
|
@@ -131,6 +135,31 @@ export function validateManifest(manifest) {
|
|
|
131
135
|
errors.push("gate is only valid for profile 'rust-hybrid'");
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
const candidate = manifest.candidate;
|
|
139
|
+
if (candidate !== undefined) {
|
|
140
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
141
|
+
errors.push("candidate must be an object");
|
|
142
|
+
} else {
|
|
143
|
+
for (const key of Object.keys(candidate)) {
|
|
144
|
+
if (!["buildScript", "checkScript", "artifactRoot"].includes(key)) errors.push(`unknown candidate field: ${key}`);
|
|
145
|
+
}
|
|
146
|
+
for (const key of ["buildScript", "checkScript"]) {
|
|
147
|
+
if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
|
|
148
|
+
errors.push(`candidate.${key} must be one safe package script name`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (typeof candidate.artifactRoot !== "string" || !GATE_PATH_RE.test(candidate.artifactRoot)) {
|
|
152
|
+
errors.push("candidate.artifactRoot must be one safe relative path");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (profile === "rust-hybrid" && lanes.includes("release-candidate") && candidate === undefined) {
|
|
157
|
+
errors.push("rust-hybrid release-candidate lane requires candidate { buildScript, checkScript, artifactRoot }");
|
|
158
|
+
}
|
|
159
|
+
if (profile !== "rust-hybrid" && candidate !== undefined) {
|
|
160
|
+
errors.push("candidate is only valid for profile 'rust-hybrid'");
|
|
161
|
+
}
|
|
162
|
+
|
|
134
163
|
const publish = manifest.publish;
|
|
135
164
|
if (publish !== undefined) {
|
|
136
165
|
if (publish === null || typeof publish !== "object" || Array.isArray(publish)) {
|
|
@@ -233,7 +262,7 @@ function parsePackageManager(spec) {
|
|
|
233
262
|
}[name];
|
|
234
263
|
const runPrefix = { pnpm: "pnpm run", npm: "npm run", yarn: "yarn run" }[name];
|
|
235
264
|
const setup = {
|
|
236
|
-
pnpm: ` - uses: pnpm/action-setup@
|
|
265
|
+
pnpm: ` - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10\n with:\n version: ${version}`,
|
|
237
266
|
npm: "",
|
|
238
267
|
yarn: ` - run: corepack enable && corepack prepare yarn@${version} --activate`,
|
|
239
268
|
}[name];
|
|
@@ -283,6 +312,9 @@ export function buildRenderContext(manifest, repoPath) {
|
|
|
283
312
|
ARTIFACT_PREFIX: artifactPrefix,
|
|
284
313
|
TEMPLATE_VERSION: manifest.templateVersion,
|
|
285
314
|
GATE_PATH: manifest.gate?.path,
|
|
315
|
+
CANDIDATE_BUILD_SCRIPT: manifest.candidate?.buildScript,
|
|
316
|
+
CANDIDATE_CHECK_SCRIPT: manifest.candidate?.checkScript,
|
|
317
|
+
CANDIDATE_ARTIFACT_ROOT: manifest.candidate?.artifactRoot,
|
|
286
318
|
};
|
|
287
319
|
|
|
288
320
|
if (manifest.publish?.npm) {
|
package/manifest.schema.json
CHANGED
|
@@ -85,6 +85,16 @@
|
|
|
85
85
|
"gate": {
|
|
86
86
|
"type": "object", "required": ["path"], "additionalProperties": false,
|
|
87
87
|
"properties": { "path": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" } }
|
|
88
|
+
},
|
|
89
|
+
"candidate": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"required": ["buildScript", "checkScript", "artifactRoot"],
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"buildScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
95
|
+
"checkScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
96
|
+
"artifactRoot": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" }
|
|
97
|
+
}
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
}
|
package/manifest.test.mjs
CHANGED
|
@@ -16,13 +16,21 @@ test("accepts a minimal valid manifest", () => {
|
|
|
16
16
|
assert.deepEqual(errors, []);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("rust-hybrid
|
|
19
|
+
test("rust-hybrid supports ci plus a configured unsigned release-candidate lane", () => {
|
|
20
20
|
const valid = { ...VALID_MINIMAL, profile: "rust-hybrid", gate: { path: "scripts/gate.sh" } };
|
|
21
21
|
assert.equal(validateManifest(valid).valid, true);
|
|
22
22
|
for (const path of ["/tmp/gate.sh", "../gate.sh", "scripts/gate sh", "${{ secrets.X }}", "scripts/gate;sh"]) {
|
|
23
23
|
assert.equal(validateManifest({ ...valid, gate: { path } }).valid, false, path);
|
|
24
24
|
}
|
|
25
25
|
assert.equal(validateManifest({ ...valid, lanes: ["ci", "qualification"] }).valid, false);
|
|
26
|
+
const candidate = {
|
|
27
|
+
...valid,
|
|
28
|
+
lanes: ["ci", "release-candidate"],
|
|
29
|
+
candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "legion-unsigned" },
|
|
30
|
+
};
|
|
31
|
+
assert.equal(validateManifest(candidate).valid, true);
|
|
32
|
+
assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, buildScript: "echo unsafe" } }).valid, false);
|
|
33
|
+
assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, artifactRoot: "../escape" } }).valid, false);
|
|
26
34
|
});
|
|
27
35
|
|
|
28
36
|
test("rejects non-object input", () => {
|
package/package.json
CHANGED
package/render.test.mjs
CHANGED
|
@@ -45,12 +45,32 @@ test("every known lane has a template file and renders without an unresolved pla
|
|
|
45
45
|
test("rust-hybrid ci renders its pinned package manager and gate", () => {
|
|
46
46
|
const manifest = { ...FULL_MANIFEST, lanes: ["ci"], profile: "rust-hybrid", gate: { path: "scripts/gate.sh" }, templateVersion: "1.1.0" };
|
|
47
47
|
const content = renderTemplate(fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8"), buildRenderContext(manifest, "/repos/widgets"));
|
|
48
|
-
assert.match(content, /uses: pnpm\/action-setup@
|
|
48
|
+
assert.match(content, /uses: pnpm\/action-setup@[a-f0-9]{40}/);
|
|
49
49
|
assert.match(content, /version: 11\.24\.0/);
|
|
50
|
-
assert.match(content, /
|
|
50
|
+
assert.match(content, /toolchain: 1\.98\.0/);
|
|
51
51
|
assert.match(content, /bash \.\/scripts\/gate\.sh/);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test("rust-hybrid release candidate renders product scripts, artifact handoff, and no signing secret", () => {
|
|
55
|
+
const manifest = {
|
|
56
|
+
...FULL_MANIFEST,
|
|
57
|
+
lanes: ["ci", "release-candidate"],
|
|
58
|
+
profile: "rust-hybrid",
|
|
59
|
+
gate: { path: "scripts/gate.sh" },
|
|
60
|
+
candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned" },
|
|
61
|
+
templateVersion: "1.2.0",
|
|
62
|
+
};
|
|
63
|
+
const content = renderTemplate(
|
|
64
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
65
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
66
|
+
);
|
|
67
|
+
assert.match(content, /pnpm run ci:candidate/);
|
|
68
|
+
assert.match(content, /pnpm run ci:candidate-check/);
|
|
69
|
+
assert.match(content, /RIGHT_GIT_ARTIFACT_ROOT/);
|
|
70
|
+
assert.match(content, /actions\/upload-artifact@[a-f0-9]{40}/);
|
|
71
|
+
assert.doesNotMatch(content, /secrets\.|AZURE_|APPLE_|certificate|private.key/i);
|
|
72
|
+
});
|
|
73
|
+
|
|
54
74
|
test("render determinism: same manifest renders byte-identical output every time", () => {
|
|
55
75
|
const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
56
76
|
const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
@@ -75,7 +95,7 @@ test("rendered workflows never request pnpm or yarn setup-node caching before pa
|
|
|
75
95
|
test("rendered workflows bootstrap each declared package manager safely", () => {
|
|
76
96
|
const pnpm = renderAllLanes({ ...FULL_MANIFEST, packageManager: "pnpm@11.24.0" }, "/repos/widgets");
|
|
77
97
|
for (const content of Object.values(pnpm)) {
|
|
78
|
-
assert.match(content, /uses: pnpm\/action-setup@
|
|
98
|
+
assert.match(content, /uses: pnpm\/action-setup@[a-f0-9]{40}/);
|
|
79
99
|
assert.match(content, /version: 11\.24\.0/);
|
|
80
100
|
assert.doesNotMatch(content, /corepack enable/);
|
|
81
101
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Managed by right-git — do not hand-edit. Run `right-git sync` to update.
|
|
2
2
|
# Template: profiles/rust-hybrid/ci.yml | Template version: ${TEMPLATE_VERSION}
|
|
3
|
-
name: ci
|
|
4
|
-
on:
|
|
3
|
+
name: legion-ci
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
5
8
|
permissions:
|
|
6
9
|
contents: read
|
|
7
10
|
jobs:
|
|
8
11
|
private-repo-guard:
|
|
9
|
-
runs-on: ubuntu-
|
|
12
|
+
runs-on: ubuntu-24.04
|
|
10
13
|
steps:
|
|
11
14
|
- name: Refuse private repository
|
|
12
15
|
if: ${{ github.event.repository.private }}
|
|
@@ -20,11 +23,17 @@ jobs:
|
|
|
20
23
|
node: ${NODE_MATRIX}
|
|
21
24
|
runs-on: ${{ matrix.os }}
|
|
22
25
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
24
|
-
- uses: actions/setup-node@
|
|
26
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
27
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
25
28
|
with:
|
|
26
29
|
node-version: ${{ matrix.node }}
|
|
27
30
|
package-manager-cache: false
|
|
28
31
|
${PACKAGE_MANAGER_SETUP}
|
|
29
|
-
-
|
|
32
|
+
- uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
|
|
33
|
+
with:
|
|
34
|
+
toolchain: 1.98.0
|
|
35
|
+
components: rustfmt, clippy
|
|
36
|
+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
37
|
+
with:
|
|
38
|
+
workspaces: engine
|
|
30
39
|
- run: bash ./${GATE_PATH}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Managed by right-git — do not hand-edit. Run `right-git sync` to update.
|
|
2
|
+
# Template: profiles/rust-hybrid/release-candidate.yml | Template version: ${TEMPLATE_VERSION}
|
|
3
|
+
# Public CI produces unsigned candidates only. Signing credentials, finalization,
|
|
4
|
+
# release publication, & stable bootstrap publication remain protected-host work.
|
|
5
|
+
name: release-candidate
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
push:
|
|
9
|
+
tags: ['v*']
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
jobs:
|
|
13
|
+
private-repo-guard:
|
|
14
|
+
runs-on: ubuntu-24.04
|
|
15
|
+
steps:
|
|
16
|
+
- name: Refuse private repository
|
|
17
|
+
if: ${{ github.event.repository.private }}
|
|
18
|
+
run: exit 1
|
|
19
|
+
candidate:
|
|
20
|
+
needs: [private-repo-guard]
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
os: ${OS_MATRIX}
|
|
25
|
+
node: ${NODE_MATRIX}
|
|
26
|
+
runs-on: ${{ matrix.os }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
30
|
+
with:
|
|
31
|
+
node-version: ${{ matrix.node }}
|
|
32
|
+
package-manager-cache: false
|
|
33
|
+
${PACKAGE_MANAGER_SETUP}
|
|
34
|
+
- uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
|
|
35
|
+
with:
|
|
36
|
+
toolchain: 1.98.0
|
|
37
|
+
components: rustfmt, clippy
|
|
38
|
+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
39
|
+
with:
|
|
40
|
+
workspaces: engine
|
|
41
|
+
- run: ${INSTALL_COMMAND}
|
|
42
|
+
- name: Build unsigned release candidate
|
|
43
|
+
run: ${PM_RUN} ${CANDIDATE_BUILD_SCRIPT}
|
|
44
|
+
env:
|
|
45
|
+
RIGHT_GIT_ARTIFACT_ROOT: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}
|
|
46
|
+
- name: Check unsigned release candidate
|
|
47
|
+
run: ${PM_RUN} ${CANDIDATE_CHECK_SCRIPT}
|
|
48
|
+
env:
|
|
49
|
+
RIGHT_GIT_ARTIFACT_ROOT: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}
|
|
50
|
+
- name: Upload unsigned candidate handoff
|
|
51
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
52
|
+
with:
|
|
53
|
+
name: ${ARTIFACT_PREFIX}-unsigned-${{ matrix.os }}-${{ matrix.node }}
|
|
54
|
+
path: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}/**
|
|
55
|
+
if-no-files-found: error
|