@rightkit/release 0.2.98 → 0.2.100
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/direct-bootstrap.mjs +2 -2
- package/nsis-payload.mjs +17 -4
- package/package.json +1 -1
- package/release-invocation.mjs +47 -1
- package/release.mjs +27 -3
- package/rightkit-versions.json +9 -4
package/direct-bootstrap.mjs
CHANGED
|
@@ -356,7 +356,7 @@ export function renderPowerShellBootstrap({
|
|
|
356
356
|
"New-Item -ItemType Junction -Path $NextLink -Target $VersionRoot | Out-Null",
|
|
357
357
|
"$Switched = $false; $PriorDetached = $false",
|
|
358
358
|
"try {",
|
|
359
|
-
" if (Test-Path $Current) { Move
|
|
359
|
+
" if (Test-Path $Current) { [IO.Directory]::Move($Current, $PreviousLink); $PriorDetached = $true }; [IO.Directory]::Move($NextLink, $Current); $Switched = $true",
|
|
360
360
|
" $StableExe = Join-Path $Current " + psQuote(executablePath.replaceAll("/", "\\")),
|
|
361
361
|
" $ActivationArgs = @(Expand-BootstrapArgs $ActivationArgsTemplate $VersionRoot $Version); $StatusArgs = @(Expand-BootstrapArgs $StatusArgsTemplate $VersionRoot $Version)",
|
|
362
362
|
" & $StableExe @ActivationArgs; if ($LASTEXITCODE -ne 0) { throw 'Activation failed' }",
|
|
@@ -367,7 +367,7 @@ export function renderPowerShellBootstrap({
|
|
|
367
367
|
// cleanup fails, catch still has a rollback anchor to restore.
|
|
368
368
|
" Remove-Item -Force $Journal; if (Test-Path $PreviousLink) { Remove-Item -Force $PreviousLink }; Write-Output (" + psQuote(product + " ") + " + $Version + ' installed')",
|
|
369
369
|
"} catch {",
|
|
370
|
-
" if (($Switched -or $PriorDetached) -and (Test-Path $Current)) { Remove-Item -Force $Current }; if ($PriorDetached -and (Test-Path $PreviousLink)) { Move
|
|
370
|
+
" if (($Switched -or $PriorDetached) -and (Test-Path $Current)) { Remove-Item -Force $Current }; if ($PriorDetached -and (Test-Path $PreviousLink)) { [IO.Directory]::Move($PreviousLink, $Current) }",
|
|
371
371
|
" if ($PriorTarget -and (Test-Path $Current)) { $PriorExe = Join-Path $Current " + psQuote(executablePath.replaceAll("/", "\\")) + "; $PriorActivationArgs = @(Expand-BootstrapArgs $ActivationArgsTemplate ([string]$PriorTarget) $PriorVersion); $PriorStatusArgs = @(Expand-BootstrapArgs $StatusArgsTemplate ([string]$PriorTarget) $PriorVersion); & $PriorExe @PriorActivationArgs; if ($LASTEXITCODE -ne 0) { throw 'Rollback activation failed' }; $PriorHealthText = (& $PriorExe @PriorStatusArgs | Out-String).Trim(); if ($LASTEXITCODE -ne 0) { throw 'Rollback health failed' }; $PriorHealth = $PriorHealthText | ConvertFrom-Json; try { Assert-Health $PriorHealth $PriorVersion } catch { throw 'Prior health was not restored' } }",
|
|
372
372
|
" if (Test-Path $VersionRoot) { Remove-Item -Recurse -Force $VersionRoot }; throw",
|
|
373
373
|
"} finally { if (Test-Path $Work) { Remove-Item -Recurse -Force $Work } }",
|
package/nsis-payload.mjs
CHANGED
|
@@ -94,10 +94,23 @@ export function extractNsisEntry(installer, entryName, { sevenZip = resolveSeven
|
|
|
94
94
|
const destination = path.join(tempRoot, `right-nsis-${process.pid}-${Math.random().toString(16).slice(2)}`);
|
|
95
95
|
mkdirSync(destination, { recursive: true });
|
|
96
96
|
try {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const
|
|
97
|
+
// NSIS entries may live below `$PLUGINSDIR/release`. A basename-only 7-Zip
|
|
98
|
+
// filter then reports `No files to process`, so resolve the actual archive
|
|
99
|
+
// member first & extract that exact path.
|
|
100
|
+
const requested = String(entryName).replace(/\\/g, "/");
|
|
101
|
+
const members = listNsisPayload(resolvedInstaller, { sevenZip, run });
|
|
102
|
+
const exact = members.filter((member) => member.name.toLowerCase() === requested.toLowerCase());
|
|
103
|
+
const matches = exact.length > 0
|
|
104
|
+
? exact
|
|
105
|
+
: members.filter((member) => path.posix.basename(member.name).toLowerCase() === base.toLowerCase());
|
|
106
|
+
if (matches.length === 0) {
|
|
107
|
+
throw new Error(`could not extract ${base} from ${resolvedInstaller}: no such payload entry`);
|
|
108
|
+
}
|
|
109
|
+
if (matches.length !== 1) {
|
|
110
|
+
throw new Error(`could not uniquely identify ${base} in ${resolvedInstaller}: found ${matches.length} payload entries`);
|
|
111
|
+
}
|
|
112
|
+
const result = run(sevenZip, ["e", "-tnsis", "-y", `-o${destination}`, "--", resolvedInstaller, matches[0].name]);
|
|
113
|
+
const extracted = path.join(destination, path.posix.basename(matches[0].name));
|
|
101
114
|
if (result.status !== 0 || !existsSync(extracted)) {
|
|
102
115
|
throw new Error(
|
|
103
116
|
`could not extract ${base} from ${resolvedInstaller}: ${(result.stderr || result.stdout || "no such entry").trim()}`,
|
package/package.json
CHANGED
package/release-invocation.mjs
CHANGED
|
@@ -32,7 +32,11 @@ export function assertPrimaryReleaseCheckout(repoRoot, env = process.env) {
|
|
|
32
32
|
throw new Error(`right-release must be invoked from the primary Git worktree: ${primary}`);
|
|
33
33
|
}
|
|
34
34
|
const branch = spawnSync("git", ["symbolic-ref", "--quiet", "HEAD"], { cwd: repoRoot, encoding: "utf8", windowsHide: true });
|
|
35
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
branch.status !== 0
|
|
37
|
+
&& !isVerifiedGithubActionsTagCheckout(repoRoot, env)
|
|
38
|
+
&& !isVerifiedGithubActionsAdmittedDispatch(repoRoot, env)
|
|
39
|
+
) {
|
|
36
40
|
throw new Error("right-release requires a branch-attached primary Git checkout; detached HEAD is forbidden");
|
|
37
41
|
}
|
|
38
42
|
}
|
|
@@ -63,6 +67,48 @@ function isVerifiedGithubActionsTagCheckout(repoRoot, env) {
|
|
|
63
67
|
return repository?.toLowerCase() === env.GITHUB_REPOSITORY.toLowerCase();
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
// A tag-last release chain never has a tag to check out: the whole point is that
|
|
71
|
+
// publication mints the tag only after qualification passes. Every stage instead
|
|
72
|
+
// checks out one exact commit SHA admitted from the default branch, which leaves
|
|
73
|
+
// HEAD detached. Refusing that would force the retired tag-first shape back.
|
|
74
|
+
//
|
|
75
|
+
// This exemption is deliberately STRICTER than the tag one above. A tag proves
|
|
76
|
+
// only that some ref points at this commit; here the commit must additionally be
|
|
77
|
+
// the exact revision this run was admitted against AND be genuinely reachable
|
|
78
|
+
// from the remote branch the dispatch came from, so a detached HEAD parked on an
|
|
79
|
+
// arbitrary, orphaned, or force-pushed commit is still refused.
|
|
80
|
+
function isVerifiedGithubActionsAdmittedDispatch(repoRoot, env) {
|
|
81
|
+
const sha = env.GITHUB_SHA;
|
|
82
|
+
const admitted = env.RIGHT_GIT_SOURCE_REVISION;
|
|
83
|
+
if (
|
|
84
|
+
env.GITHUB_ACTIONS !== "true"
|
|
85
|
+
|| env.GITHUB_EVENT_NAME !== "workflow_dispatch"
|
|
86
|
+
|| env.GITHUB_REF_TYPE !== "branch"
|
|
87
|
+
|| !/^refs\/heads\/.+$/.test(env.GITHUB_REF ?? "")
|
|
88
|
+
|| !/^[a-f0-9]{40}$/.test(sha ?? "")
|
|
89
|
+
|| !/^[a-f0-9]{40}$/.test(admitted ?? "")
|
|
90
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(env.GITHUB_REPOSITORY ?? "")
|
|
91
|
+
) return false;
|
|
92
|
+
// The checkout must be the exact revision the admission stage bound this run
|
|
93
|
+
// to — not merely some commit the dispatch happened to land on.
|
|
94
|
+
const head = git(repoRoot, ["rev-parse", "HEAD"]).trim();
|
|
95
|
+
if (head !== admitted) return false;
|
|
96
|
+
let origin;
|
|
97
|
+
try {
|
|
98
|
+
origin = git(repoRoot, ["remote", "get-url", "origin"]).trim();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const repository = origin.match(/(?:github\.com[/:])([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i)?.[1];
|
|
103
|
+
if (repository?.toLowerCase() !== env.GITHUB_REPOSITORY.toLowerCase()) return false;
|
|
104
|
+
// Reachability from the remote branch that was dispatched. A shallow or
|
|
105
|
+
// ref-less checkout cannot prove this, and so does not get the exemption.
|
|
106
|
+
const dispatchBranch = (env.GITHUB_REF ?? "").replace(/^refs\/heads\//, "");
|
|
107
|
+
const remoteRef = `refs/remotes/origin/${dispatchBranch}`;
|
|
108
|
+
if (spawnSync("git", ["rev-parse", "--verify", "--quiet", remoteRef], { cwd: repoRoot, windowsHide: true }).status !== 0) return false;
|
|
109
|
+
return spawnSync("git", ["merge-base", "--is-ancestor", head, remoteRef], { cwd: repoRoot, windowsHide: true }).status === 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
66
112
|
export function resolvePrimaryWorktree({ repoRoot, worktrees, gitDir, coreWorktree }) {
|
|
67
113
|
const listed = worktrees.match(/^worktree (.+)$/m)?.[1];
|
|
68
114
|
if (!listed) return undefined;
|
package/release.mjs
CHANGED
|
@@ -694,17 +694,41 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
694
694
|
const windowsSource = windowsSpan ? source.slice(windowsSpan.start, windowsSpan.end) : "";
|
|
695
695
|
const macosSource = macosSpan ? source.slice(macosSpan.start, macosSpan.end) : "";
|
|
696
696
|
const publishSource = publishSpan ? source.slice(publishSpan.start, publishSpan.end) : "";
|
|
697
|
+
// Two release-chain shapes are accepted: the retired tag-first lane (a pushed
|
|
698
|
+
// `refs/tags/v*` — or, for signing only, an explicit manual `signed_qualification`
|
|
699
|
+
// dispatch — gates signing/publish) and the current tag-last lane (one admitted
|
|
700
|
+
// dispatch binds every stage to an exact main commit; an `admission` job validates
|
|
701
|
+
// the dispatch envelope and publication itself mints the tag last). Both must prove
|
|
702
|
+
// the same thing — signing is unreachable without qualification actually being
|
|
703
|
+
// requested for this run, and publish is unreachable unless that qualification
|
|
704
|
+
// verified success in this same run — only the mechanism differs.
|
|
705
|
+
const legacySigningGate = (job) => new RegExp(
|
|
706
|
+
`${job}:\\n needs: \\[candidate\\]\\n if: \\$\\{\\{ startsWith\\(github\\.ref, 'refs\\/tags\\/v'\\)(?: \\|\\| \\(github\\.event_name == 'workflow_dispatch' && inputs\\.signed_qualification == true\\))? \\}\\}`,
|
|
707
|
+
);
|
|
708
|
+
const admittedSigningGate = (job) => new RegExp(
|
|
709
|
+
`${job}:\\n needs: \\[admission, candidate\\]\\n if: \\$\\{\\{ needs\\.admission\\.outputs\\.signed_qualification == 'true' \\}\\}`,
|
|
710
|
+
);
|
|
697
711
|
const protectedWindowsSigning = workflow === "release-candidate.yml"
|
|
698
712
|
&& manifest?.candidate?.signWindows === true
|
|
699
|
-
&&
|
|
713
|
+
&& (legacySigningGate("windows-sign").test(windowsSource) || admittedSigningGate("windows-sign").test(windowsSource));
|
|
700
714
|
const protectedMacosSigning = workflow === "release-candidate.yml"
|
|
701
715
|
&& manifest?.candidate?.signMacos === true
|
|
702
|
-
&&
|
|
716
|
+
&& (legacySigningGate("macos-sign").test(macosSource) || admittedSigningGate("macos-sign").test(macosSource));
|
|
703
717
|
const releaseChain = workflow === "release-candidate.yml" ? manifest?.candidate?.releaseChain : undefined;
|
|
704
718
|
const allowedReleaseSecrets = new Set(releaseChain?.publishSecrets ?? []);
|
|
705
719
|
const writePermissions = [...source.matchAll(/^\s*(actions|attestations|checks|contents|deployments|discussions|id-token|issues|models|packages|pages|pull-requests|security-events|statuses):\s*write\s*$/gmi)].map((match) => ({ name: match[1].toLowerCase(), index: match.index }));
|
|
720
|
+
// Legacy: publish requires a pushed tag AND same-run installed-qualification
|
|
721
|
+
// success — the tag is the sole gate because tag-first signing already proved
|
|
722
|
+
// qualification before the tag existed. Tag-last: there is no tag to check;
|
|
723
|
+
// instead publish requires the admission job's `publish` output (which itself
|
|
724
|
+
// implies `signed_qualification` was requested — see the admission job's own
|
|
725
|
+
// envelope validation) AND same-run success of BOTH installed-qualification
|
|
726
|
+
// (Windows) and macos-sign (macOS), so publish is unreachable unless every
|
|
727
|
+
// signing/qualification stage actually verified in this exact run.
|
|
728
|
+
const legacyReleasePublishGate = /publish:\n needs: \[[^\n]*installed-qualification[^\n]*\]\n if: \$\{\{ startsWith\(github\.ref, 'refs\/tags\/v'\) && needs\.installed-qualification\.result == 'success'/.test(publishSource);
|
|
729
|
+
const admittedReleasePublishGate = /publish:\n needs: \[(?=[^\n]*\badmission\b)(?=[^\n]*\binstalled-qualification\b)(?=[^\n]*\bmacos-sign\b)[^\n]*\]\n if: \$\{\{ needs\.admission\.outputs\.publish == 'true' && needs\.installed-qualification\.result == 'success' && needs\.macos-sign\.result == 'success' \}\}/.test(publishSource);
|
|
706
730
|
const protectedReleasePublish = releaseChain
|
|
707
|
-
&&
|
|
731
|
+
&& (legacyReleasePublishGate || admittedReleasePublishGate)
|
|
708
732
|
&& /\n permissions:\n contents: write\n/.test(publishSource);
|
|
709
733
|
if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
|
|
710
734
|
|| writePermissions.some(({ name, index }) => name === "id-token"
|
package/rightkit-versions.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"stagedNpm": {
|
|
16
16
|
"@rightkit/ax": "0.2.1",
|
|
17
|
-
"@rightkit/git": "0.2.
|
|
17
|
+
"@rightkit/git": "0.2.25",
|
|
18
18
|
"@rightkit/hooks": "0.1.1",
|
|
19
19
|
"@rightkit/legal": "0.3.1",
|
|
20
20
|
"@rightkit/legal-ui": "0.1.1",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@rightkit/logs": "0.1.4",
|
|
23
23
|
"@rightkit/platform-ui": "0.1.1",
|
|
24
24
|
"@rightkit/qa": "0.2.1",
|
|
25
|
-
"@rightkit/release": "0.2.
|
|
25
|
+
"@rightkit/release": "0.2.100",
|
|
26
26
|
"@rightkit/tauri": "0.1.1",
|
|
27
27
|
"@rightkit/updates": "0.2.4"
|
|
28
28
|
},
|
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
"0.2.18",
|
|
51
51
|
"0.2.19",
|
|
52
52
|
"0.2.20",
|
|
53
|
-
"0.2.21"
|
|
53
|
+
"0.2.21",
|
|
54
|
+
"0.2.22",
|
|
55
|
+
"0.2.23",
|
|
56
|
+
"0.2.24"
|
|
54
57
|
],
|
|
55
58
|
"@rightkit/legal-ui": [
|
|
56
59
|
"0.1.0"
|
|
@@ -105,7 +108,9 @@
|
|
|
105
108
|
"0.2.94",
|
|
106
109
|
"0.2.95",
|
|
107
110
|
"0.2.96",
|
|
108
|
-
"0.2.97"
|
|
111
|
+
"0.2.97",
|
|
112
|
+
"0.2.98",
|
|
113
|
+
"0.2.99"
|
|
109
114
|
],
|
|
110
115
|
"@rightkit/qa": [
|
|
111
116
|
"0.1.0",
|