@kungfu-tech/buildchain 3.0.6-alpha.0 → 3.0.6-alpha.1

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-08-03T02:31:12.352Z",
5
- "publishedAt": "2026-08-03T02:31:12.352Z",
4
+ "generatedAt": "2026-08-03T09:35:55.854Z",
5
+ "publishedAt": "2026-08-03T09:35:55.854Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "83280a3d21c4c75774be91ee802286b48c17fc54",
22
+ "sourceRevision": "88a8b532ce69ee3def5926a42c9cd75e4809ae88",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "3.0.6-alpha.0",
40
+ "version": "3.0.6-alpha.1",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -53,7 +53,7 @@
53
53
  "path": "docs/MAP.md",
54
54
  "plane": "use",
55
55
  "exists": true,
56
- "digest": "sha256:216e69758e78866e5a22fae1c4d017aaccf1b759937fcd953934407263344c03"
56
+ "digest": "sha256:0ea7f59030e5a087e3ff7e9eb591fec8d92c64399799e554aa1fa7ecb6a263ae"
57
57
  },
58
58
  {
59
59
  "id": "getting-started",
@@ -293,7 +293,7 @@
293
293
  "path": "docs/release-governance.md",
294
294
  "plane": "why",
295
295
  "exists": true,
296
- "digest": "sha256:a38eca4d8ada75aed5cb60f2ef23ffe4ba36b1c804ea1ee7f74b3271765b7a1c"
296
+ "digest": "sha256:fc7f0add1afce775c327d50a1e269a7040865e65ca2441dbeb8d0373c5f8d5aa"
297
297
  },
298
298
  {
299
299
  "id": "release-flow",
@@ -1681,13 +1681,14 @@
1681
1681
  "landing-mode",
1682
1682
  "max-merges",
1683
1683
  "merge-method",
1684
+ "queue-admission-context",
1684
1685
  "ready-label",
1685
1686
  "require-approval",
1686
1687
  "required-status-checks",
1687
1688
  "same-repository-only",
1688
1689
  "target-branch"
1689
1690
  ],
1690
- "inputCount": 13,
1691
+ "inputCount": 14,
1691
1692
  "secrets": [
1692
1693
  "github-token"
1693
1694
  ],
package/docs/MAP.md CHANGED
@@ -152,6 +152,7 @@ replace them.
152
152
  | How do I automatically qualify alpha candidates and publish the newest non-revoked qualified candidate at a fixed window? | [`stable-candidate-patrol.md`](stable-candidate-patrol.md) | use | preview |
153
153
  | How do I deploy a site/app preview, staging, or production surface? | [`web-surface-deployments.md`](web-surface-deployments.md) | use | stable |
154
154
  | How do I publish observed infrastructure contracts for downstream consumers? | [`infra-contract.md`](infra-contract.md) | use | preview |
