@intentius/chant 0.124.40 → 0.124.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +42 -5
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +76 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +615 -55
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +49 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +46 -2
- package/dist/cli/registry.d.ts +31 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +37 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +44 -4
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +92 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +150 -21
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +8 -3
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +28 -43
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +170 -120
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/op/stage-hooks.d.ts +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +64 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +331 -44
- package/dist/pr-loop.d.ts +107 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +84 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/pr-requires.d.ts +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +243 -243
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +66 -4
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +69 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +149 -2
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +94 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +4 -1
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +18 -1
- package/src/cli/handlers/fan-out.ts +43 -6
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-before-merge.test.ts +276 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +605 -61
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/parse-args.ts +34 -2
- package/src/cli/registry.ts +31 -2
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +58 -3
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/gate-plan.ts +3 -0
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +194 -21
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +14 -2
- package/src/components/wave-plan.ts +38 -3
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +197 -183
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/op/stage-hooks.ts +251 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +347 -35
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +170 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/pr-requires.test.ts +184 -0
- package/src/pr-requires.ts +79 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-init.ts +5 -4
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +107 -8
- package/src/workspace/ci-run.ts +74 -5
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +149 -2
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +140 -5
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +94 -3
- package/src/workspace/reason-codes.ts +1 -1
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
package/src/pr-forge.ts
CHANGED
|
@@ -361,6 +361,13 @@ export interface PrForge {
|
|
|
361
361
|
authors(pr: number): Promise<string[]>;
|
|
362
362
|
/** The merged pull request that produced `sha`, or null. */
|
|
363
363
|
pullRequestFor(sha: string): Promise<number | null>;
|
|
364
|
+
/**
|
|
365
|
+
* Whether pull request `pr` (merge request `pr` on GitLab) is still open
|
|
366
|
+
* (#4293): neither merged nor closed. A pull request the forge does not
|
|
367
|
+
* know reads as not open. Target locks (../lifecycle/target-lock.ts) read
|
|
368
|
+
* a lock whose holder is not open as released.
|
|
369
|
+
*/
|
|
370
|
+
pullRequestOpen?(pr: number): Promise<boolean>;
|
|
364
371
|
/**
|
|
365
372
|
* Open a pull request (a merge request on GitLab) from `head` into `base`,
|
|
366
373
|
* or into the default branch when `base` is not given. Over REST on every
|
|
@@ -377,6 +384,10 @@ export interface PrForge {
|
|
|
377
384
|
editPullRequest(pr: number, change: { title?: string; body?: string }): Promise<PullRequest | undefined>;
|
|
378
385
|
/** Add labels to a pull request by name. Forgejo needs them to exist; GitHub and GitLab make them. */
|
|
379
386
|
addLabels(pr: number, labels: readonly string[]): Promise<void>;
|
|
387
|
+
/** The names of the labels pull request `pr` carries (#4292), for `ci.labels`. */
|
|
388
|
+
labels?(pr: number): Promise<string[]>;
|
|
389
|
+
/** Whether pull request `pr` is open, closed unmerged, or merged (#4298), for `pr-env sweep`. */
|
|
390
|
+
pullRequestState?(pr: number): Promise<"open" | "closed" | "merged">;
|
|
380
391
|
/**
|
|
381
392
|
* Merge pull request `pr` only if its head is still `opts.sha`. A moved
|
|
382
393
|
* head, a conflict or a branch rule the forge enforces comes back as
|
|
@@ -391,6 +402,81 @@ export interface PrForge {
|
|
|
391
402
|
openPullRequests?(base: string): Promise<OpenPullRequest[]>;
|
|
392
403
|
/** GitHub and Forgejo (#4095): the commit `branch` points at, or undefined when there is no such branch. */
|
|
393
404
|
branchHead?(branch: string): Promise<string | undefined>;
|
|
405
|
+
/**
|
|
406
|
+
* When a scheduled Op's generated job last ran from its schedule or by hand
|
|
407
|
+
* (#4289). GitHub reads the workflow `workflow` names and its `schedule` and
|
|
408
|
+
* `workflow_dispatch` runs. Forgejo reads the repository's runs and keeps
|
|
409
|
+
* the ones from that workflow file. GitLab reads the active pipeline
|
|
410
|
+
* schedules whose `CHANT_SCHEDULED_OP` variable is `op` and their last
|
|
411
|
+
* pipelines. Throws a {@link ForgeApiError} when the forge will not say.
|
|
412
|
+
*/
|
|
413
|
+
scheduledRuns?(query: ScheduledRunsQuery): Promise<ScheduledRuns>;
|
|
414
|
+
/**
|
|
415
|
+
* The four checks an apply before merge requires (#4301, ws-106), each
|
|
416
|
+
* about pull request `pr` at `head`, the commit the job checked out. Each
|
|
417
|
+
* fails when the forge's head for `pr` is another commit. `approved`:
|
|
418
|
+
* someone other than the pull request's authors ({@link authors}) approved
|
|
419
|
+
* `head`, and no standing review asks for changes. On GitLab an approval
|
|
420
|
+
* counts when it came after the merge request's latest push, read from its
|
|
421
|
+
* system notes, and never from the token's own user.
|
|
422
|
+
*/
|
|
423
|
+
prApproved?(pr: number, head: string): Promise<PrRequireResult>;
|
|
424
|
+
/** The forge says `pr` merges as it stands: no conflicts, and on GitHub no branch protection blocking it. Pending while the forge works it out. */
|
|
425
|
+
prMergeable?(pr: number, head: string): Promise<PrRequireResult>;
|
|
426
|
+
/** `head` contains the tip of the branch `pr` merges into. */
|
|
427
|
+
prUndiverged?(pr: number, head: string): Promise<PrRequireResult>;
|
|
428
|
+
/**
|
|
429
|
+
* Every status and check reported on `head` passed, and at least one did.
|
|
430
|
+
* Statuses named in `ignore` are left out, such as the apply's own.
|
|
431
|
+
* Pending while one still runs or none has reported.
|
|
432
|
+
*/
|
|
433
|
+
prChecksPassed?(pr: number, head: string, ignore?: readonly string[]): Promise<PrRequireResult>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** What one of the requires checks (#4301) found. */
|
|
437
|
+
export interface PrRequireResult {
|
|
438
|
+
passed: boolean;
|
|
439
|
+
/** Not settled yet: the forge is still working it out, or a check still runs. Asking again may pass. */
|
|
440
|
+
pending?: boolean;
|
|
441
|
+
/** One line for a person. */
|
|
442
|
+
reason: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const short = (sha: string | undefined): string => (sha ?? "nothing").slice(0, 8);
|
|
446
|
+
const pass = (reason: string): PrRequireResult => ({ passed: true, reason });
|
|
447
|
+
const refuse = (reason: string, pending = false): PrRequireResult => ({ passed: false, ...(pending ? { pending: true } : {}), reason });
|
|
448
|
+
const moved = (what: string, head: string, now: string | undefined): PrRequireResult => refuse(`${what}'s head is ${short(now)}, not ${short(head)}, the commit this job checked out`);
|
|
449
|
+
|
|
450
|
+
/** The verdict over statuses and checks: failing and still-running names, and how many reported. */
|
|
451
|
+
function checksVerdict(what: string, head: string, failing: string[], waiting: string[], count: number): PrRequireResult {
|
|
452
|
+
const names = (l: string[]) => [...new Set(l)].sort().join(", ");
|
|
453
|
+
if (failing.length > 0) return refuse(`${names(failing)} failed on ${short(head)}`);
|
|
454
|
+
if (waiting.length > 0) return refuse(`${names(waiting)} still running on ${short(head)}`, true);
|
|
455
|
+
if (count === 0) return refuse(`no check has reported on ${what}'s head ${short(head)} yet`, true);
|
|
456
|
+
return pass(`${count} check${count === 1 ? "" : "s"} passed on ${short(head)}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Which scheduled Op job {@link PrForge.scheduledRuns} asks about. */
|
|
460
|
+
export interface ScheduledRunsQuery {
|
|
461
|
+
/** The Op's name, the value of GitLab's `CHANT_SCHEDULED_OP` selector. */
|
|
462
|
+
op: string;
|
|
463
|
+
/** GitHub and Forgejo: the Op's workflow file name, such as `drift-watch.yml`. */
|
|
464
|
+
workflow: string;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** What a forge says about a scheduled Op job's runs (#4289). */
|
|
468
|
+
export interface ScheduledRuns {
|
|
469
|
+
/** When the job last ran from its schedule or by hand (ISO 8601), if it ever did. */
|
|
470
|
+
last?: string;
|
|
471
|
+
/** GitHub: the workflow is off, and why (`disabled_inactivity`, `disabled_manually`). */
|
|
472
|
+
disabled?: string;
|
|
473
|
+
/** GitLab: no active pipeline schedule selects the Op. */
|
|
474
|
+
noSchedule?: boolean;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The newest of some ISO 8601 times, ignoring what does not parse. */
|
|
478
|
+
function newestTime(times: readonly (string | undefined)[]): string | undefined {
|
|
479
|
+
return times.filter((t): t is string => typeof t === "string" && !Number.isNaN(Date.parse(t))).sort((a, b) => Date.parse(b) - Date.parse(a))[0];
|
|
394
480
|
}
|
|
395
481
|
|
|
396
482
|
/** A GitHub or Forgejo client. */
|
|
@@ -581,6 +667,11 @@ export interface GithubForgeOptions {
|
|
|
581
667
|
fetch?: ForgeFetch;
|
|
582
668
|
}
|
|
583
669
|
|
|
670
|
+
/** GitLab's merge statuses while it works out whether a merge request can merge (#4301). */
|
|
671
|
+
const GITLAB_CHECKING = new Set(["checking", "unchecked", "preparing", "approvals_syncing", "cannot_be_merged_recheck"]);
|
|
672
|
+
/** GitLab's commit statuses still running. */
|
|
673
|
+
const GITLAB_RUNNING = new Set(["created", "waiting_for_resource", "preparing", "pending", "running", "scheduled"]);
|
|
674
|
+
|
|
584
675
|
/** GitHub keeps a status description to 140 characters; Forgejo follows. */
|
|
585
676
|
const GITHUB_DESCRIPTION = 140;
|
|
586
677
|
/** GitLab keeps a status description to 255 characters. */
|
|
@@ -650,6 +741,33 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
|
|
|
650
741
|
...(url ? { target_url: url } : {}),
|
|
651
742
|
});
|
|
652
743
|
};
|
|
744
|
+
const authorsOf = async (pr: number): Promise<string[]> => {
|
|
745
|
+
const logins = new Set<string>();
|
|
746
|
+
const add = (v: unknown) => {
|
|
747
|
+
if (isObject(v) && typeof v.login === "string" && v.login !== "") logins.add(v.login);
|
|
748
|
+
};
|
|
749
|
+
const opened = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
750
|
+
if (isObject(opened)) add(opened.user);
|
|
751
|
+
for (const c of await api.list(`${repo}/pulls/${pr}/commits`, pageParams, pageSize)) {
|
|
752
|
+
if (!isObject(c)) continue;
|
|
753
|
+
add(c.author);
|
|
754
|
+
add(c.committer);
|
|
755
|
+
}
|
|
756
|
+
return [...logins].sort();
|
|
757
|
+
};
|
|
758
|
+
const branchTip = async (branch: string): Promise<string | undefined> => {
|
|
759
|
+
let b: unknown;
|
|
760
|
+
try {
|
|
761
|
+
b = await api.call("GET", `${repo}/branches/${branch.split("/").map(encodeURIComponent).join("/")}`);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
if (err instanceof ForgeApiError && err.status === 404) return undefined;
|
|
764
|
+
throw err;
|
|
765
|
+
}
|
|
766
|
+
// GitHub names the commit `sha`, Forgejo `id`.
|
|
767
|
+
const commit = isObject(b) && isObject(b.commit) ? b.commit : undefined;
|
|
768
|
+
return commit ? str(commit.sha) ?? str(commit.id) : undefined;
|
|
769
|
+
};
|
|
770
|
+
const headOf = (v: unknown): string | undefined => (isObject(v) && isObject(v.head) ? str(v.head.sha) : undefined);
|
|
653
771
|
return {
|
|
654
772
|
kind,
|
|
655
773
|
host,
|
|
@@ -677,19 +795,93 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
|
|
|
677
795
|
}
|
|
678
796
|
return [...standing].filter(([, state]) => state === "APPROVED").map(([login]) => login).sort();
|
|
679
797
|
},
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
for (const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
798
|
+
authors: authorsOf,
|
|
799
|
+
async prApproved(pr, head) {
|
|
800
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
801
|
+
const at = headOf(got);
|
|
802
|
+
if (at !== head) return moved(`pull request ${pr}`, head, at);
|
|
803
|
+
// A reviewer's newest deciding review stands, as in approvers; an approval counts only of this head.
|
|
804
|
+
const standing = new Map<string, { state: string; commit?: string }>();
|
|
805
|
+
for (const r of await api.list(`${repo}/pulls/${pr}/reviews`, pageParams, pageSize)) {
|
|
806
|
+
const login = loginOf(r);
|
|
807
|
+
if (!login || !isObject(r) || typeof r.state !== "string") continue;
|
|
808
|
+
const state = r.dismissed === true ? "DISMISSED" : r.state.toUpperCase();
|
|
809
|
+
if (state === "COMMENTED" || state === "COMMENT" || state === "PENDING") continue;
|
|
810
|
+
const commit = str(r.commit_id);
|
|
811
|
+
standing.set(login, { state, ...(commit ? { commit } : {}) });
|
|
812
|
+
}
|
|
813
|
+
const changes = [...standing].filter(([, r]) => r.state === "CHANGES_REQUESTED" || r.state === "REQUEST_CHANGES").map(([l]) => l).sort();
|
|
814
|
+
if (changes.length > 0) return refuse(`${changes.join(", ")} asked for changes`);
|
|
815
|
+
const authors = new Set(await authorsOf(pr));
|
|
816
|
+
const by = [...standing].filter(([l, r]) => r.state === "APPROVED" && r.commit === head && !authors.has(l)).map(([l]) => l).sort();
|
|
817
|
+
if (by.length === 0) return refuse(`no reviewer other than its authors approved its head ${short(head)}`);
|
|
818
|
+
return pass(`approved at ${short(head)} by ${by.join(", ")}`);
|
|
819
|
+
},
|
|
820
|
+
async prMergeable(pr, head) {
|
|
821
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
822
|
+
const at = headOf(got);
|
|
823
|
+
if (at !== head) return moved(`pull request ${pr}`, head, at);
|
|
824
|
+
const m = isObject(got) ? got : {};
|
|
825
|
+
const into = isObject(m.base) ? str(m.base.ref) : undefined;
|
|
826
|
+
if (m.mergeable === false || m.mergeable_state === "dirty") return refuse(`the forge reports conflicts with ${into ?? "its base"}`);
|
|
827
|
+
if (m.mergeable !== true) return refuse("the forge has not worked out yet whether it can merge", true);
|
|
828
|
+
if (kind === "github" && m.mergeable_state === "blocked") return refuse(`branch protection on ${into ?? "its base"} blocks its merge`);
|
|
829
|
+
return pass(`it merges into ${into ?? "its base"} as it stands`);
|
|
830
|
+
},
|
|
831
|
+
async prUndiverged(pr, head) {
|
|
832
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
833
|
+
const at = headOf(got);
|
|
834
|
+
if (at !== head) return moved(`pull request ${pr}`, head, at);
|
|
835
|
+
const into = isObject(got) && isObject(got.base) ? str(got.base.ref) : undefined;
|
|
836
|
+
if (!into) return refuse(`the forge names no branch pull request ${pr} merges into`);
|
|
837
|
+
if (kind === "github") {
|
|
838
|
+
// behind_by: the commits on the base branch that the head does not contain.
|
|
839
|
+
const cmp = await api.call("GET", `${repo}/compare/${encodeURIComponent(into)}...${head}`);
|
|
840
|
+
const behind = isObject(cmp) ? num(cmp.behind_by) : undefined;
|
|
841
|
+
if (behind === undefined) return refuse(`the forge did not say how far ${short(head)} is behind ${into}`);
|
|
842
|
+
return behind === 0 ? pass(`${short(head)} contains ${into}`) : refuse(`${short(head)} is ${behind} commit${behind === 1 ? "" : "s"} behind ${into}`);
|
|
843
|
+
}
|
|
844
|
+
// Forgejo: the pull request's merge base is the base branch's tip exactly when the head contains it.
|
|
845
|
+
const tip = await branchTip(into);
|
|
846
|
+
const base = isObject(got) ? str(got.merge_base) : undefined;
|
|
847
|
+
if (!tip || !base) return refuse(`the forge did not say where ${into} is or where ${short(head)} left it`);
|
|
848
|
+
return base === tip ? pass(`${short(head)} contains ${into} at ${short(tip)}`) : refuse(`${short(head)} does not contain ${into} at ${short(tip)}`);
|
|
849
|
+
},
|
|
850
|
+
async prChecksPassed(_pr, head, ignore = []) {
|
|
851
|
+
const skip = new Set(ignore);
|
|
852
|
+
const failing: string[] = [];
|
|
853
|
+
const waiting: string[] = [];
|
|
854
|
+
let count = 0;
|
|
855
|
+
// The combined status holds each context's newest status.
|
|
856
|
+
const combined = await api.call("GET", `${repo}/commits/${head}/status`);
|
|
857
|
+
for (const st of isObject(combined) && Array.isArray(combined.statuses) ? combined.statuses : []) {
|
|
858
|
+
const context = isObject(st) ? str(st.context) : undefined;
|
|
859
|
+
if (!context || skip.has(context)) continue;
|
|
860
|
+
const state = str(st.state) ?? str(st.status) ?? "";
|
|
861
|
+
count++;
|
|
862
|
+
if (state === "failure" || state === "error" || state === "failed") failing.push(context);
|
|
863
|
+
else if (state === "pending" || state === "running" || state === "") waiting.push(context);
|
|
864
|
+
}
|
|
865
|
+
if (kind === "github") {
|
|
866
|
+
const runs = await api.call("GET", `${repo}/commits/${head}/check-runs?per_page=100`);
|
|
867
|
+
for (const run of isObject(runs) && Array.isArray(runs.check_runs) ? runs.check_runs : []) {
|
|
868
|
+
const name = (isObject(run) ? str(run.name) : undefined) ?? "a check";
|
|
869
|
+
if (skip.has(name) || !isObject(run)) continue;
|
|
870
|
+
count++;
|
|
871
|
+
if (run.status !== "completed") waiting.push(name);
|
|
872
|
+
else if (!["success", "neutral", "skipped"].includes(String(run.conclusion))) failing.push(name);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return checksVerdict(`pull request ${_pr}`, head, failing, waiting, count);
|
|
876
|
+
},
|
|
877
|
+
async pullRequestOpen(pr) {
|
|
878
|
+
try {
|
|
879
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
880
|
+
return isObject(got) && got.state === "open" && got.merged !== true;
|
|
881
|
+
} catch (err) {
|
|
882
|
+
if (err instanceof ForgeApiError && err.status === 404) return false;
|
|
883
|
+
throw err;
|
|
691
884
|
}
|
|
692
|
-
return [...logins].sort();
|
|
693
885
|
},
|
|
694
886
|
async pullRequestFor(sha) {
|
|
695
887
|
if (kind === "forgejo") {
|
|
@@ -765,17 +957,27 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
|
|
|
765
957
|
}
|
|
766
958
|
return out.sort((a, b) => a.number - b.number);
|
|
767
959
|
},
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
960
|
+
branchHead: branchTip,
|
|
961
|
+
async scheduledRuns(q) {
|
|
962
|
+
const file = encodeURIComponent(q.workflow);
|
|
963
|
+
if (kind === "github") {
|
|
964
|
+
const workflow = await api.call("GET", `${repo}/actions/workflows/${file}`);
|
|
965
|
+
const runs = await Promise.all(["schedule", "workflow_dispatch"].map((event) => api.call("GET", `${repo}/actions/workflows/${file}/runs?event=${event}&per_page=1`)));
|
|
966
|
+
const last = newestTime(runs.flatMap((r) => (isObject(r) && Array.isArray(r.workflow_runs) ? r.workflow_runs.map((w) => (isObject(w) ? str(w.created_at) : undefined)) : [])));
|
|
967
|
+
const state = (isObject(workflow) ? str(workflow.state) : undefined) ?? "active";
|
|
968
|
+
return { ...(last ? { last } : {}), ...(state.startsWith("disabled") ? { disabled: state } : {}) };
|
|
775
969
|
}
|
|
776
|
-
//
|
|
777
|
-
const
|
|
778
|
-
|
|
970
|
+
// Forgejo lists a repository's runs newest first; the event and the workflow are read from each run, whatever the query honours.
|
|
971
|
+
const listed = await api.call("GET", `${repo}/actions/runs?event=schedule&limit=50`);
|
|
972
|
+
const runs = Array.isArray(listed) ? listed : isObject(listed) && Array.isArray(listed.workflow_runs) ? listed.workflow_runs : [];
|
|
973
|
+
const ours = runs.filter((r): r is Record<string, unknown> => {
|
|
974
|
+
if (!isObject(r)) return false;
|
|
975
|
+
const event = str(r.event) ?? str(r.trigger_event);
|
|
976
|
+
const workflow = str(r.workflow_id) ?? str(r.path);
|
|
977
|
+
return (event === "schedule" || event === "workflow_dispatch") && (workflow === undefined || workflow.endsWith(q.workflow));
|
|
978
|
+
});
|
|
979
|
+
const last = newestTime(ours.map((r) => str(r.created) ?? str(r.created_at) ?? str(r.started)));
|
|
980
|
+
return last ? { last } : {};
|
|
779
981
|
},
|
|
780
982
|
async editPullRequest(pr, change) {
|
|
781
983
|
return pull(await api.call("PATCH", `${repo}/pulls/${pr}`, { ...(change.title !== undefined ? { title: change.title } : {}), ...(change.body !== undefined ? { body: change.body } : {}) }));
|
|
@@ -784,6 +986,16 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
|
|
|
784
986
|
if (labels.length === 0) return;
|
|
785
987
|
await api.call("POST", `${repo}/issues/${pr}/labels`, { labels: [...labels] });
|
|
786
988
|
},
|
|
989
|
+
async labels(pr) {
|
|
990
|
+
const listed = await api.list(`${repo}/issues/${pr}/labels`, pageParams, pageSize);
|
|
991
|
+
return listed.map((l) => (isObject(l) ? str(l.name) : undefined)).filter((n): n is string => n !== undefined);
|
|
992
|
+
},
|
|
993
|
+
async pullRequestState(pr) {
|
|
994
|
+
const p = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
995
|
+
if (!isObject(p)) return "open";
|
|
996
|
+
if (p.merged === true || (typeof p.merged_at === "string" && p.merged_at !== "")) return "merged";
|
|
997
|
+
return p.state === "closed" ? "closed" : "open";
|
|
998
|
+
},
|
|
787
999
|
async mergePullRequest(pr, m) {
|
|
788
1000
|
const method = m.method ?? "merge";
|
|
789
1001
|
let res: unknown;
|
|
@@ -915,6 +1127,22 @@ export function gitlabForge(opts: GitlabForgeOptions): PrForge {
|
|
|
915
1127
|
await api.call("PUT", `${project}/issues/${n}`, { state_event: "close" });
|
|
916
1128
|
},
|
|
917
1129
|
};
|
|
1130
|
+
const authorsOf = async (mr: number): Promise<string[]> => {
|
|
1131
|
+
const logins = new Set<string>();
|
|
1132
|
+
const merge = await api.call("GET", `${notes}/${mr}`);
|
|
1133
|
+
const author = isObject(merge) ? authorOf(merge) : undefined;
|
|
1134
|
+
if (author) logins.add(author);
|
|
1135
|
+
const emails = new Set<string>();
|
|
1136
|
+
for (const c of await api.list(`${notes}/${mr}/commits`, pageParams, 100)) {
|
|
1137
|
+
if (!isObject(c)) continue;
|
|
1138
|
+
for (const email of [str(c.author_email), str(c.committer_email)]) if (email) emails.add(email.toLowerCase());
|
|
1139
|
+
}
|
|
1140
|
+
for (const email of [...emails].sort()) {
|
|
1141
|
+
const users = await api.call("GET", `/users?search=${encodeURIComponent(email)}`);
|
|
1142
|
+
for (const u of Array.isArray(users) ? users : []) if (isObject(u) && typeof u.username === "string") logins.add(u.username);
|
|
1143
|
+
}
|
|
1144
|
+
return [...logins].sort();
|
|
1145
|
+
};
|
|
918
1146
|
return {
|
|
919
1147
|
kind: "gitlab",
|
|
920
1148
|
host,
|
|
@@ -946,21 +1174,83 @@ export function gitlabForge(opts: GitlabForgeOptions): PrForge {
|
|
|
946
1174
|
.filter((l): l is string => l !== undefined);
|
|
947
1175
|
return [...new Set(logins)].sort();
|
|
948
1176
|
},
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
const
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1177
|
+
authors: authorsOf,
|
|
1178
|
+
async prApproved(mr, head) {
|
|
1179
|
+
const got = await api.call("GET", `${notes}/${mr}`);
|
|
1180
|
+
const at = isObject(got) ? str(got.sha) : undefined;
|
|
1181
|
+
if (at !== head) return moved(`merge request !${mr}`, head, at);
|
|
1182
|
+
// The token's own user approves for the pipeline, never as a reviewer.
|
|
1183
|
+
const me = await api.call("GET", "/user");
|
|
1184
|
+
const self = isObject(me) ? str(me.username) : undefined;
|
|
1185
|
+
if (!self) return refuse("GitLab did not say which user the job's token acts as, so no approval counts");
|
|
1186
|
+
// An approval counts once it came after the latest push: the newest version of the diff.
|
|
1187
|
+
const versions = await api.call("GET", `${notes}/${mr}/versions`);
|
|
1188
|
+
const since = Math.max(-Infinity, ...(Array.isArray(versions) ? versions : []).map((v) => Date.parse((isObject(v) ? str(v.created_at) : undefined) ?? "")).filter(Number.isFinite));
|
|
1189
|
+
const said = new Map<string, { what: string; at: number }>();
|
|
1190
|
+
for (const n of await api.list(`${notes}/${mr}/notes?sort=asc&order_by=created_at`, pageParams, 100)) {
|
|
1191
|
+
if (!isObject(n) || n.system !== true) continue;
|
|
1192
|
+
const who = authorOf(n);
|
|
1193
|
+
const body = (str(n.body) ?? "").trim();
|
|
1194
|
+
const what = /^approved this merge request/.test(body) ? "approved" : /^unapproved this merge request/.test(body) ? "unapproved" : /^requested changes/.test(body) ? "changes" : undefined;
|
|
1195
|
+
const when = Date.parse(str(n.created_at) ?? "");
|
|
1196
|
+
if (!who || who === self || !what || !Number.isFinite(when)) continue;
|
|
1197
|
+
const before = said.get(who);
|
|
1198
|
+
if (!before || when >= before.at) said.set(who, { what, at: when });
|
|
1199
|
+
}
|
|
1200
|
+
const changes = [...said].filter(([, v]) => v.what === "changes").map(([w]) => w).sort();
|
|
1201
|
+
if (changes.length > 0) return refuse(`${changes.join(", ")} requested changes`);
|
|
1202
|
+
const authors = new Set(await authorsOf(mr));
|
|
1203
|
+
const by = [...said].filter(([w, v]) => v.what === "approved" && v.at > since && !authors.has(w)).map(([w]) => w).sort();
|
|
1204
|
+
if (by.length === 0) return refuse(`no member other than its authors approved it after its latest push, ${short(head)}`);
|
|
1205
|
+
return pass(`approved after ${short(head)} by ${by.join(", ")}`);
|
|
1206
|
+
},
|
|
1207
|
+
async prMergeable(mr, head) {
|
|
1208
|
+
const got = await api.call("GET", `${notes}/${mr}`);
|
|
1209
|
+
const m = isObject(got) ? got : {};
|
|
1210
|
+
if (str(m.sha) !== head) return moved(`merge request !${mr}`, head, str(m.sha));
|
|
1211
|
+
const status = str(m.detailed_merge_status) ?? (m.merge_status === "can_be_merged" ? "mergeable" : (str(m.merge_status) ?? ""));
|
|
1212
|
+
const into = str(m.target_branch) ?? "its target branch";
|
|
1213
|
+
if (status === "conflict" || m.has_conflicts === true) return refuse(`GitLab reports conflicts with ${into}`);
|
|
1214
|
+
if (GITLAB_CHECKING.has(status)) return refuse("GitLab has not worked out yet whether it can merge", true);
|
|
1215
|
+
if (status !== "mergeable") return refuse(`GitLab says ${status || "nothing"} about merging it`);
|
|
1216
|
+
return pass(`it merges into ${into} as it stands`);
|
|
1217
|
+
},
|
|
1218
|
+
async prUndiverged(mr, head) {
|
|
1219
|
+
const got = await api.call("GET", `${notes}/${mr}?include_diverged_commits_count=true`);
|
|
1220
|
+
const m = isObject(got) ? got : {};
|
|
1221
|
+
if (str(m.sha) !== head) return moved(`merge request !${mr}`, head, str(m.sha));
|
|
1222
|
+
const into = str(m.target_branch) ?? "its target branch";
|
|
1223
|
+
const behind = num(m.diverged_commits_count);
|
|
1224
|
+
if (behind === undefined) return refuse(`GitLab did not say how far ${short(head)} is behind ${into}`);
|
|
1225
|
+
return behind === 0 ? pass(`${short(head)} contains ${into}`) : refuse(`${short(head)} is ${behind} commit${behind === 1 ? "" : "s"} behind ${into}`);
|
|
1226
|
+
},
|
|
1227
|
+
async prChecksPassed(mr, head, ignore = []) {
|
|
1228
|
+
const skip = new Set(ignore);
|
|
1229
|
+
// Each name's newest status; a job allowed to fail holds nothing back.
|
|
1230
|
+
const newest = new Map<string, Record<string, unknown>>();
|
|
1231
|
+
for (const st of await api.list(`${project}/repository/commits/${head}/statuses?all=true`, pageParams, 100)) {
|
|
1232
|
+
const name = isObject(st) ? str(st.name) : undefined;
|
|
1233
|
+
if (!name || skip.has(name) || !isObject(st)) continue;
|
|
1234
|
+
const had = newest.get(name);
|
|
1235
|
+
if (!had || (num(st.id) ?? 0) > (num(had.id) ?? 0)) newest.set(name, st);
|
|
958
1236
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1237
|
+
const failing: string[] = [];
|
|
1238
|
+
const waiting: string[] = [];
|
|
1239
|
+
for (const [name, st] of newest) {
|
|
1240
|
+
const state = str(st.status) ?? "";
|
|
1241
|
+
if ((state === "failed" || state === "canceled") && st.allow_failure !== true) failing.push(name);
|
|
1242
|
+
else if (GITLAB_RUNNING.has(state)) waiting.push(name);
|
|
1243
|
+
}
|
|
1244
|
+
return checksVerdict(`merge request !${mr}`, head, failing, waiting, newest.size);
|
|
1245
|
+
},
|
|
1246
|
+
async pullRequestOpen(mr) {
|
|
1247
|
+
try {
|
|
1248
|
+
const got = await api.call("GET", `${notes}/${mr}`);
|
|
1249
|
+
return isObject(got) && got.state === "opened";
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
if (err instanceof ForgeApiError && err.status === 404) return false;
|
|
1252
|
+
throw err;
|
|
962
1253
|
}
|
|
963
|
-
return [...logins].sort();
|
|
964
1254
|
},
|
|
965
1255
|
async pullRequestFor(sha) {
|
|
966
1256
|
const mrs = await api.call("GET", `${project}/repository/commits/${sha}/merge_requests`);
|
|
@@ -984,6 +1274,19 @@ export function gitlabForge(opts: GitlabForgeOptions): PrForge {
|
|
|
984
1274
|
.filter((m): m is PullRequest => m !== undefined)
|
|
985
1275
|
.sort((a, b) => a.number - b.number)[0];
|
|
986
1276
|
},
|
|
1277
|
+
async scheduledRuns(q) {
|
|
1278
|
+
// The variable the gitlab lexicon's generated Op file selects a scheduled job by.
|
|
1279
|
+
const selects = (d: unknown): boolean =>
|
|
1280
|
+
isObject(d) && Array.isArray(d.variables) && d.variables.some((v) => isObject(v) && v.key === "CHANT_SCHEDULED_OP" && v.value === q.op);
|
|
1281
|
+
const schedules = await api.list(`${project}/pipeline_schedules?scope=active`, pageParams, 100);
|
|
1282
|
+
// A schedule's variables are on each schedule, not the list.
|
|
1283
|
+
const details = await Promise.all(schedules.map((s) => api.call("GET", `${project}/pipeline_schedules/${isObject(s) ? num(s.id) : ""}`)));
|
|
1284
|
+
const ours = details.filter(selects);
|
|
1285
|
+
const ids = ours.map((d) => (isObject(d) && isObject(d.last_pipeline) ? num(d.last_pipeline.id) : undefined)).filter((n): n is number => n !== undefined && n > 0);
|
|
1286
|
+
const pipelines = await Promise.all(ids.map((id) => api.call("GET", `${project}/pipelines/${id}`)));
|
|
1287
|
+
const last = newestTime(pipelines.map((p) => (isObject(p) ? str(p.created_at) : undefined)));
|
|
1288
|
+
return { ...(last ? { last } : {}), ...(ours.length === 0 ? { noSchedule: true } : {}) };
|
|
1289
|
+
},
|
|
987
1290
|
async editPullRequest(mr, change) {
|
|
988
1291
|
return mergeRequest(await api.call("PUT", `${notes}/${mr}`, { ...(change.title !== undefined ? { title: change.title } : {}), ...(change.body !== undefined ? { description: change.body } : {}) }));
|
|
989
1292
|
},
|
|
@@ -991,6 +1294,15 @@ export function gitlabForge(opts: GitlabForgeOptions): PrForge {
|
|
|
991
1294
|
if (labels.length === 0) return;
|
|
992
1295
|
await api.call("PUT", `${notes}/${mr}`, { add_labels: labels.join(",") });
|
|
993
1296
|
},
|
|
1297
|
+
async labels(mr) {
|
|
1298
|
+
const m = await api.call("GET", `${notes}/${mr}`);
|
|
1299
|
+
return isObject(m) && Array.isArray(m.labels) ? m.labels.filter((l): l is string => typeof l === "string") : [];
|
|
1300
|
+
},
|
|
1301
|
+
async pullRequestState(mr) {
|
|
1302
|
+
const m = await api.call("GET", `${notes}/${mr}`);
|
|
1303
|
+
if (!isObject(m)) return "open";
|
|
1304
|
+
return m.state === "merged" ? "merged" : m.state === "closed" ? "closed" : "open";
|
|
1305
|
+
},
|
|
994
1306
|
async mergePullRequest(mr, m) {
|
|
995
1307
|
const method = m.method ?? "merge";
|
|
996
1308
|
if (method === "rebase") throw new Error("GitLab has no one-call rebase merge; use method merge or squash");
|
|
@@ -41,7 +41,7 @@ describe("the pull-request loop bundle", () => {
|
|
|
41
41
|
"packages/core/src/pr-forge.ts",
|
|
42
42
|
"packages/core/src/pr-loop.ts",
|
|
43
43
|
]);
|
|
44
|
-
// About
|
|
45
|
-
expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(
|
|
44
|
+
// About 89 KB today, after the hooks line (#4288), the blocked set (#4291), the scheduled-run reads (#4289) and the requires checks (#4301).
|
|
45
|
+
expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(96 * 1024);
|
|
46
46
|
});
|
|
47
47
|
});
|
package/src/pr-loop.test.ts
CHANGED
|
@@ -366,3 +366,61 @@ describe("a gate per wave on merge (#4003)", () => {
|
|
|
366
366
|
expect(prStatusDescription({ ...waiting, waves: [wave1], refusal: "earlier-wave" })).toBe("refused: an earlier wave has not applied");
|
|
367
367
|
});
|
|
368
368
|
});
|
|
369
|
+
|
|
370
|
+
describe("stage hooks in the report and the note (#4288)", () => {
|
|
371
|
+
const hooked: PrReport = {
|
|
372
|
+
...planReport,
|
|
373
|
+
status: "plan-failed",
|
|
374
|
+
message: "The before-plan hook lint exited 1, so nothing was planned or applied.",
|
|
375
|
+
hooks: [
|
|
376
|
+
{ moment: "before-observe", name: "init", target: "a", exit: 0, outcome: "passed" },
|
|
377
|
+
{ moment: "before-plan", name: "lint", exit: 1, outcome: "failed" },
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
test("a report with hooks validates", () => {
|
|
382
|
+
expectValid(hooked);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("the note counts the hooks that passed and names the one that failed", () => {
|
|
386
|
+
const note = renderPrNote(hooked);
|
|
387
|
+
expect(note).toContain("A hook failed, so nothing can apply.");
|
|
388
|
+
expect(note).toContain("Hooks: 1 passed; `lint` before-plan failed (exit 1).");
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("the policy in the report and the note (#4294)", () => {
|
|
393
|
+
const policy = {
|
|
394
|
+
result: "denied" as const,
|
|
395
|
+
engine: "opa" as const,
|
|
396
|
+
base: "b".repeat(40),
|
|
397
|
+
denials: [{ rule: "main.deny_public", message: "bucket logs is public" }],
|
|
398
|
+
rules: ["main.deny_public"],
|
|
399
|
+
holds: [{ rule: "main.hold_iam", message: "an IAM role changes" }],
|
|
400
|
+
warnings: ["three deletes"],
|
|
401
|
+
overrideCommand: 'chant approve policy-override pr-7 --rule main.deny_public --reason "<why>"',
|
|
402
|
+
};
|
|
403
|
+
const denied: PrReport = { ...planReport, policy };
|
|
404
|
+
|
|
405
|
+
test("a report with a policy, and an apply the policy refused, validate", () => {
|
|
406
|
+
expectValid(denied);
|
|
407
|
+
expectValid({ ...denied, stage: "apply", status: "refused", refusal: "policy-denied", message: "The policy denied this plan." });
|
|
408
|
+
expectValid({ ...denied, policy: { ...policy, result: "error", denials: [], rules: undefined, holds: [], error: "opa did not run" }, stage: "apply", status: "refused", refusal: "policy-error" });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("the note names the denial, the hold and the override command", () => {
|
|
412
|
+
const note = renderPrNote(denied);
|
|
413
|
+
expect(note).toContain("Policy (opa, read at `bbbbbbbbbbbb`) denied this plan:");
|
|
414
|
+
expect(note).toContain("- `main.deny_public`: bucket logs is public");
|
|
415
|
+
expect(note).toContain("holds this plan for a person's approval of its digest:");
|
|
416
|
+
expect(note).toContain(policy.overrideCommand);
|
|
417
|
+
expect(note).toContain("Policy warnings: three deletes");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("a denied plan fails its status until an override counts; the apply's refusal says why", () => {
|
|
421
|
+
expect(prStatusState(denied)).toBe("failure");
|
|
422
|
+
expect(prStatusState({ ...denied, policy: { ...policy, override: { by: "github:carol", at: "2026-10-22T12:00:00Z", reason: "docs site", digest: "sha256:1" } } })).toBe("success");
|
|
423
|
+
expect(prStatusState({ ...denied, policy: { ...policy, result: "held", denials: [] } })).toBe("success");
|
|
424
|
+
expect(prStatusDescription({ ...denied, stage: "apply", status: "refused", refusal: "policy-denied" })).toBe("refused: the policy denied the plan (main.deny_public)");
|
|
425
|
+
});
|
|
426
|
+
});
|