@codyswann/lisa 4.22.6 → 4.22.8
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/all/copy-overwrite/scripts/check-release-package-identity.mjs +319 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +28 -14
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/sync/lifecycle-defaults.d.ts +21 -0
- package/dist/sync/lifecycle-defaults.d.ts.map +1 -1
- package/dist/sync/lifecycle-defaults.js +21 -1
- package/dist/sync/lifecycle-defaults.js.map +1 -1
- package/dist/sync/registry.d.ts.map +1 -1
- package/dist/sync/registry.js +7 -1
- package/dist/sync/registry.js.map +1 -1
- package/package.json +3 -2
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa/rules/reference/config-resolution.md +30 -3
- package/plugins/lisa/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa-agy/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-agy/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa-agy/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +30 -3
- package/plugins/lisa-copilot/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa-copilot/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-copilot/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa-copilot/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +30 -3
- package/plugins/lisa-cursor/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/lisa-cursor/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/lisa-cursor/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/lisa-cursor/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/reference/config-resolution.md +30 -3
- package/plugins/src/base/scripts/resolve-lifecycle-role.mjs +340 -0
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-github-prd-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-github-sync/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-github-write-prd/SKILL.md +7 -3
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-jira-sync/SKILL.md +3 -1
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-linear-evidence/SKILL.md +12 -11
- package/plugins/src/base/skills/lisa-linear-prd-intake/SKILL.md +6 -5
- package/plugins/src/base/skills/lisa-linear-sync/SKILL.md +36 -20
- package/plugins/src/base/skills/lisa-tracker-sync/SKILL.md +22 -2
- package/scripts/check-release-package-identity.mjs +319 -0
- package/scripts/two-channel-couplings.json +17 -4
- package/typescript/copy-overwrite/.prettierignore +1 -0
|
@@ -21,7 +21,27 @@ See the `config-resolution` rule for configuration and dispatch table.
|
|
|
21
21
|
- Anything else → stop and report `"Unknown tracker '<value>' in .lisa.config.json. Expected 'jira', 'github', or 'linear'."`
|
|
22
22
|
3. Pass through the output.
|
|
23
23
|
|
|
24
|
-
`$ARGUMENTS` is forwarded verbatim, including the optional `--rollup` flag (see "Parent status rollup" below),
|
|
24
|
+
`$ARGUMENTS` is forwarded verbatim, including the optional `--rollup` flag (see "Parent status rollup" below), `pr_url=<url>`, and `merge_sha=<sha>`. The shim never interprets these — the vendor skill does.
|
|
25
|
+
|
|
26
|
+
> **There is no per-milestone lane-write flag on any vendor.** This paragraph previously documented `--update-label` on GitHub and `--update-state` on Linear. `--update-label` was never implemented — `lisa-github-sync` says flatly *"This skill never relabels"* — so a caller passing it got a silent no-op on GitHub and a real write on Linear for the equivalent flag. Both are gone. `--rollup` is the only write path a sync skill has.
|
|
27
|
+
|
|
28
|
+
## Tracker status vocabulary — binding on all three arms
|
|
29
|
+
|
|
30
|
+
Every suggested or performed transition names **only a role the project configured**, never a status, state or label discovered from the tracker's live workflow (transition lists, board columns, `type`-derived matches, other tickets). This binds a lead performing tracker writes exactly as it binds a subagent.
|
|
31
|
+
|
|
32
|
+
Two consequences the vendor arms must not restate differently:
|
|
33
|
+
|
|
34
|
+
- **A milestone whose role is unset gets a comment, not a transition.** `review` and the `qa.*` roles are optional and carry **no default** — omitting one means the project does not run that step, and the lifecycle skips it. "Unset" must never resolve to a built-in name; that is the difference between "not customized" and "we deliberately don't do this".
|
|
35
|
+
- **A fallback may inform a read; it may never supply a write target.** Where a vendor exposes one (Linear resolves states by `type`), it selects by board position rather than intent — so on a board carrying more than one plausible state it returns whichever sits earliest, which is exactly the human-only lane a project left out of its config on purpose.
|
|
36
|
+
|
|
37
|
+
Resolve roles through the shared resolver rather than an inlined helper:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
|
|
41
|
+
--role <role> --vendor <jira|linear|github> --intent <read|write> [--env <env>]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Exit `0` with a value means configured; exit `0` with **empty** output means an optional role is unset — skip the transition; exit `2` means a required role is unset or a write was refused a fallback value.
|
|
25
45
|
|
|
26
46
|
If `$ARGUMENTS` is empty, all vendor skills auto-detect a ticket reference from the active plan file (most recently modified `.md` in `plans/`).
|
|
27
47
|
|
|
@@ -65,6 +85,6 @@ This is the reverse half of `lisa-git-submit-pr`'s PR body linkage. A PR that me
|
|
|
65
85
|
## Rules
|
|
66
86
|
|
|
67
87
|
- Idempotent updates — running sync at the same milestone twice should not produce duplicate comments. Vendor skills enforce this.
|
|
68
|
-
- Never
|
|
88
|
+
- Never transition a leaf's lane from a sync milestone on any vendor. The canonical signal differs — the `status:*` label on GitHub, the workflow status on JIRA, the native workflow state on Linear — but the owner is the same everywhere: build-intake / the vendor agent. Every arm only suggests. `--rollup` (parent derivation) is the sole exception, and it derives the parent from its children rather than advancing a leaf.
|
|
69
89
|
- Parent rollup derives state from children per the `leaf-only-lifecycle` rule; it never sets a parent to `ready` and never resolves a dev/staging `done` in this single-environment repo.
|
|
70
90
|
- Pull request backlinks are mandatory when `pr_url=<url>` is present: native first, managed-comment fallback, never silently dropped.
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fail closed when a release checkout or packed npm candidate does not bind to
|
|
7
|
+
* one immutable version and commit.
|
|
8
|
+
*
|
|
9
|
+
* Release workflows have two separate state transitions: standard-version
|
|
10
|
+
* writes and pushes the release commit, then npm builds and publishes a tagged
|
|
11
|
+
* checkout. A tag aimed at the pre-release merge let the second transition
|
|
12
|
+
* compile stale generated source and only afterwards rewrite package.json. This
|
|
13
|
+
* checker makes both boundaries executable: `checkout` proves tag/HEAD/commit
|
|
14
|
+
* identity before build, and `pack` proves the exact tarball passed to
|
|
15
|
+
* `npm publish` contains matching package metadata and generated certificate.
|
|
16
|
+
*
|
|
17
|
+
* Projects without Lisa's nightly certificate still receive the generic
|
|
18
|
+
* version/commit/tarball checks. When the source certificate exists, its built
|
|
19
|
+
* and packed counterpart becomes mandatory.
|
|
20
|
+
* @module scripts/check-release-package-identity
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import {
|
|
24
|
+
appendFileSync,
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import process from "node:process";
|
|
32
|
+
|
|
33
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
34
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
35
|
+
|
|
36
|
+
const RELEASE_COMMIT_PATTERN = /^[a-f0-9]{40}$/u;
|
|
37
|
+
const SOURCE_CERTIFICATE = "src/core/nightly-e2e-guard-behavior-certificate.ts";
|
|
38
|
+
const PACKED_CERTIFICATE =
|
|
39
|
+
"package/dist/core/nightly-e2e-guard-behavior-certificate.js";
|
|
40
|
+
|
|
41
|
+
/** Raise one consistently named fail-closed identity error. */
|
|
42
|
+
function mismatch(message) {
|
|
43
|
+
throw new Error(`Release package identity mismatch: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Return a required CLI option. */
|
|
47
|
+
function requiredOption(argv, name) {
|
|
48
|
+
const index = argv.indexOf(name);
|
|
49
|
+
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
50
|
+
if (!value || value.startsWith("--")) mismatch(`missing required ${name}`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve one git revision to its immutable commit. */
|
|
55
|
+
function gitCommit(root, revision) {
|
|
56
|
+
try {
|
|
57
|
+
return String(
|
|
58
|
+
boundedExecFileSync("git", ["rev-parse", `${revision}^{commit}`], {
|
|
59
|
+
cwd: root,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
})
|
|
62
|
+
).trim();
|
|
63
|
+
} catch {
|
|
64
|
+
mismatch(`could not resolve git revision ${revision}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parse one JSON file with a release-specific diagnostic. */
|
|
69
|
+
function readJson(filePath, label) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
72
|
+
} catch {
|
|
73
|
+
mismatch(`${label} is missing or invalid JSON (${filePath})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** SHA-256 for an immutable file or member. */
|
|
78
|
+
function sha256(bytes) {
|
|
79
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read one tar member without extracting paths into the checkout. */
|
|
83
|
+
function tarMember(tarballPath, member) {
|
|
84
|
+
try {
|
|
85
|
+
return Buffer.from(
|
|
86
|
+
boundedExecFileSync("tar", ["-xOf", tarballPath, member])
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
mismatch(`packed candidate does not contain ${member}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Extract the one workspace package version certified by generated bytes. */
|
|
94
|
+
function certificateVersion(bytes, label) {
|
|
95
|
+
const match = String(bytes).match(
|
|
96
|
+
/workspace package @codyswann\/lisa@([^\s"'()]+)\s*\(/u
|
|
97
|
+
);
|
|
98
|
+
if (!match?.[1]) mismatch(`${label} has no workspace package provenance`);
|
|
99
|
+
return match[1];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Require a valid immutable release commit spelling. */
|
|
103
|
+
export function assertReleaseCommit(releaseCommit) {
|
|
104
|
+
if (!RELEASE_COMMIT_PATTERN.test(releaseCommit)) {
|
|
105
|
+
mismatch(
|
|
106
|
+
`release commit must be 40 lowercase hexadecimal characters; got ${JSON.stringify(releaseCommit)}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return releaseCommit;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Prove that the checked-out tag and HEAD name the supplied release commit.
|
|
114
|
+
* @param {{root: string, releaseCommit: string, tag: string}} input Identity input
|
|
115
|
+
* @returns {{headCommit: string, tagCommit: string, releaseCommit: string}}
|
|
116
|
+
*/
|
|
117
|
+
export function assertCheckoutIdentity({ root, releaseCommit, tag }) {
|
|
118
|
+
assertReleaseCommit(releaseCommit);
|
|
119
|
+
const headCommit = gitCommit(root, "HEAD");
|
|
120
|
+
if (headCommit !== releaseCommit) {
|
|
121
|
+
mismatch(
|
|
122
|
+
`HEAD ${headCommit} does not match release commit ${releaseCommit}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const tagCommit = gitCommit(root, tag);
|
|
126
|
+
if (tagCommit !== releaseCommit) {
|
|
127
|
+
mismatch(`tag ${tag} resolves ${tagCommit}, expected ${releaseCommit}`);
|
|
128
|
+
}
|
|
129
|
+
return Object.freeze({ headCommit, tagCommit, releaseCommit });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Pack and validate the exact candidate later passed to npm publish.
|
|
134
|
+
* @param {{root: string, version: string, releaseCommit: string, packDestination: string}} input Candidate input
|
|
135
|
+
* @returns {{version: string, releaseCommit: string, certificateVersion: string|null, tarballPath: string, tarballSha256: string, certificateMemberSha256: string|null}}
|
|
136
|
+
*/
|
|
137
|
+
export function packAndValidateReleaseCandidate({
|
|
138
|
+
root,
|
|
139
|
+
version,
|
|
140
|
+
releaseCommit,
|
|
141
|
+
packDestination,
|
|
142
|
+
}) {
|
|
143
|
+
assertReleaseCommit(releaseCommit);
|
|
144
|
+
const packageJson = readJson(path.join(root, "package.json"), "package.json");
|
|
145
|
+
if (packageJson.version !== version) {
|
|
146
|
+
mismatch(
|
|
147
|
+
`package.json version ${JSON.stringify(packageJson.version)} expected ${version}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (packageJson.lisaReleaseCommit !== releaseCommit) {
|
|
151
|
+
mismatch(
|
|
152
|
+
`package.json lisaReleaseCommit ${JSON.stringify(packageJson.lisaReleaseCommit)} expected ${releaseCommit}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (packageJson.gitHead !== releaseCommit) {
|
|
156
|
+
mismatch(
|
|
157
|
+
`package.json gitHead ${JSON.stringify(packageJson.gitHead)} expected ${releaseCommit}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sourceCertificatePath = path.join(root, SOURCE_CERTIFICATE);
|
|
162
|
+
const hasCertificate = existsSync(sourceCertificatePath);
|
|
163
|
+
if (hasCertificate) {
|
|
164
|
+
const sourceVersion = certificateVersion(
|
|
165
|
+
readFileSync(sourceCertificatePath),
|
|
166
|
+
"source certificate"
|
|
167
|
+
);
|
|
168
|
+
if (sourceVersion !== version) {
|
|
169
|
+
mismatch(
|
|
170
|
+
`source certificate names ${sourceVersion}, expected ${version}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
mkdirSync(packDestination, { recursive: true });
|
|
176
|
+
if (readdirSync(packDestination).length !== 0) {
|
|
177
|
+
mismatch(`pack destination must be empty (${packDestination})`);
|
|
178
|
+
}
|
|
179
|
+
let packOutput;
|
|
180
|
+
try {
|
|
181
|
+
packOutput = String(
|
|
182
|
+
boundedExecFileSync(
|
|
183
|
+
"npm",
|
|
184
|
+
[
|
|
185
|
+
"pack",
|
|
186
|
+
"--ignore-scripts",
|
|
187
|
+
"--silent",
|
|
188
|
+
"--pack-destination",
|
|
189
|
+
packDestination,
|
|
190
|
+
],
|
|
191
|
+
{ cwd: root, encoding: "utf8", timeout: 120_000 }
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
mismatch(
|
|
196
|
+
`npm pack failed before publication (${error instanceof Error ? error.message : String(error)})`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const candidates = readdirSync(packDestination).filter(file =>
|
|
200
|
+
file.endsWith(".tgz")
|
|
201
|
+
);
|
|
202
|
+
if (candidates.length !== 1) {
|
|
203
|
+
mismatch("npm pack did not report exactly one candidate tarball");
|
|
204
|
+
}
|
|
205
|
+
const reportedCandidate = packOutput
|
|
206
|
+
.split(/\r?\n/u)
|
|
207
|
+
.map(line => line.trim())
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.at(-1);
|
|
210
|
+
if (path.basename(reportedCandidate ?? "") !== candidates[0]) {
|
|
211
|
+
mismatch(
|
|
212
|
+
`npm pack reported ${JSON.stringify(reportedCandidate)}, wrote ${JSON.stringify(candidates[0])}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tarballPath = path.resolve(packDestination, candidates[0]);
|
|
217
|
+
const tarballBytes = readFileSync(tarballPath);
|
|
218
|
+
const packedPackage = JSON.parse(
|
|
219
|
+
String(tarMember(tarballPath, "package/package.json"))
|
|
220
|
+
);
|
|
221
|
+
if (packedPackage.version !== version) {
|
|
222
|
+
mismatch(
|
|
223
|
+
`packed package version ${JSON.stringify(packedPackage.version)} expected ${version}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (packedPackage.lisaReleaseCommit !== releaseCommit) {
|
|
227
|
+
mismatch(
|
|
228
|
+
`packed lisaReleaseCommit ${JSON.stringify(packedPackage.lisaReleaseCommit)} expected ${releaseCommit}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (packedPackage.gitHead !== releaseCommit) {
|
|
232
|
+
mismatch(
|
|
233
|
+
`packed gitHead ${JSON.stringify(packedPackage.gitHead)} expected ${releaseCommit}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let packedCertificateVersion = null;
|
|
238
|
+
let certificateMemberSha256 = null;
|
|
239
|
+
if (hasCertificate) {
|
|
240
|
+
const member = tarMember(tarballPath, PACKED_CERTIFICATE);
|
|
241
|
+
packedCertificateVersion = certificateVersion(member, "packed certificate");
|
|
242
|
+
if (packedCertificateVersion !== version) {
|
|
243
|
+
mismatch(
|
|
244
|
+
`packed certificate names ${packedCertificateVersion}, expected ${version} at release commit ${releaseCommit} (${PACKED_CERTIFICATE})`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
certificateMemberSha256 = sha256(member);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
version,
|
|
252
|
+
releaseCommit,
|
|
253
|
+
certificateVersion: packedCertificateVersion,
|
|
254
|
+
tarballPath,
|
|
255
|
+
tarballSha256: sha256(tarballBytes),
|
|
256
|
+
certificateMemberSha256,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Append validated tarball identity to a GitHub Actions step output. */
|
|
261
|
+
function writeGithubOutput(result, outputPath) {
|
|
262
|
+
if (!outputPath) return;
|
|
263
|
+
if (result.tarballPath.includes("\n") || result.tarballPath.includes("\r")) {
|
|
264
|
+
mismatch("candidate tarball path contains a line break");
|
|
265
|
+
}
|
|
266
|
+
appendFileSync(
|
|
267
|
+
outputPath,
|
|
268
|
+
[
|
|
269
|
+
`tarball=${result.tarballPath}`,
|
|
270
|
+
`tarball_sha256=${result.tarballSha256}`,
|
|
271
|
+
`certificate_member_sha256=${result.certificateMemberSha256 ?? "not-applicable"}`,
|
|
272
|
+
"",
|
|
273
|
+
].join("\n")
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Execute the checkout or pack CLI boundary. */
|
|
278
|
+
export function runReleasePackageIdentityCli(
|
|
279
|
+
argv,
|
|
280
|
+
{ root = process.cwd(), githubOutput = process.env.GITHUB_OUTPUT } = {}
|
|
281
|
+
) {
|
|
282
|
+
const [mode, ...options] = argv;
|
|
283
|
+
if (mode === "checkout") {
|
|
284
|
+
const result = assertCheckoutIdentity({
|
|
285
|
+
root,
|
|
286
|
+
releaseCommit: requiredOption(options, "--release-commit"),
|
|
287
|
+
tag: requiredOption(options, "--tag"),
|
|
288
|
+
});
|
|
289
|
+
process.stdout.write(
|
|
290
|
+
`RELEASE_CHECKOUT_IDENTITY_OK ${JSON.stringify(result)}\n`
|
|
291
|
+
);
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
if (mode === "pack") {
|
|
295
|
+
const result = packAndValidateReleaseCandidate({
|
|
296
|
+
root,
|
|
297
|
+
version: requiredOption(options, "--version"),
|
|
298
|
+
releaseCommit: requiredOption(options, "--release-commit"),
|
|
299
|
+
packDestination: requiredOption(options, "--pack-destination"),
|
|
300
|
+
});
|
|
301
|
+
writeGithubOutput(result, githubOutput);
|
|
302
|
+
process.stdout.write(
|
|
303
|
+
`RELEASE_PACKAGE_IDENTITY_OK ${JSON.stringify(result)}\n`
|
|
304
|
+
);
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
mismatch("expected mode checkout or pack");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (invokedAsScript(import.meta.url)) {
|
|
311
|
+
try {
|
|
312
|
+
process.exitCode = runReleasePackageIdentityCli(process.argv.slice(2));
|
|
313
|
+
} catch (error) {
|
|
314
|
+
process.stderr.write(
|
|
315
|
+
`${error instanceof Error ? error.message : String(error)}\n`
|
|
316
|
+
);
|
|
317
|
+
process.exitCode = 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
},
|
|
6
6
|
"inspected": {
|
|
7
7
|
"workflows": 23,
|
|
8
|
-
"steps":
|
|
9
|
-
"couplings":
|
|
10
|
-
"inventory":
|
|
8
|
+
"steps": 639,
|
|
9
|
+
"couplings": 27,
|
|
10
|
+
"inventory": 57
|
|
11
11
|
},
|
|
12
12
|
"counts": {
|
|
13
13
|
"package-backed": 10,
|
|
14
|
-
"apply-lagged":
|
|
14
|
+
"apply-lagged": 15,
|
|
15
15
|
"never-delivered": 0,
|
|
16
16
|
"undelivered": 2
|
|
17
17
|
},
|
|
@@ -68,6 +68,19 @@
|
|
|
68
68
|
],
|
|
69
69
|
"detail": "`nightly-e2e-report.yml` is delivered at `@main`, so this step is live in every consumer on their next run. It reads `scripts/check-nightly-e2e-health.mjs` from the CALLER's tree with no package-relative candidate, and Lisa delivers that path on the `lisa apply` channel (typescript/copy-overwrite). The two halves land in the wrong order: this step is live everywhere immediately, the file it needs is live only where somebody applied. The read is unguarded, so an absent path fails the job loudly. Visible, which makes it the better of the two failures."
|
|
70
70
|
},
|
|
71
|
+
{
|
|
72
|
+
"key": "publish-to-npm.yml::scripts/check-release-package-identity.mjs",
|
|
73
|
+
"workflow": "publish-to-npm.yml",
|
|
74
|
+
"path": "scripts/check-release-package-identity.mjs",
|
|
75
|
+
"channel": "apply",
|
|
76
|
+
"verdict": "apply-lagged",
|
|
77
|
+
"remedy": "run-lisa-apply",
|
|
78
|
+
"guarded": false,
|
|
79
|
+
"lanes": [
|
|
80
|
+
"all/copy-overwrite"
|
|
81
|
+
],
|
|
82
|
+
"detail": "`publish-to-npm.yml` is delivered at `@main`, so this step is live in every consumer on their next run. It reads `scripts/check-release-package-identity.mjs` from the CALLER's tree with no package-relative candidate, and Lisa delivers that path on the `lisa apply` channel (all/copy-overwrite). The two halves land in the wrong order: this step is live everywhere immediately, the file it needs is live only where somebody applied. The read is unguarded, so an absent path fails the job loudly. Visible, which makes it the better of the two failures."
|
|
83
|
+
},
|
|
71
84
|
{
|
|
72
85
|
"key": "quality-rails.yml::scripts/check-threshold-ratchet.mjs",
|
|
73
86
|
"workflow": "quality-rails.yml",
|
|
@@ -68,6 +68,7 @@ scripts/check-conflict-markers.mjs
|
|
|
68
68
|
scripts/check-e2e-coverage.mjs
|
|
69
69
|
scripts/check-lighthouse-details.mjs
|
|
70
70
|
scripts/check-nightly-e2e-health.mjs
|
|
71
|
+
scripts/check-release-package-identity.mjs
|
|
71
72
|
scripts/check-skipped-required-checks.mjs
|
|
72
73
|
scripts/check-state-classification.mjs
|
|
73
74
|
scripts/check-threshold-ratchet.mjs
|