@rightkit/git 0.2.30 → 0.2.32
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 +29 -0
- package/manifest.mjs +222 -3
- package/manifest.schema.json +18 -0
- package/manifest.test.mjs +39 -0
- package/package.json +3 -3
- package/render.test.mjs +89 -0
- package/templates/profiles/rust-hybrid/ci.yml +3 -3
package/README.md
CHANGED
|
@@ -51,6 +51,35 @@ Set `"ciAllOs": true` to run the rust-hybrid `ci` gate on every `matrix.os` runn
|
|
|
51
51
|
|
|
52
52
|
Omit the key, or set it to `false`, and the rendered `ci.yml` keeps one runner, preferring the first `windows-*` entry. `ciAllOs: true` is rejected for other profiles.
|
|
53
53
|
|
|
54
|
+
## Incremental CI and dev artifacts (rust-hybrid)
|
|
55
|
+
|
|
56
|
+
Both keys are opt-in; unset, the rendered `ci.yml` is byte-identical.
|
|
57
|
+
|
|
58
|
+
`"ciIncremental": true` switches Rust caching to `cache-workspace-crates: true`, sets `CARGO_INCREMENTAL=1` for the ci gate, and caches `~/Library/Developer/Xcode/DerivedData` on macOS legs (key: runner + `Cargo.lock` + `Package.resolved` + `project.pbxproj` hash).
|
|
59
|
+
|
|
60
|
+
`devArtifact` publishes fast development builds as workflow artifacts, separate from the release-candidate lane. It runs on push to the default branch only, never on pull requests.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"profile": "rust-hybrid",
|
|
65
|
+
"matrix": { "os": ["xcode-27", "windows-2025"], "node": ["26.8.1"] },
|
|
66
|
+
"ciAllOs": true,
|
|
67
|
+
"ciIncremental": true,
|
|
68
|
+
"devArtifact": {
|
|
69
|
+
"mac": "sign",
|
|
70
|
+
"windows": "unsigned",
|
|
71
|
+
"retentionDays": 7,
|
|
72
|
+
"macAppPath": "target/release/bundle/macos/Pulse.app",
|
|
73
|
+
"windowsGlob": "target/release/*.exe"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `mac: "sign"` needs `macAppPath` (the built `.app`, repository-relative) and a macOS runner in the ci gate (`ciAllOs`). The unsigned app is zipped with `ditto` and handed to a separate `dev-mac-sign` job that installs the Developer ID identity from the `release` environment secrets, runs `codesign --force --deep --strict --options runtime`, verifies, zips and uploads `dev-mac-<sha>`. No notarization, admission or ownership gate. Optional `macEntitlements` passes `--entitlements`.
|
|
79
|
+
- `windows: "unsigned"` needs `windowsGlob` and a `windows-*` runner in the ci gate; matched files upload as `dev-windows-<sha>`.
|
|
80
|
+
- The `dev-mac-sign` job uses the `release` environment, so that environment's protection rules (required reviewers, branch policy) apply. The job only exists for pushes to the default branch.
|
|
81
|
+
- Install a mac dev build with `right-release install-app --dev --zip <file>` (Developer ID-signed, not notarized).
|
|
82
|
+
|
|
54
83
|
## Entry points
|
|
55
84
|
|
|
56
85
|
No importable API. The package has no `main` and no `exports` map. The supported surface is the `right-git` bin.
|
package/manifest.mjs
CHANGED
|
@@ -62,6 +62,8 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
62
62
|
"qaEvidencePath",
|
|
63
63
|
"qaEvidenceRetentionDays",
|
|
64
64
|
"ciAllOs",
|
|
65
|
+
"ciIncremental",
|
|
66
|
+
"devArtifact",
|
|
65
67
|
"candidate",
|
|
66
68
|
"ownership",
|
|
67
69
|
]);
|
|
@@ -197,6 +199,17 @@ export function validateManifest(manifest) {
|
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
// ciIncremental (rust-hybrid ci only): opt-in incremental Rust + DerivedData caching.
|
|
203
|
+
if (manifest.ciIncremental !== undefined) {
|
|
204
|
+
if (typeof manifest.ciIncremental !== "boolean") {
|
|
205
|
+
errors.push("ciIncremental must be a boolean");
|
|
206
|
+
} else if (manifest.ciIncremental === true && profile !== "rust-hybrid") {
|
|
207
|
+
errors.push("ciIncremental is only valid for profile 'rust-hybrid'");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
validateDevArtifact(manifest, profile, errors);
|
|
212
|
+
|
|
200
213
|
const rustCache = manifest.rustCache;
|
|
201
214
|
if (rustCache !== undefined) {
|
|
202
215
|
if (profile !== "rust-hybrid") {
|
|
@@ -536,6 +549,208 @@ function releaseEnvironment(manifest, role) {
|
|
|
536
549
|
return typeof configured === "string" && configured.length > 0 ? configured : "release";
|
|
537
550
|
}
|
|
538
551
|
|
|
552
|
+
// Dev artifacts (rust-hybrid ci only): fast, non-release builds on push to the default branch.
|
|
553
|
+
const DEV_ARTIFACT_DEFAULT_RETENTION_DAYS = 7;
|
|
554
|
+
const DEV_ARTIFACT_MAX_RETENTION_DAYS = 90;
|
|
555
|
+
const DEV_MAC_PATH_RE = /^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/ -]*$/;
|
|
556
|
+
const DEV_WINDOWS_GLOB_RE = /^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/*?-]*$/;
|
|
557
|
+
const CACHE_ACTION_PIN = "actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4";
|
|
558
|
+
|
|
559
|
+
function effectiveCiRunners(manifest) {
|
|
560
|
+
const os = manifest.matrix?.os;
|
|
561
|
+
if (!Array.isArray(os) || os.length === 0) return [];
|
|
562
|
+
if (manifest.ciAllOs === true) return os;
|
|
563
|
+
return [os.find((runner) => typeof runner === "string" && runner.startsWith("windows-")) ?? os[0]];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function validateDevArtifact(manifest, profile, errors) {
|
|
567
|
+
const dev = manifest.devArtifact;
|
|
568
|
+
if (dev === undefined) return;
|
|
569
|
+
if (profile !== "rust-hybrid") {
|
|
570
|
+
errors.push("devArtifact is only valid for profile 'rust-hybrid'");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (dev === null || typeof dev !== "object" || Array.isArray(dev)) {
|
|
574
|
+
errors.push("devArtifact must be an object");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
for (const key of Object.keys(dev)) {
|
|
578
|
+
if (!["mac", "windows", "retentionDays", "macAppPath", "macEntitlements", "windowsGlob"].includes(key)) errors.push(`unknown devArtifact field: ${key}`);
|
|
579
|
+
}
|
|
580
|
+
if (dev.mac !== undefined && dev.mac !== "sign" && dev.mac !== "none") errors.push("devArtifact.mac must be 'sign' or 'none'");
|
|
581
|
+
if (dev.windows !== undefined && dev.windows !== "unsigned" && dev.windows !== "none") errors.push("devArtifact.windows must be 'unsigned' or 'none'");
|
|
582
|
+
if (dev.mac !== "sign" && dev.windows !== "unsigned") errors.push("devArtifact requires mac 'sign' and/or windows 'unsigned'");
|
|
583
|
+
if (dev.retentionDays !== undefined && (!Number.isInteger(dev.retentionDays) || dev.retentionDays < 1 || dev.retentionDays > DEV_ARTIFACT_MAX_RETENTION_DAYS)) {
|
|
584
|
+
errors.push(`devArtifact.retentionDays must be an integer from 1 to ${DEV_ARTIFACT_MAX_RETENTION_DAYS}`);
|
|
585
|
+
}
|
|
586
|
+
const runners = effectiveCiRunners(manifest).filter((runner) => typeof runner === "string");
|
|
587
|
+
if (dev.mac === "sign") {
|
|
588
|
+
if (typeof dev.macAppPath !== "string" || !DEV_MAC_PATH_RE.test(dev.macAppPath) || !dev.macAppPath.endsWith(".app")) {
|
|
589
|
+
errors.push("devArtifact.macAppPath must be a safe repository-relative path ending in .app when mac is 'sign'");
|
|
590
|
+
}
|
|
591
|
+
if (dev.macEntitlements !== undefined && (typeof dev.macEntitlements !== "string" || !GATE_PATH_RE.test(dev.macEntitlements))) {
|
|
592
|
+
errors.push("devArtifact.macEntitlements must be a safe repository-relative path");
|
|
593
|
+
}
|
|
594
|
+
if (!runners.some((runner) => MACOS_RUNNER_RE.test(runner))) {
|
|
595
|
+
errors.push("devArtifact.mac 'sign' needs the ci gate to run on a macOS runner (set ciAllOs and include a macos-* or xcode-* runner in matrix.os)");
|
|
596
|
+
}
|
|
597
|
+
} else if (dev.macAppPath !== undefined || dev.macEntitlements !== undefined) {
|
|
598
|
+
errors.push("devArtifact.macAppPath and macEntitlements require mac 'sign'");
|
|
599
|
+
}
|
|
600
|
+
if (dev.windows === "unsigned") {
|
|
601
|
+
if (typeof dev.windowsGlob !== "string" || !DEV_WINDOWS_GLOB_RE.test(dev.windowsGlob)) {
|
|
602
|
+
errors.push("devArtifact.windowsGlob must be a safe repository-relative glob when windows is 'unsigned'");
|
|
603
|
+
}
|
|
604
|
+
if (!runners.some((runner) => /^windows-/i.test(runner))) {
|
|
605
|
+
errors.push("devArtifact.windows 'unsigned' needs the ci gate to run on a windows-* runner");
|
|
606
|
+
}
|
|
607
|
+
} else if (dev.windowsGlob !== undefined) {
|
|
608
|
+
errors.push("devArtifact.windowsGlob requires windows 'unsigned'");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const DEV_PUSH_CONDITION = "github.event_name == 'push' && github.ref_name == github.event.repository.default_branch";
|
|
613
|
+
|
|
614
|
+
/** ciIncremental: Xcode DerivedData cache step (leading newline; "" when unset). */
|
|
615
|
+
function derivedDataStep(manifest) {
|
|
616
|
+
if (manifest.ciIncremental !== true) return "";
|
|
617
|
+
return [
|
|
618
|
+
"",
|
|
619
|
+
" - name: Cache Xcode DerivedData",
|
|
620
|
+
" if: ${{ runner.os == 'macOS' }}",
|
|
621
|
+
` uses: ${CACHE_ACTION_PIN}`,
|
|
622
|
+
" with:",
|
|
623
|
+
" path: ~/Library/Developer/Xcode/DerivedData",
|
|
624
|
+
" key: ${{ runner.os }}-${{ matrix.os }}-deriveddata-${{ hashFiles('**/Cargo.lock', '**/Package.resolved', '**/*.xcodeproj/project.pbxproj') }}",
|
|
625
|
+
" restore-keys: |",
|
|
626
|
+
" ${{ runner.os }}-${{ matrix.os }}-deriveddata-",
|
|
627
|
+
].join("\n");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Dev artifact steps appended to the ci job (unsigned, no secrets) plus the
|
|
632
|
+
* separate signing job. Returns "" when devArtifact is unset.
|
|
633
|
+
*/
|
|
634
|
+
export function devArtifactCi(manifest) {
|
|
635
|
+
const dev = manifest.devArtifact;
|
|
636
|
+
if (dev === undefined) return "";
|
|
637
|
+
const retention = dev.retentionDays ?? DEV_ARTIFACT_DEFAULT_RETENTION_DAYS;
|
|
638
|
+
const parts = [];
|
|
639
|
+
if (dev.windows === "unsigned") {
|
|
640
|
+
parts.push([
|
|
641
|
+
" - name: Upload unsigned dev build (Windows)",
|
|
642
|
+
` if: \${{ ${DEV_PUSH_CONDITION} && runner.os == 'Windows' }}`,
|
|
643
|
+
` uses: ${UPLOAD_ARTIFACT_PIN}`,
|
|
644
|
+
" with:",
|
|
645
|
+
" name: dev-windows-${{ github.sha }}",
|
|
646
|
+
` path: ${dev.windowsGlob}`,
|
|
647
|
+
` retention-days: ${retention}`,
|
|
648
|
+
" if-no-files-found: error",
|
|
649
|
+
"",
|
|
650
|
+
].join("\n"));
|
|
651
|
+
}
|
|
652
|
+
if (dev.mac === "sign") {
|
|
653
|
+
parts.push([
|
|
654
|
+
" - name: Package unsigned dev app (macOS)",
|
|
655
|
+
` if: \${{ ${DEV_PUSH_CONDITION} && runner.os == 'macOS' }}`,
|
|
656
|
+
" shell: bash",
|
|
657
|
+
" env:",
|
|
658
|
+
` DEV_APP_PATH: ${JSON.stringify(dev.macAppPath)}`,
|
|
659
|
+
" run: |",
|
|
660
|
+
" set -euo pipefail",
|
|
661
|
+
" test -d \"$DEV_APP_PATH\"",
|
|
662
|
+
" mkdir -p \"$RUNNER_TEMP/dev-unsigned\"",
|
|
663
|
+
" ditto -c -k --keepParent \"$DEV_APP_PATH\" \"$RUNNER_TEMP/dev-unsigned/app.zip\"",
|
|
664
|
+
" - name: Upload unsigned dev app for signing (macOS)",
|
|
665
|
+
` if: \${{ ${DEV_PUSH_CONDITION} && runner.os == 'macOS' }}`,
|
|
666
|
+
` uses: ${UPLOAD_ARTIFACT_PIN}`,
|
|
667
|
+
" with:",
|
|
668
|
+
" name: dev-unsigned-mac-${{ github.sha }}",
|
|
669
|
+
" path: ${{ runner.temp }}/dev-unsigned/app.zip",
|
|
670
|
+
" retention-days: 1",
|
|
671
|
+
" if-no-files-found: error",
|
|
672
|
+
"",
|
|
673
|
+
].join("\n"));
|
|
674
|
+
}
|
|
675
|
+
let text = parts.join("");
|
|
676
|
+
if (dev.mac === "sign") text += devMacSigningJob(manifest, dev, retention);
|
|
677
|
+
return text;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function devMacSigningJob(manifest, dev, retention) {
|
|
681
|
+
const runner = effectiveCiRunners(manifest).find((os) => MACOS_RUNNER_RE.test(os));
|
|
682
|
+
const entitlements = dev.macEntitlements ? ` --entitlements "$GITHUB_WORKSPACE/${dev.macEntitlements}"` : "";
|
|
683
|
+
const checkout = dev.macEntitlements
|
|
684
|
+
? ` - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
685
|
+
with:
|
|
686
|
+
ref: \${{ github.sha }}
|
|
687
|
+
persist-credentials: false
|
|
688
|
+
`
|
|
689
|
+
: "";
|
|
690
|
+
return `
|
|
691
|
+
dev-mac-sign:
|
|
692
|
+
# Push to the default branch only. The release environment's protection
|
|
693
|
+
# rules (required reviewers, branch policy) apply to this job.
|
|
694
|
+
needs: [ci]
|
|
695
|
+
if: \${{ ${DEV_PUSH_CONDITION} }}
|
|
696
|
+
environment: ${releaseEnvironment(manifest, "signing")}
|
|
697
|
+
runs-on: ${runner}
|
|
698
|
+
permissions:
|
|
699
|
+
contents: read
|
|
700
|
+
steps:
|
|
701
|
+
${checkout} - name: Download unsigned dev app
|
|
702
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
703
|
+
with:
|
|
704
|
+
name: dev-unsigned-mac-\${{ github.sha }}
|
|
705
|
+
path: \${{ runner.temp }}/dev-unsigned
|
|
706
|
+
- name: Install Apple signing identity in temporary keychain
|
|
707
|
+
shell: bash
|
|
708
|
+
env:
|
|
709
|
+
APPLE_CERTIFICATE_BASE64: \${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
|
710
|
+
APPLE_CERTIFICATE_PASSWORD: \${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
711
|
+
APPLE_KEYCHAIN_PASSWORD: \${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
|
712
|
+
run: |
|
|
713
|
+
set -euo pipefail
|
|
714
|
+
certificate_path="$RUNNER_TEMP/apple-signing.p12"
|
|
715
|
+
keychain_path="$RUNNER_TEMP/apple-signing.keychain-db"
|
|
716
|
+
printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$certificate_path"
|
|
717
|
+
security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
|
|
718
|
+
security set-keychain-settings -lut 21600 "$keychain_path"
|
|
719
|
+
security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
|
|
720
|
+
security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
|
|
721
|
+
security set-key-partition-list -S apple-tool:,apple: -k "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
|
|
722
|
+
security list-keychain -d user -s "$keychain_path"
|
|
723
|
+
- name: Developer ID sign dev app (no notarization)
|
|
724
|
+
shell: bash
|
|
725
|
+
env:
|
|
726
|
+
APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
|
|
727
|
+
run: |
|
|
728
|
+
set -euo pipefail
|
|
729
|
+
work="$RUNNER_TEMP/dev-sign"
|
|
730
|
+
rm -rf "$work" && mkdir -p "$work"
|
|
731
|
+
ditto -x -k "$RUNNER_TEMP/dev-unsigned/app.zip" "$work"
|
|
732
|
+
app="$(find "$work" -maxdepth 1 -name '*.app' -print -quit)"
|
|
733
|
+
test -n "$app"
|
|
734
|
+
codesign --force --deep --strict --options runtime --timestamp${entitlements} --sign "$APPLE_DEVELOPER_ID" "$app"
|
|
735
|
+
codesign --verify --deep --strict "$app"
|
|
736
|
+
mkdir -p "$RUNNER_TEMP/dev-signed"
|
|
737
|
+
ditto -c -k --keepParent "$app" "$RUNNER_TEMP/dev-signed/$(basename "$app").zip"
|
|
738
|
+
- name: Upload signed (not notarized) dev app
|
|
739
|
+
uses: ${UPLOAD_ARTIFACT_PIN}
|
|
740
|
+
with:
|
|
741
|
+
name: dev-mac-\${{ github.sha }}
|
|
742
|
+
path: \${{ runner.temp }}/dev-signed/*.zip
|
|
743
|
+
retention-days: ${retention}
|
|
744
|
+
if-no-files-found: error
|
|
745
|
+
- name: Remove Apple signing material
|
|
746
|
+
if: \${{ always() }}
|
|
747
|
+
shell: bash
|
|
748
|
+
run: |
|
|
749
|
+
security delete-keychain "$RUNNER_TEMP/apple-signing.keychain-db" 2>/dev/null || true
|
|
750
|
+
rm -f "$RUNNER_TEMP/apple-signing.p12"
|
|
751
|
+
`;
|
|
752
|
+
}
|
|
753
|
+
|
|
539
754
|
function windowsSigningJob(manifest, pm, artifactPrefix) {
|
|
540
755
|
if (manifest.candidate?.signWindows !== true) return "";
|
|
541
756
|
const nodeVersion = String(manifest.matrix.node[0]);
|
|
@@ -1032,8 +1247,9 @@ export function ownershipStep(manifest, lane = "ci") {
|
|
|
1032
1247
|
const command = name === "pnpm" ? "pnpm exec right-release ownership --app ."
|
|
1033
1248
|
: name === "npm" ? "npm exec -- right-release ownership --app ."
|
|
1034
1249
|
: "yarn exec right-release ownership --app .";
|
|
1035
|
-
//
|
|
1036
|
-
|
|
1250
|
+
// Every lane stays advisory until ownership approval keys are enrolled (Adrian, 2026-10-09);
|
|
1251
|
+
// once enrolled, the flag is inert and findings block again.
|
|
1252
|
+
const ownershipCommand = `${command} --advisory-when-pending`;
|
|
1037
1253
|
// Only release-candidate dispatches declare source_revision. Referencing
|
|
1038
1254
|
// inputs.source_revision from other lanes makes actionlint reject their
|
|
1039
1255
|
// generated workflows, even when guarded by an event-name expression.
|
|
@@ -1101,7 +1317,10 @@ export function buildRenderContext(manifest, repoPath, lane = "ci") {
|
|
|
1101
1317
|
MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
|
|
1102
1318
|
RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
|
|
1103
1319
|
OWNERSHIP_STEP: ownershipStep(manifest, lane),
|
|
1104
|
-
QA_EVIDENCE_STEP: qaEvidenceStep(manifest),
|
|
1320
|
+
QA_EVIDENCE_STEP: qaEvidenceStep(manifest) + devArtifactCi(manifest),
|
|
1321
|
+
CI_CACHE_WORKSPACE_CRATES: manifest.ciIncremental === true ? "true" : "false",
|
|
1322
|
+
CI_CARGO_INCREMENTAL: manifest.ciIncremental === true ? "1" : "0",
|
|
1323
|
+
CI_DERIVED_DATA_STEP: derivedDataStep(manifest),
|
|
1105
1324
|
};
|
|
1106
1325
|
|
|
1107
1326
|
// Every key below is populated ONLY when its source manifest section is
|
package/manifest.schema.json
CHANGED
|
@@ -133,6 +133,24 @@
|
|
|
133
133
|
"default": false,
|
|
134
134
|
"description": "rust-hybrid only. When true, the ci job runs the gate on every matrix.os runner in declared order (fail-fast: false). When false or unset, the ci job uses one runner (preferring the first windows-* entry, else the first matrix.os entry)."
|
|
135
135
|
},
|
|
136
|
+
"ciIncremental": {
|
|
137
|
+
"type": "boolean",
|
|
138
|
+
"default": false,
|
|
139
|
+
"description": "rust-hybrid only. When true, the ci job uses Swatinem/rust-cache with cache-workspace-crates, CARGO_INCREMENTAL=1, and caches Xcode DerivedData on macOS legs."
|
|
140
|
+
},
|
|
141
|
+
"devArtifact": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"additionalProperties": false,
|
|
144
|
+
"description": "rust-hybrid only. Dev builds on push to the default branch (never PRs). mac 'sign' Developer ID-signs (no notarization) in a separate job using the release environment; windows 'unsigned' uploads built files as-is.",
|
|
145
|
+
"properties": {
|
|
146
|
+
"mac": { "enum": ["sign", "none"] },
|
|
147
|
+
"windows": { "enum": ["unsigned", "none"] },
|
|
148
|
+
"retentionDays": { "type": "integer", "minimum": 1, "maximum": 90, "default": 7 },
|
|
149
|
+
"macAppPath": { "type": "string", "pattern": "^(?!.*\\.\\.)[A-Za-z0-9][A-Za-z0-9._/ -]*\\.app$", "description": "Repository-relative path of the built .app (required when mac is 'sign')." },
|
|
150
|
+
"macEntitlements": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$", "description": "Optional repository-relative entitlements plist for codesign." },
|
|
151
|
+
"windowsGlob": { "type": "string", "pattern": "^(?!.*\\.\\.)[A-Za-z0-9][A-Za-z0-9._/*?-]*$", "description": "Repository-relative glob of built files to upload (required when windows is 'unsigned')." }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
136
154
|
"candidate": {
|
|
137
155
|
"type": "object",
|
|
138
156
|
"required": ["buildScript", "checkScript", "admissionScript", "stageSummaryScript", "artifactRoot", "os"],
|
package/manifest.test.mjs
CHANGED
|
@@ -418,3 +418,42 @@ test("ciAllOs is an optional boolean, rust-hybrid only", () => {
|
|
|
418
418
|
assert.equal(validateManifest({ ...VALID_MINIMAL, ciAllOs: true }).valid, false, "node profile rejects ciAllOs true");
|
|
419
419
|
assert.equal(validateManifest({ ...VALID_MINIMAL, ciAllOs: false }).valid, true, "false is accepted for any profile");
|
|
420
420
|
});
|
|
421
|
+
|
|
422
|
+
test("ciIncremental is an optional boolean, rust-hybrid only", () => {
|
|
423
|
+
const valid = { ...VALID_MINIMAL, profile: "rust-hybrid", lanes: ["ci"], gate: { path: "scripts/gate.sh" } };
|
|
424
|
+
assert.equal(validateManifest({ ...valid, ciIncremental: true }).valid, true);
|
|
425
|
+
assert.equal(validateManifest({ ...valid, ciIncremental: false }).valid, true);
|
|
426
|
+
assert.equal(validateManifest({ ...valid, ciIncremental: "yes" }).valid, false);
|
|
427
|
+
assert.equal(validateManifest({ ...VALID_MINIMAL, ciIncremental: true }).valid, false);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("devArtifact validates keys, paths, and runner coverage", () => {
|
|
431
|
+
const base = {
|
|
432
|
+
...VALID_MINIMAL,
|
|
433
|
+
profile: "rust-hybrid",
|
|
434
|
+
lanes: ["ci"],
|
|
435
|
+
gate: { path: "scripts/gate.sh" },
|
|
436
|
+
matrix: { os: ["xcode-27", "windows-2025"], node: ["26.8.1"] },
|
|
437
|
+
ciAllOs: true,
|
|
438
|
+
};
|
|
439
|
+
const full = { mac: "sign", windows: "unsigned", macAppPath: "target/release/bundle/macos/Pulse.app", windowsGlob: "target/release/*.exe", retentionDays: 7 };
|
|
440
|
+
assert.equal(validateManifest({ ...base, devArtifact: full }).valid, true);
|
|
441
|
+
assert.equal(validateManifest({ ...base, devArtifact: { windows: "unsigned", windowsGlob: "dist/*.exe" } }).valid, true);
|
|
442
|
+
|
|
443
|
+
const bad = (devArtifact, manifest = base) => validateManifest({ ...manifest, devArtifact }).valid;
|
|
444
|
+
assert.equal(bad({ mac: "none", windows: "none" }), false, "needs at least one active target");
|
|
445
|
+
assert.equal(bad({ ...full, macAppPath: undefined }), false, "mac sign needs macAppPath");
|
|
446
|
+
assert.equal(bad({ ...full, windowsGlob: undefined }), false, "windows needs windowsGlob");
|
|
447
|
+
assert.equal(bad({ ...full, macAppPath: "../Pulse.app" }), false);
|
|
448
|
+
assert.equal(bad({ ...full, macAppPath: "target/Pulse" }), false, "must end in .app");
|
|
449
|
+
assert.equal(bad({ ...full, macAppPath: "a;b.app" }), false);
|
|
450
|
+
assert.equal(bad({ ...full, windowsGlob: "${{ x }}" }), false);
|
|
451
|
+
assert.equal(bad({ ...full, retentionDays: 0 }), false);
|
|
452
|
+
assert.equal(bad({ ...full, retentionDays: 91 }), false);
|
|
453
|
+
assert.equal(bad({ ...full, mac: "notarize" }), false);
|
|
454
|
+
assert.equal(bad({ ...full, extra: 1 }), false);
|
|
455
|
+
assert.equal(bad({ windows: "unsigned", windowsGlob: "a/*.exe", macAppPath: "x.app" }), false, "macAppPath requires mac sign");
|
|
456
|
+
assert.equal(bad(full, { ...base, ciAllOs: false }), false, "single windows runner cannot build the mac app");
|
|
457
|
+
assert.equal(bad(full, { ...VALID_MINIMAL, devArtifact: full }), false);
|
|
458
|
+
assert.equal(validateManifest({ ...VALID_MINIMAL, devArtifact: full }).valid, false, "node profile rejects devArtifact");
|
|
459
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/git",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.32",
|
|
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",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/bogusyogi/
|
|
25
|
-
"directory": "
|
|
24
|
+
"url": "git+https://github.com/bogusyogi/rightkit.git",
|
|
25
|
+
"directory": "packages/git"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "node --test *.test.mjs"
|
package/render.test.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { renderTemplate, MANAGED_MARKER } from "./render.mjs";
|
|
7
9
|
import { buildRenderContext, LANE_FILENAMES, KNOWN_LANES, validateManifest } from "./manifest.mjs";
|
|
@@ -879,3 +881,90 @@ test("rust-hybrid ci uploads QA evidence after the gate only when qaEvidencePath
|
|
|
879
881
|
const custom = renderTemplate(template, buildRenderContext({ ...structuredClone(base), qaEvidencePath: "pulse-hub-qa", qaEvidenceRetentionDays: 30 }, "/repos/widgets"));
|
|
880
882
|
assert.match(custom, /retention-days: 30\n/);
|
|
881
883
|
});
|
|
884
|
+
|
|
885
|
+
const INCREMENTAL_BASE = {
|
|
886
|
+
...FULL_MANIFEST,
|
|
887
|
+
lanes: ["ci"],
|
|
888
|
+
profile: "rust-hybrid",
|
|
889
|
+
gate: { path: "scripts/gate.sh" },
|
|
890
|
+
matrix: { os: ["xcode-27", "windows-2025"], node: ["26.8.1"] },
|
|
891
|
+
ciAllOs: true,
|
|
892
|
+
};
|
|
893
|
+
const renderRustCi = (manifest) => renderTemplate(
|
|
894
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8"),
|
|
895
|
+
buildRenderContext(structuredClone(manifest), "/repos/pulse"),
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
test("ciIncremental unset or false renders the legacy cache settings; true opts in", () => {
|
|
899
|
+
const unset = renderRustCi(INCREMENTAL_BASE);
|
|
900
|
+
assert.match(unset, /cache-workspace-crates: false/);
|
|
901
|
+
assert.match(unset, /CARGO_INCREMENTAL: "0"/);
|
|
902
|
+
assert.doesNotMatch(unset, /DerivedData|actions\/cache@|dev-mac|dev-windows/);
|
|
903
|
+
assert.equal(renderRustCi({ ...INCREMENTAL_BASE, ciIncremental: false }), unset);
|
|
904
|
+
|
|
905
|
+
const on = renderRustCi({ ...INCREMENTAL_BASE, ciIncremental: true });
|
|
906
|
+
assert.match(on, /cache-workspace-crates: true/);
|
|
907
|
+
assert.match(on, /CARGO_INCREMENTAL: "1"/);
|
|
908
|
+
assert.match(on, /Cache Xcode DerivedData\n if: \$\{\{ runner\.os == 'macOS' \}\}\n uses: actions\/cache@[0-9a-f]{40} # v4\.2\.4/);
|
|
909
|
+
assert.match(on, /hashFiles\('\*\*\/Cargo\.lock', '\*\*\/Package\.resolved'/);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const DEV_MANIFEST = {
|
|
913
|
+
...INCREMENTAL_BASE,
|
|
914
|
+
devArtifact: {
|
|
915
|
+
mac: "sign",
|
|
916
|
+
windows: "unsigned",
|
|
917
|
+
retentionDays: 5,
|
|
918
|
+
macAppPath: "target/release/bundle/macos/Pulse.app",
|
|
919
|
+
windowsGlob: "target/release/*.exe",
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
test("devArtifact renders push-to-default-branch dev steps and a signing job; unset adds nothing", () => {
|
|
924
|
+
const out = renderRustCi(DEV_MANIFEST);
|
|
925
|
+
assert.match(out, /name: dev-windows-\$\{\{ github\.sha \}\}\n path: target\/release\/\*\.exe\n retention-days: 5/);
|
|
926
|
+
assert.match(out, /DEV_APP_PATH: "target\/release\/bundle\/macos\/Pulse\.app"/);
|
|
927
|
+
assert.match(out, /dev-mac-sign:\n[\s\S]*needs: \[ci\]\n if: \$\{\{ github\.event_name == 'push' && github\.ref_name == github\.event\.repository\.default_branch \}\}\n environment: release\n runs-on: xcode-27/);
|
|
928
|
+
assert.match(out, /codesign --force --deep --strict --options runtime --timestamp --sign "\$APPLE_DEVELOPER_ID"/);
|
|
929
|
+
assert.match(out, /name: dev-mac-\$\{\{ github\.sha \}\}/);
|
|
930
|
+
assert.doesNotMatch(out, /notarytool|APPLE_API_KEY|stapler/);
|
|
931
|
+
// Signing material never reaches the PR-reachable ci job.
|
|
932
|
+
assert.doesNotMatch(out.split(" dev-mac-sign:")[0], /secrets\./);
|
|
933
|
+
// mac/windows only render the legs they declare.
|
|
934
|
+
const winOnly = renderRustCi({ ...DEV_MANIFEST, devArtifact: { windows: "unsigned", windowsGlob: "dist/*.exe" } });
|
|
935
|
+
assert.doesNotMatch(winOnly, /dev-mac-sign|dev-unsigned-mac/);
|
|
936
|
+
assert.match(winOnly, /retention-days: 7/);
|
|
937
|
+
assert.equal(renderRustCi({ ...INCREMENTAL_BASE, devArtifact: undefined }), renderRustCi(INCREMENTAL_BASE));
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("devArtifact entitlements add a checkout and --entitlements", () => {
|
|
941
|
+
const out = renderRustCi({ ...DEV_MANIFEST, devArtifact: { ...DEV_MANIFEST.devArtifact, macEntitlements: "build/pulse.entitlements" } });
|
|
942
|
+
assert.match(out, /--entitlements "\$GITHUB_WORKSPACE\/build\/pulse\.entitlements"/);
|
|
943
|
+
assert.match(out.split(" dev-mac-sign:")[1], /actions\/checkout@/);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("rendered ciIncremental + devArtifact workflows pass actionlint", (t) => {
|
|
947
|
+
const bin = spawnSync("actionlint", ["-version"], { encoding: "utf8" });
|
|
948
|
+
if (bin.error) return t.skip("actionlint not installed");
|
|
949
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "right-git-actionlint-"));
|
|
950
|
+
try {
|
|
951
|
+
fs.mkdirSync(path.join(dir, ".github", "workflows"), { recursive: true });
|
|
952
|
+
fs.writeFileSync(path.join(dir, ".github", "actionlint.yaml"), "self-hosted-runner:\n labels:\n - xcode-*\n");
|
|
953
|
+
const samples = {
|
|
954
|
+
"ci-default.yml": INCREMENTAL_BASE,
|
|
955
|
+
"ci-incremental.yml": { ...INCREMENTAL_BASE, ciIncremental: true },
|
|
956
|
+
"ci-dev.yml": { ...DEV_MANIFEST, ciIncremental: true },
|
|
957
|
+
"ci-dev-entitlements.yml": { ...DEV_MANIFEST, devArtifact: { ...DEV_MANIFEST.devArtifact, macEntitlements: "build/pulse.entitlements" } },
|
|
958
|
+
};
|
|
959
|
+
const files = [];
|
|
960
|
+
for (const [name, manifest] of Object.entries(samples)) {
|
|
961
|
+
const file = path.join(dir, ".github", "workflows", name);
|
|
962
|
+
fs.writeFileSync(file, renderRustCi(manifest));
|
|
963
|
+
files.push(file);
|
|
964
|
+
}
|
|
965
|
+
const result = spawnSync("actionlint", ["-shellcheck=", "-pyflakes=", "-config-file", path.join(dir, ".github", "actionlint.yaml"), ...files], { encoding: "utf8" });
|
|
966
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
967
|
+
} finally {
|
|
968
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
969
|
+
}
|
|
970
|
+
});
|
|
@@ -63,7 +63,7 @@ ${CI_MATRIX_OS}
|
|
|
63
63
|
package-manager-cache: false
|
|
64
64
|
${PACKAGE_MANAGER_SETUP}
|
|
65
65
|
- run: ${INSTALL_COMMAND}
|
|
66
|
-
${OWNERSHIP_STEP}
|
|
66
|
+
${OWNERSHIP_STEP}${CI_DERIVED_DATA_STEP}
|
|
67
67
|
- uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
|
|
68
68
|
if: steps.rust-changes.outputs.required == 'true'
|
|
69
69
|
with:
|
|
@@ -75,7 +75,7 @@ ${OWNERSHIP_STEP}
|
|
|
75
75
|
${RUST_CACHE_WORKSPACES}
|
|
76
76
|
shared-key: ${ARTIFACT_PREFIX}-rust
|
|
77
77
|
add-job-id-key: false
|
|
78
|
-
cache-workspace-crates:
|
|
78
|
+
cache-workspace-crates: ${CI_CACHE_WORKSPACE_CRATES}
|
|
79
79
|
cache-on-failure: true
|
|
80
80
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
81
81
|
- uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
|
@@ -88,7 +88,7 @@ ${RUST_CACHE_WORKSPACES}
|
|
|
88
88
|
RIGHT_GIT_RUST_REQUIRED: ${{ steps.rust-changes.outputs.required }}
|
|
89
89
|
SCCACHE_GHA_ENABLED: "true"
|
|
90
90
|
RUSTC_WRAPPER: ${{ steps.rust-changes.outputs.required == 'true' && 'sccache' || '' }}
|
|
91
|
-
CARGO_INCREMENTAL: "
|
|
91
|
+
CARGO_INCREMENTAL: "${CI_CARGO_INCREMENTAL}"
|
|
92
92
|
CARGO_PROFILE_DEV_DEBUG: "0"
|
|
93
93
|
CARGO_PROFILE_TEST_DEBUG: "0"
|
|
94
94
|
${QA_EVIDENCE_STEP}
|