@rightkit/git 0.2.26 → 0.2.27
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 +41 -25
- package/manifest.mjs +45 -0
- package/manifest.schema.json +12 -0
- package/manifest.test.mjs +14 -0
- package/package.json +1 -1
- package/render.test.mjs +21 -0
- package/rightkit-ownership.json +15 -0
- package/templates/profiles/rust-hybrid/ci.yml +1 -0
- package/templates/publish-npm.yml +23 -3
package/README.md
CHANGED
|
@@ -1,43 +1,59 @@
|
|
|
1
1
|
# @rightkit/git
|
|
2
2
|
|
|
3
|
-
Public-
|
|
3
|
+
Public-repository-only GitHub Actions workflow layer for Right Suite products. The CLI `right-git` renders and installs CI lanes into `.github/workflows/` from a `.rightgit.json` manifest.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
| Item | Value |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Where it runs | Build and release tooling (repository CI setup) |
|
|
8
|
+
| Rust pair | None |
|
|
9
|
+
| Version | see INDEX.md |
|
|
10
|
+
| License | MIT OR Apache-2.0 |
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
## Commands
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
- `right-git install <repo> --lanes ci,...` writes `.rightgit.json` and renders workflows.
|
|
15
|
+
- `right-git sync` re-renders installed workflows from the manifest.
|
|
16
|
+
- `right-git drift --all` checks installed workflows against templates and re-checks visibility.
|
|
17
|
+
- `right-git adopt` imports existing workflows into the template library.
|
|
18
|
+
- `right-git uninstall` removes every managed file.
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
`install` and `sync` refuse private, internal and unconfirmed-visibility repositories. There is no override flag.
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
- **`right-git drift`** — detect divergence between installed workflows and templates, and orphaned managed workflows.
|
|
17
|
-
- **`right-git adopt`** — import existing workflows into the template library.
|
|
18
|
-
- **`right-git uninstall`** — remove every managed file, leaving zero behind.
|
|
22
|
+
## Optional QA evidence upload (rust-hybrid)
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
Set `qaEvidencePath` in `.rightgit.json` to upload the gate's QA output as a workflow artifact. The path is relative to `$RUNNER_TEMP`; the rendered step uses `${{ runner.temp }}/<path>`. Absolute paths and expression syntax are rejected.
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"profile": "rust-hybrid",
|
|
29
|
+
"gate": { "path": "scripts/gate.sh" },
|
|
30
|
+
"qaEvidencePath": "pulse-hub-qa",
|
|
31
|
+
"qaEvidenceRetentionDays": 14
|
|
32
|
+
}
|
|
33
|
+
```
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
- The step runs after the gate with `if: always()` and `if-no-files-found: ignore`.
|
|
36
|
+
- Artifact name: `qa-evidence-${{ matrix.os || runner.os }}-${{ github.run_attempt }}`.
|
|
37
|
+
- `qaEvidenceRetentionDays` is an integer from 1 to 90 and defaults to 7. It requires `qaEvidencePath`.
|
|
38
|
+
- Omit the key and the rendered `ci.yml` is unchanged.
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
- **Exception, disclosed:** the optional, opt-in `publish-package-managers` lane *does* require two owner-provisioned, long-lived tokens in GitHub Actions `secrets.*` (`HOMEBREW_TAP_TOKEN`, `WINGET_CREATE_GITHUB_TOKEN`) to open tap/manifest PRs. These are real bearer tokens, public-repo-scoped by the visibility gate but not zero-exposure. The "no signing credential in CI" invariant is specifically about code-signing material; it does not mean "no credential of any kind."
|
|
40
|
+
## Entry points
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
No importable API. The package has no `main` and no `exports` map. The supported surface is the `right-git` bin.
|
|
30
43
|
|
|
31
|
-
##
|
|
44
|
+
## Peer dependencies
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
npm install -g @rightkit/git # or: npx @rightkit/git --help
|
|
35
|
-
```
|
|
46
|
+
None. No runtime dependencies (Node builtins only).
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
## Example
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
```sh
|
|
51
|
+
pnpm add -D @rightkit/git
|
|
52
|
+
pnpm exec right-git install . --lanes ci
|
|
53
|
+
pnpm exec right-git drift --all
|
|
54
|
+
```
|
|
40
55
|
|
|
41
|
-
|
|
56
|
+
## Known issues
|
|
42
57
|
|
|
43
|
-
|
|
58
|
+
- The optional `publish-package-managers` lane needs two long-lived tokens in GitHub Actions secrets. Their names and scope are in the package templates, and they are not zero-exposure. Read `docs/rules/github-access.md` in the workspace before enabling it.
|
|
59
|
+
- Manifest fields are charset-checked before they reach workflow YAML.
|
package/manifest.mjs
CHANGED
|
@@ -59,9 +59,17 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
59
59
|
"profile",
|
|
60
60
|
"gate",
|
|
61
61
|
"rustCache",
|
|
62
|
+
"qaEvidencePath",
|
|
63
|
+
"qaEvidenceRetentionDays",
|
|
62
64
|
"candidate",
|
|
63
65
|
"ownership",
|
|
64
66
|
]);
|
|
67
|
+
// Optional QA evidence upload (rust-hybrid ci only). qaEvidencePath is a safe
|
|
68
|
+
// relative path under $RUNNER_TEMP; it renders as `${{ runner.temp }}/<path>`.
|
|
69
|
+
const QA_EVIDENCE_DEFAULT_RETENTION_DAYS = 7;
|
|
70
|
+
const QA_EVIDENCE_MAX_RETENTION_DAYS = 90;
|
|
71
|
+
// Pinned to the same upload-artifact release the rust-hybrid release-candidate template uses.
|
|
72
|
+
const UPLOAD_ARTIFACT_PIN = "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4";
|
|
65
73
|
const GATE_PATH_RE = /^(?!.*\.\.)(?!.*\s)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
66
74
|
const RUST_WORKSPACE_PATH_RE = /^(?:\.|(?!.*\.\.)(?!.*\s)[A-Za-z0-9][A-Za-z0-9._/-]*)$/;
|
|
67
75
|
const PACKAGE_SCRIPT_RE = /^[A-Za-z0-9][A-Za-z0-9:_-]*$/;
|
|
@@ -160,6 +168,21 @@ export function validateManifest(manifest) {
|
|
|
160
168
|
errors.push("gate is only valid for profile 'rust-hybrid'");
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
if (manifest.qaEvidencePath !== undefined) {
|
|
172
|
+
if (profile !== "rust-hybrid") {
|
|
173
|
+
errors.push("qaEvidencePath is only valid for profile 'rust-hybrid'");
|
|
174
|
+
} else if (typeof manifest.qaEvidencePath !== "string" || !GATE_PATH_RE.test(manifest.qaEvidencePath)) {
|
|
175
|
+
errors.push("qaEvidencePath must be a safe relative path under $RUNNER_TEMP (no '..', whitespace, leading '/', or expression syntax)");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (manifest.qaEvidenceRetentionDays !== undefined) {
|
|
179
|
+
if (manifest.qaEvidencePath === undefined) {
|
|
180
|
+
errors.push("qaEvidenceRetentionDays requires qaEvidencePath");
|
|
181
|
+
} else if (!Number.isInteger(manifest.qaEvidenceRetentionDays) || manifest.qaEvidenceRetentionDays < 1 || manifest.qaEvidenceRetentionDays > QA_EVIDENCE_MAX_RETENTION_DAYS) {
|
|
182
|
+
errors.push(`qaEvidenceRetentionDays must be an integer from 1 to ${QA_EVIDENCE_MAX_RETENTION_DAYS}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
163
186
|
const rustCache = manifest.rustCache;
|
|
164
187
|
if (rustCache !== undefined) {
|
|
165
188
|
if (profile !== "rust-hybrid") {
|
|
@@ -1004,6 +1027,27 @@ export function ownershipStep(manifest, lane = "ci") {
|
|
|
1004
1027
|
return `\n - name: RightKit ownership (protected registry roots)\n shell: bash\n run: test "$(git rev-parse HEAD)" = "${sourceRevision}" && ${command}`;
|
|
1005
1028
|
}
|
|
1006
1029
|
|
|
1030
|
+
/**
|
|
1031
|
+
* Optional QA evidence upload, rendered after the rust-hybrid gate step.
|
|
1032
|
+
* Returns "" when qaEvidencePath is unset, so the rendered workflow stays
|
|
1033
|
+
* byte-identical to a manifest without the key.
|
|
1034
|
+
*/
|
|
1035
|
+
export function qaEvidenceStep(manifest) {
|
|
1036
|
+
if (manifest.qaEvidencePath === undefined) return "";
|
|
1037
|
+
const retentionDays = manifest.qaEvidenceRetentionDays ?? QA_EVIDENCE_DEFAULT_RETENTION_DAYS;
|
|
1038
|
+
return [
|
|
1039
|
+
" - name: Upload QA evidence",
|
|
1040
|
+
" if: always()",
|
|
1041
|
+
` uses: ${UPLOAD_ARTIFACT_PIN}`,
|
|
1042
|
+
" with:",
|
|
1043
|
+
" name: qa-evidence-${{ matrix.os || runner.os }}-${{ github.run_attempt }}",
|
|
1044
|
+
` path: \${{ runner.temp }}/${manifest.qaEvidencePath}`,
|
|
1045
|
+
` retention-days: ${retentionDays}`,
|
|
1046
|
+
" if-no-files-found: ignore",
|
|
1047
|
+
"",
|
|
1048
|
+
].join("\n");
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1007
1051
|
export function buildRenderContext(manifest, repoPath, lane = "ci") {
|
|
1008
1052
|
const pm = parsePackageManager(manifest.packageManager);
|
|
1009
1053
|
const projectName = deriveProjectName(repoPath);
|
|
@@ -1031,6 +1075,7 @@ export function buildRenderContext(manifest, repoPath, lane = "ci") {
|
|
|
1031
1075
|
MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
|
|
1032
1076
|
RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
|
|
1033
1077
|
OWNERSHIP_STEP: ownershipStep(manifest, lane),
|
|
1078
|
+
QA_EVIDENCE_STEP: qaEvidenceStep(manifest),
|
|
1034
1079
|
};
|
|
1035
1080
|
|
|
1036
1081
|
// Every key below is populated ONLY when its source manifest section is
|
package/manifest.schema.json
CHANGED
|
@@ -116,6 +116,18 @@
|
|
|
116
116
|
"type": "object", "required": ["path"], "additionalProperties": false,
|
|
117
117
|
"properties": { "path": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" } }
|
|
118
118
|
},
|
|
119
|
+
"qaEvidencePath": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "rust-hybrid only. Safe relative path under $RUNNER_TEMP where the gate writes QA evidence; rendered as ${{ runner.temp }}/<path> and uploaded with if: always().",
|
|
122
|
+
"pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$"
|
|
123
|
+
},
|
|
124
|
+
"qaEvidenceRetentionDays": {
|
|
125
|
+
"type": "integer",
|
|
126
|
+
"minimum": 1,
|
|
127
|
+
"maximum": 90,
|
|
128
|
+
"default": 7,
|
|
129
|
+
"description": "rust-hybrid only, requires qaEvidencePath. Artifact retention in days."
|
|
130
|
+
},
|
|
119
131
|
"candidate": {
|
|
120
132
|
"type": "object",
|
|
121
133
|
"required": ["buildScript", "checkScript", "admissionScript", "stageSummaryScript", "artifactRoot", "os"],
|
package/manifest.test.mjs
CHANGED
|
@@ -389,3 +389,17 @@ test("F15 non-regression: realistic homebrew/winget/matrix values still validate
|
|
|
389
389
|
});
|
|
390
390
|
assert.equal(result.valid, true, JSON.stringify(result.errors));
|
|
391
391
|
});
|
|
392
|
+
|
|
393
|
+
test("qaEvidencePath and qaEvidenceRetentionDays are optional, rust-hybrid only, and charset-checked", () => {
|
|
394
|
+
const valid = { ...VALID_MINIMAL, profile: "rust-hybrid", gate: { path: "scripts/gate.sh" } };
|
|
395
|
+
assert.equal(validateManifest({ ...valid, qaEvidencePath: "pulse-hub-qa" }).valid, true);
|
|
396
|
+
assert.equal(validateManifest({ ...valid, qaEvidencePath: "pulse-hub-qa", qaEvidenceRetentionDays: 30 }).valid, true);
|
|
397
|
+
for (const qaEvidencePath of ["/abs", "../up", "pulse hub", "${{ runner.temp }}/x", "a;b", "", null]) {
|
|
398
|
+
assert.equal(validateManifest({ ...valid, qaEvidencePath }).valid, false, String(qaEvidencePath));
|
|
399
|
+
}
|
|
400
|
+
for (const qaEvidenceRetentionDays of [0, 91, 1.5, "7"]) {
|
|
401
|
+
assert.equal(validateManifest({ ...valid, qaEvidencePath: "pulse-hub-qa", qaEvidenceRetentionDays }).valid, false, String(qaEvidenceRetentionDays));
|
|
402
|
+
}
|
|
403
|
+
assert.equal(validateManifest({ ...valid, qaEvidenceRetentionDays: 7 }).valid, false, "retention requires a path");
|
|
404
|
+
assert.equal(validateManifest({ ...VALID_MINIMAL, qaEvidencePath: "pulse-hub-qa" }).valid, false, "node profile rejects qaEvidencePath");
|
|
405
|
+
});
|
package/package.json
CHANGED
package/render.test.mjs
CHANGED
|
@@ -829,3 +829,24 @@ test("ci.yml ownership check cannot be removed by deleting optional repo metadat
|
|
|
829
829
|
const without = renderTemplate(template, buildRenderContext(structuredClone(FULL_MANIFEST), "/repos/widgets"));
|
|
830
830
|
assert.match(without, /RightKit ownership \(protected registry roots\)/);
|
|
831
831
|
});
|
|
832
|
+
|
|
833
|
+
test("rust-hybrid ci uploads QA evidence after the gate only when qaEvidencePath is set", () => {
|
|
834
|
+
const base = { ...FULL_MANIFEST, lanes: ["ci"], profile: "rust-hybrid", gate: { path: "scripts/gate.sh" }, templateVersion: "1.1.0" };
|
|
835
|
+
const template = fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8");
|
|
836
|
+
|
|
837
|
+
const unset = renderTemplate(template, buildRenderContext(structuredClone(base), "/repos/widgets"));
|
|
838
|
+
assert.doesNotMatch(unset, /upload-artifact|QA evidence|runner\.temp/);
|
|
839
|
+
assert.match(unset, /CARGO_PROFILE_TEST_DEBUG: "0"\n$/);
|
|
840
|
+
|
|
841
|
+
const set = renderTemplate(template, buildRenderContext({ ...structuredClone(base), qaEvidencePath: "pulse-hub-qa" }, "/repos/widgets"));
|
|
842
|
+
assert.ok(set.indexOf("bash ./scripts/gate.sh") < set.indexOf("- name: Upload QA evidence"), "upload must follow the gate step");
|
|
843
|
+
assert.match(
|
|
844
|
+
set,
|
|
845
|
+
/ - name: Upload QA evidence\n if: always\(\)\n uses: actions\/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4\n with:\n name: qa-evidence-\$\{\{ matrix\.os \|\| runner\.os \}\}-\$\{\{ github\.run_attempt \}\}\n path: \$\{\{ runner\.temp \}\}\/pulse-hub-qa\n retention-days: 7\n if-no-files-found: ignore\n$/,
|
|
846
|
+
);
|
|
847
|
+
// The upload step is the only difference between the two renders.
|
|
848
|
+
assert.equal(set.replace(/ - name: Upload QA evidence[\s\S]*$/, ""), unset);
|
|
849
|
+
|
|
850
|
+
const custom = renderTemplate(template, buildRenderContext({ ...structuredClone(base), qaEvidencePath: "pulse-hub-qa", qaEvidenceRetentionDays: 30 }, "/repos/widgets"));
|
|
851
|
+
assert.match(custom, /retention-days: 30\n/);
|
|
852
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@rightkit/git",
|
|
3
|
+
"capabilities": [
|
|
4
|
+
{
|
|
5
|
+
"capability": "github-workflows",
|
|
6
|
+
"summary": "Public-repo-only GitHub Actions workflow layer: templates, visibility gate, sync/drift/adopt/uninstall for right-git.; declaration-only detection (no precise implementation fingerprint)",
|
|
7
|
+
"fingerprints": [],
|
|
8
|
+
"detection": "declaration-only",
|
|
9
|
+
"policyNotes": [
|
|
10
|
+
"Package contract retained; dependency declarations do not verify integration.",
|
|
11
|
+
"No precise implementation fingerprint; declaration-only inventory, never enforced ownership or verified integration."
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -43,10 +43,30 @@ ${PACKAGE_MANAGER_SETUP}
|
|
|
43
43
|
${OWNERSHIP_STEP}
|
|
44
44
|
- run: node scripts/release/build-candidate.mjs --version "${{ github.event.release.tag_name }}" --platform current --out "release/candidates/publish"
|
|
45
45
|
- run: node scripts/release/check-release.mjs release/candidates/publish
|
|
46
|
+
# check-release must supply publication-evidence.json with artifactSha256.
|
|
47
|
+
# Capture gated bytes before publication; never hash the source path during readback.
|
|
46
48
|
- id: tarball
|
|
47
49
|
shell: bash
|
|
48
50
|
run: |
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
node --input-type=module <<'NODE'
|
|
52
|
+
import fs from 'node:fs';
|
|
53
|
+
import { prepareNpmPublication } from '@rightkit/release/publication-gate.mjs';
|
|
54
|
+
const root = 'release/candidates/publish';
|
|
55
|
+
const c = JSON.parse(fs.readFileSync(`${root}/compatibility.json`));
|
|
56
|
+
const tarball = `${root}/${c.packageName.replace(/^@/, '').replace('/', '-')}-${c.version}.tgz`;
|
|
57
|
+
const proof = prepareNpmPublication({ name: c.packageName, version: c.version, tarball, evidencePath: `${root}/publication-evidence.json` });
|
|
58
|
+
// Only safe single-line metadata may enter GitHub outputs.
|
|
59
|
+
for (const value of [tarball, proof.name, proof.version]) if (/[\r\n]/.test(value)) throw new Error('unsafe npm publication identity');
|
|
60
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `path=${tarball}\nname=${proof.name}\nversion=${proof.version}\nsha1=${proof.sha1}\n`);
|
|
61
|
+
NODE
|
|
52
62
|
- run: npm publish "${{ steps.tarball.outputs.path }}"${NPM_PROVENANCE_FLAG} --access public
|
|
63
|
+
- name: Require registry byte equality
|
|
64
|
+
env:
|
|
65
|
+
NPM_PUBLICATION_NAME: ${{ steps.tarball.outputs.name }}
|
|
66
|
+
NPM_PUBLICATION_VERSION: ${{ steps.tarball.outputs.version }}
|
|
67
|
+
NPM_PUBLICATION_SHA1: ${{ steps.tarball.outputs.sha1 }}
|
|
68
|
+
run: |
|
|
69
|
+
node --input-type=module <<'NODE'
|
|
70
|
+
import { verifyNpmRegistryBytes } from '@rightkit/release/publication-gate.mjs';
|
|
71
|
+
verifyNpmRegistryBytes({ name: process.env.NPM_PUBLICATION_NAME, version: process.env.NPM_PUBLICATION_VERSION, sha1: process.env.NPM_PUBLICATION_SHA1 });
|
|
72
|
+
NODE
|