@rightkit/git 0.1.0 → 0.2.0
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 +4 -0
- package/cli/right-git.mjs +4 -2
- package/guard-coverage.test.mjs +8 -0
- package/install.mjs +3 -1
- package/manifest.mjs +16 -0
- package/manifest.schema.json +5 -0
- package/manifest.test.mjs +9 -0
- package/package.json +5 -6
- package/render.test.mjs +21 -0
- package/sync.mjs +3 -1
- package/templates/ci.yml +0 -1
- package/templates/package-smoke.yml +0 -1
- package/templates/profiles/rust-hybrid/ci.yml +29 -0
- package/templates/publish-npm.yml +0 -1
- package/templates/publish-package-managers.yml +0 -1
- package/templates/qualification.yml +0 -5
- package/templates/release-candidate.yml +0 -1
package/README.md
CHANGED
|
@@ -33,3 +33,7 @@ npm install -g @rightkit/git # or: npx @rightkit/git --help
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Node builtins only; no runtime dependencies.
|
|
36
|
+
|
|
37
|
+
## Profiles
|
|
38
|
+
|
|
39
|
+
`profile` defaults to `node`. `rust-hybrid` accepts only `ci`, requires safe relative `gate.path`, installs pinned Node plus `rustup stable`, then runs that gate. It never adds release, signing, publishing, or approval steps.
|
package/cli/right-git.mjs
CHANGED
|
@@ -79,7 +79,7 @@ async function runInstall(argv) {
|
|
|
79
79
|
const { positional, flags } = parseFlags(argv);
|
|
80
80
|
const repoPath = positional[0];
|
|
81
81
|
if (!repoPath) {
|
|
82
|
-
console.error("right-git install <repo-path> --lanes ci,qualification,...");
|
|
82
|
+
console.error("right-git install <repo-path> --lanes ci,qualification,... [--profile node|rust-hybrid --gate-path scripts/gate.sh]");
|
|
83
83
|
return 2;
|
|
84
84
|
}
|
|
85
85
|
const opts = {
|
|
@@ -88,6 +88,8 @@ async function runInstall(argv) {
|
|
|
88
88
|
homebrewFormula: flags["homebrew-formula"],
|
|
89
89
|
wingetId: flags["winget-id"],
|
|
90
90
|
provenance: flags["no-provenance"] ? false : undefined,
|
|
91
|
+
profile: flags.profile,
|
|
92
|
+
gatePath: flags["gate-path"],
|
|
91
93
|
};
|
|
92
94
|
const result = await installCommand(path.resolve(repoPath), opts);
|
|
93
95
|
if (!result.ok) {
|
|
@@ -196,7 +198,7 @@ function printHelp() {
|
|
|
196
198
|
console.log(`right-git <command> [options]
|
|
197
199
|
|
|
198
200
|
Commands:
|
|
199
|
-
install <repo-path> --lanes ci
|
|
201
|
+
install <repo-path> --lanes ci,... [--profile rust-hybrid --gate-path scripts/gate.sh] Write .rightgit.json and render workflows; refuses private repos
|
|
200
202
|
sync <repo-path> [--dry-run] Re-render templates from .rightgit.json, show diff, apply
|
|
201
203
|
drift <repo-path> CI-safe check: installed workflows == rendered templates?
|
|
202
204
|
drift --all [workspace-root] Drift-check + re-check visibility for every managed repo found
|
package/guard-coverage.test.mjs
CHANGED
|
@@ -218,3 +218,11 @@ test("F5: private-repo-guard job has no job-level `if` (always runs) and fails o
|
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
test("rust-hybrid ci keeps its work behind the public-repository guard", () => {
|
|
223
|
+
const manifest = { ...FULL_MANIFEST, lanes: ["ci"], profile: "rust-hybrid", gate: { path: "scripts/gate.sh" } };
|
|
224
|
+
const template = fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8");
|
|
225
|
+
const jobs = parseJobs(renderTemplate(template, buildRenderContext(manifest, "/repos/widgets")));
|
|
226
|
+
assert.ok(isGuarded(jobs, "private-repo-guard"));
|
|
227
|
+
assert.ok(isGuarded(jobs, "ci"));
|
|
228
|
+
});
|
package/install.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { syncCommand } from "./sync.mjs";
|
|
|
13
13
|
|
|
14
14
|
export const DEFAULT_MATRIX = { os: ["ubuntu-latest", "macos-latest", "windows-latest"], node: ["22.13", "24"] };
|
|
15
15
|
export const DEFAULT_PACKAGE_MANAGER = "pnpm@11.18.0";
|
|
16
|
-
export const DEFAULT_TEMPLATE_VERSION = "1.
|
|
16
|
+
export const DEFAULT_TEMPLATE_VERSION = "1.1.0";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* opts: { lanes: string[], matrix?, packageManager?, templateVersion?,
|
|
@@ -43,6 +43,8 @@ export async function installCommand(repoPath, opts = {}, deps = {}) {
|
|
|
43
43
|
packageManager: opts.packageManager || DEFAULT_PACKAGE_MANAGER,
|
|
44
44
|
templateVersion: opts.templateVersion || DEFAULT_TEMPLATE_VERSION,
|
|
45
45
|
};
|
|
46
|
+
if (opts.profile) manifest.profile = opts.profile;
|
|
47
|
+
if (opts.gatePath) manifest.gate = { path: opts.gatePath };
|
|
46
48
|
|
|
47
49
|
if (lanes.includes("publish-npm")) {
|
|
48
50
|
manifest.publish = { ...manifest.publish, npm: { oidc: true, provenance: opts.provenance !== false } };
|
package/manifest.mjs
CHANGED
|
@@ -55,7 +55,10 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
55
55
|
"packageManager",
|
|
56
56
|
"publish",
|
|
57
57
|
"templateVersion",
|
|
58
|
+
"profile",
|
|
59
|
+
"gate",
|
|
58
60
|
]);
|
|
61
|
+
const GATE_PATH_RE = /^(?!.*\.\.)(?!.*\s)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Validate a parsed manifest object against the right-git manifest contract.
|
|
@@ -117,6 +120,17 @@ export function validateManifest(manifest) {
|
|
|
117
120
|
errors.push("templateVersion must be a semver string 'X.Y.Z'");
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
const profile = manifest.profile === undefined ? "node" : manifest.profile;
|
|
124
|
+
if (profile !== "node" && profile !== "rust-hybrid") errors.push("profile must be 'node' or 'rust-hybrid'");
|
|
125
|
+
if (profile === "rust-hybrid") {
|
|
126
|
+
if (lanes.length !== 1 || lanes[0] !== "ci") errors.push("profile 'rust-hybrid' supports only lane 'ci'");
|
|
127
|
+
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
|
+
errors.push("profile 'rust-hybrid' requires gate.path with a safe relative path");
|
|
129
|
+
}
|
|
130
|
+
} else if (manifest.gate !== undefined) {
|
|
131
|
+
errors.push("gate is only valid for profile 'rust-hybrid'");
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
const publish = manifest.publish;
|
|
121
135
|
if (publish !== undefined) {
|
|
122
136
|
if (publish === null || typeof publish !== "object" || Array.isArray(publish)) {
|
|
@@ -256,11 +270,13 @@ export function buildRenderContext(manifest, repoPath) {
|
|
|
256
270
|
NODE_MATRIX: yamlFlowStringList(manifest.matrix.node),
|
|
257
271
|
NODE_PUBLISH_VERSION: JSON.stringify(String(manifest.matrix.node[0])),
|
|
258
272
|
PM_NAME: pm.name,
|
|
273
|
+
PM_VERSION: pm.version,
|
|
259
274
|
PM_CACHE: pm.name,
|
|
260
275
|
INSTALL_COMMAND: pm.installCommand,
|
|
261
276
|
PM_RUN: pm.runPrefix,
|
|
262
277
|
ARTIFACT_PREFIX: artifactPrefix,
|
|
263
278
|
TEMPLATE_VERSION: manifest.templateVersion,
|
|
279
|
+
GATE_PATH: manifest.gate?.path,
|
|
264
280
|
};
|
|
265
281
|
|
|
266
282
|
if (manifest.publish?.npm) {
|
package/manifest.schema.json
CHANGED
|
@@ -81,5 +81,10 @@
|
|
|
81
81
|
"type": "string",
|
|
82
82
|
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
|
83
83
|
}
|
|
84
|
+
,"profile": { "enum": ["node", "rust-hybrid"] },
|
|
85
|
+
"gate": {
|
|
86
|
+
"type": "object", "required": ["path"], "additionalProperties": false,
|
|
87
|
+
"properties": { "path": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" } }
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
}
|
package/manifest.test.mjs
CHANGED
|
@@ -16,6 +16,15 @@ test("accepts a minimal valid manifest", () => {
|
|
|
16
16
|
assert.deepEqual(errors, []);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
test("rust-hybrid requires only ci and a safe gate path", () => {
|
|
20
|
+
const valid = { ...VALID_MINIMAL, profile: "rust-hybrid", gate: { path: "scripts/gate.sh" } };
|
|
21
|
+
assert.equal(validateManifest(valid).valid, true);
|
|
22
|
+
for (const path of ["/tmp/gate.sh", "../gate.sh", "scripts/gate sh", "${{ secrets.X }}", "scripts/gate;sh"]) {
|
|
23
|
+
assert.equal(validateManifest({ ...valid, gate: { path } }).valid, false, path);
|
|
24
|
+
}
|
|
25
|
+
assert.equal(validateManifest({ ...valid, lanes: ["ci", "qualification"] }).valid, false);
|
|
26
|
+
});
|
|
27
|
+
|
|
19
28
|
test("rejects non-object input", () => {
|
|
20
29
|
for (const bad of [null, undefined, "not an object", 42, ["array"]]) {
|
|
21
30
|
const { valid, errors } = validateManifest(bad);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/git",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Public-repo-only GitHub Actions workflow layer: templates, visibility gate, sync/drift/adopt/uninstall for right-git.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,9 +14,6 @@
|
|
|
14
14
|
"*.json"
|
|
15
15
|
],
|
|
16
16
|
"sideEffects": false,
|
|
17
|
-
"scripts": {
|
|
18
|
-
"test": "node --test *.test.mjs"
|
|
19
|
-
},
|
|
20
17
|
"publishConfig": {
|
|
21
18
|
"registry": "https://registry.npmjs.org/",
|
|
22
19
|
"access": "public"
|
|
@@ -26,5 +23,7 @@
|
|
|
26
23
|
"url": "git+https://github.com/adrdsouza/claude.git",
|
|
27
24
|
"directory": "tools/rightkit/packages/git"
|
|
28
25
|
},
|
|
29
|
-
"
|
|
30
|
-
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test *.test.mjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/render.test.mjs
CHANGED
|
@@ -42,6 +42,14 @@ test("every known lane has a template file and renders without an unresolved pla
|
|
|
42
42
|
assert.equal(Object.keys(rendered).length, KNOWN_LANES.length);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test("rust-hybrid ci renders its pinned package manager and gate", () => {
|
|
46
|
+
const manifest = { ...FULL_MANIFEST, lanes: ["ci"], profile: "rust-hybrid", gate: { path: "scripts/gate.sh" }, templateVersion: "1.1.0" };
|
|
47
|
+
const content = renderTemplate(fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8"), buildRenderContext(manifest, "/repos/widgets"));
|
|
48
|
+
assert.match(content, /corepack prepare pnpm@11\.18\.0 --activate/);
|
|
49
|
+
assert.match(content, /rustup toolchain install stable/);
|
|
50
|
+
assert.match(content, /bash \.\/scripts\/gate\.sh/);
|
|
51
|
+
});
|
|
52
|
+
|
|
45
53
|
test("render determinism: same manifest renders byte-identical output every time", () => {
|
|
46
54
|
const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
47
55
|
const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
@@ -50,6 +58,19 @@ test("render determinism: same manifest renders byte-identical output every time
|
|
|
50
58
|
}
|
|
51
59
|
});
|
|
52
60
|
|
|
61
|
+
test("rendered workflows never request pnpm or yarn setup-node caching before package-manager activation", () => {
|
|
62
|
+
for (const packageManager of ["pnpm@11.18.0", "yarn@4.6.0"]) {
|
|
63
|
+
const rendered = renderAllLanes({ ...FULL_MANIFEST, packageManager }, "/repos/widgets");
|
|
64
|
+
for (const [lane, content] of Object.entries(rendered)) {
|
|
65
|
+
assert.doesNotMatch(
|
|
66
|
+
content,
|
|
67
|
+
/^\s*cache:\s*(?:pnpm|yarn)\s*$/m,
|
|
68
|
+
`lane ${lane} must not ask actions/setup-node for ${packageManager.split("@")[0]} caching before corepack activation`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
53
74
|
// F5 fix: a job-level `if: !private` on the real work job means GitHub
|
|
54
75
|
// SKIPS that job entirely for a private repo — the nested "refuse" step
|
|
55
76
|
// inside it is then unreachable and never runs, so nothing fails and the
|
package/sync.mjs
CHANGED
|
@@ -18,7 +18,9 @@ export function renderLanes(manifest, repoPath) {
|
|
|
18
18
|
const context = buildRenderContext(manifest, repoPath);
|
|
19
19
|
return manifest.lanes.map((lane) => {
|
|
20
20
|
const filename = LANE_FILENAMES[lane];
|
|
21
|
-
const templatePath =
|
|
21
|
+
const templatePath = manifest.profile === "rust-hybrid"
|
|
22
|
+
? path.join(TEMPLATES_DIR, "profiles", "rust-hybrid", filename)
|
|
23
|
+
: path.join(TEMPLATES_DIR, filename);
|
|
22
24
|
const templateText = fs.readFileSync(templatePath, "utf8");
|
|
23
25
|
return { lane, filename, content: renderTemplate(templateText, context) };
|
|
24
26
|
});
|
package/templates/ci.yml
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Managed by right-git — do not hand-edit. Run `right-git sync` to update.
|
|
2
|
+
# Template: profiles/rust-hybrid/ci.yml | Template version: ${TEMPLATE_VERSION}
|
|
3
|
+
name: ci
|
|
4
|
+
on: [push, pull_request]
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
jobs:
|
|
8
|
+
private-repo-guard:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Refuse private repository
|
|
12
|
+
if: ${{ github.event.repository.private }}
|
|
13
|
+
run: exit 1
|
|
14
|
+
ci:
|
|
15
|
+
needs: [private-repo-guard]
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
os: ${OS_MATRIX}
|
|
20
|
+
node: ${NODE_MATRIX}
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v5
|
|
24
|
+
- uses: actions/setup-node@v5
|
|
25
|
+
with:
|
|
26
|
+
node-version: ${{ matrix.node }}
|
|
27
|
+
- run: corepack enable && corepack prepare ${PM_NAME}@${PM_VERSION} --activate
|
|
28
|
+
- run: rustup toolchain install stable --profile minimal
|
|
29
|
+
- run: bash ./${GATE_PATH}
|
|
@@ -26,7 +26,6 @@ jobs:
|
|
|
26
26
|
- uses: actions/setup-node@v5
|
|
27
27
|
with:
|
|
28
28
|
node-version: ${{ fromJSON('${NODE_MATRIX}')[0] }}
|
|
29
|
-
cache: ${PM_CACHE}
|
|
30
29
|
- run: corepack enable
|
|
31
30
|
- run: ${INSTALL_COMMAND}
|
|
32
31
|
- run: ${PM_RUN} test:all
|
|
@@ -38,7 +37,6 @@ jobs:
|
|
|
38
37
|
- uses: actions/setup-node@v5
|
|
39
38
|
with:
|
|
40
39
|
node-version: ${{ fromJSON('${NODE_MATRIX}')[0] }}
|
|
41
|
-
cache: ${PM_CACHE}
|
|
42
40
|
- run: corepack enable
|
|
43
41
|
- run: ${INSTALL_COMMAND}
|
|
44
42
|
- run: node scripts/ci/check-schema.mjs || true
|
|
@@ -50,7 +48,6 @@ jobs:
|
|
|
50
48
|
- uses: actions/setup-node@v5
|
|
51
49
|
with:
|
|
52
50
|
node-version: ${{ fromJSON('${NODE_MATRIX}')[0] }}
|
|
53
|
-
cache: ${PM_CACHE}
|
|
54
51
|
- run: corepack enable
|
|
55
52
|
- run: ${INSTALL_COMMAND}
|
|
56
53
|
- run: node scripts/ci/check-network-boundary.mjs
|
|
@@ -64,7 +61,6 @@ jobs:
|
|
|
64
61
|
- uses: actions/setup-node@v5
|
|
65
62
|
with:
|
|
66
63
|
node-version: ${{ fromJSON('${NODE_MATRIX}')[0] }}
|
|
67
|
-
cache: ${PM_CACHE}
|
|
68
64
|
- run: corepack enable
|
|
69
65
|
- run: ${INSTALL_COMMAND}
|
|
70
66
|
- run: ${PM_RUN} test:hardening
|
|
@@ -76,7 +72,6 @@ jobs:
|
|
|
76
72
|
- uses: actions/setup-node@v5
|
|
77
73
|
with:
|
|
78
74
|
node-version: ${{ fromJSON('${NODE_MATRIX}')[0] }}
|
|
79
|
-
cache: ${PM_CACHE}
|
|
80
75
|
- run: corepack enable
|
|
81
76
|
- run: ${INSTALL_COMMAND}
|
|
82
77
|
- run: ${PM_RUN} test:conformance
|