@webpresso/agent-kit 3.1.26 → 3.1.30
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/catalog/AGENTS.md.tpl +7 -11
- package/catalog/agent/commands/verify.md +2 -2
- package/catalog/agent/rules/changeset-release.md +4 -2
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/pre-implementation.md +2 -1
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/workflow-skills-routing.md +9 -4
- package/catalog/agent/skills/autopilot/SKILL.md +48 -11
- package/catalog/agent/skills/claude/SKILL.md +7 -0
- package/catalog/agent/skills/codex/SKILL.md +7 -0
- package/catalog/agent/skills/fix/SKILL.md +36 -8
- package/catalog/agent/skills/grok/SKILL.md +7 -0
- package/catalog/agent/skills/investigate/SKILL.md +12 -1
- package/catalog/agent/skills/pll/SKILL.md +53 -48
- package/catalog/agent/skills/team/SKILL.md +26 -9
- package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
- package/catalog/agent/skills/verify/SKILL.md +89 -51
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
- package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +3 -0
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
- package/dist/esm/ci/native-session-memory-cache.js +9 -1
- package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
- package/dist/esm/ci/vitest-ci-shards.js +6 -4
- package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
- package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
- package/dist/esm/cli/auto-update/installer.d.ts +4 -17
- package/dist/esm/cli/auto-update/installer.js +6 -57
- package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
- package/dist/esm/cli/auto-update/self-invocation.js +62 -0
- package/dist/esm/cli/commands/audit.js +4 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
- package/dist/esm/cli/commands/blueprint/router.js +27 -2
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/dash/index.js +3 -3
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
- package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +142 -0
- package/dist/esm/cli/commands/dash/plan-once.js +261 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
- package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
- package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
- package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
- package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +311 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +1 -4
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/package-manager.js +2 -1
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.js +5 -8
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
- package/dist/esm/cli/commands/self-install-guard.js +17 -63
- package/dist/esm/cli/direct-provider-launch.js +25 -1
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +56 -0
- package/dist/esm/hooks/guard-switch/index.js +8 -4
- package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
- package/dist/esm/hooks/precompact/index.js +5 -3
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
- package/dist/esm/hooks/sessionstart/index.js +61 -4
- package/dist/esm/hooks/shared/types.d.ts +11 -0
- package/dist/esm/hooks/shared/types.js +21 -0
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +10 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/paths/state-root.js +9 -0
- package/dist/esm/review/authority.js +17 -2
- package/dist/esm/review/availability.js +18 -5
- package/dist/esm/review/subject.d.ts +15 -0
- package/dist/esm/review/subject.js +105 -10
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/session-memory/current-session.js +2 -0
- package/dist/esm/session-memory/native-runtime.js +5 -3
- package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
- package/dist/esm/session-memory/native-warm-markers.js +22 -0
- package/dist/esm/session-memory/sync/types.d.ts +4 -0
- package/dist/esm/session-memory/types.d.ts +3 -1
- package/dist/esm/session-memory/types.js +2 -0
- package/dist/esm/ultragoal/runtime.js +44 -2
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/package.json +17 -14
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readReviewAuthorityAtRef } from "#review/authority.js";
|
|
5
|
-
import { parseReviewEventLog } from "#review/events.js";
|
|
6
|
-
import { normalizePlanSubjectContent } from "#review/subject.js";
|
|
4
|
+
import { readReviewAuthorityAtRef, readWorkingReviewEvents } from "#review/authority.js";
|
|
5
|
+
import { evaluateReviewEvents, parseReviewEventLog } from "#review/events.js";
|
|
6
|
+
import { createPlanSubjectDigestForContent, normalizePlanSubjectContent } from "#review/subject.js";
|
|
7
7
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
8
8
|
const REVIEW_ENTRY_MARKER = "<!-- wp:review-entry ";
|
|
9
9
|
function normalizePath(file) {
|
|
@@ -52,6 +52,53 @@ function stableSlugForBlueprintFile(file) {
|
|
|
52
52
|
}
|
|
53
53
|
return path.basename(path.dirname(file));
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
|
|
57
|
+
* if missing (lifecycle move draft→planned before the promote commit), resolve
|
|
58
|
+
* by stable slug under any lifecycle directory at that ref.
|
|
59
|
+
*/
|
|
60
|
+
export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
|
|
61
|
+
const relative = normalizePath(path.relative(repoRoot, file));
|
|
62
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
63
|
+
return null;
|
|
64
|
+
try {
|
|
65
|
+
return execFileSync("git", ["show", `${ref}:${relative}`], {
|
|
66
|
+
cwd: repoRoot,
|
|
67
|
+
encoding: "buffer",
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
timeout: 5_000,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Path may have moved across lifecycle dirs (draft→planned) while the
|
|
74
|
+
// promote commit is still uncommitted. Stable slug is the authority key.
|
|
75
|
+
const slug = stableSlugForBlueprintFile(file);
|
|
76
|
+
if (!slug || slug.includes(".."))
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
const names = execFileSync("git", ["ls-tree", "-r", "--name-only", ref], {
|
|
80
|
+
cwd: repoRoot,
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
+
timeout: 5_000,
|
|
84
|
+
});
|
|
85
|
+
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
86
|
+
const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}/_overview\\.md$`, "u");
|
|
87
|
+
const hits = names.split("\n").filter((name) => pattern.test(normalizePath(name)));
|
|
88
|
+
if (hits.length !== 1)
|
|
89
|
+
return null;
|
|
90
|
+
return execFileSync("git", ["show", `${ref}:${hits[0]}`], {
|
|
91
|
+
cwd: repoRoot,
|
|
92
|
+
encoding: "buffer",
|
|
93
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
94
|
+
timeout: 5_000,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
55
102
|
function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
56
103
|
const relative = path.relative(repoRoot, file);
|
|
57
104
|
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
@@ -63,12 +110,11 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
63
110
|
// in the working tree before the promotion commit exists, and that
|
|
64
111
|
// window must not strip a legitimately promoted blueprint of its plan
|
|
65
112
|
// authority. Substantive edits still differ after normalization.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
113
|
+
// Also resolve by stable slug when the lifecycle path changed (draft→planned)
|
|
114
|
+
// so pre-commit on a rename does not falsely drop plan authority.
|
|
115
|
+
const atRef = readBlueprintOverviewAtRef(repoRoot, ref, file);
|
|
116
|
+
if (!atRef)
|
|
117
|
+
return false;
|
|
72
118
|
const working = readFileSync(file);
|
|
73
119
|
return normalizePlanSubjectContent(working).equals(normalizePlanSubjectContent(atRef));
|
|
74
120
|
}
|
|
@@ -84,27 +130,62 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
84
130
|
return false;
|
|
85
131
|
}
|
|
86
132
|
}
|
|
133
|
+
// Working-ledger acceptance: pre-commit validates a blueprint whose new review
|
|
134
|
+
// events are staged alongside it and therefore not yet at any git ref. An
|
|
135
|
+
// event still counts only when its subjectDigest matches the digest of the
|
|
136
|
+
// exact working bytes being committed and its artifact hash-verifies on disk —
|
|
137
|
+
// content-addressing keeps this sound before the commit exists.
|
|
138
|
+
function collectWorkingV2ReviewProvenance(file, issues) {
|
|
139
|
+
try {
|
|
140
|
+
const slug = stableSlugForBlueprintFile(file);
|
|
141
|
+
const working = readWorkingReviewEvents(file, slug);
|
|
142
|
+
if (working.events.length === 0)
|
|
143
|
+
return [];
|
|
144
|
+
issues?.push(...working.issues);
|
|
145
|
+
const digest = createPlanSubjectDigestForContent(slug, readFileSync(file));
|
|
146
|
+
const evaluation = evaluateReviewEvents(working.events, {
|
|
147
|
+
purpose: "plan",
|
|
148
|
+
blueprintSlug: slug,
|
|
149
|
+
subjectScheme: "wp-blueprint-v1",
|
|
150
|
+
subjectDigest: digest,
|
|
151
|
+
}, working.compromisedIds);
|
|
152
|
+
return evaluation.approvals
|
|
153
|
+
.filter((event) => event.artifactPath)
|
|
154
|
+
.map((event) => ({
|
|
155
|
+
reviewer: event.reviewer,
|
|
156
|
+
artifact: event.artifactPath,
|
|
157
|
+
targetHash: event.subjectDigest,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
issues?.push(`Working review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
87
165
|
export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
|
|
88
166
|
const repoRoot = findGitRoot(file);
|
|
89
167
|
if (!repoRoot)
|
|
90
168
|
return [];
|
|
169
|
+
const workingApprovals = purpose === "plan" ? collectWorkingV2ReviewProvenance(file, issues) : [];
|
|
91
170
|
if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
|
|
92
|
-
return
|
|
171
|
+
return workingApprovals;
|
|
93
172
|
try {
|
|
94
173
|
const authority = readReviewAuthorityAtRef(repoRoot, ref, stableSlugForBlueprintFile(file), purpose);
|
|
95
174
|
issues?.push(...authority.issues);
|
|
96
|
-
|
|
175
|
+
const refApprovals = authority.approvals.map((event) => ({
|
|
97
176
|
reviewer: event.reviewer,
|
|
98
177
|
artifact: event.artifactPath,
|
|
99
178
|
targetHash: event.subjectDigest,
|
|
100
179
|
}));
|
|
180
|
+
const seen = new Set(refApprovals.map((approval) => approval.reviewer));
|
|
181
|
+
return [...refApprovals, ...workingApprovals.filter((entry) => !seen.has(entry.reviewer))];
|
|
101
182
|
}
|
|
102
183
|
catch (error) {
|
|
103
184
|
// Container-level failure (unparseable ledger, duplicate ids, cross-slug
|
|
104
185
|
// events, unresolved commit): fail closed for the whole file, but surface
|
|
105
186
|
// the reason instead of silently reporting zero authority.
|
|
106
187
|
issues?.push(`Review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
-
return
|
|
188
|
+
return workingApprovals;
|
|
108
189
|
}
|
|
109
190
|
}
|
|
110
191
|
function reviewEventLogsAtRef(cwd, ref) {
|
|
@@ -67,10 +67,14 @@ export function updateBlockedReason(content, taskId, reason) {
|
|
|
67
67
|
const trimmedReason = reason.trim();
|
|
68
68
|
const blockedPattern = /\n?\*\*Blocked:\*\*\s*.*(?:\n+)?/i;
|
|
69
69
|
if (!trimmedReason) {
|
|
70
|
-
|
|
70
|
+
// Preserve the section's trailing whitespace: trimEnd on the whole
|
|
71
|
+
// section would also delete the blank line separating this task from the
|
|
72
|
+
// next heading, changing bytes the writer does not own.
|
|
73
|
+
const trailingWhitespace = section.slice(section.trimEnd().length);
|
|
74
|
+
return (section
|
|
71
75
|
.replace(blockedPattern, "\n")
|
|
72
76
|
.replace(/\n{3,}/g, "\n\n")
|
|
73
|
-
.trimEnd();
|
|
77
|
+
.trimEnd() + trailingWhitespace);
|
|
74
78
|
}
|
|
75
79
|
const blockedLine = `**Blocked:** ${trimmedReason}`;
|
|
76
80
|
if (/\*\*Blocked:\*\*/i.test(section)) {
|
|
@@ -31,8 +31,105 @@ export function buildReplayWorkflowSource(sourceYaml, options) {
|
|
|
31
31
|
: basename(options.workflowPath);
|
|
32
32
|
workflow.name = `replay: ${originalName}`;
|
|
33
33
|
workflow.on = normalizeWorkflowOnSection(workflow.on, options.eventName);
|
|
34
|
+
workflow.jobs = rewriteReplayJobs(workflow.jobs, options.eventName);
|
|
34
35
|
return stringify(workflow);
|
|
35
36
|
}
|
|
37
|
+
function rewriteReplayJobs(jobs, eventName) {
|
|
38
|
+
if (typeof jobs !== "object" || jobs === null || Array.isArray(jobs)) {
|
|
39
|
+
return jobs;
|
|
40
|
+
}
|
|
41
|
+
return Object.fromEntries(Object.entries(jobs).map(([jobId, job]) => [
|
|
42
|
+
jobId,
|
|
43
|
+
rewriteReplayJob(job, eventName),
|
|
44
|
+
]));
|
|
45
|
+
}
|
|
46
|
+
function rewriteReplayJob(job, eventName) {
|
|
47
|
+
if (typeof job !== "object" || job === null || Array.isArray(job)) {
|
|
48
|
+
return job;
|
|
49
|
+
}
|
|
50
|
+
const jobRecord = { ...job };
|
|
51
|
+
if (Array.isArray(jobRecord.steps)) {
|
|
52
|
+
jobRecord.steps = jobRecord.steps.map((step) => rewriteReplayStep(step, eventName));
|
|
53
|
+
}
|
|
54
|
+
return jobRecord;
|
|
55
|
+
}
|
|
56
|
+
function rewriteReplayStep(step, eventName) {
|
|
57
|
+
if (!isRecord(step) || !isSameRepoCheckoutStep(step, eventName)) {
|
|
58
|
+
return step;
|
|
59
|
+
}
|
|
60
|
+
const rewritten = {
|
|
61
|
+
name: typeof step.name === "string" ? step.name : "Use prepared local replay workspace",
|
|
62
|
+
run: [
|
|
63
|
+
'echo "wp ci act replay: using prepared local workspace instead of actions/checkout"',
|
|
64
|
+
"test -d .git",
|
|
65
|
+
"git rev-parse --is-inside-work-tree",
|
|
66
|
+
"git rev-parse --verify HEAD",
|
|
67
|
+
].join("\n"),
|
|
68
|
+
"working-directory": "${{ github.workspace }}",
|
|
69
|
+
};
|
|
70
|
+
copyOptionalStepField(step, rewritten, "id");
|
|
71
|
+
copyOptionalStepField(step, rewritten, "if");
|
|
72
|
+
copyOptionalStepField(step, rewritten, "timeout-minutes");
|
|
73
|
+
copyOptionalStepField(step, rewritten, "continue-on-error");
|
|
74
|
+
return rewritten;
|
|
75
|
+
}
|
|
76
|
+
function isSameRepoCheckoutStep(step, eventName) {
|
|
77
|
+
if (typeof step.uses !== "string" || !/^actions\/checkout(?:@|$)/u.test(step.uses)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (step.env !== undefined && step.env !== null) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const withConfig = step.with;
|
|
84
|
+
if (withConfig === undefined || withConfig === null) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (!isRecord(withConfig)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (hasAnyCheckoutInputExcept(withConfig, ["ref"])) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return isCurrentReplayRef(withConfig.ref, eventName);
|
|
94
|
+
}
|
|
95
|
+
function isCurrentReplayRef(value, eventName) {
|
|
96
|
+
if (value === undefined || value === null || String(value).trim().length === 0) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const normalized = normalizeGithubExpression(String(value));
|
|
100
|
+
if (normalized === null)
|
|
101
|
+
return false;
|
|
102
|
+
if (normalized === "github.sha") {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (normalized ===
|
|
106
|
+
"github.event_name=='pull_request'&&github.event.pull_request.base.sha||github.sha") {
|
|
107
|
+
return eventName !== "pull_request";
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function normalizeGithubExpression(value) {
|
|
112
|
+
const match = /^\$\{\{\s*([\s\S]*?)\s*\}\}$/u.exec(value.trim());
|
|
113
|
+
if (!match?.[1])
|
|
114
|
+
return null;
|
|
115
|
+
return match[1].replace(/"/gu, "'").replace(/\s+/gu, "");
|
|
116
|
+
}
|
|
117
|
+
function hasAnyCheckoutInputExcept(withConfig, allowedKeys) {
|
|
118
|
+
const allowed = new Set(allowedKeys);
|
|
119
|
+
return Object.entries(withConfig).some(([key, value]) => {
|
|
120
|
+
if (allowed.has(key))
|
|
121
|
+
return false;
|
|
122
|
+
return value !== undefined && value !== null && String(value).trim().length > 0;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function copyOptionalStepField(source, target, field) {
|
|
126
|
+
if (source[field] !== undefined) {
|
|
127
|
+
target[field] = source[field];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isRecord(value) {
|
|
131
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
132
|
+
}
|
|
36
133
|
export function createReplayWorkflow(options) {
|
|
37
134
|
const sourcePath = resolve(options.cwd, options.workflowPath);
|
|
38
135
|
const sourceYaml = readFileSync(sourcePath, "utf8");
|
|
@@ -42,11 +139,17 @@ export function createReplayWorkflow(options) {
|
|
|
42
139
|
});
|
|
43
140
|
const dir = mkdtempSync(join(tmpdir(), "wp-ci-act-replay-"));
|
|
44
141
|
const workflowPath = join(dir, "workflow.yml");
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(workflowPath, generatedYaml, "utf8");
|
|
144
|
+
return {
|
|
145
|
+
workflowPath,
|
|
146
|
+
cleanup() {
|
|
147
|
+
rmSync(dir, { recursive: true, force: true });
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
rmSync(dir, { recursive: true, force: true });
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
52
155
|
}
|
|
@@ -6,6 +6,7 @@ export interface PublicCiActOptions {
|
|
|
6
6
|
readonly workflow?: string;
|
|
7
7
|
readonly workflowPath?: string;
|
|
8
8
|
readonly job?: string;
|
|
9
|
+
readonly matrix?: readonly string[];
|
|
9
10
|
readonly eventName?: CiActEventName;
|
|
10
11
|
readonly eventPath?: string;
|
|
11
12
|
readonly envProfile?: string;
|
|
@@ -22,6 +23,7 @@ export interface PublicCiActCommand {
|
|
|
22
23
|
readonly actArgs: readonly string[];
|
|
23
24
|
}
|
|
24
25
|
export interface PreparedPublicCiActExecution {
|
|
26
|
+
readonly cwd: string;
|
|
25
27
|
readonly command: PublicCiActCommand;
|
|
26
28
|
readonly mode: CiActExecutionMode;
|
|
27
29
|
readonly nonSecurityEquivalent: boolean;
|
|
@@ -36,5 +38,7 @@ export declare function buildPublicCiActArgs(options?: PublicCiActOptions, workf
|
|
|
36
38
|
export declare function buildPublicCiActCommand(options?: PublicCiActOptions): PublicCiActCommand;
|
|
37
39
|
export declare function preparePublicCiActExecution(options?: PublicCiActOptions): PreparedPublicCiActExecution;
|
|
38
40
|
export declare function resolveCiActSecretEnvProfile(options?: PublicCiActOptions): string | undefined;
|
|
41
|
+
export declare function shouldForceCiActSecretGate(envProfile: string | undefined): boolean;
|
|
39
42
|
export declare function sanitizePublicCiActArgv(command: PublicCiActCommand): PublicCiActCommand;
|
|
40
43
|
export declare function assertNoForbiddenCiActArgs(args: readonly string[]): void;
|
|
44
|
+
export declare function normalizeCiActMatrixSelectors(selectors: readonly string[] | undefined): readonly string[];
|
|
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { buildSecretGateCommand } from "#secret-gate/runner.js";
|
|
3
3
|
import { resolveSecretsConfigProfileEnvironment } from "#runtime/secrets-config.js";
|
|
4
4
|
import { createReplayWorkflow, GENERATED_REPLAY_WORKFLOW_PLACEHOLDER, } from "./act-replay.js";
|
|
5
|
+
import { prepareActGitWorkspace } from "./act-worktree-git.js";
|
|
5
6
|
export { GENERATED_REPLAY_WORKFLOW_PLACEHOLDER } from "./act-replay.js";
|
|
6
7
|
const DEFAULT_WORKFLOW = "ci-e2e";
|
|
7
8
|
export const DEFAULT_PLATFORM_IMAGE = "ghcr.io/catthehacker/ubuntu:full-latest";
|
|
@@ -43,6 +44,7 @@ export function buildPublicCiActArgs(options = {}, workflowPathOverride) {
|
|
|
43
44
|
"--rm",
|
|
44
45
|
];
|
|
45
46
|
pushOption(args, "-j", options.job);
|
|
47
|
+
pushRepeatableOption(args, "--matrix", normalizeCiActMatrixSelectors(options.matrix));
|
|
46
48
|
pushOption(args, "-e", options.eventPath ? resolve(cwd, options.eventPath) : undefined);
|
|
47
49
|
pushOption(args, "--container-architecture", containerArchitecture);
|
|
48
50
|
return args;
|
|
@@ -57,7 +59,7 @@ export function buildPublicCiActCommand(options = {}) {
|
|
|
57
59
|
sink: "act",
|
|
58
60
|
profile: secretEnvProfile,
|
|
59
61
|
envProfile: options.envProfile ?? "secrets-only",
|
|
60
|
-
forceSecretGate:
|
|
62
|
+
forceSecretGate: shouldForceCiActSecretGate(options.envProfile),
|
|
61
63
|
command: "act",
|
|
62
64
|
args: actArgs,
|
|
63
65
|
});
|
|
@@ -65,32 +67,63 @@ export function buildPublicCiActCommand(options = {}) {
|
|
|
65
67
|
}
|
|
66
68
|
export function preparePublicCiActExecution(options = {}) {
|
|
67
69
|
const cwd = options.cwd ?? process.cwd();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
let gitWorkspace = null;
|
|
71
|
+
let replay = null;
|
|
72
|
+
try {
|
|
73
|
+
gitWorkspace = prepareActGitWorkspace(cwd);
|
|
74
|
+
const executionCwd = gitWorkspace.cwd;
|
|
75
|
+
const mode = resolveCiActExecutionMode(options);
|
|
76
|
+
const sourceWorkflowPath = resolve(cwd, resolveCiActWorkflowPath(options));
|
|
77
|
+
replay =
|
|
78
|
+
mode === "replay"
|
|
79
|
+
? createReplayWorkflow({
|
|
80
|
+
cwd,
|
|
81
|
+
workflowPath: sourceWorkflowPath,
|
|
82
|
+
eventName: options.eventName ?? "pull_request",
|
|
83
|
+
})
|
|
84
|
+
: null;
|
|
85
|
+
const executionOptions = {
|
|
86
|
+
...options,
|
|
87
|
+
cwd: executionCwd,
|
|
88
|
+
eventPath: options.eventPath ? resolve(cwd, options.eventPath) : undefined,
|
|
89
|
+
};
|
|
90
|
+
const actArgs = buildPublicCiActArgs(executionOptions, replay ? replay.workflowPath : sourceWorkflowPath);
|
|
91
|
+
if (gitWorkspace.prepared) {
|
|
92
|
+
actArgs.push("--bind");
|
|
93
|
+
}
|
|
94
|
+
const secretEnvProfile = resolveCiActSecretEnvProfile({ ...options, cwd }) ?? "preview";
|
|
95
|
+
const wrapped = buildSecretGateCommand({
|
|
96
|
+
sink: "act",
|
|
97
|
+
profile: secretEnvProfile,
|
|
98
|
+
envProfile: options.envProfile ?? "secrets-only",
|
|
99
|
+
forceSecretGate: shouldForceCiActSecretGate(options.envProfile),
|
|
100
|
+
command: "act",
|
|
101
|
+
args: actArgs,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
cwd: executionCwd,
|
|
105
|
+
command: { command: wrapped.command, args: wrapped.args, actArgs },
|
|
106
|
+
mode,
|
|
107
|
+
nonSecurityEquivalent: mode === "replay",
|
|
108
|
+
cleanup() {
|
|
109
|
+
try {
|
|
110
|
+
replay?.cleanup();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
gitWorkspace?.cleanup();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
try {
|
|
91
120
|
replay?.cleanup();
|
|
92
|
-
}
|
|
93
|
-
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
gitWorkspace?.cleanup();
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
94
127
|
}
|
|
95
128
|
export function resolveCiActSecretEnvProfile(options = {}) {
|
|
96
129
|
const secretEnvProfile = options.secretEnvProfile?.trim();
|
|
@@ -103,6 +136,9 @@ export function resolveCiActSecretEnvProfile(options = {}) {
|
|
|
103
136
|
}
|
|
104
137
|
return undefined;
|
|
105
138
|
}
|
|
139
|
+
export function shouldForceCiActSecretGate(envProfile) {
|
|
140
|
+
return envProfile !== "none" && envProfile !== "public";
|
|
141
|
+
}
|
|
106
142
|
export function sanitizePublicCiActArgv(command) {
|
|
107
143
|
return {
|
|
108
144
|
command: command.command,
|
|
@@ -126,11 +162,34 @@ export function assertNoForbiddenCiActArgs(args) {
|
|
|
126
162
|
if (found)
|
|
127
163
|
throw new Error(`Unsupported unsafe ci act argument: ${found}`);
|
|
128
164
|
}
|
|
165
|
+
export function normalizeCiActMatrixSelectors(selectors) {
|
|
166
|
+
if (!selectors)
|
|
167
|
+
return [];
|
|
168
|
+
return selectors.map((selector) => normalizeCiActMatrixSelector(selector));
|
|
169
|
+
}
|
|
129
170
|
function pushOption(args, flag, value) {
|
|
130
171
|
if (value === undefined || value === "")
|
|
131
172
|
return;
|
|
132
173
|
args.push(flag, String(value));
|
|
133
174
|
}
|
|
175
|
+
function pushRepeatableOption(args, flag, values) {
|
|
176
|
+
for (const value of values) {
|
|
177
|
+
args.push(flag, value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function normalizeCiActMatrixSelector(selector) {
|
|
181
|
+
const trimmed = selector.trim();
|
|
182
|
+
const delimiter = trimmed.indexOf(":");
|
|
183
|
+
if (trimmed.length === 0 || delimiter <= 0) {
|
|
184
|
+
throw new Error(`Invalid ci act matrix selector "${selector}". Use key:value with non-empty key and value.`);
|
|
185
|
+
}
|
|
186
|
+
const key = trimmed.slice(0, delimiter).trim();
|
|
187
|
+
const value = trimmed.slice(delimiter + 1).trim();
|
|
188
|
+
if (key.length === 0 || value.length === 0) {
|
|
189
|
+
throw new Error(`Invalid ci act matrix selector "${selector}". Use key:value with non-empty key and value.`);
|
|
190
|
+
}
|
|
191
|
+
return `${key}:${value}`;
|
|
192
|
+
}
|
|
134
193
|
function sanitizeArg(value) {
|
|
135
194
|
if (/wp-ci-act-[^/\\]+[/\\]secrets\.env$/u.test(value))
|
|
136
195
|
return "[INTERNAL_SECRET_FILE]";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface PreparedActGitWorkspace {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly prepared: boolean;
|
|
4
|
+
cleanup(): void;
|
|
5
|
+
}
|
|
6
|
+
interface LinkedWorktreeGitMetadata {
|
|
7
|
+
readonly gitDir: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function prepareActGitWorkspace(cwd: string): PreparedActGitWorkspace;
|
|
10
|
+
export declare function readLinkedWorktreeGitMetadata(cwd: string): LinkedWorktreeGitMetadata | null;
|
|
11
|
+
export {};
|