@patronage/factory-ci 1.0.0-alpha.34 → 1.0.0-alpha.35
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/README.md +14 -2
- package/dist/.build-fingerprint.json +1 -1
- package/dist/alchemy/index.d.ts +122 -13
- package/dist/alchemy/index.js +191 -29
- package/dist/{execute-alchemy-entry-DB8fFZTu.d.ts → execute-alchemy-entry-B397GSk5.d.ts} +19 -1
- package/dist/{execute-alchemy-entry-BZpk0eOQ.js → execute-alchemy-entry-CZ7Knz4q.js} +6 -1
- package/dist/index.d.ts +26 -2
- package/dist/index.js +63 -2
- package/package.json +2 -2
- package/src/alchemy/child-diagnostics.ts +148 -0
- package/src/alchemy/credential-preflight.ts +21 -9
- package/src/alchemy/index.ts +2 -0
- package/src/alchemy/lifecycle-failure.ts +20 -1
- package/src/alchemy/lifecycle.ts +185 -27
- package/src/execute-alchemy-entry.ts +23 -1
- package/src/index.ts +5 -0
- package/src/preview-receipt-workflow.ts +83 -0
package/README.md
CHANGED
|
@@ -688,6 +688,12 @@ const result = await runAlchemyLifecycle({
|
|
|
688
688
|
|
|
689
689
|
The consumer passes typed policy and two capabilities. `state` is the store from `@patronage/alchemy-d1-state`: `snapshotStage`, `get`, and `acquireStageOwnership`, whose lease carries `renew`, `release`, `deleteEmptyStage` and `childEnvironment`, matched structurally, so this package learns no table, account, or SQL. `entry` is what `executeAlchemyEntry` needs minus what the lifecycle decides: the arguments are the lifecycle's, stdio is always piped so the plan can be read, and a `bundle` is built once up front, with every child launched in `cwd`, then the bundle root, then this process's directory — the same chain `executeAlchemyEntry` resolves. A bundle that cannot build is `PREPARATION_FAILED` and nothing else: the bundler's own diagnostics are silenced at the source. There is no `redact` and no echo because there is no sink: each child's captured output is parsed and dropped, written nowhere, so nothing in it can reach a job log or an evidence file through this operation. Every child's environment carries the stage lease; see ownership below.
|
|
690
690
|
|
|
691
|
+
The optional `childDiagnostics: { path, redact }` is the one way any of that captured output reaches a file. Omitted — the default, and byte-identical to the behavior above — nothing is written and nothing changes. Set, every child of the operation (both dry runs, the apply, the destroy) has its stdout and its stderr appended to `path` under a header naming the operation, the child, and the stage. Each stream is passed **whole** through `redact`, not a line at a time, because a declared value can itself span lines — a PEM key, a pasted service-account JSON — and a per-line pass would find no line holding the whole value and write every one of them verbatim. The consumer chooses the path and supplies the redactor — `secretNamePolicy({ names, values })` builds the usual one — because the secret names are project policy and this package holds none. The record is a copy: `redact` never runs over what the child hands back, so the plan text parsed by `parsePlanEffects` and hashed for a first deploy is the child's own bytes. A `CHILD_FAILED` from such a run carries `childDiagnosticsPath`, the path and never the output, so an operator knows where to look.
|
|
692
|
+
|
|
693
|
+
A record that cannot be written is `CHILD_DIAGNOSTICS_UNWRITABLE`, never a misreported child failure, and **when** it is reported depends on what has already happened to the stage. The file is opened and headed before each child starts, so while no mutating child has run — an unwritable path at the first child, a redactor that throws on a dry run — the run is refused up front with the stage still untouched. Once the apply or the destroy child has started, the failure is retained and reported only after the operation has run to its end, the way `LEASE_RELEASE_FAILED` is: the mutation succeeded, and abandoning the convergence check over a missing record would invite an operator to destroy and retry a stage that is fine. So a good deploy still converges and is still checked, and the run then ends in `CHILD_DIAGNOSTICS_UNWRITABLE` with the mutation certainty it actually reached. A child that failed is reported ahead of a record that could not be written, because the child is the finding. The file is only as redacted as the declared name list makes it — a value the project did not declare, and any re-encoded form of one it did, reaches the file verbatim — so it belongs outside the repository and outside anything uploaded as an artifact.
|
|
694
|
+
|
|
695
|
+
The optional `entry.envFile` selects the dotenv file for every child, including both dry runs. Its path follows Alchemy’s normal resolution from the child working directory. Omit it to preserve Alchemy’s default `.env` behavior. A consumer that requires environment-only credentials can supply its own private empty file for the operation’s lifetime; file creation and cleanup remain consumer policy.
|
|
696
|
+
|
|
691
697
|
The sequence, per operation:
|
|
692
698
|
|
|
693
699
|
- **`plan`** takes the stage lease, reads the persisted state, runs `deploy --dry-run`, reads the state again, and refuses with `STATE_CHANGED` if any resource record or the stack output differs — all under the one lease, so no other writer can reach the stage while the child plans. That refusal wins over the child's own exit status, because a planning child that wrote is the more serious finding. On an empty protected stage with a `firstDeploy` policy the result carries `planHash`, the value an operator authorizes.
|
|
@@ -698,7 +704,7 @@ Ownership follows #1074 and #1107. Each operation runs under **one** stage lease
|
|
|
698
704
|
|
|
699
705
|
The consumer's entry point passes its environment to the adapter: `d1State({ ..., ownership: { stack, stage, holder, adoptFrom: process.env } })`. That is the whole wiring, the same for a plan, a deploy and a destroy, and the same when an operator runs the entry by hand with no parent (the adapter then acquires as usual). Nothing in the child is trusted to opt in: while the parent holds the stage, a child whose state layer takes its own lease is refused at its first state call, and one that takes none is refused at its first write, by the store's fence. The token crosses the process boundary only through that environment; it is on no result, no failure, and no type this package exports. The store fences its rows, not Cloudflare: a provider call that reached Cloudflare before a lease was lost is not undone, and the refused state write is how the loss surfaces (see the adapter's README). The lifecycle does not reimplement any of this; it consumes it.
|
|
700
706
|
|
|
701
|
-
Every failure is an `AlchemyLifecycleError` with an enumerated `code`, `phase` (`request`, `admission`, `state`, `execution`, `postcondition`), and `mutation` (`not-started`, `read-only-started`, `mutation-may-have-occurred`). The message is the fixed sentence `LIFECYCLE_FAILURE_MESSAGES[code]` and nothing else; no failure carries a `cause`. Nothing a child printed, nothing the store said, and nothing a credential resolver quoted can reach an evidence sink through this error. After a deploy or destroy child was started, no failure reports `not-started`.
|
|
707
|
+
Every failure is an `AlchemyLifecycleError` with an enumerated `code`, `phase` (`request`, `admission`, `state`, `execution`, `postcondition`), and `mutation` (`not-started`, `read-only-started`, `mutation-may-have-occurred`). The message is the fixed sentence `LIFECYCLE_FAILURE_MESSAGES[code]` and nothing else; no failure carries a `cause`. The one field that says anything about a child's output is `childDiagnosticsPath`, present on `CHILD_FAILED` and `CHILD_DIAGNOSTICS_UNWRITABLE` when the run configured `childDiagnostics`: it is the path the consumer chose, never the output, and never part of the message. Nothing a child printed, nothing the store said, and nothing a credential resolver quoted can reach an evidence sink through this error. After a deploy or destroy child was started, no failure reports `not-started`.
|
|
702
708
|
|
|
703
709
|
The plan is read once, by `parsePlanEffects`, from the lines Alchemy's `formatPlanLines` prints: one `Plan:` summary, one `[fqn] action` row per resource, one `[fqn/name] action` row per binding of that resource, and one `[fqn] run|drop [action]` row per task. Every line arrives wrapped in Effect's pretty log prefix (`[HH:MM:SS.mmm] INFO (#1): `) and, when the child inherits `FORCE_COLOR`, in colour; both are stripped first. The id in a row is the resource's fully qualified name, the identity it is persisted under, so every check that binds a row to a resource resolves it to the persisted FQN it names exactly — a planned create to that FQN recorded after the apply and not before it, a convergence row to that FQN recorded for it — and refuses when it is absent. A plan that prints one FQN twice cannot come from the renderer and is refused (`AMBIGUOUS_RESOURCE`). The summary counts `create`, `update`, `adopted`, `replace`, `delete` and `orphaned` resources, then `N binding changes` and `N tasks`; `noop` rows print but are not counted. A binding row extends its resource's FQN by `/name`, and so does a resource inside a namespace, so when a resource named `Parent` is followed by rows tagged `Parent/...` the renderer gives no evidence which are its bindings and which are namespaced resources; the summary's counts decide, the one reading that adds up is taken, a plan the counts can place in more than one way is refused (`ambiguous-rows`; the counts are global, so two such resources in one plan couple and are refused together), the search is memoised so a plan of many such resources costs rows times counts and never the product of their readings, and a `noop` row under such a resource — placed by no count and read by no decision — is read as a binding. `orphaned` (a retained resource leaving the stack) is refused wherever `delete` is; `adopted` (a resource the provider already finds, planned only for a record with no prior state) is permitted wherever `create` is. The adapter is bound to `PLAN_OUTPUT_ALCHEMY_VERSION`, which is `ALCHEMY_BASELINE.alchemy`, and the lifecycle refuses an installed Alchemy of any other version (`UNSUPPORTED_ALCHEMY_VERSION`) before any child runs, so a renderer change cannot be read as a plan with different effects. The version is read from the manifest of the package that owns the CLI the children run — `alchemy/bin/alchemy.js` is resolved from the consumer and the nearest `package.json` named `alchemy` above it is read from disk — because `alchemy/package.json` is not on the package's `exports` map and cannot be resolved as a specifier. The adapter fails on output it cannot account for — no summary, two summaries, an unknown action, a summary it cannot parse, a count that does not match the rows, rows the counts place in more than one way — and reports only the reason, never the output. An effect it could not account for is never reported as zero.
|
|
704
710
|
|
|
@@ -717,7 +723,7 @@ import {
|
|
|
717
723
|
|
|
718
724
|
Four checks that surround an Alchemy run. Paitronage and firedup each built the first one and the last one, and paitronage built all four, so the check repeats and the policy does not: the account, the permission list, the eligible stages, the authorized plans, the state store, and the secret names are all inputs.
|
|
719
725
|
|
|
720
|
-
`credentialPreflight({ accountId, environment, requiredPermissionGroups, resolveCredential, resourcesFor?, source })` proves the deploy holds the credential it claims to. `source: "hosted"` requires `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in the environment and requires the account to be the one the deploy allows; `source: "trusted-local"` requires that neither is there, because a local run resolves its credential from the project's own secret manager and an ambient token means the run is not the run it says it is. `resolveCredential` is where that resolution happens — a 1Password read, a hosted environment read, whatever the project uses. The token is then verified against Cloudflare, matched to the reviewed policy document by token id, and reduced to permission-group names, which must equal `requiredPermissionGroups` with nothing missing, extra, or repeated. `resourcesFor` names the exact resource identifiers a group may be scoped to and defaults to the account resource, which is how a zone-scoped group is declared. On the hosted path the resolved token must be the environment token, compared and never named, because that is the token the deploy will use. A `CloudflareCredentialUnavailableError` means Cloudflare could not be asked, or the project's own resolver failed; a resolver failure is reported as one fixed message with no `cause`, because a secret-manager error can quote the value it was reading. Every other failure means the credential is wrong. No error and no result carries a credential value.
|
|
726
|
+
`credentialPreflight({ accountId, environment, requiredPermissionGroups, resolveCredential, resourcesFor?, source, tokenKind? })` proves the deploy holds the credential it claims to. `source: "hosted"` requires `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in the environment and requires the account to be the one the deploy allows; `source: "trusted-local"` requires that neither is there, because a local run resolves its credential from the project's own secret manager and an ambient token means the run is not the run it says it is. `resolveCredential` is where that resolution happens — a 1Password read, a hosted environment read, whatever the project uses. `tokenKind` selects the verification endpoint: `"account"` (the default) uses `/accounts/<accountId>/tokens/verify`; `"user"` uses `/user/tokens/verify`. Token ownership is independent of credential source and permission scope: an account-scoped user token still selects `"user"`. Unknown kinds refuse before resolving credentials; there is no endpoint fallback. Both kinds enforce the same account, receipt, permission, and resource checks. The token is then verified against Cloudflare, matched to the reviewed policy document by token id, and reduced to permission-group names, which must equal `requiredPermissionGroups` with nothing missing, extra, or repeated. `resourcesFor` names the exact resource identifiers a group may be scoped to and defaults to the account resource, which is how a zone-scoped group is declared. On the hosted path the resolved token must be the environment token, compared and never named, because that is the token the deploy will use. A `CloudflareCredentialUnavailableError` means Cloudflare could not be asked, or the project's own resolver failed; a resolver failure is reported as one fixed message with no `cause`, because a secret-manager error can quote the value it was reading. Every other failure means the credential is wrong. No error and no result carries a credential value.
|
|
721
727
|
|
|
722
728
|
`firstDeployAuthorization({ authorizedHashes, planText, sourceSha, stack, stage })` binds the one deploy no earlier state can constrain. The plan, the target, and the reviewed source hash to one value, and that value must appear on a list the project owns. `sourceSha` is required. A full lowercase 40-character SHA binds the plan to that commit; `null` binds the plan to no commit, and that is the project's explicit choice rather than a field it forgot — an omitted field and a deliberate `null` are different decisions, so only the second one is spellable. Anything else, an empty string included, is refused. It fails closed on an empty list, a list entry that is not a sha256 hash, an empty plan, plan output that is not well-formed, plan output the plan adapter cannot read, an unlisted plan, and a stack, stage, or source SHA carrying surrounding whitespace — padding would mint a second hash for one target. The refusal names the hash to review, never the plan. `firstDeployPlanHash` computes the same value on its own, for the run that produces a plan for an operator to read.
|
|
723
729
|
|
|
@@ -877,3 +883,9 @@ Attended and hand-cut, on the same terms as `@patronage/alchemy-d1-state`: bump
|
|
|
877
883
|
## License
|
|
878
884
|
|
|
879
885
|
MIT.
|
|
886
|
+
|
|
887
|
+
### Hosted preview receipts
|
|
888
|
+
|
|
889
|
+
`factoryPreviewReceiptWorkflow({ target: "preview:app" })` supplies a `beginStep`, `completionSteps`, `invalidateStep`, and job `permissions` for `psf preview:receipt`. Place `beginStep` before the selected target's first mutation, then the consumer deploy and smoke steps, then `completionSteps`. Keep the bind step name intact. Apply the target selection guard to the begin step; completion requires its nonempty intent output. Canceled runs leave the intent pending. Place `invalidateStep` before cleanup, including cleanup of a failed deployment that never registered a pass.
|
|
890
|
+
|
|
891
|
+
This artifact does not select targets, deploy, smoke, or certify current health. The consumer's reviewed default-branch profile declares publisher workflow paths, job names/IDs, required deploy/smoke step names, cleanup paths, and targets under `extensions.hostedPreviewReceipts`. The CLI reads that profile from GitHub at an immutable default-branch SHA. Candidate workflows must have the same blob as that reviewed version for a receipt to be trusted. See the software-factory command reference for the profile shape and ordered rollout. This source addition does not deploy or adopt the contract in Firedup.
|
package/dist/alchemy/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as ExecuteAlchemyEntryOptions } from "../execute-alchemy-entry-
|
|
1
|
+
import { n as ExecuteAlchemyEntryOptions } from "../execute-alchemy-entry-B397GSk5.js";
|
|
2
2
|
import * as Alchemy from "alchemy";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import * as Layer from "effect/Layer";
|
|
@@ -334,13 +334,16 @@ declare const stagePolicy: (table: StagePolicyTable) => StagePolicy;
|
|
|
334
334
|
* repeat is the account, the permission list, the resource scope, and where
|
|
335
335
|
* the credential comes from, so every one of those is an input.
|
|
336
336
|
*
|
|
337
|
-
* **No credential value is ever printed.** Errors name environment variables
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
337
|
+
* **No credential value is ever printed.** Errors name environment variables and
|
|
338
|
+
* missing groups from the caller-owned permission requirements. Untrusted API
|
|
339
|
+
* status, receipt group names, and resource identifiers are never reflected.
|
|
340
|
+
* The result carries the token id, which Cloudflare's own verification endpoint
|
|
341
|
+
* returns and which is not a secret.
|
|
341
342
|
*/
|
|
342
343
|
/** Where the deploy's Cloudflare credential comes from. */
|
|
343
344
|
type CredentialSource = "hosted" | "trusted-local";
|
|
345
|
+
/** Ownership of the token, independent of where its value is resolved. */
|
|
346
|
+
type CloudflareTokenKind = "account" | "user";
|
|
344
347
|
/**
|
|
345
348
|
* The environment variables a hosted deploy carries and a trusted-local
|
|
346
349
|
* deploy must not. A trusted-local run resolves its credential from the
|
|
@@ -385,6 +388,8 @@ interface CredentialPreflightOptions {
|
|
|
385
388
|
*/
|
|
386
389
|
readonly resourcesFor?: (permissionGroup: string) => readonly string[];
|
|
387
390
|
readonly source: CredentialSource;
|
|
391
|
+
/** Defaults to account-owned token verification. No endpoint fallback is attempted. */
|
|
392
|
+
readonly tokenKind?: CloudflareTokenKind;
|
|
388
393
|
}
|
|
389
394
|
interface CredentialPreflightResult {
|
|
390
395
|
readonly accountId: string;
|
|
@@ -429,7 +434,8 @@ declare const credentialPreflight: ({
|
|
|
429
434
|
requiredPermissionGroups,
|
|
430
435
|
resolveCredential,
|
|
431
436
|
resourcesFor,
|
|
432
|
-
source
|
|
437
|
+
source,
|
|
438
|
+
tokenKind
|
|
433
439
|
}: CredentialPreflightOptions) => Promise<CredentialPreflightResult>;
|
|
434
440
|
//#endregion
|
|
435
441
|
//#region src/alchemy/destroy-postcondition.d.ts
|
|
@@ -666,7 +672,10 @@ declare const secretNamePolicy: ({
|
|
|
666
672
|
* sentence looked up by code. Nothing a child process printed, nothing a
|
|
667
673
|
* state store said, and nothing a credential resolver quoted ever reaches
|
|
668
674
|
* the message, and no failure carries a `cause`. Evidence sinks (`--json`,
|
|
669
|
-
* the job summary) can print any field of this error verbatim.
|
|
675
|
+
* the job summary) can print any field of this error verbatim. The one thing
|
|
676
|
+
* a failure may say about a child's output is where a consumer asked for it
|
|
677
|
+
* to be written (`childDiagnosticsPath`), which is a path the consumer chose
|
|
678
|
+
* and never the output.
|
|
670
679
|
*/
|
|
671
680
|
type AlchemyLifecycleOperation = "plan" | "deploy" | "destroy";
|
|
672
681
|
/** Where in the sequence the failure was raised. */
|
|
@@ -678,13 +687,19 @@ type AlchemyLifecyclePhase = /** The request itself or the policy is malformed.
|
|
|
678
687
|
* or destroy child was started, so the stage may differ from before.
|
|
679
688
|
*/
|
|
680
689
|
type MutationCertainty = "not-started" | "read-only-started" | "mutation-may-have-occurred";
|
|
681
|
-
type AlchemyLifecycleFailureCode = "INVALID_REQUEST" | "INVALID_POLICY" | "UNSUPPORTED_ALCHEMY_VERSION" | "UNKNOWN_STAGE" | "AMBIGUOUS_STAGE" | "PROTECTED_STAGE" | "UNKNOWN_STACK" | "STAGE_NOT_OWNED" | "PREPARATION_FAILED" | "STAGE_HELD" | "OWNERSHIP_LOST" | "LEASE_RELEASE_FAILED" | "STATE_STORE_UNAVAILABLE" | "STATE_STORE_INVALID" | "STATE_CHANGED" | "RESOURCE_IDENTITY_MISMATCH" | "FIRST_DEPLOY_UNAUTHORIZED" | "EFFECT_NOT_PERMITTED" | "UNKNOWN_EFFECT" | "AMBIGUOUS_RESOURCE" | "CHILD_FAILED" | "DEPLOY_INCOMPLETE" | "NOT_CONVERGED" | "DESTROY_RESIDUE";
|
|
690
|
+
type AlchemyLifecycleFailureCode = "INVALID_REQUEST" | "INVALID_POLICY" | "UNSUPPORTED_ALCHEMY_VERSION" | "UNKNOWN_STAGE" | "AMBIGUOUS_STAGE" | "PROTECTED_STAGE" | "UNKNOWN_STACK" | "STAGE_NOT_OWNED" | "PREPARATION_FAILED" | "STAGE_HELD" | "OWNERSHIP_LOST" | "LEASE_RELEASE_FAILED" | "STATE_STORE_UNAVAILABLE" | "STATE_STORE_INVALID" | "STATE_CHANGED" | "RESOURCE_IDENTITY_MISMATCH" | "FIRST_DEPLOY_UNAUTHORIZED" | "EFFECT_NOT_PERMITTED" | "UNKNOWN_EFFECT" | "AMBIGUOUS_RESOURCE" | "CHILD_FAILED" | "CHILD_DIAGNOSTICS_UNWRITABLE" | "DEPLOY_INCOMPLETE" | "NOT_CONVERGED" | "DESTROY_RESIDUE";
|
|
682
691
|
/**
|
|
683
692
|
* The one sentence each code is reported with. The table is the whole
|
|
684
693
|
* vocabulary: a message that is not in it cannot be raised.
|
|
685
694
|
*/
|
|
686
695
|
declare const LIFECYCLE_FAILURE_MESSAGES: Readonly<Record<AlchemyLifecycleFailureCode, string>>;
|
|
687
696
|
interface AlchemyLifecycleFailureFields {
|
|
697
|
+
/**
|
|
698
|
+
* The file the run's child output was being written to, on a child
|
|
699
|
+
* failure of a run that asked for one. It is a path, so an operator knows
|
|
700
|
+
* where to look; the output itself is never here and never in the message.
|
|
701
|
+
*/
|
|
702
|
+
readonly childDiagnosticsPath?: string;
|
|
688
703
|
readonly code: AlchemyLifecycleFailureCode;
|
|
689
704
|
readonly mutation: MutationCertainty;
|
|
690
705
|
readonly operation: AlchemyLifecycleOperation;
|
|
@@ -697,6 +712,7 @@ interface AlchemyLifecycleFailureFields {
|
|
|
697
712
|
}
|
|
698
713
|
/** A sanitized lifecycle failure. See the module comment. */
|
|
699
714
|
declare class AlchemyLifecycleError extends Error implements AlchemyLifecycleFailureFields {
|
|
715
|
+
readonly childDiagnosticsPath?: string;
|
|
700
716
|
readonly code: AlchemyLifecycleFailureCode;
|
|
701
717
|
readonly mutation: MutationCertainty;
|
|
702
718
|
readonly operation: AlchemyLifecycleOperation;
|
|
@@ -797,6 +813,59 @@ declare class PlanEffectsError extends Error {
|
|
|
797
813
|
*/
|
|
798
814
|
declare const parsePlanEffects: (output: string) => PlanEffects;
|
|
799
815
|
//#endregion
|
|
816
|
+
//#region src/alchemy/child-diagnostics.d.ts
|
|
817
|
+
/**
|
|
818
|
+
* The optional record of what an Alchemy child printed (#1260).
|
|
819
|
+
*
|
|
820
|
+
* `runAlchemyLifecycle` reports a failed child as `CHILD_FAILED` and nothing
|
|
821
|
+
* else: the child's output is parsed and dropped, so no secret in it can
|
|
822
|
+
* reach a job log or an evidence file through the operation. That is the
|
|
823
|
+
* right default and it stays the default. It also leaves an operator with a
|
|
824
|
+
* failure code and no record of what failed, which twice cost a paitronage
|
|
825
|
+
* investigation its evidence.
|
|
826
|
+
*
|
|
827
|
+
* This is the opt-in other half: a consumer that names a file gets every
|
|
828
|
+
* child's streams appended to it, each stream passed whole through the
|
|
829
|
+
* consumer's own redactor, under a header naming the child. The consumer chooses the path
|
|
830
|
+
* and owns the redactor; this module never picks either, and nothing here
|
|
831
|
+
* changes what the lifecycle decides.
|
|
832
|
+
*
|
|
833
|
+
* The file is opened, and the header written, **before** the child starts,
|
|
834
|
+
* so an unwritable path is known before anything runs and a parent that dies
|
|
835
|
+
* mid-child still leaves the marker for the child that was in flight. The
|
|
836
|
+
* streams themselves arrive after the child exits, because the child is
|
|
837
|
+
* captured (`spawnSync`), not streamed.
|
|
838
|
+
*
|
|
839
|
+
* Nothing here throws into the lifecycle's control flow once the handle is
|
|
840
|
+
* open. A write failure is retained and reported by the caller under its own
|
|
841
|
+
* failure code, because a record that could not be written is a different
|
|
842
|
+
* fact from a child that failed, and reporting it as the second one would
|
|
843
|
+
* put a false cause in front of the operator this file exists for. When the
|
|
844
|
+
* caller reports it — up front, or only after the operation has run to the
|
|
845
|
+
* end — is the caller's rule, not this module's; see `runAlchemyLifecycle`.
|
|
846
|
+
*/
|
|
847
|
+
interface ChildDiagnostics {
|
|
848
|
+
/**
|
|
849
|
+
* The file every child's output is appended to. The consumer chooses it,
|
|
850
|
+
* and it belongs outside the repository and outside any uploaded artifact:
|
|
851
|
+
* redaction covers the declared secret names and nothing else, so a value
|
|
852
|
+
* the consumer did not declare reaches this file verbatim.
|
|
853
|
+
*/
|
|
854
|
+
readonly path: string;
|
|
855
|
+
/**
|
|
856
|
+
* Applied to each captured stream **as one piece of text** before it
|
|
857
|
+
* reaches the file. The consumer owns it; `secretNamePolicy({ names,
|
|
858
|
+
* values })` builds the usual one.
|
|
859
|
+
*
|
|
860
|
+
* Whole text, not a line at a time, because a declared value can itself
|
|
861
|
+
* span lines — a PEM private key, a pasted service-account JSON — and a
|
|
862
|
+
* per-line pass would find no line holding the whole value and write every
|
|
863
|
+
* one of them verbatim. A redactor that works line by line is still
|
|
864
|
+
* correct here; one that needs the whole text only works here.
|
|
865
|
+
*/
|
|
866
|
+
readonly redact: (text: string) => string;
|
|
867
|
+
}
|
|
868
|
+
//#endregion
|
|
800
869
|
//#region src/alchemy/lifecycle.d.ts
|
|
801
870
|
interface AlchemyLifecycleRequest {
|
|
802
871
|
readonly operation: AlchemyLifecycleOperation;
|
|
@@ -920,16 +989,47 @@ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K>
|
|
|
920
989
|
* The entry capability: what `executeAlchemyEntry` needs minus what the
|
|
921
990
|
* lifecycle decides. Arguments are the lifecycle's; stdio is always piped so
|
|
922
991
|
* the plan can be read; the capture limit is `EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER`.
|
|
923
|
-
* There is no `redact` because
|
|
924
|
-
*
|
|
925
|
-
*
|
|
992
|
+
* There is no `redact` because `redact` shapes what the child hands back,
|
|
993
|
+
* and the plan text must be the child's own bytes. There is no `onCaptured`
|
|
994
|
+
* for the same reason in the other direction: the sink is the lifecycle's,
|
|
995
|
+
* so a consumer-supplied one would be dropped, and an option that
|
|
996
|
+
* type-checks and does nothing is worse than one that does not exist. By
|
|
997
|
+
* default the captured output is parsed and dropped, written nowhere, so
|
|
998
|
+
* nothing in it can reach a job log or an evidence file through this
|
|
999
|
+
* operation; a consumer that wants a record opts in with `childDiagnostics`,
|
|
1000
|
+
* which owns its own redactor and its own file.
|
|
926
1001
|
*/
|
|
927
|
-
type AlchemyLifecycleEntry = DistributiveOmit<ExecuteAlchemyEntryOptions, "args" | "stdio" | "maxBuffer" | "redact"
|
|
1002
|
+
type AlchemyLifecycleEntry = DistributiveOmit<ExecuteAlchemyEntryOptions, "args" | "onCaptured" | "stdio" | "maxBuffer" | "redact"> & {
|
|
1003
|
+
/** Explicit dotenv file for every child; omitted preserves Alchemy defaults. */readonly envFile?: string;
|
|
1004
|
+
};
|
|
928
1005
|
interface RunAlchemyLifecycleOptions {
|
|
929
1006
|
readonly request: AlchemyLifecycleRequest;
|
|
930
1007
|
readonly policy: AlchemyLifecyclePolicy;
|
|
931
1008
|
readonly state: StageStateStore;
|
|
932
1009
|
readonly entry: AlchemyLifecycleEntry;
|
|
1010
|
+
/**
|
|
1011
|
+
* Opt in to keeping what the children printed (#1260). Omitted, the
|
|
1012
|
+
* captured output is parsed and dropped, exactly as before: this is the
|
|
1013
|
+
* only way any of it reaches a file through this operation.
|
|
1014
|
+
*
|
|
1015
|
+
* Set, every child of the operation — the dry runs, the apply, the
|
|
1016
|
+
* destroy — has both streams appended to `path`, each stream passed whole
|
|
1017
|
+
* through `redact` (never a line at a time, so a declared value that
|
|
1018
|
+
* spans lines is covered), under a header naming the child. The consumer
|
|
1019
|
+
* chooses the
|
|
1020
|
+
* path and supplies the redactor; the lifecycle chooses neither and holds
|
|
1021
|
+
* no secret-name list. `secretNamePolicy({ names, values })` from this
|
|
1022
|
+
* subpath builds the redactor from the names the consumer declares.
|
|
1023
|
+
*
|
|
1024
|
+
* The record is a copy: the plan text the operation parses and hashes is
|
|
1025
|
+
* the child's own output, untouched by `redact`. Writing it changes no
|
|
1026
|
+
* exit status and no decision here, and a write that fails is reported as
|
|
1027
|
+
* `CHILD_DIAGNOSTICS_UNWRITABLE` rather than as a child failure — up front
|
|
1028
|
+
* while nothing has been mutated, and otherwise only after the operation
|
|
1029
|
+
* has run to its end, so a record that could not be kept never abandons a
|
|
1030
|
+
* good apply before its convergence check.
|
|
1031
|
+
*/
|
|
1032
|
+
readonly childDiagnostics?: ChildDiagnostics;
|
|
933
1033
|
}
|
|
934
1034
|
interface AlchemyLifecycleResult {
|
|
935
1035
|
readonly operation: AlchemyLifecycleOperation;
|
|
@@ -985,6 +1085,15 @@ interface AlchemyLifecycleResult {
|
|
|
985
1085
|
*
|
|
986
1086
|
* Every failure is an `AlchemyLifecycleError`. After a deploy or destroy
|
|
987
1087
|
* child was started, no failure reports `not-started`.
|
|
1088
|
+
*
|
|
1089
|
+
* `childDiagnostics` is the one opt-in that keeps what the children printed:
|
|
1090
|
+
* both streams of every child, each passed whole through the consumer's own
|
|
1091
|
+
* redactor, are appended to the consumer's own file under a per-child
|
|
1092
|
+
* header, and a `CHILD_FAILED` then names that path so an operator knows
|
|
1093
|
+
* where to look. Nothing about the operation changes otherwise: a record
|
|
1094
|
+
* that cannot be written refuses the run only while nothing has been
|
|
1095
|
+
* mutated, and after that is reported as `CHILD_DIAGNOSTICS_UNWRITABLE` once
|
|
1096
|
+
* the operation has finished, never in place of its postconditions.
|
|
988
1097
|
*/
|
|
989
1098
|
declare const runAlchemyLifecycle: (options: RunAlchemyLifecycleOptions) => Promise<AlchemyLifecycleResult>;
|
|
990
1099
|
//#endregion
|
|
@@ -1062,4 +1171,4 @@ declare const deleteStageRows: (state: StateService, target: StageTarget) => Pro
|
|
|
1062
1171
|
/** The specifier a consumer imports this surface by. */
|
|
1063
1172
|
declare const ALCHEMY_SUBPATH = "@patronage/factory-ci/alchemy";
|
|
1064
1173
|
//#endregion
|
|
1065
|
-
export { ALCHEMY_SUBPATH, type AlchemyLifecycleEntry, AlchemyLifecycleError, type AlchemyLifecycleFailureCode, type AlchemyLifecycleFailureFields, type AlchemyLifecycleOperation, type AlchemyLifecyclePhase, type AlchemyLifecyclePolicy, type AlchemyLifecycleRequest, type AlchemyLifecycleResult, type AssertDestroyLeftNothingOptions, type AssertRetainedIdentitiesOptions, type AssertUrlImpliesAuthOptions, type AuthBindingNameMatcher, type CloudflareCredential, CloudflareCredentialUnavailableError, type CredentialPreflightOptions, type CredentialPreflightResult, type CredentialSource, type EvaluateStackInput, type EvaluatedBinding, type EvaluatedGraph, type EvaluatedResource, type FirstDeployAuthorization, type FirstDeployAuthorizationOptions, type FirstDeployPlanHashOptions, type FirstDeployPolicy, HOSTED_CLOUDFLARE_CREDENTIAL_NAMES, LIFECYCLE_FAILURE_MESSAGES, LOCAL_PREVIEW_STAGES, MINIMUM_REDACTABLE_SECRET_LENGTH, type MutationCertainty, PLAN_ACTIONS, PLAN_OUTPUT_ALCHEMY_VERSION, type PlanAction, type PlanEffects, PlanEffectsError, type PlanEffectsRejection, type PlanResourceEffect, type PlanTaskAction, type PlanTaskEffect, RetainedIdentityError, type RetainedIdentityMarker, type RetainedIdentitySection, type RetainedIdentityViolation, type RetainedIdentityViolationReason, type RunAlchemyLifecycleOptions, SECRET_REDACTION_PLACEHOLDER, type SecretNamePolicyOptions, type StackOutputOccupancy, type StackStageOwnership, type StageCleanupOutcome, type StageLease, type StageMatcher, type StagePolicy, StagePolicyError, type StagePolicyErrorCode, type StagePolicyTable, type StageSnapshotView, type StageStateReader, type StageStateStore, type StageStateTarget, type StageTarget, type StateSnapshot, type StateSnapshotResource, type StateSnapshotScope, type UrlImpliesAuthAllowance, UrlImpliesAuthError, type UrlImpliesAuthViolation, type UrlImpliesAuthViolationReason, anyStage, assertDestroyLeftNothing, assertRetainedIdentities, assertUrlImpliesAuth, credentialPreflight, deleteStageRows, evaluateStack, firstDeployAuthorization, firstDeployPlanHash, inventoryStages, isAlchemyLifecycleError, isLocalEmulationStage, localEmulationEnvironment, parsePlanEffects, parseStateSnapshot, readStageSnapshot, runAlchemyLifecycle, secretNamePolicy, stagePolicy };
|
|
1174
|
+
export { ALCHEMY_SUBPATH, type AlchemyLifecycleEntry, AlchemyLifecycleError, type AlchemyLifecycleFailureCode, type AlchemyLifecycleFailureFields, type AlchemyLifecycleOperation, type AlchemyLifecyclePhase, type AlchemyLifecyclePolicy, type AlchemyLifecycleRequest, type AlchemyLifecycleResult, type AssertDestroyLeftNothingOptions, type AssertRetainedIdentitiesOptions, type AssertUrlImpliesAuthOptions, type AuthBindingNameMatcher, type ChildDiagnostics, type CloudflareCredential, CloudflareCredentialUnavailableError, type CloudflareTokenKind, type CredentialPreflightOptions, type CredentialPreflightResult, type CredentialSource, type EvaluateStackInput, type EvaluatedBinding, type EvaluatedGraph, type EvaluatedResource, type FirstDeployAuthorization, type FirstDeployAuthorizationOptions, type FirstDeployPlanHashOptions, type FirstDeployPolicy, HOSTED_CLOUDFLARE_CREDENTIAL_NAMES, LIFECYCLE_FAILURE_MESSAGES, LOCAL_PREVIEW_STAGES, MINIMUM_REDACTABLE_SECRET_LENGTH, type MutationCertainty, PLAN_ACTIONS, PLAN_OUTPUT_ALCHEMY_VERSION, type PlanAction, type PlanEffects, PlanEffectsError, type PlanEffectsRejection, type PlanResourceEffect, type PlanTaskAction, type PlanTaskEffect, RetainedIdentityError, type RetainedIdentityMarker, type RetainedIdentitySection, type RetainedIdentityViolation, type RetainedIdentityViolationReason, type RunAlchemyLifecycleOptions, SECRET_REDACTION_PLACEHOLDER, type SecretNamePolicyOptions, type StackOutputOccupancy, type StackStageOwnership, type StageCleanupOutcome, type StageLease, type StageMatcher, type StagePolicy, StagePolicyError, type StagePolicyErrorCode, type StagePolicyTable, type StageSnapshotView, type StageStateReader, type StageStateStore, type StageStateTarget, type StageTarget, type StateSnapshot, type StateSnapshotResource, type StateSnapshotScope, type UrlImpliesAuthAllowance, UrlImpliesAuthError, type UrlImpliesAuthViolation, type UrlImpliesAuthViolationReason, anyStage, assertDestroyLeftNothing, assertRetainedIdentities, assertUrlImpliesAuth, credentialPreflight, deleteStageRows, evaluateStack, firstDeployAuthorization, firstDeployPlanHash, inventoryStages, isAlchemyLifecycleError, isLocalEmulationStage, localEmulationEnvironment, parsePlanEffects, parseStateSnapshot, readStageSnapshot, runAlchemyLifecycle, secretNamePolicy, stagePolicy };
|