forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
|
@@ -2,17 +2,112 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
+
const { secureExecFileSync } = require('./shell-utils');
|
|
5
6
|
|
|
6
7
|
const {
|
|
7
8
|
createProtectedStateAuditRecord,
|
|
8
9
|
recordProtectedStateAuditEvent,
|
|
10
|
+
removeProtectedFile,
|
|
9
11
|
writeProtectedFile,
|
|
10
12
|
} = require('./protected-state-surfaces');
|
|
11
13
|
const {
|
|
14
|
+
completeNpmPublishWorkflowAuthorization,
|
|
12
15
|
issueNpmPublishWorkflowAuthorization,
|
|
13
16
|
} = require('./protected-state-authority');
|
|
14
17
|
|
|
15
18
|
const NPM_PUBLISH_WORKFLOW_PATH = '.github/workflows/npm-publish.yml';
|
|
19
|
+
const FULL_COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
20
|
+
const BUN_VERSION = /^bun@(\d+\.\d+\.\d+)$/.exec(require('../package.json').packageManager)?.[1];
|
|
21
|
+
if (!BUN_VERSION) throw new Error('package.json must pin an exact stable Bun version.');
|
|
22
|
+
|
|
23
|
+
function resolveCurrentHead(projectRoot, execGit = secureExecFileSync) {
|
|
24
|
+
return execGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
25
|
+
cwd: projectRoot,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readNpmPublishWorkflow(projectRoot, revision, execGit = secureExecFileSync) {
|
|
33
|
+
const options = {
|
|
34
|
+
cwd: projectRoot,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
};
|
|
39
|
+
const indexed = revision === ':';
|
|
40
|
+
const entry = execGit('git', indexed
|
|
41
|
+
? ['ls-files', '--stage', '--', NPM_PUBLISH_WORKFLOW_PATH]
|
|
42
|
+
: ['ls-tree', revision, '--', NPM_PUBLISH_WORKFLOW_PATH], options).trim();
|
|
43
|
+
const expected = indexed
|
|
44
|
+
? /^100(?:644|755) [0-9a-f]+ 0\t\.github\/workflows\/npm-publish\.yml$/
|
|
45
|
+
: /^100(?:644|755) blob [0-9a-f]+\t\.github\/workflows\/npm-publish\.yml$/;
|
|
46
|
+
if (!expected.test(entry)) {
|
|
47
|
+
throw new Error(`${NPM_PUBLISH_WORKFLOW_PATH} is not one regular file ${indexed ? 'in the Git index' : `at ${revision}`}.`);
|
|
48
|
+
}
|
|
49
|
+
return Buffer.from(execGit('git', ['show', `${revision}${indexed ? '' : ':'}${NPM_PUBLISH_WORKFLOW_PATH}`], {
|
|
50
|
+
...options,
|
|
51
|
+
encoding: 'buffer',
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readSourceNpmPublishWorkflow(projectRoot, sourceHead, execGit = secureExecFileSync) {
|
|
56
|
+
return readNpmPublishWorkflow(projectRoot, sourceHead, execGit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readIndexedNpmPublishWorkflow(projectRoot, execGit = secureExecFileSync) {
|
|
60
|
+
return readNpmPublishWorkflow(projectRoot, ':', execGit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function verifyExpectedHead(projectRoot, expectedHead, resolveHead = resolveCurrentHead) {
|
|
64
|
+
if (!expectedHead) {
|
|
65
|
+
return { success: false, error: '--expect-head is required for protected workflow generation.' };
|
|
66
|
+
}
|
|
67
|
+
if (!FULL_COMMIT_SHA.test(expectedHead)) {
|
|
68
|
+
return { success: false, error: '--expect-head must be a full 40-character lowercase commit SHA.' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let currentHead;
|
|
72
|
+
try {
|
|
73
|
+
currentHead = resolveHead(projectRoot);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { success: false, error: `Could not resolve current HEAD: ${error.message}` };
|
|
76
|
+
}
|
|
77
|
+
if (!FULL_COMMIT_SHA.test(currentHead)) {
|
|
78
|
+
return { success: false, error: 'Current HEAD did not resolve to a full 40-character lowercase commit SHA.' };
|
|
79
|
+
}
|
|
80
|
+
if (currentHead !== expectedHead) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: `Expected HEAD ${expectedHead} does not match current HEAD ${currentHead}.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { success: true, sourceHead: currentHead };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderNpmDistTagResolverScript() {
|
|
90
|
+
return `CORE_RE='(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)'
|
|
91
|
+
NUMERIC_ID_RE='(0|[1-9][0-9]*)'
|
|
92
|
+
BUILD_RE='(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?'
|
|
93
|
+
BETA_RE="^\${CORE_RE}-beta(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
|
|
94
|
+
RC_RE="^\${CORE_RE}-rc(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
|
|
95
|
+
STABLE_RE="^\${CORE_RE}\${BUILD_RE}$"
|
|
96
|
+
|
|
97
|
+
if [[ "$VERSION" =~ $BETA_RE ]]; then
|
|
98
|
+
echo "tag=beta" >> "$GITHUB_OUTPUT"
|
|
99
|
+
elif [[ "$VERSION" =~ $RC_RE ]]; then
|
|
100
|
+
echo "tag=rc" >> "$GITHUB_OUTPUT"
|
|
101
|
+
elif [[ "$VERSION" =~ $STABLE_RE ]]; then
|
|
102
|
+
echo "tag=latest" >> "$GITHUB_OUTPUT"
|
|
103
|
+
elif [[ "$VERSION" == *-* ]]; then
|
|
104
|
+
echo "::error::Unsupported npm prerelease: $VERSION"
|
|
105
|
+
exit 1
|
|
106
|
+
else
|
|
107
|
+
echo "::error::Invalid npm version: $VERSION"
|
|
108
|
+
exit 1
|
|
109
|
+
fi`;
|
|
110
|
+
}
|
|
16
111
|
|
|
17
112
|
function snapshotWorkflow(projectRoot) {
|
|
18
113
|
const fullPath = path.resolve(projectRoot, NPM_PUBLISH_WORKFLOW_PATH);
|
|
@@ -29,24 +124,57 @@ function snapshotWorkflow(projectRoot) {
|
|
|
29
124
|
}
|
|
30
125
|
}
|
|
31
126
|
|
|
32
|
-
function
|
|
127
|
+
function sameSnapshot(expected, actual) {
|
|
128
|
+
if (!expected || typeof expected !== 'object' || Array.isArray(expected)) return false;
|
|
129
|
+
if (typeof expected.existed !== 'boolean') return false;
|
|
130
|
+
if (expected.existed && !Buffer.isBuffer(expected.content)) return false;
|
|
131
|
+
if (!expected.existed && expected.content !== null) return false;
|
|
132
|
+
return expected.existed === actual.existed
|
|
133
|
+
&& (!expected.existed || expected.content.equals(actual.content));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function recoverWorkflow(snapshot, projectRoot, generatedContent, protectedWriter, protectedRemover, writeOptions) {
|
|
137
|
+
const recoveryOptions = {
|
|
138
|
+
...writeOptions,
|
|
139
|
+
operation: 'recover_npm_workflow',
|
|
140
|
+
expectedContent: generatedContent,
|
|
141
|
+
};
|
|
33
142
|
if (!snapshot.existed) {
|
|
34
|
-
|
|
35
|
-
return;
|
|
143
|
+
return protectedRemover(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, recoveryOptions);
|
|
36
144
|
}
|
|
37
145
|
if (!Buffer.isBuffer(snapshot.content)) {
|
|
38
|
-
|
|
146
|
+
return { allowed: false, reason: 'Previous workflow was not a regular file' };
|
|
39
147
|
}
|
|
40
|
-
|
|
148
|
+
return protectedWriter(
|
|
41
149
|
projectRoot,
|
|
42
150
|
NPM_PUBLISH_WORKFLOW_PATH,
|
|
43
151
|
snapshot.content,
|
|
44
|
-
|
|
152
|
+
recoveryOptions,
|
|
45
153
|
);
|
|
46
|
-
if (!restored.allowed) throw new Error(restored.reason || 'Protected workflow restore was denied');
|
|
47
154
|
}
|
|
48
155
|
|
|
49
|
-
function
|
|
156
|
+
async function prepareWorkflowAuthorization(prepareAuthorization, projectRoot, params, authorizationOptions) {
|
|
157
|
+
try {
|
|
158
|
+
return await prepareAuthorization(projectRoot, params, authorizationOptions);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { success: false, error: error.message };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function recordWorkflowAudit(auditWriter, auditRecord, projectRoot) {
|
|
165
|
+
try {
|
|
166
|
+
return auditWriter(auditRecord, { cwd: projectRoot });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { success: false, error: error.message };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderNpmPublishWorkflow(bunVersion = BUN_VERSION) {
|
|
173
|
+
if (!/^\d+\.\d+\.\d+$/.test(bunVersion)) throw new Error('npm workflow requires an exact stable Bun version.');
|
|
174
|
+
const distTagResolver = renderNpmDistTagResolverScript()
|
|
175
|
+
.split('\n')
|
|
176
|
+
.map(line => line ? ` ${line}` : '')
|
|
177
|
+
.join('\n');
|
|
50
178
|
return `# Generated by \`forge release generate-npm-workflow\`. Do not edit directly.
|
|
51
179
|
name: Bun Package
|
|
52
180
|
|
|
@@ -61,6 +189,7 @@ jobs:
|
|
|
61
189
|
resolve-release:
|
|
62
190
|
name: Resolve immutable release SHA
|
|
63
191
|
runs-on: ubuntu-latest
|
|
192
|
+
timeout-minutes: 10
|
|
64
193
|
permissions:
|
|
65
194
|
contents: read
|
|
66
195
|
outputs:
|
|
@@ -88,6 +217,7 @@ jobs:
|
|
|
88
217
|
build:
|
|
89
218
|
needs: resolve-release
|
|
90
219
|
runs-on: ubuntu-latest
|
|
220
|
+
timeout-minutes: 25
|
|
91
221
|
permissions:
|
|
92
222
|
contents: read
|
|
93
223
|
outputs:
|
|
@@ -105,7 +235,7 @@ jobs:
|
|
|
105
235
|
node-version: 22
|
|
106
236
|
- uses: oven-sh/setup-bun@v2
|
|
107
237
|
with:
|
|
108
|
-
bun-version:
|
|
238
|
+
bun-version: ${bunVersion}
|
|
109
239
|
- run: bun install --frozen-lockfile
|
|
110
240
|
- name: Setup test fixtures
|
|
111
241
|
shell: bash
|
|
@@ -124,6 +254,7 @@ jobs:
|
|
|
124
254
|
publish-npm:
|
|
125
255
|
needs: [resolve-release, build]
|
|
126
256
|
runs-on: ubuntu-latest
|
|
257
|
+
timeout-minutes: 20
|
|
127
258
|
permissions:
|
|
128
259
|
contents: read
|
|
129
260
|
id-token: write
|
|
@@ -146,7 +277,7 @@ jobs:
|
|
|
146
277
|
run: node scripts/npm-release-receipt.js verify
|
|
147
278
|
- uses: oven-sh/setup-bun@v2
|
|
148
279
|
with:
|
|
149
|
-
bun-version:
|
|
280
|
+
bun-version: ${bunVersion}
|
|
150
281
|
- run: bun install --frozen-lockfile
|
|
151
282
|
- name: Release tag matches package.json version
|
|
152
283
|
shell: bash
|
|
@@ -164,36 +295,92 @@ jobs:
|
|
|
164
295
|
run: npm pack --dry-run
|
|
165
296
|
- name: Ensure OIDC-capable npm
|
|
166
297
|
run: npm install --global npm@11.5.1 && npm --version
|
|
167
|
-
- name:
|
|
298
|
+
- name: Resolve npm dist-tag
|
|
299
|
+
id: dist-tag
|
|
168
300
|
shell: bash
|
|
169
301
|
run: |
|
|
170
302
|
set -euo pipefail
|
|
171
303
|
VERSION="$(node -p "require('./package.json').version")"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
304
|
+
${distTagResolver}
|
|
305
|
+
- name: Publish to npm
|
|
306
|
+
shell: bash
|
|
307
|
+
run: |
|
|
308
|
+
set -euo pipefail
|
|
309
|
+
DIST_TAG="\${{ steps.dist-tag.outputs.tag }}"
|
|
310
|
+
case "$DIST_TAG" in
|
|
311
|
+
beta)
|
|
312
|
+
npm publish --provenance --tag beta
|
|
313
|
+
;;
|
|
314
|
+
rc)
|
|
315
|
+
npm publish --provenance --tag rc
|
|
316
|
+
;;
|
|
317
|
+
latest)
|
|
318
|
+
npm publish --provenance
|
|
319
|
+
;;
|
|
320
|
+
*)
|
|
321
|
+
echo "::error::Unsupported npm dist-tag: $DIST_TAG"
|
|
322
|
+
exit 1
|
|
323
|
+
;;
|
|
324
|
+
esac
|
|
178
325
|
`;
|
|
179
326
|
}
|
|
180
327
|
|
|
181
328
|
async function generateNpmPublishWorkflow(projectRoot, options = {}) {
|
|
182
|
-
const
|
|
329
|
+
const head = verifyExpectedHead(projectRoot, options.expectedHead, options.resolveHead);
|
|
330
|
+
if (!head.success) return head;
|
|
331
|
+
const sourceHead = head.sourceHead;
|
|
332
|
+
let targetBunVersion;
|
|
333
|
+
try {
|
|
334
|
+
targetBunVersion = options.targetBunVersion
|
|
335
|
+
|| require('./bun-workflow-pins').resolveBunVersionTarget(projectRoot, options).version;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { success: false, error: error.message };
|
|
338
|
+
}
|
|
339
|
+
const bunVersion = options.bunVersion || targetBunVersion;
|
|
340
|
+
if (bunVersion !== targetBunVersion) {
|
|
341
|
+
return { success: false, error: 'npm workflow Bun version must match the staged package.json Bun pin.' };
|
|
342
|
+
}
|
|
343
|
+
const content = renderNpmPublishWorkflow(bunVersion);
|
|
183
344
|
const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
|
|
184
345
|
const protectedWriter = options.writeProtectedFile || writeProtectedFile;
|
|
346
|
+
const protectedRemover = options.removeProtectedFile || removeProtectedFile;
|
|
185
347
|
const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
|
|
186
|
-
const
|
|
348
|
+
const prepareAuthorization = options.prepareNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
|
|
349
|
+
const activateAuthorization = options.activateNpmPublishWorkflowAuthorization || completeNpmPublishWorkflowAuthorization;
|
|
187
350
|
const snapshot = snapshotWorkflow(projectRoot);
|
|
351
|
+
if (options.expectedSnapshot && !sameSnapshot(options.expectedSnapshot, snapshot)) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: `${NPM_PUBLISH_WORKFLOW_PATH} changed after Bun pin batch preflight.`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
188
357
|
const writeOptions = {
|
|
189
358
|
actor,
|
|
190
359
|
operation: 'generate_npm_workflow',
|
|
191
360
|
viaForgeApi: true,
|
|
192
361
|
surface: 'workflows',
|
|
193
362
|
};
|
|
363
|
+
const trustedAuthorization = await prepareWorkflowAuthorization(
|
|
364
|
+
prepareAuthorization,
|
|
365
|
+
projectRoot,
|
|
366
|
+
{ actor, sourceHead, bunVersion, targetBunVersion },
|
|
367
|
+
{ deps: options.kernelDeps, capabilityId: options.capabilityId },
|
|
368
|
+
);
|
|
369
|
+
if (!trustedAuthorization.success) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
|
|
373
|
+
trustedAuthorization,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
194
377
|
let write;
|
|
195
378
|
try {
|
|
196
|
-
write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content,
|
|
379
|
+
write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, {
|
|
380
|
+
...writeOptions,
|
|
381
|
+
expectedContent: snapshot.existed ? snapshot.content : null,
|
|
382
|
+
...(options.beforeAtomicCommit ? { beforeAtomicCommit: options.beforeAtomicCommit } : {}),
|
|
383
|
+
});
|
|
197
384
|
} catch (error) {
|
|
198
385
|
return { success: false, error: error.message };
|
|
199
386
|
}
|
|
@@ -207,66 +394,72 @@ async function generateNpmPublishWorkflow(projectRoot, options = {}) {
|
|
|
207
394
|
path: NPM_PUBLISH_WORKFLOW_PATH,
|
|
208
395
|
content,
|
|
209
396
|
operation: 'generate_npm_workflow',
|
|
397
|
+
viaForgeApi: true,
|
|
398
|
+
sourceHead,
|
|
210
399
|
});
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
audit = auditWriter(auditRecord, { cwd: projectRoot });
|
|
214
|
-
} catch (error) {
|
|
215
|
-
audit = { success: false, error: error.message };
|
|
216
|
-
}
|
|
400
|
+
const audit = recordWorkflowAudit(auditWriter, auditRecord, projectRoot);
|
|
217
401
|
if (!audit.success) {
|
|
218
|
-
let
|
|
402
|
+
let recovery;
|
|
219
403
|
try {
|
|
220
|
-
|
|
404
|
+
recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
|
|
221
405
|
} catch (error) {
|
|
222
|
-
|
|
406
|
+
recovery = { allowed: false, reason: error.message };
|
|
223
407
|
}
|
|
224
408
|
return {
|
|
225
409
|
success: false,
|
|
226
410
|
error: `Could not record protected-state authorization: ${audit.error}`,
|
|
227
411
|
audit,
|
|
228
|
-
|
|
412
|
+
recovery,
|
|
229
413
|
};
|
|
230
414
|
}
|
|
231
415
|
|
|
232
|
-
let
|
|
416
|
+
let writeCompletion;
|
|
233
417
|
try {
|
|
234
|
-
|
|
418
|
+
writeCompletion = await activateAuthorization(
|
|
235
419
|
projectRoot,
|
|
236
|
-
{ actor },
|
|
420
|
+
{ actor, sourceHead, capabilityId: trustedAuthorization.capabilityId, bunVersion, targetBunVersion },
|
|
237
421
|
{ deps: options.kernelDeps },
|
|
238
422
|
);
|
|
239
423
|
} catch (error) {
|
|
240
|
-
|
|
424
|
+
writeCompletion = { success: false, error: error.message };
|
|
241
425
|
}
|
|
242
|
-
if (!
|
|
243
|
-
let
|
|
426
|
+
if (!writeCompletion.success) {
|
|
427
|
+
let recovery;
|
|
244
428
|
try {
|
|
245
|
-
|
|
429
|
+
recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
|
|
246
430
|
} catch (error) {
|
|
247
|
-
|
|
431
|
+
recovery = { allowed: false, reason: error.message };
|
|
248
432
|
}
|
|
249
433
|
return {
|
|
250
434
|
success: false,
|
|
251
|
-
error: `Could not
|
|
435
|
+
error: `Could not complete trusted protected-state authorization: ${writeCompletion.error}`,
|
|
252
436
|
audit,
|
|
253
437
|
trustedAuthorization,
|
|
254
|
-
|
|
438
|
+
writeCompletion,
|
|
439
|
+
recovery,
|
|
255
440
|
};
|
|
256
441
|
}
|
|
257
442
|
|
|
258
443
|
return {
|
|
259
444
|
success: true,
|
|
260
445
|
path: NPM_PUBLISH_WORKFLOW_PATH,
|
|
446
|
+
sourceHead,
|
|
447
|
+
bunVersion,
|
|
261
448
|
contentHash: write.contentHash,
|
|
262
449
|
write,
|
|
263
450
|
audit,
|
|
264
451
|
trustedAuthorization,
|
|
452
|
+
writeCompletion,
|
|
265
453
|
};
|
|
266
454
|
}
|
|
267
455
|
|
|
268
456
|
module.exports = {
|
|
269
457
|
NPM_PUBLISH_WORKFLOW_PATH,
|
|
458
|
+
readIndexedNpmPublishWorkflow,
|
|
459
|
+
readSourceNpmPublishWorkflow,
|
|
460
|
+
renderNpmDistTagResolverScript,
|
|
270
461
|
renderNpmPublishWorkflow,
|
|
462
|
+
resolveCurrentHead,
|
|
463
|
+
verifyExpectedHead,
|
|
271
464
|
generateNpmPublishWorkflow,
|
|
272
465
|
};
|
package/lib/package-root.js
CHANGED
|
@@ -35,6 +35,17 @@ const INFRA_CONCLUSIONS = new Set(['CANCELLED', 'TIMED_OUT', 'STALE', 'STARTUP_F
|
|
|
35
35
|
/** Conclusions that mean the code is genuinely broken — NEVER auto-rerun these. */
|
|
36
36
|
const REAL_FAILURE_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'ACTION_REQUIRED']);
|
|
37
37
|
|
|
38
|
+
const AUTO_ACTION_STATUS = Object.freeze({
|
|
39
|
+
PASS: 'PASS',
|
|
40
|
+
UNCHANGED: 'UNCHANGED',
|
|
41
|
+
STALE: 'STALE',
|
|
42
|
+
INCOMPLETE: 'INCOMPLETE',
|
|
43
|
+
CONFLICT: 'CONFLICT',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const MAX_AUTO_ACTIONS = 8;
|
|
47
|
+
const MAX_AUTO_ATTEMPTS = 5;
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* Pull the numeric Actions run id from a job/run "details" URL
|
|
40
51
|
* (`.../actions/runs/<run>/job/<job>` or `.../actions/runs/<run>`). Returns null
|
|
@@ -99,16 +110,40 @@ function decideUpdateBranch(payload, opts = {}) {
|
|
|
99
110
|
return { should: true, reason: 'otherwise-clean-behind — only blocker is behind-base; merge base in' };
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
function indexFailures(failures) {
|
|
114
|
+
const byName = new Map();
|
|
115
|
+
for (const failure of failures) {
|
|
116
|
+
if (!failure?.name || byName.has(failure.name)) continue;
|
|
117
|
+
byName.set(failure.name, {
|
|
118
|
+
conclusion: String(failure.conclusion || '').toUpperCase(),
|
|
119
|
+
url: failure.jobUrl || failure.detailsUrl || null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return byName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function selectRerunChecks(failingNames, failuresByName) {
|
|
126
|
+
const checks = [];
|
|
127
|
+
for (const name of failingNames) {
|
|
128
|
+
const failure = failuresByName.get(name);
|
|
129
|
+
if (!failure?.conclusion) {
|
|
130
|
+
return { reason: `required check "${name}" has no known conclusion — cannot confirm flaky, fail closed` };
|
|
131
|
+
}
|
|
132
|
+
if (REAL_FAILURE_CONCLUSIONS.has(failure.conclusion)) {
|
|
133
|
+
return { reason: `required check "${name}" is a real failure (${failure.conclusion}) — never rerun` };
|
|
134
|
+
}
|
|
135
|
+
if (!INFRA_CONCLUSIONS.has(failure.conclusion)) {
|
|
136
|
+
return { reason: `required check "${name}" conclusion ${failure.conclusion} is not classified infrastructural — fail closed` };
|
|
137
|
+
}
|
|
138
|
+
checks.push({ name, conclusion: failure.conclusion, runId: runIdFromUrl(failure.url) });
|
|
139
|
+
}
|
|
140
|
+
return { checks };
|
|
141
|
+
}
|
|
142
|
+
|
|
102
143
|
/**
|
|
103
144
|
* Decide whether to re-run flaky REQUIRED checks. Fires ONLY when EVERY failing
|
|
104
|
-
* required check is infrastructural
|
|
105
|
-
*
|
|
106
|
-
* single real failure, an unclassifiable conclusion, a required-failing check
|
|
107
|
-
* with no matching `failures[]` entry, or a missing run id fails the WHOLE
|
|
108
|
-
* decision closed (never rerun a genuinely-broken PR, never loop on a real bug).
|
|
109
|
-
*
|
|
110
|
-
* @param {object} payload - the `--pull --json` payload.
|
|
111
|
-
* @returns {{ should: boolean, checks: Array<{name:string,conclusion:string,runId:string|null}>, runIds: string[], reason: string }}
|
|
145
|
+
* required check is infrastructural with a derivable run id, and NONE is a real
|
|
146
|
+
* failure or an unclassifiable signal.
|
|
112
147
|
*/
|
|
113
148
|
function decideRerun(payload) {
|
|
114
149
|
const empty = (reason) => ({ should: false, checks: [], runIds: [], reason });
|
|
@@ -119,28 +154,15 @@ function decideRerun(payload) {
|
|
|
119
154
|
const failingNames = Array.isArray(rc.failing) ? rc.failing : [];
|
|
120
155
|
if (failingNames.length === 0) return empty('no failing required checks');
|
|
121
156
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!conclByName.has(f.name)) {
|
|
128
|
-
conclByName.set(f.name, String(f.conclusion || '').toUpperCase());
|
|
129
|
-
urlByName.set(f.name, f.jobUrl || f.detailsUrl || null);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const picked = [];
|
|
134
|
-
for (const name of failingNames) {
|
|
135
|
-
const concl = conclByName.get(name);
|
|
136
|
-
if (!concl) return empty(`required check "${name}" has no known conclusion — cannot confirm flaky, fail closed`);
|
|
137
|
-
if (REAL_FAILURE_CONCLUSIONS.has(concl)) return empty(`required check "${name}" is a real failure (${concl}) — never rerun`);
|
|
138
|
-
if (!INFRA_CONCLUSIONS.has(concl)) return empty(`required check "${name}" conclusion ${concl} is not classified infrastructural — fail closed`);
|
|
139
|
-
picked.push({ name, conclusion: concl, runId: runIdFromUrl(urlByName.get(name)) });
|
|
140
|
-
}
|
|
157
|
+
const selection = selectRerunChecks(
|
|
158
|
+
failingNames,
|
|
159
|
+
indexFailures(Array.isArray(payload.failures) ? payload.failures : []),
|
|
160
|
+
);
|
|
161
|
+
if (selection.reason) return empty(selection.reason);
|
|
141
162
|
|
|
163
|
+
const picked = selection.checks;
|
|
142
164
|
const runIds = [...new Set(picked.map((p) => p.runId).filter(Boolean))];
|
|
143
|
-
if (
|
|
165
|
+
if (picked.some((item) => !item.runId)) return empty('no run id derivable from failure jobUrl — fail closed');
|
|
144
166
|
return {
|
|
145
167
|
should: true,
|
|
146
168
|
checks: picked,
|
|
@@ -164,6 +186,123 @@ function decideAutoActions(payload, opts = {}) {
|
|
|
164
186
|
};
|
|
165
187
|
}
|
|
166
188
|
|
|
189
|
+
function boundedPolicyValue(value, fallback, maximum) {
|
|
190
|
+
const selected = value ?? fallback;
|
|
191
|
+
return Number.isSafeInteger(selected) && selected > 0 && selected <= maximum ? selected : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeActionHistory(history) {
|
|
195
|
+
if (!Array.isArray(history)) return { ok: false, reason: 'action history is incomplete' };
|
|
196
|
+
const byKey = new Map();
|
|
197
|
+
for (const item of history) {
|
|
198
|
+
if (!item || typeof item.key !== 'string' || !item.key
|
|
199
|
+
|| !['pending', 'applied', 'failed'].includes(item.state)
|
|
200
|
+
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0) {
|
|
201
|
+
return { ok: false, reason: 'action history is incomplete' };
|
|
202
|
+
}
|
|
203
|
+
const prior = byKey.get(item.key);
|
|
204
|
+
if (prior && (prior.state !== item.state || prior.attempts !== item.attempts)) {
|
|
205
|
+
return { ok: false, conflict: true, reason: `action history conflicts for ${item.key}` };
|
|
206
|
+
}
|
|
207
|
+
if (!prior) byKey.set(item.key, item);
|
|
208
|
+
}
|
|
209
|
+
return { ok: true, byKey };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validateAutoActionAuthority(payload) {
|
|
213
|
+
if (!payload?.evidence || !Array.isArray(payload.evidence.unreadable)
|
|
214
|
+
|| typeof payload.evidence.tornRead !== 'boolean') {
|
|
215
|
+
return { ok: false, reason: 'verdict evidence is incomplete' };
|
|
216
|
+
}
|
|
217
|
+
if (payload.draft !== false) {
|
|
218
|
+
return { ok: false, reason: 'non-draft authority is incomplete' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const failuresByName = new Map();
|
|
222
|
+
for (const failure of Array.isArray(payload.failures) ? payload.failures : []) {
|
|
223
|
+
if (!failure || typeof failure.name !== 'string' || !failure.name) continue;
|
|
224
|
+
const evidence = {
|
|
225
|
+
conclusion: String(failure.conclusion || '').toUpperCase(),
|
|
226
|
+
url: failure.jobUrl || failure.detailsUrl || null,
|
|
227
|
+
};
|
|
228
|
+
const prior = failuresByName.get(failure.name);
|
|
229
|
+
if (prior && (prior.conclusion !== evidence.conclusion || prior.url !== evidence.url)) {
|
|
230
|
+
return { ok: false, conflict: true, reason: `required check evidence conflicts for ${failure.name}` };
|
|
231
|
+
}
|
|
232
|
+
if (!prior) failuresByName.set(failure.name, evidence);
|
|
233
|
+
}
|
|
234
|
+
return { ok: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildAutoActionCandidates(decision, subjectRevision) {
|
|
238
|
+
const candidates = [];
|
|
239
|
+
if (decision.updateBranch.should) {
|
|
240
|
+
candidates.push({ type: 'updateBranch', key: `updateBranch:${subjectRevision}`, subjectRevision });
|
|
241
|
+
}
|
|
242
|
+
if (decision.rerunFlaky.should) {
|
|
243
|
+
const runIds = [...decision.rerunFlaky.runIds].sort((left, right) => left.localeCompare(right));
|
|
244
|
+
for (const runId of runIds) {
|
|
245
|
+
candidates.push({
|
|
246
|
+
type: 'rerunFlaky',
|
|
247
|
+
key: `rerunFlaky:${subjectRevision}:${runId}`,
|
|
248
|
+
subjectRevision,
|
|
249
|
+
runId,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return candidates;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function selectBoundedActions(candidates, history, maxActions, maxAttempts) {
|
|
257
|
+
const actions = [];
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const prior = history.get(candidate.key);
|
|
260
|
+
if (prior && (prior.state === 'pending' || prior.state === 'applied' || prior.attempts >= maxAttempts)) continue;
|
|
261
|
+
actions.push({ ...candidate, attempt: prior ? prior.attempts + 1 : 1 });
|
|
262
|
+
if (actions.length === maxActions) break;
|
|
263
|
+
}
|
|
264
|
+
return actions;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Select a deterministic, per-head, bounded action/retry batch. */
|
|
268
|
+
function decideBoundedAutoActions(payload, opts = {}) {
|
|
269
|
+
const closed = (status, reason) => ({ status, reason, actions: [] });
|
|
270
|
+
const subjectRevision = payload?.headSha;
|
|
271
|
+
if (typeof subjectRevision !== 'string' || !/^[0-9a-f]{40}$/i.test(subjectRevision)) {
|
|
272
|
+
return closed(AUTO_ACTION_STATUS.INCOMPLETE, 'exact head authority is incomplete');
|
|
273
|
+
}
|
|
274
|
+
if (opts.expectedRevision != null && opts.expectedRevision !== subjectRevision) {
|
|
275
|
+
return closed(AUTO_ACTION_STATUS.STALE, 'payload head does not match expected revision');
|
|
276
|
+
}
|
|
277
|
+
const authority = validateAutoActionAuthority(payload);
|
|
278
|
+
if (!authority.ok) {
|
|
279
|
+
return closed(authority.conflict ? AUTO_ACTION_STATUS.CONFLICT : AUTO_ACTION_STATUS.INCOMPLETE, authority.reason);
|
|
280
|
+
}
|
|
281
|
+
const maxActions = boundedPolicyValue(opts.maxActions, 1, MAX_AUTO_ACTIONS);
|
|
282
|
+
const maxAttempts = boundedPolicyValue(opts.maxAttempts, 2, MAX_AUTO_ATTEMPTS);
|
|
283
|
+
if (maxActions == null || maxAttempts == null) {
|
|
284
|
+
return closed(AUTO_ACTION_STATUS.INCOMPLETE, 'auto-action policy exceeds bounded limits');
|
|
285
|
+
}
|
|
286
|
+
const history = normalizeActionHistory(opts.history || []);
|
|
287
|
+
if (!history.ok) {
|
|
288
|
+
return closed(history.conflict ? AUTO_ACTION_STATUS.CONFLICT : AUTO_ACTION_STATUS.INCOMPLETE, history.reason);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const decision = decideAutoActions(payload, opts);
|
|
292
|
+
if (decision.updateBranch.should && decision.rerunFlaky.should) {
|
|
293
|
+
return closed(AUTO_ACTION_STATUS.CONFLICT, 'payload authorizes conflicting update and rerun actions');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const candidates = buildAutoActionCandidates(decision, subjectRevision);
|
|
297
|
+
const actions = selectBoundedActions(candidates, history.byKey, maxActions, maxAttempts);
|
|
298
|
+
if (actions.length === 0) {
|
|
299
|
+
return closed(AUTO_ACTION_STATUS.UNCHANGED, candidates.length === 0
|
|
300
|
+
? 'no safe auto-action transition'
|
|
301
|
+
: 'identical actions are pending, applied, or retry-exhausted');
|
|
302
|
+
}
|
|
303
|
+
return { status: AUTO_ACTION_STATUS.PASS, reason: 'bounded auto-actions selected', actions };
|
|
304
|
+
}
|
|
305
|
+
|
|
167
306
|
module.exports = {
|
|
168
307
|
decideAutoActions,
|
|
169
308
|
decideUpdateBranch,
|
|
@@ -172,4 +311,6 @@ module.exports = {
|
|
|
172
311
|
isDegraded,
|
|
173
312
|
INFRA_CONCLUSIONS,
|
|
174
313
|
REAL_FAILURE_CONCLUSIONS,
|
|
314
|
+
AUTO_ACTION_STATUS,
|
|
315
|
+
decideBoundedAutoActions,
|
|
175
316
|
};
|