155
+ | How do I operate a repeatable, disabled-by-default, budget-fail-closed AWS Windows JIT campaign? | [`aws-us-elastic-runner-burst-plane.md`](aws-us-elastic-runner-burst-plane.md#phase-2-operator-workflow) | use/verify | preview |
155
156
  | How do I use the active actions directly? | [`../actions/validate-config/README.md`](../actions/validate-config/README.md), [`../actions/run-lifecycle/README.md`](../actions/run-lifecycle/README.md), [`../actions/promote-buildchain-ref/README.md`](../actions/promote-buildchain-ref/README.md), [`../actions/report-buildchain-issue/README.md`](../actions/report-buildchain-issue/README.md), [`../actions/github-artifact-attestation/README.md`](../actions/github-artifact-attestation/README.md), [`../actions/macos-credential-island/README.md`](../actions/macos-credential-island/README.md); `dist/site/workflow-registry.json#actions` is authoritative for the six-entry inventory. | use | stable |
156
157
  | How can a consumer workflow report a Buildchain-owned failure back to Buildchain? | [`consumer-issue-reporting.md`](consumer-issue-reporting.md) + [`../actions/report-buildchain-issue/README.md`](../actions/report-buildchain-issue/README.md) | use | stable |
157
158
  | What do the fixture repositories demonstrate? | [`../fixtures/libnode-shaped/README.md`](../fixtures/libnode-shaped/README.md), [`../fixtures/publish-transaction-shaped/README.md`](../fixtures/publish-transaction-shaped/README.md), [`../fixtures/web-surface-shaped/README.md`](../fixtures/web-surface-shaped/README.md), [`../fixtures/publication-artifact-shaped/README.md`](../fixtures/publication-artifact-shaped/README.md) | verify | stable |
@@ -8,11 +8,11 @@ confidence: high
8
8
  sensitivity: public
9
9
  evidence_grade: A
10
10
  review_state: unreviewed
11
- last_reviewed: 2026-08-02
11
+ last_reviewed: 2026-08-03
12
12
  ai_provenance:
13
13
  model_family: GPT-5
14
14
  product: Codex
15
- generated_at: 2026-08-01
15
+ generated_at: 2026-08-03
16
16
  invisible_information: No hidden model checkpoint, parameters, or private training data were available.
17
17
  ---
18
18
 
@@ -189,7 +189,7 @@ and atomically refuses a sixth accepted instance. Five accepted instances
189
189
  therefore reserve at most USD 21.75. The campaign also persists the
190
190
  operator-observed spend from earlier Windows work, and refuses to arm unless
191
191
  that baseline, all five reservations, and one USD 4.35 fail-closed race
192
- allowance remain below the USD 80 phase cap.
192
+ allowance remain below the USD 110 phase cap.
193
193
 
194
194
  The campaign starts unarmed and expires within 24 hours. Its `CONTROL` record
195
195
  can be created only once: a killed or expired campaign cannot be re-armed by
@@ -199,16 +199,34 @@ paid launch. Reservations are never refunded: a controller crash, ambiguous
199
199
  launch, or successful launch all remain charged to the campaign, favoring a
200
200
  false stop over an accidental budget overrun.
201
201
 
202
+ The 2026-08-03 timeout-only campaign decision narrows the campaign to two
203
+ accepted instances with one active instance at a time. The second reservation
204
+ is an operator-gated repair retry: it may be used only after the first attempt
205
+ is classified as non-counting and runner, EC2, EBS, SSM, and workflow residue
206
+ have returned to zero. The two-slot ledger is a maximum spend boundary, not an
207
+ authorization to consume both reservations.
208
+
202
209
  Each stack owns a stack-scoped reaper log group, so an independent retained
203
210
  one-shot campaign stack can be created without colliding with another
204
211
  campaign's audit log resource.
205
212
 
206
213
  The tag-filtered AWS Budget is defense in depth, not the authoritative launch
207
- gate. It is disabled by default because a linked account cannot activate a
208
- cost-allocation tag. Set `EnableTagFilteredBudget=true` only after the AWS
209
- Organizations management account has activated `kungfu:provider` and a Cost
210
- Explorer readback proves that `windows-ec2-jit` spend is visible. The DynamoDB
211
- reservation cap remains mandatory in either mode.
214
+ gate. It is owned by the singleton
215
+ `kungfu-buildchain-windows-jit-budget-guard` stack rather than any retained
216
+ campaign stack. This prevents Budget-name collisions and prevents a stale
217
+ campaign reaper from becoming the provider-wide cost authority. The Budget
218
+ filters exactly `user:kungfu:provider$windows-ec2-jit`; its 80% and 95% actual
219
+ notifications persist the provider kill sentinel, terminate every tagged
220
+ Windows JIT instance, and delete scoped JIT parameters. Every launch controller
221
+ refuses to proceed when the sentinel exists or when the Budget identity or tag
222
+ filter does not match.
223
+
224
+ Budget installation fails closed until the AWS Organizations management
225
+ account activates `kungfu:provider` and Cost Explorer returns
226
+ `windows-ec2-jit` for that key. A linked account cannot activate the tag. Do not
227
+ create an unfiltered fallback Budget or treat a tag-filtered zero as evidence.
228
+ The DynamoDB campaign reservation remains the atomic launch authority because
229
+ Cost Explorer and AWS Budgets can lag provider activity.
212
230
 
213
231
  Qualification requires one runner-profile smoke and three trusted exact-source
214
232
  full Windows jobs all bound to the same campaign, independent cancellation and
@@ -216,82 +234,94 @@ timeout cleanup exercises, and zero repository runner, EC2 instance,
216
234
  disposable volume, min capacity, and desired capacity within 15 minutes of the
217
235
  final job.
218
236
 
219
- ### Phase 2 campaign controller
220
-
221
- `scripts/aws-windows-jit-campaign.mjs` is the one-shot operator boundary.
222
- Without a mutation mode it emits the arm plan. `arm-campaign` requires the
223
- campaign id, exact source SHA, state table, observed prior phase spend, and a
224
- bounded one-to-five accepted-instance ceiling to be repeated as confirmations,
225
- then creates `CONTROL` and `CAMPAIGN#<id>` with
226
- `attribute_not_exists`
227
- conditions. DynamoDB therefore refuses a second campaign in the same retained
228
- state table. The operator can always use `kill-campaign`; there is deliberately
229
- no clear or re-arm operation.
230
-
231
- Every `scripts/aws-windows-jit-controller.mjs --execute` call must provide the
232
- same `--campaign-id`, `--confirm-campaign-id`, `--state-table`, and
233
- `--confirm-state-table`. After the GitHub, AMI, active-instance, SSM, and EC2
234
- DryRun checks pass, the controller
235
- atomically reserves one run. Duplicate run-attempt-qualification identities,
236
- source mismatch, expiry, `KILLED`, the sixth accepted instance, or a
237
- reservation that would exceed the baseline-adjusted USD 80 phase ceiling all
238
- fail closed before `RunInstances`.
239
-
240
- Example dry-run and arm boundary (do not execute without a new campaign budget
241
- decision):
237
+ ### Phase 2 operator workflow
238
+
239
+ `pnpm operator:windows-jit` is the reusable lifecycle entrypoint. Its default
240
+ mode is `plan`, which performs no AWS or GitHub call. A plan binds the account,
241
+ region, unique campaign and stack names, source SHA/ref, Cost Explorer window,
242
+ workflow id, network, OIDC provider, expiry, slot ceiling, singleton Budget
243
+ identity, and exact confirmation digest.
244
+
245
+ The modes are deliberately separated:
246
+
247
+ - `plan` emits the deterministic mutation boundary and digest.
248
+ - `audit` reads AWS and GitHub only. It verifies the account, disabled workflow,
249
+ singleton guard stack, exact Budget filter, SNS thresholds/subscribers,
250
+ provider kill sentinel, campaign stack, and zero EC2/EBS/SSM/JIT/runner
251
+ residue.
252
+ - `install-budget --execute` deploys or updates only the singleton Budget guard.
253
+ It refuses to mutate unless the provider tag value is visible, the Windows
254
+ workflow is disabled, and the account, campaign, source, Budget, and plan
255
+ digest confirmations match.
256
+ - `prepare --execute` requires the installed Budget guard, absent kill
257
+ sentinel, fresh Cost Explorer readback filtered by both
258
+ `kungfu:provider=windows-ec2-jit` and `BoxUsage:c7i.4xlarge`, zero residue, a
259
+ never-used campaign stack name, and the disabled workflow. The receipt binds
260
+ the query timestamp and exact filter identity. Preparation deploys the
261
+ campaign stack and atomically arms the ledger with that provider-spend
262
+ baseline. It never enables or dispatches the workflow and never creates EC2
263
+ capacity.
264
+ - `close --execute` disables the workflow first, persists `KILLED`, publishes
265
+ the campaign kill switch, and reports terminal success only after EC2, EBS,
266
+ SSM, JIT parameter, and GitHub runner residue is zero. It is safe to rerun
267
+ while the reaper settles.
268
+
269
+ All mutating modes require `--execute`, `--confirm-plan-digest`,
270
+ `--confirm-account-id`, `--confirm-campaign-id`, and
271
+ `--confirm-source-sha`. Budget installation and preparation additionally
272
+ require `--confirm-budget-name`. A future paid workload still requires a
273
+ separate exact workflow/run authorization and uses
274
+ `scripts/aws-windows-jit-controller.mjs`; preparation is not paid-launch
275
+ authority.
276
+
277
+ Start by recording one reproducible plan:
242
278
 
243
279
  ```bash
244
- windows_campaign=win-REPLACE_WITH_CAMPAIGN_ID
245
- windows_source=REPLACE_WITH_EXACT_40_CHARACTER_SHA
246
- windows_state_table=REPLACE_WITH_CAMPAIGN_STATE_TABLE
247
- windows_expires_at=REPLACE_WITH_ISO_TIMESTAMP_WITHIN_24_HOURS
248
- windows_phase_spend_baseline_usd=REPLACE_WITH_OBSERVED_PRIOR_WINDOWS_SPEND
249
- windows_max_accepted_instances=REPLACE_WITH_INTEGER_FROM_1_THROUGH_5
250
-
251
- node scripts/aws-windows-jit-campaign.mjs plan-arm \
252
- --campaign-id "$windows_campaign" \
253
- --source-sha "$windows_source" \
254
- --state-table "$windows_state_table" \
255
- --expires-at "$windows_expires_at" \
256
- --phase-spend-baseline-usd "$windows_phase_spend_baseline_usd" \
257
- --max-accepted-instances "$windows_max_accepted_instances"
258
-
259
- node scripts/aws-windows-jit-campaign.mjs arm-campaign \
260
- --campaign-id "$windows_campaign" \
261
- --confirm-campaign-id "$windows_campaign" \
262
- --source-sha "$windows_source" \
263
- --confirm-source-sha "$windows_source" \
264
- --state-table "$windows_state_table" \
265
- --confirm-state-table "$windows_state_table" \
266
- --expires-at "$windows_expires_at" \
267
- --phase-spend-baseline-usd "$windows_phase_spend_baseline_usd" \
268
- --confirm-phase-spend-baseline-usd "$windows_phase_spend_baseline_usd" \
269
- --max-accepted-instances "$windows_max_accepted_instances" \
270
- --confirm-max-accepted-instances "$windows_max_accepted_instances"
280
+ pnpm operator:windows-jit plan \
281
+ --aws-profile us \
282
+ --account-id 727884401362 \
283
+ --campaign-id win-REPLACE \
284
+ --source-sha REPLACE_WITH_EXACT_40_CHARACTER_SHA \
285
+ --source-ref refs/heads/dev/v4/v4.0 \
286
+ --observed-at REPLACE_WITH_ISO_TIMESTAMP \
287
+ --expires-at REPLACE_WITH_ISO_TIMESTAMP_WITHIN_24_HOURS \
288
+ --cost-start REPLACE_WITH_PHASE_START_DATE \
289
+ --cost-end REPLACE_WITH_EXCLUSIVE_END_DATE \
290
+ --max-accepted-instances 1 \
291
+ --workflow-id 322620360 \
292
+ --vpc-id REPLACE_WITH_VPC_ID \
293
+ --subnet-id REPLACE_WITH_SUBNET_ID \
294
+ --oidc-provider-arn REPLACE_WITH_GITHUB_OIDC_PROVIDER_ARN
271
295
  ```
272
296
 
273
- Arming creates a permanent one-shot control record and admits only the
274
- explicitly confirmed number of paid instances, never more than five. Its
275
- rollback is fail-closed, not deletion: `kill-campaign` first
276
- persists `KILLED`, then publishes to the dedicated SNS topic so the reaper
277
- terminates active card-owned instances and removes their JIT parameters. The
278
- command is idempotent, but the operator must read back DynamoDB, EC2, SSM, and
279
- GitHub runners before treating cleanup as complete:
297
+ Reuse those exact arguments for `audit`, `install-budget`, `prepare`, or
298
+ `close`; never regenerate `--observed-at` between the plan and its confirmed
299
+ mutation. Capture stdout as the operator receipt. Do not put credentials,
300
+ tokens, JIT configuration, or signed URLs in arguments or receipts.
280
301
 
281
- ```bash
282
- windows_kill_topic=REPLACE_WITH_DEDICATED_KILL_SWITCH_TOPIC_ARN
283
-
284
- node scripts/aws-windows-jit-campaign.mjs kill-campaign \
285
- --campaign-id "$windows_campaign" \
286
- --confirm-campaign-id "$windows_campaign" \
287
- --source-sha "$windows_source" \
288
- --confirm-source-sha "$windows_source" \
289
- --state-table "$windows_state_table" \
290
- --confirm-state-table "$windows_state_table" \
291
- --kill-switch-topic "$windows_kill_topic" \
292
- --confirm-kill-switch-topic "$windows_kill_topic" \
293
- --reason operator-kill
294
- ```
302
+ ### Lower-level campaign and launch controllers
303
+
304
+ `scripts/aws-windows-jit-campaign-core.mjs` owns the pure one-shot ledger
305
+ contract used by the operator and launch controller. Arming creates `CONTROL`
306
+ and `CAMPAIGN#<id>` with `attribute_not_exists` conditions, so DynamoDB refuses
307
+ a second campaign in the same retained state table. There is deliberately no
308
+ clear or re-arm operation.
309
+
310
+ Every `scripts/aws-windows-jit-controller.mjs --execute` call must provide the
311
+ same `--account-id`, `--campaign-id`, `--confirm-campaign-id`, `--state-table`,
312
+ and `--confirm-state-table`. Before GitHub JIT material is created, the
313
+ controller verifies the exact provider Budget/tag filter and proves the global
314
+ Budget kill sentinel absent. After the GitHub, AMI, active-instance, SSM, and
315
+ EC2 DryRun checks pass, the controller
316
+ atomically reserves one run. Duplicate run-attempt-qualification identities,
317
+ source mismatch, expiry, `KILLED`, the sixth accepted instance, or a
318
+ reservation that would exceed the USD 110 ceiling after combining the persisted
319
+ fresh Cost Explorer baseline with all in-flight campaign reservations all fail
320
+ closed in one DynamoDB transaction before `RunInstances`. AWS Budget alarms are
321
+ defense in depth for delayed billing telemetry; the atomic ledger is the
322
+ authoritative launch-time guard. The operator is the only supported mutation
323
+ surface for campaign preparation and closeout; direct imports of the core are
324
+ not operator authority.
295
325
 
296
326
  ## Phase 3 contract
297
327
 
@@ -648,10 +648,11 @@ jobs:
648
648
  contents: write
649
649
  pull-requests: write
650
650
  checks: read
651
- statuses: read
651
+ statuses: write
652
652
  with:
653
653
  target-branch: dev/v3/v3.0
654
654
  required-status-checks: check / check
655
+ queue-admission-context: Queue admission lease
655
656
  ready-label: ready
656
657
  block-labels: blocked,do-not-merge
657
658
  max-merges: 1
@@ -659,6 +660,12 @@ jobs:
659
660
  dry-run: ${{ inputs.dry-run || false }}
660
661
  ```
661
662
 
663
+ When the protected branch requires a merge-group-only queue lease, the wrapper
664
+ posts that configured context as a temporary success status on the exact PR
665
+ head only after the ready, review, and required-check gates pass. It then
666
+ enqueues with `expectedHeadOid`; a rejected enqueue rewrites the temporary
667
+ status to failure, while the merge group must still produce its own final check.
668
+
662
669
  ## Buildchain Patrol
663
670
 
664
671
  `dev-pr-auto-merge.yml` remains the focused merge primitive. For repositories
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.6-alpha.0",
3
+ "version": "3.0.6-alpha.1",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -108,6 +108,7 @@
108
108
  },
109
109
  "packageManager": "pnpm@11.7.0",
110
110
  "scripts": {
111
+ "operator:windows-jit": "bash scripts/aws-windows-jit-operator.sh",
111
112
  "check": "pnpm run check:static && node scripts/check-maintainability.mjs && node scripts/check-internal-architecture.mjs && node scripts/check-inventory.mjs && pnpm run check:site && pnpm run check:workflows && pnpm run test:unit && pnpm run check:golden-path && node scripts/check-action-bundles.mjs",
112
113
  "check:static": "node scripts/check-javascript-syntax.mjs && prettier --check architecture/*.json bin/internal/command-registry.mjs scripts/authorize-promotion-runtime-override.cjs scripts/check-javascript-syntax.mjs scripts/check-maintainability.mjs scripts/maintainability-metrics.mjs scripts/public-reference.mjs scripts/site-reference-registry.mjs scripts/generate-public-reference.mjs scripts/verify-golden-path.mjs tests/authorize-promotion-runtime-override.test.mjs tests/command-registry.test.mjs tests/maintainability.test.mjs tests/promotion-responsibilities.test.mjs tests/public-reference.test.mjs",
113
114
  "check:workflows": "bash scripts/check-workflows.sh",
@@ -128,7 +129,7 @@
128
129
  "typescript": "^5.9.0"
129
130
  },
130
131
  "dependencies": {
131
- "@kungfu-tech/kfd": "1.0.0-alpha.47",
132
+ "@kungfu-tech/kfd": "1.0.0-alpha.53",
132
133
  "smol-toml": "1.7.0"
133
134
  }
134
135
  }
@@ -35,18 +35,15 @@ function tableName(value) {
35
35
  }
36
36
 
37
37
  function number(value) {
38
- return { N: String(value) };
38
+ return { N: String(Math.round(value * 100_000_000) / 100_000_000) };
39
39
  }
40
40
 
41
41
  function money(value, label) {
42
42
  const normalized = String(value ?? "").trim();
43
- if (!normalized) {
44
- throw new Error(`${label} is required`);
45
- }
43
+ if (!normalized) throw new Error(`${label} is required`);
46
44
  const parsed = Number(normalized);
47
- if (!Number.isFinite(parsed) || parsed < 0) {
45
+ if (!Number.isFinite(parsed) || parsed < 0)
48
46
  throw new Error(`${label} must be a non-negative finite number`);
49
- }
50
47
  return Math.round(parsed * 100_000_000) / 100_000_000;
51
48
  }
52
49
 
@@ -90,9 +87,11 @@ export function createWindowsJitCampaignArmPlan(values = {}) {
90
87
  );
91
88
  const maxAcceptedInstances = acceptedInstances(values.maxAcceptedInstances);
92
89
  const campaignReservationCeilingUsd = reservationUsd * maxAcceptedInstances;
93
- const campaignSafetyCeilingUsd =
90
+ const campaignSafetyCeilingUsd = money(
94
91
  campaignReservationCeilingUsd +
95
- reservationUsd * WINDOWS_EC2_JIT.maxConcurrentInstances;
92
+ reservationUsd * WINDOWS_EC2_JIT.maxConcurrentInstances,
93
+ "campaignSafetyCeilingUsd",
94
+ );
96
95
  const remainingPhaseBudgetUsd =
97
96
  WINDOWS_EC2_JIT.budgetLimitUsd - phaseSpendBaselineUsd;
98
97
  if (campaignSafetyCeilingUsd >= remainingPhaseBudgetUsd) {
@@ -164,6 +164,7 @@ function assertLivePreflight(plan, profile) {
164
164
  throw new Error("Windows AMI identity or availability mismatch");
165
165
  }
166
166
  return {
167
+ budgetGuard: jsonResult(commandResult("/bin/bash", ["scripts/aws-windows-jit-operator.sh", "launch-gate", "--region", plan.aws.region, ...(profile ? ["--aws-profile", profile] : [])]), "provider Budget launch gate"),
167
168
  runStatus: run.status,
168
169
  jobStatus: job.status,
169
170
  activeInstances: activeInstances.length,
@@ -12,7 +12,7 @@ export const WINDOWS_EC2_JIT = Object.freeze({
12
12
  maximumInstanceLifetimeMinutes: 180,
13
13
  maxConcurrentInstances: 1,
14
14
  maxAcceptedInstances: 5,
15
- budgetLimitUsd: 80,
15
+ budgetLimitUsd: 110,
16
16
  minimumSmokeJobs: 1,
17
17
  minimumFullJobs: 3,
18
18
  maximumCleanupLatencySeconds: 900,
@@ -8,13 +8,7 @@ const DEFAULT_REQUIRED_CHECKS = ["check"];
8
8
  const SUCCESS_STATES = new Set(["success"]);
9
9
  const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
10
10
  const VALID_LANDING_MODES = new Set(["auto", "direct", "queue"]);
11
- const STATIC_SKIP_REASONS = new Set([
12
- "draft",
13
- "fork-or-cross-repository-head",
14
- "head-prefix-not-allowed",
15
- "missing-ready-label",
16
- "blocked-label",
17
- ]);
11
+ const STATIC_SKIP_REASONS = new Set(["draft", "fork-or-cross-repository-head", "head-prefix-not-allowed", "missing-ready-label", "blocked-label"]);
18
12
  const ADMISSION_CONTRACT = "kungfu-buildchain-dev-merge-queue-admission";
19
13
 
20
14
  function splitList(value, fallback = []) {
@@ -62,6 +56,7 @@ function normalizeOptions(options = {}) {
62
56
  blockLabels: splitList(options.blockLabels, DEFAULT_BLOCK_LABELS).map((label) => label.toLowerCase()),
63
57
  allowedHeadPrefixes: splitList(options.allowedHeadPrefixes, DEFAULT_ALLOWED_HEAD_PREFIXES),
64
58
  requiredChecks: splitList(options.requiredChecks, DEFAULT_REQUIRED_CHECKS),
59
+ queueAdmissionContext: String(options.queueAdmissionContext || "").trim(),
65
60
  requireApproval: boolOption(options.requireApproval, true),
66
61
  sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
67
62
  maxMerges: intOption(options.maxMerges, 1),
@@ -155,8 +150,12 @@ function summarizeChecks({ statuses = [], checkRuns = [] } = {}, requiredChecks
155
150
  function mergeableAccepted(pr, landingMode = "direct") {
156
151
  if (pr.mergeable === false) return false;
157
152
  const state = String(pr.mergeable_state || pr.mergeStateStatus || "").toLowerCase();
158
- if (!state) return pr.mergeable === true;
159
- return ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked"] : [])].includes(state);
153
+ return state ? ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked"] : [])].includes(state) : pr.mergeable === true;
154
+ }
155
+ async function setQueueAdmissionStatus(client, repository, sha, context, state) {
156
+ if (!context) return null;
157
+ await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, { body: { state, context, description: state === "success" ? "Buildchain admitted this exact PR head to the merge queue" : "Buildchain rejected merge queue admission for this exact PR head" } });
158
+ return { context, state, sha };
160
159
  }
161
160
 
162
161
  function skip(reason, details = {}) {
@@ -521,9 +520,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
521
520
  client.getMergeQueueState(options.targetBranch),
522
521
  ]);
523
522
  const landingMode = initialQueueState.enabled ? "queue" : options.landingMode === "queue" ? "queue" : "direct";
524
- const orderedPullRequests = landingMode === "queue"
525
- ? [...pullRequests].sort((left, right) => Number(left.number) - Number(right.number))
526
- : pullRequests;
523
+ const orderedPullRequests = landingMode === "queue" ? [...pullRequests].sort((left, right) => Number(left.number) - Number(right.number)) : pullRequests;
527
524
  const result = {
528
525
  schemaVersion: 1,
529
526
  contract: "kungfu-buildchain-dev-pr-auto-merge",
@@ -662,6 +659,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
662
659
  result.skipped.push(entry);
663
660
  } else {
664
661
  try {
662
+ entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "success");
665
663
  const queueEntry = await client.enqueuePullRequest({
666
664
  pullRequestId: decision.pullRequestId,
667
665
  expectedHeadOid: expectedHeadSha,
@@ -673,6 +671,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
673
671
  result.actions.push(entry);
674
672
  result.enqueued.push(entry);
675
673
  } catch (error) {
674
+ entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "failure");
676
675
  entry.action = "skip";
677
676
  entry.reason = "enqueue-rejected";
678
677
  entry.enqueueError = {
@@ -744,6 +743,7 @@ async function main() {
744
743
  blockLabels: process.env.BUILDCHAIN_DEV_PR_BLOCK_LABELS,
745
744
  allowedHeadPrefixes: process.env.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES,
746
745
  requiredChecks: process.env.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS,
746
+ queueAdmissionContext: process.env.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT,
747
747
  requireApproval: process.env.BUILDCHAIN_DEV_PR_REQUIRE_APPROVAL,
748
748
  sameRepositoryOnly: process.env.BUILDCHAIN_DEV_PR_SAME_REPOSITORY_ONLY,
749
749
  maxMerges: process.env.BUILDCHAIN_DEV_PR_MAX_MERGES,
@@ -86,10 +86,8 @@ export function validateArtifactSigningAuthorityRun(
86
86
  ) {
87
87
  throw new Error("Buildchain signing authority repository mismatch");
88
88
  }
89
- if (
90
- run.path &&
91
- !String(run.path).startsWith(`.github/workflows/${AUTHORITY_WORKFLOW}@`)
92
- ) {
89
+ const runPath = String(run.path || "").split("@", 1)[0];
90
+ if (run.path && runPath !== `.github/workflows/${AUTHORITY_WORKFLOW}`) {
93
91
  throw new Error("Buildchain signing authority workflow path mismatch");
94
92
  }
95
93
  return run;