@codyswann/lisa 2.277.4 → 2.279.0
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/dist/cli/doctor-readiness-credentials.d.ts +46 -0
- package/dist/cli/doctor-readiness-credentials.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-credentials.js +199 -0
- package/dist/cli/doctor-readiness-credentials.js.map +1 -0
- package/dist/cli/doctor-readiness-delivery.d.ts +16 -0
- package/dist/cli/doctor-readiness-delivery.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-delivery.js +243 -0
- package/dist/cli/doctor-readiness-delivery.js.map +1 -0
- package/dist/cli/doctor-readiness-journey.d.ts +4 -10
- package/dist/cli/doctor-readiness-journey.d.ts.map +1 -1
- package/dist/cli/doctor-readiness-journey.js +17 -1
- package/dist/cli/doctor-readiness-journey.js.map +1 -1
- package/dist/cli/doctor-readiness-release-path.d.ts +62 -0
- package/dist/cli/doctor-readiness-release-path.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-release-path.js +275 -0
- package/dist/cli/doctor-readiness-release-path.js.map +1 -0
- package/dist/cli/doctor-readiness-types.d.ts +25 -0
- package/dist/cli/doctor-readiness-types.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-types.js +13 -0
- package/dist/cli/doctor-readiness-types.js.map +1 -0
- package/dist/cli/doctor-readiness-workflows.d.ts +64 -0
- package/dist/cli/doctor-readiness-workflows.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-workflows.js +198 -0
- package/dist/cli/doctor-readiness-workflows.js.map +1 -0
- package/dist/cli/doctor-readiness.d.ts +19 -3
- package/dist/cli/doctor-readiness.d.ts.map +1 -1
- package/dist/cli/doctor-readiness.js +87 -26
- package/dist/cli/doctor-readiness.js.map +1 -1
- package/dist/cli/ui-automations-adapters.d.ts +22 -0
- package/dist/cli/ui-automations-adapters.d.ts.map +1 -1
- package/dist/cli/ui-automations-adapters.js +24 -12
- package/dist/cli/ui-automations-adapters.js.map +1 -1
- package/dist/cli/ui-automations.d.ts +1 -0
- package/dist/cli/ui-automations.d.ts.map +1 -1
- package/dist/cli/ui-automations.js +1 -0
- package/dist/cli/ui-automations.js.map +1 -1
- package/dist/cli/ui-cmd.d.ts +4 -1
- package/dist/cli/ui-cmd.d.ts.map +1 -1
- package/dist/cli/ui-cmd.js +72 -17
- package/dist/cli/ui-cmd.js.map +1 -1
- package/dist/cli/ui-confined-project-read.d.ts +22 -0
- package/dist/cli/ui-confined-project-read.d.ts.map +1 -0
- package/dist/cli/ui-confined-project-read.js +38 -0
- package/dist/cli/ui-confined-project-read.js.map +1 -0
- package/dist/cli/ui-deploy-pipeline.d.ts.map +1 -1
- package/dist/cli/ui-deploy-pipeline.js +4 -15
- package/dist/cli/ui-deploy-pipeline.js.map +1 -1
- package/dist/cli/ui-detected-stacks.d.ts +9 -8
- package/dist/cli/ui-detected-stacks.d.ts.map +1 -1
- package/dist/cli/ui-detected-stacks.js +111 -0
- package/dist/cli/ui-detected-stacks.js.map +1 -1
- package/dist/cli/ui-github-repo-gh.d.ts +6 -0
- package/dist/cli/ui-github-repo-gh.d.ts.map +1 -1
- package/dist/cli/ui-github-repo-gh.js.map +1 -1
- package/dist/cli/ui-github-repo-map.d.ts.map +1 -1
- package/dist/cli/ui-github-repo-map.js +26 -1
- package/dist/cli/ui-github-repo-map.js.map +1 -1
- package/dist/cli/ui-setup-readiness-config.d.ts +28 -0
- package/dist/cli/ui-setup-readiness-config.d.ts.map +1 -0
- package/dist/cli/ui-setup-readiness-config.js +116 -0
- package/dist/cli/ui-setup-readiness-config.js.map +1 -0
- package/dist/cli/ui-setup-readiness-contract.d.ts +32 -0
- package/dist/cli/ui-setup-readiness-contract.d.ts.map +1 -0
- package/dist/cli/ui-setup-readiness-contract.js +61 -0
- package/dist/cli/ui-setup-readiness-contract.js.map +1 -0
- package/dist/cli/ui-setup-readiness-install.d.ts +24 -0
- package/dist/cli/ui-setup-readiness-install.d.ts.map +1 -0
- package/dist/cli/ui-setup-readiness-install.js +207 -0
- package/dist/cli/ui-setup-readiness-install.js.map +1 -0
- package/dist/cli/ui-setup-readiness-local.d.ts +40 -0
- package/dist/cli/ui-setup-readiness-local.d.ts.map +1 -0
- package/dist/cli/ui-setup-readiness-local.js +235 -0
- package/dist/cli/ui-setup-readiness-local.js.map +1 -0
- package/dist/cli/ui-setup-readiness-remote.d.ts +30 -0
- package/dist/cli/ui-setup-readiness-remote.d.ts.map +1 -0
- package/dist/cli/ui-setup-readiness-remote.js +108 -0
- package/dist/cli/ui-setup-readiness-remote.js.map +1 -0
- package/dist/cli/ui-setup-readiness.d.ts +52 -0
- package/dist/cli/ui-setup-readiness.d.ts.map +1 -0
- package/dist/cli/ui-setup-readiness.js +125 -0
- package/dist/cli/ui-setup-readiness.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +24 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/health/read-only-fs.d.ts +7 -0
- package/dist/health/read-only-fs.d.ts.map +1 -1
- package/dist/health/read-only-fs.js +40 -1
- package/dist/health/read-only-fs.js.map +1 -1
- package/dist/sync/registry.d.ts +14 -0
- package/dist/sync/registry.d.ts.map +1 -1
- package/dist/sync/registry.js +54 -15
- package/dist/sync/registry.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +270 -23
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +270 -23
- 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/scripts/automation-status-codex-adapter.mjs +270 -23
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +270 -23
- 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/scripts/automation-status-codex-adapter.mjs +270 -23
- package/ui/index.html +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-path assessment for the delivery/authority readiness dimension — ship
|
|
3
|
+
* blocker B2 (PRD #1739, #1896).
|
|
4
|
+
*
|
|
5
|
+
* B2 asks whether the thing that ships equals the thing that was validated. The
|
|
6
|
+
* question is answered offline, from the workflow files alone, which imposes a
|
|
7
|
+
* hard discipline: **absence of evidence is not evidence of a bypass.** A
|
|
8
|
+
* reusable `workflow_call` publish workflow has no validating job of its own
|
|
9
|
+
* because validation is the caller's obligation; a `workflow_run`-triggered
|
|
10
|
+
* deploy is gated by the upstream workflow that fired it; a default-branch push
|
|
11
|
+
* may be gated by branch protection. None of those are visible here, so none of
|
|
12
|
+
* them may be reported as a violation.
|
|
13
|
+
*
|
|
14
|
+
* The rule is therefore: stand B2 only when the bypass is provable from the file
|
|
15
|
+
* alone — a job that self-builds AND publishes with no validating ancestor, in a
|
|
16
|
+
* workflow whose trigger makes in-file validation the expected place for it
|
|
17
|
+
* (a tag push, `workflow_dispatch`, `schedule`, a non-default-branch push, or an
|
|
18
|
+
* unstated trigger). Everything else that cannot be settled offline returns a
|
|
19
|
+
* stated-reason SKIP, never a FAIL.
|
|
20
|
+
* @module cli/doctor-readiness-release-path
|
|
21
|
+
*/
|
|
22
|
+
import type { ParsedWorkflow, ParsedWorkflowJob, ParsedWorkflowStep } from "./doctor-readiness-workflows.js";
|
|
23
|
+
/** The action that promotes the artifact CI already built and validated. */
|
|
24
|
+
export declare const PROMOTION_ACTION = "actions/download-artifact";
|
|
25
|
+
/** What assessing one publishing job established. */
|
|
26
|
+
export type ReleasePathOutcome =
|
|
27
|
+
/** The bypass is provable from this file alone. */
|
|
28
|
+
{
|
|
29
|
+
readonly kind: "violation";
|
|
30
|
+
readonly evidence: string;
|
|
31
|
+
}
|
|
32
|
+
/** The proof, if any, lives somewhere not resolvable offline. */
|
|
33
|
+
| {
|
|
34
|
+
readonly kind: "unresolved";
|
|
35
|
+
readonly reason: string;
|
|
36
|
+
}
|
|
37
|
+
/** Validation provably precedes the publish, or the artifact is promoted. */
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: "clean";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Describe a step for persisted evidence: prefer its `name`, else the first
|
|
43
|
+
* non-empty line of its `run`, truncated. A raw multi-line `run` would dump a
|
|
44
|
+
* shell script into `.lisa/readiness.json` and into an operator's face.
|
|
45
|
+
* @param step - The step to describe
|
|
46
|
+
* @returns A single-line label, at most {@link MAX_STEP_LABEL} characters
|
|
47
|
+
*/
|
|
48
|
+
export declare function describeStep(step: ParsedWorkflowStep): string;
|
|
49
|
+
/**
|
|
50
|
+
* Find the first step in a job that ships something.
|
|
51
|
+
* @param job - The parsed job
|
|
52
|
+
* @returns The publishing step, or undefined when the job ships nothing
|
|
53
|
+
*/
|
|
54
|
+
export declare function findPublishStep(job: ParsedWorkflowJob): ParsedWorkflowStep | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Assess one job's release path against B2.
|
|
57
|
+
* @param workflow - The workflow declaring the job
|
|
58
|
+
* @param job - The job to assess
|
|
59
|
+
* @returns What this job established, or undefined when it ships nothing
|
|
60
|
+
*/
|
|
61
|
+
export declare function assessReleasePath(workflow: ParsedWorkflow, job: ParsedWorkflowJob): ReleasePathOutcome | undefined;
|
|
62
|
+
//# sourceMappingURL=doctor-readiness-release-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-release-path.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-release-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,iCAAiC,CAAC;AA+CzC,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAQ5D,qDAAqD;AACrD,MAAM,MAAM,kBAAkB;AAC5B,mDAAmD;AACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAC3D,iEAAiE;GAC/D;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC1D,6EAA6E;GAC3E;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/B;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAW7D;AAqBD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,iBAAiB,GACrB,kBAAkB,GAAG,SAAS,CAEhC;AAsJD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,cAAc,EACxB,GAAG,EAAE,iBAAiB,GACrB,kBAAkB,GAAG,SAAS,CAyBhC"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/** Commands that put an artifact in front of users. */
|
|
2
|
+
const PUBLISH_VERBS = [
|
|
3
|
+
"npm publish",
|
|
4
|
+
"docker push",
|
|
5
|
+
"gh release upload",
|
|
6
|
+
"aws s3 sync",
|
|
7
|
+
"cdk deploy",
|
|
8
|
+
"eas submit",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Commands that actually run a test suite. Deliberately narrow: the invariant B2
|
|
12
|
+
* protects is "this artifact was *tested*", and a loose substring match (`echo
|
|
13
|
+
* "test the release notes"`, or a lint step) would manufacture false
|
|
14
|
+
* reassurance — the wrong direction to err for a gate.
|
|
15
|
+
*/
|
|
16
|
+
const VALIDATING_COMMANDS = [
|
|
17
|
+
/\b(npm|yarn|pnpm|bun|npx|bunx)\s+(run\s+)?test\b/,
|
|
18
|
+
/\btest:[\w:-]+/,
|
|
19
|
+
/\b(vitest|jest|pytest|rspec|phpunit|minitest)\b/,
|
|
20
|
+
/\b(go|cargo|mvn|gradle|make|dotnet)\s+test\b/,
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Whether a shell command runs a test suite.
|
|
24
|
+
* @param run - The step's `run` text
|
|
25
|
+
* @returns True when the command matches a known test invocation
|
|
26
|
+
*/
|
|
27
|
+
function isValidatingCommand(run) {
|
|
28
|
+
return VALIDATING_COMMANDS.some(pattern => pattern.test(run));
|
|
29
|
+
}
|
|
30
|
+
/** Reusable workflows whose whole purpose is validation. */
|
|
31
|
+
const VALIDATING_WORKFLOW = /(quality|quality-rails|test|ci)\.ya?ml/;
|
|
32
|
+
/** Commands that produce an artifact locally, inside the shipping job. */
|
|
33
|
+
const LOCAL_BUILD_COMMAND = /\b(npm pack|npm run build|yarn build|bun run build|bun run build:\w+|pnpm build|docker build|tsc)\b/;
|
|
34
|
+
/** A publish argument naming a local path rather than a registry coordinate. */
|
|
35
|
+
const LOCAL_PATH_ARGUMENT = /\s\.{0,2}\//;
|
|
36
|
+
/** A publish argument naming a packaged artifact file on disk. */
|
|
37
|
+
const LOCAL_ARTIFACT_FILE = /\.(tgz|tar\.gz|zip|whl)\b/;
|
|
38
|
+
/** The action that promotes the artifact CI already built and validated. */
|
|
39
|
+
export const PROMOTION_ACTION = "actions/download-artifact";
|
|
40
|
+
/** Branch names treated as the repository's default branch. */
|
|
41
|
+
const DEFAULT_BRANCHES = new Set(["main", "master"]);
|
|
42
|
+
/** Longest step description carried into persisted evidence. */
|
|
43
|
+
const MAX_STEP_LABEL = 80;
|
|
44
|
+
/**
|
|
45
|
+
* Describe a step for persisted evidence: prefer its `name`, else the first
|
|
46
|
+
* non-empty line of its `run`, truncated. A raw multi-line `run` would dump a
|
|
47
|
+
* shell script into `.lisa/readiness.json` and into an operator's face.
|
|
48
|
+
* @param step - The step to describe
|
|
49
|
+
* @returns A single-line label, at most {@link MAX_STEP_LABEL} characters
|
|
50
|
+
*/
|
|
51
|
+
export function describeStep(step) {
|
|
52
|
+
const source = step.name.trim() !== ""
|
|
53
|
+
? step.name.trim()
|
|
54
|
+
: (step.run
|
|
55
|
+
.split("\n")
|
|
56
|
+
.find(line => line.trim() !== "")
|
|
57
|
+
?.trim() ?? "");
|
|
58
|
+
return source.length > MAX_STEP_LABEL
|
|
59
|
+
? `${source.slice(0, MAX_STEP_LABEL).trimEnd()}…`
|
|
60
|
+
: source;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A rehearsal flag: the command goes through the motions and ships nothing.
|
|
64
|
+
*/
|
|
65
|
+
const DRY_RUN_FLAG = /--dry[-_]?run\b/;
|
|
66
|
+
/**
|
|
67
|
+
* Whether a step actually puts an artifact in front of users. A `--dry-run`
|
|
68
|
+
* invocation names a publish verb but ships nothing, so faulting its release
|
|
69
|
+
* path would be a finding about something that cannot reach anyone.
|
|
70
|
+
* @param step - The step to classify
|
|
71
|
+
* @returns True when the step really publishes
|
|
72
|
+
*/
|
|
73
|
+
function isPublishStep(step) {
|
|
74
|
+
return (PUBLISH_VERBS.some(verb => step.run.includes(verb)) &&
|
|
75
|
+
!DRY_RUN_FLAG.test(step.run));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Find the first step in a job that ships something.
|
|
79
|
+
* @param job - The parsed job
|
|
80
|
+
* @returns The publishing step, or undefined when the job ships nothing
|
|
81
|
+
*/
|
|
82
|
+
export function findPublishStep(job) {
|
|
83
|
+
return job.steps.find(isPublishStep);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether a job proves anything about the artifact.
|
|
87
|
+
* @param job - The parsed job
|
|
88
|
+
* @returns True when the job runs a test suite or calls a validating workflow
|
|
89
|
+
*/
|
|
90
|
+
function isValidatingJob(job) {
|
|
91
|
+
if (job.uses !== "" && VALIDATING_WORKFLOW.test(job.uses)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return job.steps.some(step => isValidatingCommand(step.run) ||
|
|
95
|
+
(step.uses !== "" && VALIDATING_WORKFLOW.test(step.uses)));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Walk a job's transitive `needs:` closure within its workflow. The `seen` list
|
|
99
|
+
* makes a `needs:` cycle terminate instead of recursing forever.
|
|
100
|
+
* @param workflow - The workflow declaring the job
|
|
101
|
+
* @param job - The job whose ancestors to resolve
|
|
102
|
+
* @returns Every job the given job transitively depends on
|
|
103
|
+
*/
|
|
104
|
+
function ancestorJobs(workflow, job) {
|
|
105
|
+
const byId = new Map(workflow.jobs.map(entry => [entry.id, entry]));
|
|
106
|
+
const walk = (ids, seen) => {
|
|
107
|
+
const fresh = ids.filter(id => !seen.includes(id));
|
|
108
|
+
if (fresh.length === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const jobs = fresh.flatMap(id => {
|
|
112
|
+
const ancestor = byId.get(id);
|
|
113
|
+
return ancestor ? [ancestor] : [];
|
|
114
|
+
});
|
|
115
|
+
return [
|
|
116
|
+
...jobs,
|
|
117
|
+
...walk(jobs.flatMap(ancestor => ancestor.needs), [...seen, ...fresh]),
|
|
118
|
+
];
|
|
119
|
+
};
|
|
120
|
+
return walk(job.needs, []);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether validation provably precedes the publish: either an ancestor job
|
|
124
|
+
* validated, or a step earlier in this same job did.
|
|
125
|
+
* @param workflow - The workflow declaring the job
|
|
126
|
+
* @param job - The publishing job
|
|
127
|
+
* @param publishStep - The step that ships
|
|
128
|
+
* @returns True when something was proved before the artifact left
|
|
129
|
+
*/
|
|
130
|
+
function validationPrecedesPublish(workflow, job, publishStep) {
|
|
131
|
+
if (ancestorJobs(workflow, job).some(isValidatingJob)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return job.steps
|
|
135
|
+
.slice(0, job.steps.indexOf(publishStep))
|
|
136
|
+
.some(step => isValidatingCommand(step.run));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether the job ships an artifact it produced itself rather than promoting the
|
|
140
|
+
* one CI built and validated. Downloading the CI artifact only exempts the job
|
|
141
|
+
* when nothing rebuilds *after* the download — a rebuild past the promotion is
|
|
142
|
+
* the very bypass B2 exists to catch.
|
|
143
|
+
* @param job - The publishing job
|
|
144
|
+
* @param publishStep - The step that ships
|
|
145
|
+
* @returns True when a self-built artifact is shipped
|
|
146
|
+
*/
|
|
147
|
+
function shipsSelfBuiltArtifact(job, publishStep) {
|
|
148
|
+
const downloadIndex = job.steps.findIndex(step => step.uses.includes(PROMOTION_ACTION));
|
|
149
|
+
if (downloadIndex >= 0) {
|
|
150
|
+
const rebuildsAfterDownload = job.steps
|
|
151
|
+
.slice(downloadIndex + 1)
|
|
152
|
+
.some(step => LOCAL_BUILD_COMMAND.test(step.run));
|
|
153
|
+
if (!rebuildsAfterDownload) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return (LOCAL_PATH_ARGUMENT.test(publishStep.run) ||
|
|
158
|
+
LOCAL_ARTIFACT_FILE.test(publishStep.run) ||
|
|
159
|
+
job.steps.some(step => LOCAL_BUILD_COMMAND.test(step.run)));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Decide whether a workflow's trigger makes in-file validation the expected
|
|
163
|
+
* place for it — the only case where absent validation proves a bypass.
|
|
164
|
+
* @param workflow - The workflow to classify
|
|
165
|
+
* @returns The reason validation may live elsewhere, or null when it must be here
|
|
166
|
+
*/
|
|
167
|
+
function offlineUnresolvableTrigger(workflow) {
|
|
168
|
+
const events = workflow.on.events;
|
|
169
|
+
if (events.length > 0 && events.every(event => event === "workflow_call")) {
|
|
170
|
+
return ("it is a reusable `workflow_call` workflow, so validation is the calling " +
|
|
171
|
+
"workflow's obligation and callers are not resolved offline");
|
|
172
|
+
}
|
|
173
|
+
if (events.includes("workflow_run")) {
|
|
174
|
+
return ("it is triggered by `workflow_run`, so the validating workflow that fired " +
|
|
175
|
+
"it runs upstream and is not resolved offline");
|
|
176
|
+
}
|
|
177
|
+
if (events.includes("release")) {
|
|
178
|
+
return ("it is triggered by a `release` event, so validation may have run on the " +
|
|
179
|
+
"commit or tag the release was cut from, which is not visible in this file");
|
|
180
|
+
}
|
|
181
|
+
// An unfiltered `on: [push]` is a SUPERSET of a default-branch push, so it
|
|
182
|
+
// cannot be treated more strictly than the spelling that names the branch.
|
|
183
|
+
const unfilteredPush = workflow.on.events.includes("push") &&
|
|
184
|
+
workflow.on.pushBranches.length === 0 &&
|
|
185
|
+
workflow.on.pushTags.length === 0;
|
|
186
|
+
const defaultBranchPush = workflow.on.pushBranches.some(branch => DEFAULT_BRANCHES.has(branch));
|
|
187
|
+
if (unfilteredPush || defaultBranchPush) {
|
|
188
|
+
return ("it is triggered by a push to the default branch, where branch protection " +
|
|
189
|
+
"may already require the validating checks — a rule that is not visible " +
|
|
190
|
+
"in this file");
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Assess one job's release path against B2.
|
|
196
|
+
* @param workflow - The workflow declaring the job
|
|
197
|
+
* @param job - The job to assess
|
|
198
|
+
* @returns What this job established, or undefined when it ships nothing
|
|
199
|
+
*/
|
|
200
|
+
export function assessReleasePath(workflow, job) {
|
|
201
|
+
const publishStep = findPublishStep(job);
|
|
202
|
+
if (!publishStep) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const where = `${workflow.file} job \`${job.id}\` step \`${describeStep(publishStep)}\``;
|
|
206
|
+
const validated = validationPrecedesPublish(workflow, job, publishStep);
|
|
207
|
+
if (shipsSelfBuiltArtifact(job, publishStep)) {
|
|
208
|
+
return validated
|
|
209
|
+
? rebuildPastValidation(where)
|
|
210
|
+
: unvalidatedSelfBuild(where, workflow);
|
|
211
|
+
}
|
|
212
|
+
if (validated || promotesValidatedArtifact(job)) {
|
|
213
|
+
return { kind: "clean" };
|
|
214
|
+
}
|
|
215
|
+
// Neither built here nor promoted from CI, and nothing validating precedes it:
|
|
216
|
+
// the link between what ships and what was validated is simply not observable
|
|
217
|
+
// in this file. That is unestablished, not clean.
|
|
218
|
+
return {
|
|
219
|
+
kind: "unresolved",
|
|
220
|
+
reason: `${where} neither builds its own artifact nor promotes a CI-built one via ` +
|
|
221
|
+
`\`${PROMOTION_ACTION}\`, and no validating job precedes it, so what it ` +
|
|
222
|
+
"ships cannot be tied to anything that was validated",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The provable bypass that survives every trigger: the pipeline validated the
|
|
227
|
+
* source, then the release job rebuilt and shipped its own artifact. Whatever
|
|
228
|
+
* ran upstream, the bytes that reached users are not the bytes that were
|
|
229
|
+
* checked — so no trigger exempts this.
|
|
230
|
+
* @param where - Evidence location label
|
|
231
|
+
* @returns The violation outcome
|
|
232
|
+
*/
|
|
233
|
+
function rebuildPastValidation(where) {
|
|
234
|
+
return {
|
|
235
|
+
kind: "violation",
|
|
236
|
+
evidence: `${where} rebuilds and ships its own artifact after validation ran — the ` +
|
|
237
|
+
"pipeline proved things about the source tree, then shipped bytes that " +
|
|
238
|
+
`were produced afterwards and never validated. Promote the CI-built ` +
|
|
239
|
+
`artifact via \`${PROMOTION_ACTION}\` instead of repacking at release time`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A self-built, self-shipped artifact with nothing validating in front of it:
|
|
244
|
+
* a violation when the trigger makes in-file validation the expected place, and
|
|
245
|
+
* a stated-reason SKIP when the proof could live somewhere not visible offline.
|
|
246
|
+
* @param where - Evidence location label
|
|
247
|
+
* @param workflow - The workflow declaring the job
|
|
248
|
+
* @returns The violation or unresolved outcome
|
|
249
|
+
*/
|
|
250
|
+
function unvalidatedSelfBuild(where, workflow) {
|
|
251
|
+
const unresolvable = offlineUnresolvableTrigger(workflow);
|
|
252
|
+
if (unresolvable !== null) {
|
|
253
|
+
return {
|
|
254
|
+
kind: "unresolved",
|
|
255
|
+
reason: `${where} builds and ships its own artifact with no validating job in ` +
|
|
256
|
+
`its \`needs:\` closure, but ${unresolvable}. Whether what ships equals ` +
|
|
257
|
+
"what was validated is therefore not established either way",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
kind: "violation",
|
|
262
|
+
evidence: `${where} builds its own artifact and ships it with no validating job in ` +
|
|
263
|
+
`its \`needs:\` closure and no \`${PROMOTION_ACTION}\` promotion of the ` +
|
|
264
|
+
"CI-built one — nothing was proved about the bytes that reached users",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Whether the job promotes the artifact CI already built and validated.
|
|
269
|
+
* @param job - The publishing job
|
|
270
|
+
* @returns True when a download-artifact step is present
|
|
271
|
+
*/
|
|
272
|
+
function promotesValidatedArtifact(job) {
|
|
273
|
+
return job.steps.some(step => step.uses.includes(PROMOTION_ACTION));
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=doctor-readiness-release-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-release-path.js","sourceRoot":"","sources":["../../src/cli/doctor-readiness-release-path.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared readiness record vocabulary (PRD #1739, #1896).
|
|
3
|
+
*
|
|
4
|
+
* Every readiness producer — the journey wiring, the delivery/authority
|
|
5
|
+
* producers, and the collector's own reasoned-SKIP fallback — returns the same
|
|
6
|
+
* per-dimension record. Defining that shape once here is what keeps the
|
|
7
|
+
* collector able to dispatch to any producer by dimension id: a second, drifting
|
|
8
|
+
* copy of this type would make one producer's record silently incompatible with
|
|
9
|
+
* the report the collector persists.
|
|
10
|
+
* @module cli/doctor-readiness-types
|
|
11
|
+
*/
|
|
12
|
+
/** Per-dimension status, mirroring the shared doctor `DOCTOR_STATUSES`. */
|
|
13
|
+
export type ReadinessStatus = "PASS" | "WARN" | "FAIL" | "SKIP";
|
|
14
|
+
/** A persisted per-dimension record inside `.lisa/readiness.json`. */
|
|
15
|
+
export interface ReadinessDimensionRecord {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly status: ReadinessStatus;
|
|
18
|
+
/**
|
|
19
|
+
* Findings are intentionally untyped: they are persisted as JSON and read
|
|
20
|
+
* back by the blocker engine, which coerces them itself. Producers may attach
|
|
21
|
+
* extra rubric fields without a schema bump.
|
|
22
|
+
*/
|
|
23
|
+
readonly findings: readonly unknown[];
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=doctor-readiness-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-types.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,2EAA2E;AAC3E,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhE,sEAAsE;AACtE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;CACvC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared readiness record vocabulary (PRD #1739, #1896).
|
|
3
|
+
*
|
|
4
|
+
* Every readiness producer — the journey wiring, the delivery/authority
|
|
5
|
+
* producers, and the collector's own reasoned-SKIP fallback — returns the same
|
|
6
|
+
* per-dimension record. Defining that shape once here is what keeps the
|
|
7
|
+
* collector able to dispatch to any producer by dimension id: a second, drifting
|
|
8
|
+
* copy of this type would make one producer's record silently incompatible with
|
|
9
|
+
* the report the collector persists.
|
|
10
|
+
* @module cli/doctor-readiness-types
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=doctor-readiness-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-types.js","sourceRoot":"","sources":["../../src/cli/doctor-readiness-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `permissions`/`secrets` declaration: GitHub allows either a map of scopes
|
|
3
|
+
* or a bare scalar (`write-all`, `read-all`, `inherit`); `null` means the block
|
|
4
|
+
* was not declared at all, which is itself load-bearing evidence.
|
|
5
|
+
*/
|
|
6
|
+
export type WorkflowBlock = Record<string, unknown> | string | null;
|
|
7
|
+
/**
|
|
8
|
+
* A workflow's `on:` trigger block, normalized across its three legal spellings
|
|
9
|
+
* (scalar, list, map). The trigger is load-bearing for readiness: it decides
|
|
10
|
+
* whether the *absence* of an in-file validating job proves a bypass or merely
|
|
11
|
+
* means the proof lives somewhere this parser cannot see.
|
|
12
|
+
*/
|
|
13
|
+
export interface ParsedWorkflowTrigger {
|
|
14
|
+
/** Declared event names, e.g. `push`, `workflow_call`, `workflow_run`. */
|
|
15
|
+
readonly events: readonly string[];
|
|
16
|
+
/** Branch filters on a `push` trigger (empty when unfiltered or absent). */
|
|
17
|
+
readonly pushBranches: readonly string[];
|
|
18
|
+
/** Tag filters on a `push` trigger (empty when unfiltered or absent). */
|
|
19
|
+
readonly pushTags: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/** One step inside a parsed workflow job. */
|
|
22
|
+
export interface ParsedWorkflowStep {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
/** The shell command the step runs, or `""` for an action step. */
|
|
25
|
+
readonly run: string;
|
|
26
|
+
/** The action the step uses, or `""` for a `run` step. */
|
|
27
|
+
readonly uses: string;
|
|
28
|
+
/** Raw serialized `env` + `with` text, used for credential evidence. */
|
|
29
|
+
readonly inputs: string;
|
|
30
|
+
}
|
|
31
|
+
/** One job inside a parsed workflow. */
|
|
32
|
+
export interface ParsedWorkflowJob {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
/** Repo-relative path of the workflow file declaring this job. */
|
|
35
|
+
readonly workflow: string;
|
|
36
|
+
readonly needs: readonly string[];
|
|
37
|
+
readonly steps: readonly ParsedWorkflowStep[];
|
|
38
|
+
/** The reusable workflow this job calls, or `""` when it runs steps. */
|
|
39
|
+
readonly uses: string;
|
|
40
|
+
/** Job-level `permissions`: a map, a scalar such as `write-all`, or null. */
|
|
41
|
+
readonly permissions: WorkflowBlock;
|
|
42
|
+
/** Job-level `secrets`: a map, the scalar `inherit`, or null. */
|
|
43
|
+
readonly secrets: WorkflowBlock;
|
|
44
|
+
/** Declared deployment environments (a scalar or list, normalized). */
|
|
45
|
+
readonly environment: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** One parsed `.github/workflows/*.yml` file. */
|
|
48
|
+
export interface ParsedWorkflow {
|
|
49
|
+
/** Repo-relative path, always with forward slashes. */
|
|
50
|
+
readonly file: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** Workflow-level `permissions`: a map, a scalar, or null. */
|
|
53
|
+
readonly permissions: WorkflowBlock;
|
|
54
|
+
/** The normalized `on:` trigger block. */
|
|
55
|
+
readonly on: ParsedWorkflowTrigger;
|
|
56
|
+
readonly jobs: readonly ParsedWorkflowJob[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read and parse every `.github/workflows/*.yml` file in a repository, offline.
|
|
60
|
+
* @param root - Repository root
|
|
61
|
+
* @returns Parsed workflows in stable file-name order (empty when there are none)
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseRepositoryWorkflows(root: string): Promise<readonly ParsedWorkflow[]>;
|
|
64
|
+
//# sourceMappingURL=doctor-readiness-workflows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-workflows.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-workflows.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,6CAA6C;AAC7C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC;IACpC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,uEAAuE;IACvE,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC;IACpC,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,EAAE,qBAAqB,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC7C;AA0LD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CAQpC"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline GitHub Actions workflow parser for the readiness producers
|
|
3
|
+
* (PRD #1739, #1896).
|
|
4
|
+
*
|
|
5
|
+
* The delivery/authority readiness questions — does the thing that ships equal
|
|
6
|
+
* the thing that was validated, and does the shipping credential carry only the
|
|
7
|
+
* authority it needs — are answered from what the repository's CI *declares*,
|
|
8
|
+
* not from what a live API reports. Parsing is therefore deliberately offline by
|
|
9
|
+
* construction: nothing in this module reaches the network, so `lisa doctor
|
|
10
|
+
* --offline --readiness` needs no flag threading to stay honest.
|
|
11
|
+
*
|
|
12
|
+
* Reading is best-effort by design. A workflow file that cannot be parsed is
|
|
13
|
+
* skipped rather than aborting the readiness run, because one malformed YAML
|
|
14
|
+
* file must not turn the whole report into an error the operator cannot act on.
|
|
15
|
+
* @module cli/doctor-readiness-workflows
|
|
16
|
+
*/
|
|
17
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import yaml from "js-yaml";
|
|
20
|
+
import { isJsonObject } from "../sync/json-path.js";
|
|
21
|
+
/** The workflows directory every GitHub repository declares its CI in. */
|
|
22
|
+
const WORKFLOWS_DIR = path.join(".github", "workflows");
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a YAML scalar-or-list into a list of trimmed strings.
|
|
25
|
+
* @param value - Candidate YAML value
|
|
26
|
+
* @returns Normalized string list (empty when unreadable)
|
|
27
|
+
*/
|
|
28
|
+
function asStringList(value) {
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
return value.trim() === "" ? [] : [value.trim()];
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value
|
|
34
|
+
.filter((entry) => typeof entry === "string")
|
|
35
|
+
.map(entry => entry.trim())
|
|
36
|
+
.filter(entry => entry !== "");
|
|
37
|
+
}
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a `permissions`/`secrets` block, which GitHub allows as either a
|
|
42
|
+
* map or a bare scalar (`write-all`, `read-all`, `inherit`).
|
|
43
|
+
* @param value - Candidate YAML value
|
|
44
|
+
* @returns The map, the scalar string, or null when absent
|
|
45
|
+
*/
|
|
46
|
+
function asBlock(value) {
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
return value.trim();
|
|
49
|
+
}
|
|
50
|
+
return isJsonObject(value) ? value : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a job's `environment`, which may be a scalar name, a list, or a
|
|
54
|
+
* `{ name, url }` map.
|
|
55
|
+
* @param value - Candidate YAML value
|
|
56
|
+
* @returns Declared environment names
|
|
57
|
+
*/
|
|
58
|
+
function asEnvironments(value) {
|
|
59
|
+
if (isJsonObject(value)) {
|
|
60
|
+
return typeof value.name === "string" ? [value.name.trim()] : [];
|
|
61
|
+
}
|
|
62
|
+
return asStringList(value);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalize the `on:` block, which GitHub accepts as a scalar (`on: push`), a
|
|
66
|
+
* list (`on: [push, workflow_dispatch]`), or a map with per-event filters.
|
|
67
|
+
* @param value - Candidate YAML `on` value
|
|
68
|
+
* @returns The normalized trigger (all fields empty when absent or unreadable)
|
|
69
|
+
*/
|
|
70
|
+
function parseTrigger(value) {
|
|
71
|
+
if (!isJsonObject(value)) {
|
|
72
|
+
return { events: asStringList(value), pushBranches: [], pushTags: [] };
|
|
73
|
+
}
|
|
74
|
+
const push = value.push;
|
|
75
|
+
const filters = isJsonObject(push) ? push : undefined;
|
|
76
|
+
return {
|
|
77
|
+
events: Object.keys(value),
|
|
78
|
+
pushBranches: asStringList(filters?.branches),
|
|
79
|
+
pushTags: asStringList(filters?.tags),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Serialize a step's `env` and `with` blocks to searchable text so credential
|
|
84
|
+
* evidence can quote the offending line without a second traversal.
|
|
85
|
+
* @param step - The raw step object
|
|
86
|
+
* @returns Flattened `key: value` text, one pair per line
|
|
87
|
+
*/
|
|
88
|
+
function serializeStepInputs(step) {
|
|
89
|
+
return ["env", "with"]
|
|
90
|
+
.flatMap(key => {
|
|
91
|
+
const block = step[key];
|
|
92
|
+
return isJsonObject(block) ? Object.entries(block) : [];
|
|
93
|
+
})
|
|
94
|
+
.map(([name, value]) => `${name}: ${typeof value === "string" ? value : String(value)}`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse one job's `steps` list.
|
|
99
|
+
* @param value - Candidate YAML `steps` value
|
|
100
|
+
* @returns Normalized steps
|
|
101
|
+
*/
|
|
102
|
+
function parseSteps(value) {
|
|
103
|
+
if (!Array.isArray(value)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return value.filter(isJsonObject).map(step => ({
|
|
107
|
+
name: typeof step.name === "string" ? step.name : "",
|
|
108
|
+
run: typeof step.run === "string" ? step.run.trim() : "",
|
|
109
|
+
uses: typeof step.uses === "string" ? step.uses.trim() : "",
|
|
110
|
+
inputs: serializeStepInputs(step),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse the `jobs` map of one workflow document.
|
|
115
|
+
* @param document - Parsed workflow root
|
|
116
|
+
* @param file - Repo-relative workflow path (stamped on each job)
|
|
117
|
+
* @returns Normalized jobs, in declaration order
|
|
118
|
+
*/
|
|
119
|
+
function parseJobs(document, file) {
|
|
120
|
+
const jobs = document.jobs;
|
|
121
|
+
if (!isJsonObject(jobs)) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
return Object.entries(jobs).flatMap(([id, raw]) => {
|
|
125
|
+
if (!isJsonObject(raw)) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const job = raw;
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
id,
|
|
132
|
+
workflow: file,
|
|
133
|
+
needs: asStringList(job.needs),
|
|
134
|
+
steps: parseSteps(job.steps),
|
|
135
|
+
uses: typeof job.uses === "string" ? job.uses.trim() : "",
|
|
136
|
+
permissions: asBlock(job.permissions),
|
|
137
|
+
secrets: asBlock(job.secrets),
|
|
138
|
+
environment: asEnvironments(job.environment),
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* List the workflow files a repository declares, in stable name order.
|
|
145
|
+
* @param root - Repository root
|
|
146
|
+
* @returns Workflow file names, or an empty list when the directory is absent
|
|
147
|
+
*/
|
|
148
|
+
async function listWorkflowFiles(root) {
|
|
149
|
+
try {
|
|
150
|
+
const entries = await readdir(path.join(root, WORKFLOWS_DIR));
|
|
151
|
+
return entries
|
|
152
|
+
.filter(entry => entry.endsWith(".yml") || entry.endsWith(".yaml"))
|
|
153
|
+
.sort((a, b) => a.localeCompare(b));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Parse one workflow file, returning null when it cannot be read or parsed.
|
|
161
|
+
* @param root - Repository root
|
|
162
|
+
* @param fileName - Workflow file name
|
|
163
|
+
* @returns The parsed workflow, or null
|
|
164
|
+
*/
|
|
165
|
+
async function parseOneWorkflow(root, fileName) {
|
|
166
|
+
const file = `${WORKFLOWS_DIR.split(path.sep).join("/")}/${fileName}`;
|
|
167
|
+
try {
|
|
168
|
+
const source = await readFile(path.join(root, WORKFLOWS_DIR, fileName), "utf8");
|
|
169
|
+
const document = yaml.load(source);
|
|
170
|
+
if (!isJsonObject(document)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const record = document;
|
|
174
|
+
return {
|
|
175
|
+
file,
|
|
176
|
+
name: typeof record.name === "string" ? record.name : fileName,
|
|
177
|
+
permissions: asBlock(record.permissions),
|
|
178
|
+
on: parseTrigger(record.on),
|
|
179
|
+
jobs: parseJobs(record, file),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// A malformed workflow is skipped, never fatal: one bad file must not turn
|
|
184
|
+
// the readiness report into an error the operator cannot act on.
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Read and parse every `.github/workflows/*.yml` file in a repository, offline.
|
|
190
|
+
* @param root - Repository root
|
|
191
|
+
* @returns Parsed workflows in stable file-name order (empty when there are none)
|
|
192
|
+
*/
|
|
193
|
+
export async function parseRepositoryWorkflows(root) {
|
|
194
|
+
const fileNames = await listWorkflowFiles(root);
|
|
195
|
+
const parsed = await Promise.all(fileNames.map(async (fileName) => await parseOneWorkflow(root, fileName)));
|
|
196
|
+
return parsed.filter((workflow) => workflow !== null);
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=doctor-readiness-workflows.js.map
|