@intentius/chant 0.124.40 → 0.124.41
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 +15 -1
- package/dist/cli/handlers/fan-out.js +16 -3
- 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 +74 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +503 -53
- 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/main.d.ts.map +1 -1
- package/dist/cli/main.js +41 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -2
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/fan-out-run.d.ts +28 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +27 -3
- 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 +72 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +96 -5
- 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.map +1 -1
- package/dist/components/pr-run.js +5 -3
- 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/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 +62 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +190 -11
- 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/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 +36 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +82 -0
- package/dist/pr-loop.d.ts +90 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +77 -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/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 +242 -242
- 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/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-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 +64 -3
- package/dist/workspace/declaration.d.ts +63 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +147 -0
- 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 +73 -2
- 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 +15 -1
- package/src/cli/handlers/fan-out.ts +17 -4
- 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-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 +496 -59
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/main.ts +41 -2
- package/src/cli/parse-args.ts +23 -2
- package/src/cli/registry.ts +12 -2
- package/src/components/fan-out-run.ts +39 -2
- package/src/components/fan-out-waves.test.ts +45 -1
- 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 +130 -5
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +5 -2
- 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/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 +233 -11
- 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/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 +114 -0
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +146 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -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/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-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 +72 -4
- package/src/workspace/declaration.schema.json +147 -0
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +134 -2
- 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 +73 -2
- 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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan and apply cloud identities (#4296): the pull-request jobs plan with
|
|
3
|
+
* one identity and apply with another, per cloud, and the plan job trades
|
|
4
|
+
* its OIDC token before the change's code is checked out.
|
|
5
|
+
*
|
|
6
|
+
* The workspace declaration's `ci.identity.cloud` gives each cloud a list of
|
|
7
|
+
* rules. A rule names environment globs and member globs, and a plan and an
|
|
8
|
+
* apply identity: an AWS role, a GCP service account or an Azure client id.
|
|
9
|
+
* The first rule whose globs match the pipeline's environment and member is
|
|
10
|
+
* the pipeline's ({@link resolveCloudIdentity}). A workspace generates one
|
|
11
|
+
* pull-request pipeline per member and environment, so each member's apply
|
|
12
|
+
* jobs hold that member's apply identity and no other.
|
|
13
|
+
*
|
|
14
|
+
* The plan job runs the pull request's code. Its first step, before the
|
|
15
|
+
* checkout, runs {@link tradeJs} with `node -e`: it asks the forge for an
|
|
16
|
+
* OIDC token, trades it for the plan identity's own credentials, and leaves
|
|
17
|
+
* those credentials for the later steps. Each later step then starts again
|
|
18
|
+
* without the variables a token is requested with ({@link OIDC_REQUEST_VARS},
|
|
19
|
+
* through `dropForgeTokens` in ./ci-handover.ts), so the change's code holds
|
|
20
|
+
* no OIDC token and cannot request one. The plan job's file names no apply
|
|
21
|
+
* identity. The record job plans the merged commit with the plan identity,
|
|
22
|
+
* and the apply jobs trade for the apply identity.
|
|
23
|
+
*
|
|
24
|
+
* The trade is terragucci's `oidc-trade.ts` (INTENTIUS/terragucci e6cb431,
|
|
25
|
+
* packages/terragucci/src/oidc-trade.ts) with these clients:
|
|
26
|
+
*
|
|
27
|
+
* AWS STS AssumeRoleWithWebIdentity, as session keys in
|
|
28
|
+
* AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN
|
|
29
|
+
* GCP Google's STS token exchange, then IAM generateAccessToken for the
|
|
30
|
+
* service account, in GOOGLE_OAUTH_ACCESS_TOKEN and
|
|
31
|
+
* CLOUDSDK_AUTH_ACCESS_TOKEN
|
|
32
|
+
* Azure Entra's client credentials grant with the token as the client
|
|
33
|
+
* assertion, as an ARM access token in AZURE_ACCESS_TOKEN, with
|
|
34
|
+
* AZURE_CLIENT_ID and AZURE_TENANT_ID (and ARM_ spellings)
|
|
35
|
+
*
|
|
36
|
+
* terragucci leaves Azure untraded because the azurerm provider takes only
|
|
37
|
+
* the OIDC token. chant trades it, so the change's code never holds a token
|
|
38
|
+
* another client could be traded for; a tool that needs the OIDC token
|
|
39
|
+
* itself cannot plan under this block.
|
|
40
|
+
*/
|
|
41
|
+
/** The clouds `ci.identity.cloud` names. */
|
|
42
|
+
export declare const CLOUDS: readonly ["aws", "gcp", "azure"];
|
|
43
|
+
export type Cloud = (typeof CLOUDS)[number];
|
|
44
|
+
/** One rule: the environments and members it covers, and their two identities. */
|
|
45
|
+
export interface CloudIdentityRule {
|
|
46
|
+
/** Environment-name globs. Absent: every environment. */
|
|
47
|
+
environments?: string[];
|
|
48
|
+
/** Member-name globs. Absent: every member, and a pipeline outside a workspace member. */
|
|
49
|
+
members?: string[];
|
|
50
|
+
/** The identity the plan and record jobs take. */
|
|
51
|
+
plan: string;
|
|
52
|
+
/** The identity the apply jobs take. */
|
|
53
|
+
apply: string;
|
|
54
|
+
}
|
|
55
|
+
export interface AwsCloudIdentity {
|
|
56
|
+
/** The STS region. Default `us-east-1`. */
|
|
57
|
+
region?: string;
|
|
58
|
+
/** The OIDC token's audience. Default `sts.amazonaws.com`. */
|
|
59
|
+
audience?: string;
|
|
60
|
+
/** The STS endpoint, for an emulator. Default the region's, or `AWS_ENDPOINT_URL_STS` in the job. */
|
|
61
|
+
endpoint?: string;
|
|
62
|
+
rules: CloudIdentityRule[];
|
|
63
|
+
}
|
|
64
|
+
export interface GcpCloudIdentity {
|
|
65
|
+
/** The workload identity provider: `projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>`. */
|
|
66
|
+
provider: string;
|
|
67
|
+
/** The OIDC token's audience. Default `https://iam.googleapis.com/<provider>`. */
|
|
68
|
+
audience?: string;
|
|
69
|
+
/** Google's STS, for an emulator. Default `https://sts.googleapis.com`. */
|
|
70
|
+
stsEndpoint?: string;
|
|
71
|
+
/** IAM credentials, for an emulator. Default `https://iamcredentials.googleapis.com`. */
|
|
72
|
+
iamEndpoint?: string;
|
|
73
|
+
rules: CloudIdentityRule[];
|
|
74
|
+
}
|
|
75
|
+
export interface AzureCloudIdentity {
|
|
76
|
+
/** The Entra tenant. */
|
|
77
|
+
tenantId: string;
|
|
78
|
+
/** Set as AZURE_SUBSCRIPTION_ID and ARM_SUBSCRIPTION_ID when given. */
|
|
79
|
+
subscriptionId?: string;
|
|
80
|
+
/** The OIDC token's audience. Default `api://AzureADTokenExchange`. */
|
|
81
|
+
audience?: string;
|
|
82
|
+
/** The Entra authority host, for an emulator. Default `https://login.microsoftonline.com`. */
|
|
83
|
+
authority?: string;
|
|
84
|
+
rules: CloudIdentityRule[];
|
|
85
|
+
}
|
|
86
|
+
/** `ci.identity.cloud` as the declaration holds it. */
|
|
87
|
+
export interface CloudIdentityDeclaration {
|
|
88
|
+
aws?: AwsCloudIdentity;
|
|
89
|
+
gcp?: GcpCloudIdentity;
|
|
90
|
+
azure?: AzureCloudIdentity;
|
|
91
|
+
}
|
|
92
|
+
/** What one job trades for: per cloud, the one identity and where to trade it. */
|
|
93
|
+
export interface CloudTradeSpec {
|
|
94
|
+
aws?: {
|
|
95
|
+
role: string;
|
|
96
|
+
region: string;
|
|
97
|
+
audience: string;
|
|
98
|
+
endpoint?: string;
|
|
99
|
+
};
|
|
100
|
+
gcp?: {
|
|
101
|
+
serviceAccount: string;
|
|
102
|
+
provider: string;
|
|
103
|
+
audience: string;
|
|
104
|
+
stsEndpoint?: string;
|
|
105
|
+
iamEndpoint?: string;
|
|
106
|
+
};
|
|
107
|
+
azure?: {
|
|
108
|
+
clientId: string;
|
|
109
|
+
tenantId: string;
|
|
110
|
+
audience: string;
|
|
111
|
+
subscriptionId?: string;
|
|
112
|
+
authority?: string;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** A pipeline's two trades: the plan and record jobs', and the apply jobs'. */
|
|
116
|
+
export interface PipelineCloudIdentity {
|
|
117
|
+
plan: CloudTradeSpec;
|
|
118
|
+
apply: CloudTradeSpec;
|
|
119
|
+
}
|
|
120
|
+
/** The first rule that covers `env` and `member`, or undefined. */
|
|
121
|
+
export declare function ruleFor(rules: readonly CloudIdentityRule[], env: string, member?: string): CloudIdentityRule | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* The plan and apply trades of the pipeline for `env` and `member`, or
|
|
124
|
+
* undefined when no rule of any cloud covers them. A cloud no rule covers
|
|
125
|
+
* is left out of both.
|
|
126
|
+
*/
|
|
127
|
+
export declare function resolveCloudIdentity(decl: CloudIdentityDeclaration | undefined, env: string, member?: string): PipelineCloudIdentity | undefined;
|
|
128
|
+
/** The clouds a trade covers, in {@link CLOUDS} order. */
|
|
129
|
+
export declare function tradedClouds(spec: CloudTradeSpec): Cloud[];
|
|
130
|
+
/** Every identity a trade names: the role, the service account, the client id. */
|
|
131
|
+
export declare function tradeIdentities(spec: CloudTradeSpec): string[];
|
|
132
|
+
/**
|
|
133
|
+
* The problems of a declared `ci.identity.cloud`, each with its JSON
|
|
134
|
+
* pointer: a rule whose plan and apply are one identity, or a plan identity
|
|
135
|
+
* that is another rule's apply identity. Either would let the plan job,
|
|
136
|
+
* which runs the change's code, act as an apply identity.
|
|
137
|
+
*/
|
|
138
|
+
export declare function cloudIdentityProblems(decl: CloudIdentityDeclaration): Array<{
|
|
139
|
+
pointer: string;
|
|
140
|
+
message: string;
|
|
141
|
+
}>;
|
|
142
|
+
/** The variable the trade step reads its {@link CloudTradeSpec} from, as JSON. */
|
|
143
|
+
export declare const TRADE_SPEC_ENV = "CHANT_CLOUD_TRADE";
|
|
144
|
+
/** The GitLab `id_tokens` variable a cloud's token comes in: `CHANT_ID_TOKEN_AWS`, `_GCP`, `_AZURE`. */
|
|
145
|
+
export declare function idTokenVar(cloud: Cloud): string;
|
|
146
|
+
/**
|
|
147
|
+
* The variables a GitHub or Forgejo step requests an OIDC token with, and
|
|
148
|
+
* the GitLab `id_tokens` variables the trade reads. A step that runs the
|
|
149
|
+
* change's code starts without them, once the trade is done.
|
|
150
|
+
*/
|
|
151
|
+
export declare const OIDC_REQUEST_VARS: readonly ["ACTIONS_ID_TOKEN_REQUEST_TOKEN", "ACTIONS_ID_TOKEN_REQUEST_URL", ...string[], "CHANT_CLOUD_TRADE"];
|
|
152
|
+
/** The variables the trade sets, per cloud. */
|
|
153
|
+
export declare const TRADED_VARS: Record<Cloud, readonly string[]>;
|
|
154
|
+
/**
|
|
155
|
+
* How the trade leaves what it got. `github-env`: `NAME=value` lines
|
|
156
|
+
* appended to the file argv 1 names (`$GITHUB_ENV`), and an `::add-mask::`
|
|
157
|
+
* line for each secret, so every later step of the job has them. `sh`: a
|
|
158
|
+
* file of `export` lines a GitLab script sources, mode 0600.
|
|
159
|
+
*/
|
|
160
|
+
export type TradeFormat = "github-env" | "sh";
|
|
161
|
+
/**
|
|
162
|
+
* The trade, a script for `node -e` with no dependency, so it runs before
|
|
163
|
+
* the checkout and reads none of the change's files. argv 1 is the file it
|
|
164
|
+
* writes and argv 2 its {@link TradeFormat}; {@link TRADE_SPEC_ENV} is the
|
|
165
|
+
* spec. The token comes from a cloud's {@link idTokenVar} when the job has
|
|
166
|
+
* it (GitLab's `id_tokens`), else from the forge's request URL with the
|
|
167
|
+
* cloud's audience. A value that holds anything but printable ASCII with no
|
|
168
|
+
* space is refused, so no answer can add a line of its own to the file. The
|
|
169
|
+
* script never prints a token.
|
|
170
|
+
*/
|
|
171
|
+
export declare const TRADE_JS: string;
|
|
172
|
+
/**
|
|
173
|
+
* The shell line of a trade step: `node -e` from `/`, so no file of the
|
|
174
|
+
* checkout (a `package.json`, an `.npmrc`) is in reach, writing `out` in
|
|
175
|
+
* `format`.
|
|
176
|
+
*/
|
|
177
|
+
export declare function tradeLine(out: string, format: TradeFormat): string;
|
|
178
|
+
/** The GitLab file the trade writes and the plan script sources, outside the project directory. */
|
|
179
|
+
export declare const GITLAB_TRADE_FILE = "${TMPDIR:-/tmp}/chant-cloud-$CI_JOB_ID.sh";
|
|
180
|
+
/**
|
|
181
|
+
* The lines a GitLab job's script starts with when it trades: the trade,
|
|
182
|
+
* the unset of the token variables (and, with `extraUnset`, the forge
|
|
183
|
+
* tokens'), and the source of what the trade wrote, which it then deletes.
|
|
184
|
+
*/
|
|
185
|
+
export declare function gitlabTradeLines(extraUnset?: readonly string[]): string[];
|
|
186
|
+
/** GitLab's `id_tokens:` for a trade: one per cloud, with the cloud's audience. */
|
|
187
|
+
export declare function gitlabTradeIdTokens(spec: CloudTradeSpec): Record<string, {
|
|
188
|
+
aud: string;
|
|
189
|
+
}>;
|
|
190
|
+
//# sourceMappingURL=ci-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-identity.d.ts","sourceRoot":"","sources":["../src/ci-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAKH,4CAA4C;AAC5C,eAAO,MAAM,MAAM,YAAI,KAAK,EAAE,KAAK,EAAE,OAAO,CAAU,CAAC;AACvD,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qGAAqG;IACrG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,8HAA8H;IAC9H,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED,uDAAuD;AACvD,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,GAAG,CAAC,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjH,KAAK,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/G;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,cAAc,CAAC;CACvB;AAKD,mEAAmE;AACnE,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAExH;AAKD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAkChJ;AAED,0DAA0D;AAC1D,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,KAAK,EAAE,CAE1D;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,EAAE,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,wBAAwB,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAYjH;AAED,kFAAkF;AAClF,eAAO,MAAM,cAAc,sBAAsB,CAAC;AAElD,wGAAwG;AACxG,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE/C;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,+GAAyH,CAAC;AAExJ,+CAA+C;AAC/C,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAIxD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,QA2CX,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAElE;AAED,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,8CAA8C,CAAC;AAE7E;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,EAAE,CAM7E;AAED,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAEzF"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan and apply cloud identities (#4296): the pull-request jobs plan with
|
|
3
|
+
* one identity and apply with another, per cloud, and the plan job trades
|
|
4
|
+
* its OIDC token before the change's code is checked out.
|
|
5
|
+
*
|
|
6
|
+
* The workspace declaration's `ci.identity.cloud` gives each cloud a list of
|
|
7
|
+
* rules. A rule names environment globs and member globs, and a plan and an
|
|
8
|
+
* apply identity: an AWS role, a GCP service account or an Azure client id.
|
|
9
|
+
* The first rule whose globs match the pipeline's environment and member is
|
|
10
|
+
* the pipeline's ({@link resolveCloudIdentity}). A workspace generates one
|
|
11
|
+
* pull-request pipeline per member and environment, so each member's apply
|
|
12
|
+
* jobs hold that member's apply identity and no other.
|
|
13
|
+
*
|
|
14
|
+
* The plan job runs the pull request's code. Its first step, before the
|
|
15
|
+
* checkout, runs {@link tradeJs} with `node -e`: it asks the forge for an
|
|
16
|
+
* OIDC token, trades it for the plan identity's own credentials, and leaves
|
|
17
|
+
* those credentials for the later steps. Each later step then starts again
|
|
18
|
+
* without the variables a token is requested with ({@link OIDC_REQUEST_VARS},
|
|
19
|
+
* through `dropForgeTokens` in ./ci-handover.ts), so the change's code holds
|
|
20
|
+
* no OIDC token and cannot request one. The plan job's file names no apply
|
|
21
|
+
* identity. The record job plans the merged commit with the plan identity,
|
|
22
|
+
* and the apply jobs trade for the apply identity.
|
|
23
|
+
*
|
|
24
|
+
* The trade is terragucci's `oidc-trade.ts` (INTENTIUS/terragucci e6cb431,
|
|
25
|
+
* packages/terragucci/src/oidc-trade.ts) with these clients:
|
|
26
|
+
*
|
|
27
|
+
* AWS STS AssumeRoleWithWebIdentity, as session keys in
|
|
28
|
+
* AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN
|
|
29
|
+
* GCP Google's STS token exchange, then IAM generateAccessToken for the
|
|
30
|
+
* service account, in GOOGLE_OAUTH_ACCESS_TOKEN and
|
|
31
|
+
* CLOUDSDK_AUTH_ACCESS_TOKEN
|
|
32
|
+
* Azure Entra's client credentials grant with the token as the client
|
|
33
|
+
* assertion, as an ARM access token in AZURE_ACCESS_TOKEN, with
|
|
34
|
+
* AZURE_CLIENT_ID and AZURE_TENANT_ID (and ARM_ spellings)
|
|
35
|
+
*
|
|
36
|
+
* terragucci leaves Azure untraded because the azurerm provider takes only
|
|
37
|
+
* the OIDC token. chant trades it, so the change's code never holds a token
|
|
38
|
+
* another client could be traded for; a tool that needs the OIDC token
|
|
39
|
+
* itself cannot plan under this block.
|
|
40
|
+
*/
|
|
41
|
+
// @ts-ignore: picomatch has no types declaration
|
|
42
|
+
import picomatch from "picomatch";
|
|
43
|
+
/** The clouds `ci.identity.cloud` names. */
|
|
44
|
+
export const CLOUDS = ["aws", "gcp", "azure"];
|
|
45
|
+
const matches = (globs, name) => globs === undefined || (name !== undefined && globs.some((g) => picomatch(g)(name)));
|
|
46
|
+
/** The first rule that covers `env` and `member`, or undefined. */
|
|
47
|
+
export function ruleFor(rules, env, member) {
|
|
48
|
+
return rules.find((r) => matches(r.environments, env) && matches(r.members, member));
|
|
49
|
+
}
|
|
50
|
+
/** The provider's resource name without the `//iam.googleapis.com/` prefix. */
|
|
51
|
+
const providerPath = (p) => p.replace(/^(https:)?\/\/iam\.googleapis\.com\//, "");
|
|
52
|
+
/**
|
|
53
|
+
* The plan and apply trades of the pipeline for `env` and `member`, or
|
|
54
|
+
* undefined when no rule of any cloud covers them. A cloud no rule covers
|
|
55
|
+
* is left out of both.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveCloudIdentity(decl, env, member) {
|
|
58
|
+
if (!decl)
|
|
59
|
+
return undefined;
|
|
60
|
+
const plan = {};
|
|
61
|
+
const apply = {};
|
|
62
|
+
const aws = decl.aws && ruleFor(decl.aws.rules, env, member);
|
|
63
|
+
if (decl.aws && aws) {
|
|
64
|
+
const at = { region: decl.aws.region ?? "us-east-1", audience: decl.aws.audience ?? "sts.amazonaws.com", ...(decl.aws.endpoint ? { endpoint: decl.aws.endpoint } : {}) };
|
|
65
|
+
plan.aws = { role: aws.plan, ...at };
|
|
66
|
+
apply.aws = { role: aws.apply, ...at };
|
|
67
|
+
}
|
|
68
|
+
const gcp = decl.gcp && ruleFor(decl.gcp.rules, env, member);
|
|
69
|
+
if (decl.gcp && gcp) {
|
|
70
|
+
const provider = providerPath(decl.gcp.provider);
|
|
71
|
+
const at = {
|
|
72
|
+
provider,
|
|
73
|
+
audience: decl.gcp.audience ?? `https://iam.googleapis.com/${provider}`,
|
|
74
|
+
...(decl.gcp.stsEndpoint ? { stsEndpoint: decl.gcp.stsEndpoint } : {}),
|
|
75
|
+
...(decl.gcp.iamEndpoint ? { iamEndpoint: decl.gcp.iamEndpoint } : {}),
|
|
76
|
+
};
|
|
77
|
+
plan.gcp = { serviceAccount: gcp.plan, ...at };
|
|
78
|
+
apply.gcp = { serviceAccount: gcp.apply, ...at };
|
|
79
|
+
}
|
|
80
|
+
const azure = decl.azure && ruleFor(decl.azure.rules, env, member);
|
|
81
|
+
if (decl.azure && azure) {
|
|
82
|
+
const at = {
|
|
83
|
+
tenantId: decl.azure.tenantId,
|
|
84
|
+
audience: decl.azure.audience ?? "api://AzureADTokenExchange",
|
|
85
|
+
...(decl.azure.subscriptionId ? { subscriptionId: decl.azure.subscriptionId } : {}),
|
|
86
|
+
...(decl.azure.authority ? { authority: decl.azure.authority } : {}),
|
|
87
|
+
};
|
|
88
|
+
plan.azure = { clientId: azure.plan, ...at };
|
|
89
|
+
apply.azure = { clientId: azure.apply, ...at };
|
|
90
|
+
}
|
|
91
|
+
return Object.keys(plan).length > 0 ? { plan, apply } : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** The clouds a trade covers, in {@link CLOUDS} order. */
|
|
94
|
+
export function tradedClouds(spec) {
|
|
95
|
+
return CLOUDS.filter((c) => spec[c] !== undefined);
|
|
96
|
+
}
|
|
97
|
+
/** Every identity a trade names: the role, the service account, the client id. */
|
|
98
|
+
export function tradeIdentities(spec) {
|
|
99
|
+
return [spec.aws?.role, spec.gcp?.serviceAccount, spec.azure?.clientId].filter((v) => v !== undefined);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The problems of a declared `ci.identity.cloud`, each with its JSON
|
|
103
|
+
* pointer: a rule whose plan and apply are one identity, or a plan identity
|
|
104
|
+
* that is another rule's apply identity. Either would let the plan job,
|
|
105
|
+
* which runs the change's code, act as an apply identity.
|
|
106
|
+
*/
|
|
107
|
+
export function cloudIdentityProblems(decl) {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const cloud of CLOUDS) {
|
|
110
|
+
const rules = decl[cloud]?.rules ?? [];
|
|
111
|
+
const applies = new Set(rules.map((r) => r.apply));
|
|
112
|
+
rules.forEach((r, i) => {
|
|
113
|
+
const pointer = `/ci/identity/cloud/${cloud}/rules/${i}`;
|
|
114
|
+
if (r.plan === r.apply)
|
|
115
|
+
out.push({ pointer, message: `ci.identity.cloud.${cloud}.rules[${i}] plans and applies as ${r.plan}; the plan runs the pull request's code, so give it an identity of its own` });
|
|
116
|
+
else if (applies.has(r.plan))
|
|
117
|
+
out.push({ pointer, message: `ci.identity.cloud.${cloud}.rules[${i}] plans as ${r.plan}, which another rule applies as; the plan runs the pull request's code, so give it an identity no rule applies as` });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** The variable the trade step reads its {@link CloudTradeSpec} from, as JSON. */
|
|
123
|
+
export const TRADE_SPEC_ENV = "CHANT_CLOUD_TRADE";
|
|
124
|
+
/** The GitLab `id_tokens` variable a cloud's token comes in: `CHANT_ID_TOKEN_AWS`, `_GCP`, `_AZURE`. */
|
|
125
|
+
export function idTokenVar(cloud) {
|
|
126
|
+
return `CHANT_ID_TOKEN_${cloud.toUpperCase()}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The variables a GitHub or Forgejo step requests an OIDC token with, and
|
|
130
|
+
* the GitLab `id_tokens` variables the trade reads. A step that runs the
|
|
131
|
+
* change's code starts without them, once the trade is done.
|
|
132
|
+
*/
|
|
133
|
+
export const OIDC_REQUEST_VARS = ["ACTIONS_ID_TOKEN_REQUEST_TOKEN", "ACTIONS_ID_TOKEN_REQUEST_URL", ...CLOUDS.map(idTokenVar), TRADE_SPEC_ENV];
|
|
134
|
+
/** The variables the trade sets, per cloud. */
|
|
135
|
+
export const TRADED_VARS = {
|
|
136
|
+
aws: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_REGION", "AWS_DEFAULT_REGION"],
|
|
137
|
+
gcp: ["GOOGLE_OAUTH_ACCESS_TOKEN", "CLOUDSDK_AUTH_ACCESS_TOKEN"],
|
|
138
|
+
azure: ["AZURE_ACCESS_TOKEN", "AZURE_CLIENT_ID", "AZURE_TENANT_ID", "ARM_CLIENT_ID", "ARM_TENANT_ID", "AZURE_SUBSCRIPTION_ID", "ARM_SUBSCRIPTION_ID"],
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* The trade, a script for `node -e` with no dependency, so it runs before
|
|
142
|
+
* the checkout and reads none of the change's files. argv 1 is the file it
|
|
143
|
+
* writes and argv 2 its {@link TradeFormat}; {@link TRADE_SPEC_ENV} is the
|
|
144
|
+
* spec. The token comes from a cloud's {@link idTokenVar} when the job has
|
|
145
|
+
* it (GitLab's `id_tokens`), else from the forge's request URL with the
|
|
146
|
+
* cloud's audience. A value that holds anything but printable ASCII with no
|
|
147
|
+
* space is refused, so no answer can add a line of its own to the file. The
|
|
148
|
+
* script never prints a token.
|
|
149
|
+
*/
|
|
150
|
+
export const TRADE_JS = [
|
|
151
|
+
'const fs=require("fs");',
|
|
152
|
+
"const [out,format]=process.argv.slice(1);",
|
|
153
|
+
"const Q=String.fromCharCode(39);",
|
|
154
|
+
'const fail=(m)=>{console.error("chant: the OIDC trade failed: "+m);process.exit(1)};',
|
|
155
|
+
`let spec;try{spec=JSON.parse(process.env.${TRADE_SPEC_ENV}||"")}catch{fail("${TRADE_SPEC_ENV} is not JSON")}`,
|
|
156
|
+
'const clean=(v)=>typeof v==="string"&&/^[\\x21-\\x7e]+$/.test(v);',
|
|
157
|
+
'const trim=(u)=>String(u).replace(/\\/+$/,"");',
|
|
158
|
+
'const post=async(url,headers,body)=>{const r=await fetch(url,{method:"POST",headers,body});const t=await r.text();if(!r.ok)throw new Error(url+" answered "+r.status+": "+t.slice(0,200).replace(/\\s+/g," "));return t};',
|
|
159
|
+
'const json=(t,what)=>{try{return JSON.parse(t)}catch{throw new Error(what+" answered with no JSON")}};',
|
|
160
|
+
"const idToken=async(cloud,aud)=>{",
|
|
161
|
+
'const named=process.env["CHANT_ID_TOKEN_"+cloud.toUpperCase()];if(named)return named;',
|
|
162
|
+
"const u=process.env.ACTIONS_ID_TOKEN_REQUEST_URL,k=process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;",
|
|
163
|
+
'if(!u||!k)throw new Error("the job can request no OIDC token for "+cloud+": it needs id-token: write, and a pull request from a fork gets none");',
|
|
164
|
+
'const r=await fetch(u+(u.includes("?")?"&":"?")+"audience="+encodeURIComponent(aud),{headers:{authorization:"bearer "+k,accept:"application/json"}});',
|
|
165
|
+
'if(!r.ok)throw new Error("the OIDC token request answered "+r.status);',
|
|
166
|
+
'const v=json(await r.text(),"the OIDC token request").value;if(!clean(v))throw new Error("the OIDC token request gave no token");return v};',
|
|
167
|
+
"const vars={},secrets=[],said=[];",
|
|
168
|
+
"const run=async()=>{",
|
|
169
|
+
"if(spec.aws){const a=spec.aws;const tok=await idToken(\"aws\",a.audience);",
|
|
170
|
+
'const ep=trim(a.endpoint||process.env.AWS_ENDPOINT_URL_STS||("https://sts."+a.region+".amazonaws.com"+(a.region.startsWith("cn-")?".cn":"")));',
|
|
171
|
+
'const t=await post(ep+"/",{"content-type":"application/x-www-form-urlencoded",accept:"application/xml"},new URLSearchParams({Action:"AssumeRoleWithWebIdentity",Version:"2011-06-15",RoleArn:a.role,RoleSessionName:"chant-"+(process.env.GITHUB_RUN_ID||process.env.CI_JOB_ID||"job"),WebIdentityToken:tok}).toString());',
|
|
172
|
+
'const g=(k)=>{const m=new RegExp("<"+k+">([^<]+)</"+k+">").exec(t);return m?m[1].trim():undefined};',
|
|
173
|
+
'const id=g("AccessKeyId"),key=g("SecretAccessKey"),st=g("SessionToken");if(!clean(id)||!clean(key)||!clean(st))throw new Error("STS gave no credentials for "+a.role);',
|
|
174
|
+
'Object.assign(vars,{AWS_ACCESS_KEY_ID:id,AWS_SECRET_ACCESS_KEY:key,AWS_SESSION_TOKEN:st,AWS_REGION:a.region,AWS_DEFAULT_REGION:a.region});secrets.push(key,st);said.push(a.role)}',
|
|
175
|
+
"if(spec.gcp){const c=spec.gcp;const tok=await idToken(\"gcp\",c.audience);",
|
|
176
|
+
'const sts=trim(c.stsEndpoint||"https://sts.googleapis.com"),iam=trim(c.iamEndpoint||"https://iamcredentials.googleapis.com");',
|
|
177
|
+
'const fed=json(await post(sts+"/v1/token",{"content-type":"application/json"},JSON.stringify({grantType:"urn:ietf:params:oauth:grant-type:token-exchange",audience:"//iam.googleapis.com/"+c.provider,scope:"https://www.googleapis.com/auth/cloud-platform",requestedTokenType:"urn:ietf:params:oauth:token-type:access_token",subjectToken:tok,subjectTokenType:"urn:ietf:params:oauth:token-type:jwt"})),"Google STS").access_token;',
|
|
178
|
+
'if(!clean(fed))throw new Error("Google STS gave no token");',
|
|
179
|
+
'const at=json(await post(iam+"/v1/projects/-/serviceAccounts/"+encodeURIComponent(c.serviceAccount)+":generateAccessToken",{"content-type":"application/json",authorization:"Bearer "+fed},JSON.stringify({scope:["https://www.googleapis.com/auth/cloud-platform"]})),"IAM credentials").accessToken;',
|
|
180
|
+
'if(!clean(at))throw new Error("IAM credentials gave no token for "+c.serviceAccount);',
|
|
181
|
+
"Object.assign(vars,{GOOGLE_OAUTH_ACCESS_TOKEN:at,CLOUDSDK_AUTH_ACCESS_TOKEN:at});secrets.push(at);said.push(c.serviceAccount)}",
|
|
182
|
+
"if(spec.azure){const z=spec.azure;const tok=await idToken(\"azure\",z.audience);",
|
|
183
|
+
'const host=trim(z.authority||"https://login.microsoftonline.com");',
|
|
184
|
+
'const at=json(await post(host+"/"+encodeURIComponent(z.tenantId)+"/oauth2/v2.0/token",{"content-type":"application/x-www-form-urlencoded"},new URLSearchParams({client_id:z.clientId,scope:"https://management.azure.com/.default",grant_type:"client_credentials",client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:tok}).toString()),"Entra").access_token;',
|
|
185
|
+
'if(!clean(at))throw new Error("Entra gave no token for "+z.clientId);',
|
|
186
|
+
"Object.assign(vars,{AZURE_ACCESS_TOKEN:at,AZURE_CLIENT_ID:z.clientId,AZURE_TENANT_ID:z.tenantId,ARM_CLIENT_ID:z.clientId,ARM_TENANT_ID:z.tenantId});",
|
|
187
|
+
"if(z.subscriptionId)Object.assign(vars,{AZURE_SUBSCRIPTION_ID:z.subscriptionId,ARM_SUBSCRIPTION_ID:z.subscriptionId});secrets.push(at);said.push(z.clientId)}",
|
|
188
|
+
'for(const [k,v] of Object.entries(vars))if(!clean(v))throw new Error(k+" is not a value the job can hold");',
|
|
189
|
+
'if(format==="github-env"){for(const s of secrets)console.log("::add-mask::"+s);fs.appendFileSync(out,Object.entries(vars).map(([k,v])=>k+"="+v+"\\n").join(""))}',
|
|
190
|
+
'else fs.writeFileSync(out,Object.entries(vars).map(([k,v])=>"export "+k+"="+Q+v.split(Q).join(Q+"\\\\"+Q+Q)+Q+"\\n").join(""),{mode:0o600});',
|
|
191
|
+
'for(const s of said)console.error("chant: traded the job\\u0027s OIDC token for "+s)};',
|
|
192
|
+
"run().catch((e)=>fail(e&&e.message?e.message:String(e)));",
|
|
193
|
+
].join("");
|
|
194
|
+
/**
|
|
195
|
+
* The shell line of a trade step: `node -e` from `/`, so no file of the
|
|
196
|
+
* checkout (a `package.json`, an `.npmrc`) is in reach, writing `out` in
|
|
197
|
+
* `format`.
|
|
198
|
+
*/
|
|
199
|
+
export function tradeLine(out, format) {
|
|
200
|
+
return `(cd / && node -e '${TRADE_JS}' "${out}" ${format})`;
|
|
201
|
+
}
|
|
202
|
+
/** The GitLab file the trade writes and the plan script sources, outside the project directory. */
|
|
203
|
+
export const GITLAB_TRADE_FILE = "${TMPDIR:-/tmp}/chant-cloud-$CI_JOB_ID.sh";
|
|
204
|
+
/**
|
|
205
|
+
* The lines a GitLab job's script starts with when it trades: the trade,
|
|
206
|
+
* the unset of the token variables (and, with `extraUnset`, the forge
|
|
207
|
+
* tokens'), and the source of what the trade wrote, which it then deletes.
|
|
208
|
+
*/
|
|
209
|
+
export function gitlabTradeLines(extraUnset = []) {
|
|
210
|
+
return [
|
|
211
|
+
tradeLine(GITLAB_TRADE_FILE, "sh"),
|
|
212
|
+
`unset ${[...extraUnset, ...OIDC_REQUEST_VARS].join(" ")}`,
|
|
213
|
+
`. "${GITLAB_TRADE_FILE}" && rm -f "${GITLAB_TRADE_FILE}"`,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
/** GitLab's `id_tokens:` for a trade: one per cloud, with the cloud's audience. */
|
|
217
|
+
export function gitlabTradeIdTokens(spec) {
|
|
218
|
+
return Object.fromEntries(tradedClouds(spec).map((c) => [idTokenVar(c), { aud: spec[c].audience }]));
|
|
219
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GateResolutionRecord, PendingGateRecord } from "../../lifecycle/gate-ledger.js";
|
|
2
2
|
import { type GateOrigin } from "../../lifecycle/gate-origin.js";
|
|
3
3
|
import { type GatePolicyEvaluator, type ResolvedGateApproval } from "../../op/gate-approval.js";
|
|
4
|
+
import { type GateQuorumJudge } from "../../op/gate-quorum.js";
|
|
4
5
|
import type { CommandContext } from "../registry.js";
|
|
5
6
|
export declare function runApprove(ctx: CommandContext): Promise<number>;
|
|
6
7
|
/** What a caller supplies alongside the op and gate names. */
|
|
@@ -94,5 +95,5 @@ export declare function recordGateApproval(opName: string, gate: string, opts: G
|
|
|
94
95
|
* for this plan. The run decides the gate from the ledger; these lines only
|
|
95
96
|
* say what it will find.
|
|
96
97
|
*/
|
|
97
|
-
export declare function describeApprovalProgress(approval: ResolvedGateApproval, record: GateResolutionRecord, prior: GateResolutionRecord[], standing: PendingGateRecord | undefined, pending?: readonly PendingGateRecord[]): string[];
|
|
98
|
+
export declare function describeApprovalProgress(approval: ResolvedGateApproval, record: GateResolutionRecord, prior: GateResolutionRecord[], standing: PendingGateRecord | undefined, pending?: readonly PendingGateRecord[], judge?: GateQuorumJudge): string[];
|
|
98
99
|
//# sourceMappingURL=approve.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approve.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/approve.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EACuE,KAAK,UAAU,EAC5F,MAAM,6BAA6B,CAAC;AAOrC,OAAO,EAEuC,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAChG,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"approve.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/approve.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EACuE,KAAK,UAAU,EAC5F,MAAM,6BAA6B,CAAC;AAOrC,OAAO,EAEuC,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAChG,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAiE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAO3H,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkDlD,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6GrE;AAED,8DAA8D;AAC9D,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,oGAAoG;IACpG,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,SAAS,CAAC,EAAE,mBAAmB,CAAC;CACjC;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC3D;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC;AA0ClB;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CAqX9B;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,oBAAoB,EAAE,EAC7B,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,OAAO,GAAE,SAAS,iBAAiB,EAA+B,EAClE,KAAK,GAAE,eAAgC,GACtC,MAAM,EAAE,CAuCV"}
|
|
@@ -11,6 +11,7 @@ import { appendGateResolution, appendPendingGate, readGateLedger, latestPendingG
|
|
|
11
11
|
import { isPlanDigest, samePlanDigest } from "../../lifecycle/plan-digest.js";
|
|
12
12
|
import { gateContextDigest, gatePolicyRequest, loadGatePolicyEvaluator, } from "../../op/gate-approval.js";
|
|
13
13
|
import { approverOf, humanQuorumSince, tallyGateApprovals } from "../../op/gate.js";
|
|
14
|
+
import { quorumRuleLine, quorumRules, trustQuorumJudge, UNPROVEN_JUDGE } from "../../op/gate-quorum.js";
|
|
14
15
|
import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, RELEASE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name.js";
|
|
15
16
|
import { PR_GATE_OP } from "../../pr-loop.js";
|
|
16
17
|
import { pushLifecycleReport, requireLifecycleLedger } from "../../lifecycle/git.js";
|
|
@@ -557,7 +558,9 @@ export async function recordGateApproval(opName, gate, opts) {
|
|
|
557
558
|
"after it plans again. A moved plan or another commit starts it at the first phase."));
|
|
558
559
|
}
|
|
559
560
|
if (approval) {
|
|
560
|
-
|
|
561
|
+
// #4290: the rules count roles from .chant/trust.json at base, for a proven approver.
|
|
562
|
+
const judge = trustQuorumJudge({ signers: scope.policy, roles: scope.policy.roles });
|
|
563
|
+
for (const line of describeApprovalProgress(approval, record, ledger.resolutions, standingForOrigin, ledger.pending, judge)) {
|
|
561
564
|
console.error(formatInfo(line));
|
|
562
565
|
}
|
|
563
566
|
}
|
|
@@ -569,7 +572,7 @@ export async function recordGateApproval(opName, gate, opts) {
|
|
|
569
572
|
* for this plan. The run decides the gate from the ledger; these lines only
|
|
570
573
|
* say what it will find.
|
|
571
574
|
*/
|
|
572
|
-
export function describeApprovalProgress(approval, record, prior, standing, pending = standing ? [standing] : []) {
|
|
575
|
+
export function describeApprovalProgress(approval, record, prior, standing, pending = standing ? [standing] : [], judge = UNPROVEN_JUDGE) {
|
|
573
576
|
const lines = [];
|
|
574
577
|
const decision = record.policyDecision;
|
|
575
578
|
if (decision) {
|
|
@@ -586,14 +589,25 @@ export function describeApprovalProgress(approval, record, prior, standing, pend
|
|
|
586
589
|
const since = standing?.timestamp ?? new Date(0).toISOString();
|
|
587
590
|
// #3206: count the humans the way the run will, back past facts re-recorded for this plan.
|
|
588
591
|
const humansSince = standing && samePlanDigest(standing.planDigest, record.planDigest) ? humanQuorumSince(pending, standing) : since;
|
|
589
|
-
const tally = tallyGateApprovals([...prior, record], record.gate, since, record.planDigest, approval, humansSince);
|
|
590
|
-
const roles = approval.quorum?.roles ? ` with role ${approval.quorum.roles.join(" or ")}` : "";
|
|
592
|
+
const tally = tallyGateApprovals([...prior, record], record.gate, since, record.planDigest, approval, humansSince, judge);
|
|
591
593
|
const who = tally.counted.map((r) => r.resolvedBy).join(", ") || "none yet";
|
|
592
|
-
|
|
594
|
+
if (!Array.isArray(approval.quorum)) {
|
|
595
|
+
const one = quorumRules(approval.quorum)[0];
|
|
596
|
+
const roles = one?.roles ? ` with role ${one.roles.join(" or ")}` : "";
|
|
597
|
+
lines.push(`Quorum: ${tally.counted.length} of ${tally.need} human approval(s)${roles} for this plan (${who}).`);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
// #4290: one line per approval rule the gate takes.
|
|
601
|
+
for (const p of tally.rules ?? [])
|
|
602
|
+
lines.push(`Quorum: ${quorumRuleLine(p)}.`);
|
|
603
|
+
}
|
|
593
604
|
if (!tally.counted.includes(record)) {
|
|
605
|
+
const roles = quorumRules(approval.quorum).flatMap((r) => r.roles ?? []);
|
|
594
606
|
lines.push(approverOf(record).kind === "agent"
|
|
595
607
|
? "An agent's approval does not count toward the quorum."
|
|
596
|
-
:
|
|
608
|
+
: judge(record).proven
|
|
609
|
+
? `This approval does not count toward the quorum: .chant/trust.json at base gives ${record.resolvedBy} none of its roles (${[...new Set(roles)].join(", ")}).`
|
|
610
|
+
: `This approval does not count toward the quorum: its roles are read from .chant/trust.json at base, and only for an approval sealed by a signer listed at base (chant approve --sign).`);
|
|
597
611
|
}
|
|
598
612
|
return lines;
|
|
599
613
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/build.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/build.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAoOlD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsJnE"}
|
|
@@ -18,6 +18,7 @@ import { resolvePromotionSource } from "../../components/promote-order.js";
|
|
|
18
18
|
* `review-rules` security group (#4168); off otherwise. `requireRunnerStart`
|
|
19
19
|
* is `--require-runner-start`, or the `isolated-runners` group. `--apply-on-merge`
|
|
20
20
|
* (#4168) writes the apply job on merge in place of the record job.
|
|
21
|
+
* `--pr-env` (#4298) adds the jobs of a pull request's environment.
|
|
21
22
|
*/
|
|
22
23
|
function prLoopOption(args, reportUrl, requireReview = false, requireRunnerStart = false) {
|
|
23
24
|
return {
|
|
@@ -30,6 +31,7 @@ function prLoopOption(args, reportUrl, requireReview = false, requireRunnerStart
|
|
|
30
31
|
...(args.requireReview === true || requireReview ? { requireReview: true } : {}),
|
|
31
32
|
...(args.requireRunnerStart === true || requireRunnerStart ? { requireRunnerStart: true } : {}),
|
|
32
33
|
...(args.applyOnMerge ? { applyOnMerge: true } : {}),
|
|
34
|
+
...(args.prEnv ? { prEnvironment: true } : {}),
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
@@ -136,6 +138,17 @@ async function runGenerateComponents(ctx) {
|
|
|
136
138
|
return 1;
|
|
137
139
|
}
|
|
138
140
|
}
|
|
141
|
+
// #4296: the plan and apply cloud identities ci.identity.cloud gives this pipeline's environment and member.
|
|
142
|
+
let identity;
|
|
143
|
+
if (found && args.prLoop) {
|
|
144
|
+
try {
|
|
145
|
+
identity = await (await import("../../workspace/ci-cloud-identity.js")).declaredCloudIdentity(resolve(args.path), args.env ?? "production", plan?.member.name);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
139
152
|
// Which lexicons support generate mode is a property of the loaded lexicon
|
|
140
153
|
// plugins (those implementing `generateComponentPipeline`, #688), not a
|
|
141
154
|
// hard-coded core list — `generateComponentsPipeline` returns a descriptive
|
|
@@ -153,7 +166,8 @@ async function runGenerateComponents(ctx) {
|
|
|
153
166
|
? { gatedWaves: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } }
|
|
154
167
|
: {}),
|
|
155
168
|
// #3183: a plan job per pull request and an apply job per merge.
|
|
156
|
-
|
|
169
|
+
// #4292: on the branch ci.branches routes the member to, when it routes it.
|
|
170
|
+
...(args.prLoop ? { prLoop: { ...prLoopOption(args, reportUrl, reviewRules, isolatedRunners), ...(plan?.branch ? { branch: plan.branch } : {}), ...(identity ? { identity } : {}) } } : {}),
|
|
157
171
|
// #4002: the job image and the lines that install what it lacks, chant itself included.
|
|
158
172
|
...(args.image ? { image: args.image } : {}),
|
|
159
173
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
@@ -55,7 +55,7 @@ import { samePlanDigest } from "../../lifecycle/plan-digest.js";
|
|
|
55
55
|
import { affectedStacks } from "../../lifecycle/affected.js";
|
|
56
56
|
import { changedFilesBetween, lexiconChangedUnits } from "../../components/changed-files.js";
|
|
57
57
|
import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support.js";
|
|
58
|
-
import { EarlierWaveNotAppliedError, runFanOut } from "../../components/fan-out-run.js";
|
|
58
|
+
import { EarlierWaveNotAppliedError, EarlierWaveWaitingError, runFanOut } from "../../components/fan-out-run.js";
|
|
59
59
|
import { readFanOutAttempt, writeFanOutAttempt } from "../../components/fan-out-record.js";
|
|
60
60
|
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs.js";
|
|
61
61
|
import { appliedDigestOf } from "../../components/auto-release.js";
|
|
@@ -267,6 +267,7 @@ async function fanOut(ctx, projectPath, config) {
|
|
|
267
267
|
let priorOutputs = {};
|
|
268
268
|
let priorWaves = [];
|
|
269
269
|
let priorCarried = {};
|
|
270
|
+
let priorFailed = [];
|
|
270
271
|
let progress;
|
|
271
272
|
if (resumePath) {
|
|
272
273
|
let attempt;
|
|
@@ -311,6 +312,12 @@ async function fanOut(ctx, projectPath, config) {
|
|
|
311
312
|
priorOutputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => carried.has(name)));
|
|
312
313
|
progress = { completed: split.carried };
|
|
313
314
|
priorWaves = attempt.waves ?? [];
|
|
315
|
+
// #4291: what failed stays in the record until it applies, so each later wave's job blocks its dependents and runs the rest.
|
|
316
|
+
priorFailed = attempt.failed.filter((name) => !carried.has(name));
|
|
317
|
+
if (waveGate?.only !== undefined) {
|
|
318
|
+
waveGate.failedEarlier = priorFailed;
|
|
319
|
+
waveGate.waitingEarlier = priorWaves.filter((w) => w.status === "gated").map((w) => ({ wave: w.wave, gate: w.gate, components: w.components }));
|
|
320
|
+
}
|
|
314
321
|
// What steps kept per member (#3459), such as a choudoufu root's wave
|
|
315
322
|
// resume file. Each step decides whether what it kept still applies.
|
|
316
323
|
priorCarried = attempt.carried ?? {};
|
|
@@ -370,7 +377,7 @@ async function fanOut(ctx, projectPath, config) {
|
|
|
370
377
|
// repeated command picks up from there instead of redoing them.
|
|
371
378
|
const completedSoFar = new Set(priorCompleted);
|
|
372
379
|
const inputsSoFar = { ...priorInputs };
|
|
373
|
-
const failedSoFar =
|
|
380
|
+
const failedSoFar = new Set(priorFailed);
|
|
374
381
|
const outputsSoFar = { ...priorOutputs };
|
|
375
382
|
let wavesSoFar = priorWaves;
|
|
376
383
|
const carriedSoFar = { ...priorCarried };
|
|
@@ -429,9 +436,10 @@ async function fanOut(ctx, projectPath, config) {
|
|
|
429
436
|
inputsSoFar[settled.component] = inputsNow[settled.component];
|
|
430
437
|
if (outputs)
|
|
431
438
|
outputsSoFar[settled.component] = outputs;
|
|
439
|
+
failedSoFar.delete(settled.component);
|
|
432
440
|
}
|
|
433
441
|
else if (settled.status === "fail") {
|
|
434
|
-
failedSoFar.
|
|
442
|
+
failedSoFar.add(settled.component);
|
|
435
443
|
}
|
|
436
444
|
recordAttempt();
|
|
437
445
|
},
|
|
@@ -442,6 +450,11 @@ async function fanOut(ctx, projectPath, config) {
|
|
|
442
450
|
catch (err) {
|
|
443
451
|
events?.settle("failed", err instanceof Error ? err.message : String(err));
|
|
444
452
|
// `--wave <n>` reached before an earlier wave applied: nothing ran.
|
|
453
|
+
// #4291: an earlier wave waits at its gate, so this one waits too.
|
|
454
|
+
if (err instanceof EarlierWaveWaitingError) {
|
|
455
|
+
console.error(formatInfo(err.message));
|
|
456
|
+
return GATED_EXIT_CODE;
|
|
457
|
+
}
|
|
445
458
|
if (err instanceof EarlierWaveNotAppliedError) {
|
|
446
459
|
console.error(formatError({ message: err.message }));
|
|
447
460
|
return 1;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { type GateResolutionRecord, type PendingGateRecord } from "../../lifecycle/gate-ledger.js";
|
|
21
21
|
import { type AppliedGateLine, type FinishedGateRecord } from "../../lifecycle/gate-applied.js";
|
|
22
|
+
import { type GateQuorumJudge } from "../../op/gate-quorum.js";
|
|
22
23
|
import { type GateResumeOutcome } from "../../op/gate-resume.js";
|
|
23
24
|
import { type GateResumeCandidate, type GateResumeReason } from "../../op/gate-resume-plan.js";
|
|
24
25
|
import type { GateRunLocator } from "../../op/gate-run-locator.js";
|
|
@@ -77,6 +78,8 @@ export interface ResumeGateOptions {
|
|
|
77
78
|
/** The lines of `_gates/<op>/applied.jsonl` (#3858). Read from `chant/lifecycle` by {@link resumeOpGates} when not given. */
|
|
78
79
|
applied?: readonly AppliedGateLine[];
|
|
79
80
|
finished?: readonly FinishedGateRecord[];
|
|
81
|
+
/** Proves approvers for approval rules that name roles (#4290). Read from the trust policy at base when a fact needs one and none is given. */
|
|
82
|
+
judge?: GateQuorumJudge;
|
|
80
83
|
}
|
|
81
84
|
/** Every question the workspace at `cwd` lists, by id, the lifecycle ledger's included. Empty when the workspace can't be read. */
|
|
82
85
|
export declare function workspaceQuestions(cwd: string): Promise<Map<string, PointQuestionState>>;
|