@rightkit/git 0.2.25 → 0.2.26

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.
@@ -166,12 +166,11 @@ function isGuarded(jobs, jobName, visiting = new Set()) {
166
166
  }
167
167
 
168
168
  function renderAllLanes() {
169
- const context = buildRenderContext(FULL_MANIFEST, "/repos/widgets");
170
169
  const rendered = {};
171
170
  for (const lane of FULL_MANIFEST.lanes) {
172
171
  const templatePath = path.join(templatesDir, LANE_FILENAMES[lane]);
173
172
  const templateText = fs.readFileSync(templatePath, "utf8");
174
- rendered[lane] = renderTemplate(templateText, context);
173
+ rendered[lane] = renderTemplate(templateText, buildRenderContext(FULL_MANIFEST, "/repos/widgets", lane));
175
174
  }
176
175
  return rendered;
177
176
  }
package/manifest.mjs CHANGED
@@ -990,15 +990,21 @@ export function deriveProjectName(repoPath) {
990
990
  * validated manifest plus the target repo's derived project name.
991
991
  */
992
992
  /** Every managed public workflow runs ownership scanning; repository metadata cannot opt it out. */
993
- export function ownershipStep(manifest) {
993
+ export function ownershipStep(manifest, lane = "ci") {
994
994
  const { name } = parsePackageManager(manifest.packageManager);
995
995
  const command = name === "pnpm" ? "pnpm exec right-release ownership --app ."
996
996
  : name === "npm" ? "npm exec -- right-release ownership --app ."
997
997
  : "yarn exec right-release ownership --app .";
998
- return `\n - name: RightKit ownership (protected registry roots)\n shell: bash\n run: test "$(git rev-parse HEAD)" = "\${{ github.event_name == 'workflow_dispatch' && inputs.source_revision || github.sha }}" && ${command}`;
998
+ // Only release-candidate dispatches declare source_revision. Referencing
999
+ // inputs.source_revision from other lanes makes actionlint reject their
1000
+ // generated workflows, even when guarded by an event-name expression.
1001
+ const sourceRevision = lane === "release-candidate"
1002
+ ? "\${{ inputs.source_revision }}"
1003
+ : "\${{ github.sha }}";
1004
+ return `\n - name: RightKit ownership (protected registry roots)\n shell: bash\n run: test "$(git rev-parse HEAD)" = "${sourceRevision}" && ${command}`;
999
1005
  }
1000
1006
 
1001
- export function buildRenderContext(manifest, repoPath) {
1007
+ export function buildRenderContext(manifest, repoPath, lane = "ci") {
1002
1008
  const pm = parsePackageManager(manifest.packageManager);
1003
1009
  const projectName = deriveProjectName(repoPath);
1004
1010
  const artifactPrefix = projectName.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
@@ -1024,7 +1030,7 @@ export function buildRenderContext(manifest, repoPath) {
1024
1030
  WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
1025
1031
  MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
1026
1032
  RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
1027
- OWNERSHIP_STEP: ownershipStep(manifest),
1033
+ OWNERSHIP_STEP: ownershipStep(manifest, lane),
1028
1034
  };
1029
1035
 
1030
1036
  // Every key below is populated ONLY when its source manifest section is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/git",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
4
4
  "description": "Public-repo-only GitHub Actions workflow layer: templates, visibility gate, sync/drift/adopt/uninstall for right-git.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/render.test.mjs CHANGED
@@ -24,12 +24,11 @@ const FULL_MANIFEST = {
24
24
  };
25
25
 
26
26
  function renderAllLanes(manifest, repoPath) {
27
- const context = buildRenderContext(manifest, repoPath);
28
27
  const rendered = {};
29
28
  for (const lane of manifest.lanes) {
30
29
  const templatePath = path.join(templatesDir, LANE_FILENAMES[lane]);
31
30
  const templateText = fs.readFileSync(templatePath, "utf8");
32
- rendered[lane] = renderTemplate(templateText, context);
31
+ rendered[lane] = renderTemplate(templateText, buildRenderContext(manifest, repoPath, lane));
33
32
  }
34
33
  return rendered;
35
34
  }
@@ -60,6 +59,7 @@ test("rust-hybrid ci renders its pinned package manager and gate", () => {
60
59
  assert.match(content, /changed=true[\s\S]*\^0\{40\}\$[\s\S]*git cat-file -e[\s\S]*git diff --name-only --diff-filter=ACDMRTUXB/);
61
60
  assert.match(content, /--diff-filter=ACDMRTUXB/, "deleted Rust inputs must trigger Rust setup");
62
61
  assert.match(content, /\*\.rs\|Cargo\.toml[\s\S]*scripts\/gate\.sh/);
62
+ assert.doesNotMatch(content, /\|build\.rs\||\|\*\/build\.rs\|/);
63
63
  assert.match(content, /\.rightgit\.json\|\.github\/workflows\/\*\.yml\|\.github\/workflows\/\*\.yaml/);
64
64
  assert.equal((content.match(/if: steps\.rust-changes\.outputs\.required == 'true'/g) ?? []).length, 3);
65
65
  assert.match(content, /RIGHT_GIT_RUST_CHANGED: \$\{\{ steps\.rust-changes\.outputs\.changed \}\}/);
@@ -94,6 +94,15 @@ test("ordinary CI templates cancel superseded runs on the same ref", () => {
94
94
  assert.match(generic, /cancel-in-progress: true/);
95
95
  });
96
96
 
97
+ test("ownership revision expression matches each lane trigger contract", () => {
98
+ const rendered = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
99
+ assert.match(rendered.ci, /git rev-parse HEAD\)" = "\$\{\{ github\.sha \}\}/);
100
+ assert.doesNotMatch(rendered.ci, /inputs\.source_revision/);
101
+ assert.match(rendered["publish-package-managers"], /git rev-parse HEAD\)" = "\$\{\{ github\.sha \}\}/);
102
+ assert.doesNotMatch(rendered["publish-package-managers"], /inputs\.source_revision/);
103
+ assert.match(rendered["release-candidate"], /git rev-parse HEAD\)" = "\$\{\{ inputs\.source_revision \}\}/);
104
+ });
105
+
97
106
  test("rust-hybrid ci preserves a declared macOS-only lane", () => {
98
107
  const manifest = {
99
108
  ...FULL_MANIFEST,
@@ -195,7 +204,7 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
195
204
  };
196
205
  const content = renderTemplate(
197
206
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
198
- buildRenderContext(manifest, "/repos/widgets"),
207
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
199
208
  );
200
209
  assert.match(content, /environment: release/);
201
210
  // Tag-last contract: signing is gated on the admitted, same-run
@@ -212,7 +221,7 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
212
221
  // out the exact admitted revision by SHA, never by resolving HEAD after a
213
222
  // ref-triggered checkout.
214
223
  assert.match(content, /windows-sign:[\s\S]*?ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/);
215
- assert.match(content, /git rev-parse HEAD.*inputs\.source_revision \|\| github\.sha/);
224
+ assert.match(content, /git rev-parse HEAD.*inputs\.source_revision/);
216
225
  assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
217
226
  assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
218
227
  assert.doesNotMatch(content, /secrets\.|client-secret|private.key/i);
@@ -240,7 +249,7 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
240
249
  };
241
250
  const content = renderTemplate(
242
251
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
243
- buildRenderContext(manifest, "/repos/widgets"),
252
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
244
253
  );
245
254
  assert.match(content, /macos-sign:/);
246
255
  assert.match(content, /macos-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
@@ -251,7 +260,7 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
251
260
  assert.match(content, /right-release build --platform mac --skip-checks/);
252
261
  assert.match(content, /secrets\.APPLE_API_KEY_BASE64/);
253
262
  assert.match(content, /macos-sign:[\s\S]*?ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/);
254
- assert.match(content, /git rev-parse HEAD.*inputs\.source_revision \|\| github\.sha/);
263
+ assert.match(content, /git rev-parse HEAD.*inputs\.source_revision/);
255
264
  assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
256
265
  assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
257
266
  assert.match(content, /security delete-keychain/);
@@ -287,7 +296,7 @@ test("rust-hybrid multi-architecture candidates bind every native target to exac
287
296
  };
288
297
  const content = renderTemplate(
289
298
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
290
- buildRenderContext(manifest, "/repos/widgets"),
299
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
291
300
  );
292
301
  for (const [runner, triple] of [["windows-2025", "x86_64-pc-windows-msvc"], ["windows-11-arm", "aarch64-pc-windows-msvc"], ["macos-15", "aarch64-apple-darwin"], ["macos-15-intel", "x86_64-apple-darwin"]]) {
293
302
  assert.match(content, new RegExp(`os: ${JSON.stringify(runner).replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}`));
@@ -338,7 +347,7 @@ test("rust-hybrid release chain preserves signed candidate, installed qualificat
338
347
  };
339
348
  const content = renderTemplate(
340
349
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
341
- buildRenderContext(manifest, "/repos/widgets"),
350
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
342
351
  );
343
352
  assert.match(content, /windows-sign:[\s\S]*pnpm run release:finalize:win/);
344
353
  assert.match(content, /macos-sign:[\s\S]*pnpm run release:finalize:mac/);
@@ -372,7 +381,7 @@ test("rust-hybrid release chain preserves signed candidate, installed qualificat
372
381
  assert.match(content, /CLOUDFLARE_API_TOKEN: \$\{\{ secrets\.CLOUDFLARE_API_TOKEN \}\}/);
373
382
  // Ownership scan binds to the source revision input before admission emits
374
383
  // that same revision for every downstream job.
375
- assert.match(content, /git rev-parse HEAD.*inputs\.source_revision \|\| github\.sha/);
384
+ assert.match(content, /git rev-parse HEAD.*inputs\.source_revision/);
376
385
  assert.doesNotMatch(content, /RIGHT_GIT_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
377
386
  // candidate, windows-sign, macos-sign, installed-qualification and publish all
378
387
  // check out the admitted revision with full history; admission checks out
@@ -430,7 +439,7 @@ test("every 'run <script>' in a full release-chain workflow names a script the m
430
439
  };
431
440
  const content = renderTemplate(
432
441
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
433
- buildRenderContext(manifest, "/repos/widgets"),
442
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
434
443
  );
435
444
  const declaredScripts = new Set([
436
445
  manifest.candidate.buildScript,
@@ -491,7 +500,7 @@ test("release chain: installed-qualification has a stage summary and publish dow
491
500
  };
492
501
  const content = renderTemplate(
493
502
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
494
- buildRenderContext(manifest, "/repos/widgets"),
503
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
495
504
  );
496
505
  const installedQualificationJob = content.slice(
497
506
  content.search(/^ {2}installed-qualification:\n {4}needs:/m),
@@ -689,7 +698,7 @@ test("every job checking out an admitted revision fetches full history", () => {
689
698
  };
690
699
  const content = renderTemplate(
691
700
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
692
- buildRenderContext(manifest, "/repos/widgets"),
701
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
693
702
  );
694
703
  const checkouts = content
695
704
  .split(/^ {6}- uses: actions\/checkout@/m)
@@ -732,7 +741,7 @@ test("release-candidate template exposes a dry_run input threaded through admiss
732
741
  };
733
742
  const content = renderTemplate(
734
743
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
735
- buildRenderContext(manifest, "/repos/widgets"),
744
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
736
745
  );
737
746
  assert.match(content, /^ dry_run:\n description: .*\n required: false\n type: boolean\n default: false$/m);
738
747
  assert.match(content, /dry_run: \$\{\{ steps\.admit\.outputs\.dry_run \}\}/);
@@ -785,7 +794,7 @@ function environmentManifest(environments) {
785
794
  function renderRelease(manifest) {
786
795
  return renderTemplate(
787
796
  fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
788
- buildRenderContext(manifest, "/repos/widgets"),
797
+ buildRenderContext(manifest, "/repos/widgets", "release-candidate"),
789
798
  );
790
799
  }
791
800
 
package/sync.mjs CHANGED
@@ -31,8 +31,8 @@ function releaseChainDependencyError(repoPath, manifest) {
31
31
 
32
32
  /** Render every lane declared in the manifest. Returns { lane, filename, content }[]. */
33
33
  export function renderLanes(manifest, repoPath) {
34
- const context = buildRenderContext(manifest, repoPath);
35
34
  return manifest.lanes.map((lane) => {
35
+ const context = buildRenderContext(manifest, repoPath, lane);
36
36
  const filename = LANE_FILENAMES[lane];
37
37
  const templatePath = manifest.profile === "rust-hybrid"
38
38
  ? path.join(TEMPLATES_DIR, "profiles", "rust-hybrid", filename)
@@ -45,7 +45,7 @@ jobs:
45
45
  changed=false
46
46
  while IFS= read -r changed_path; do
47
47
  case "$changed_path" in
48
- *.rs|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|build.rs|*/build.rs|rust-toolchain|*/rust-toolchain|rust-toolchain.toml|*/rust-toolchain.toml|.cargo/*|*/.cargo/*|.rightgit.json|.github/workflows/*.yml|.github/workflows/*.yaml|${GATE_PATH})
48
+ *.rs|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|rust-toolchain|*/rust-toolchain|rust-toolchain.toml|*/rust-toolchain.toml|.cargo/*|*/.cargo/*|.rightgit.json|.github/workflows/*.yml|.github/workflows/*.yaml|${GATE_PATH})
49
49
  changed=true
50
50
  break
51
51
  ;;