@intentius/chant-lexicon-fly 0.90.0 → 0.91.0
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/components/capability-plugin.d.ts +1 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/fly-release.d.ts +148 -0
- package/dist/components/fly-release.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +11 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/op/activities/fly-apply.d.ts +8 -22
- package/dist/op/activities/fly-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/machine-release.d.ts +176 -0
- package/dist/op/activities/machine-release.d.ts.map +1 -0
- package/dist/op/activities/machines-contract.d.ts +8 -0
- package/dist/op/activities/machines-contract.d.ts.map +1 -1
- package/dist/op/activities/machines-fake.d.ts +53 -0
- package/dist/op/activities/machines-fake.d.ts.map +1 -0
- package/dist/op/builders.d.ts +13 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/release-metadata.d.ts +48 -0
- package/dist/release-metadata.d.ts.map +1 -0
- package/dist/release-store.d.ts +36 -0
- package/dist/release-store.d.ts.map +1 -0
- package/dist/skills/chant-fly-ops.md +33 -0
- package/package.json +2 -2
- package/src/components/capability-plugin.ts +11 -3
- package/src/components/fly-release.test.ts +244 -0
- package/src/components/fly-release.ts +341 -0
- package/src/components/index.ts +16 -0
- package/src/describe-resources.test.ts +18 -3
- package/src/describe-resources.ts +69 -5
- package/src/index.ts +12 -0
- package/src/op/activities/fly-apply.ts +15 -3
- package/src/op/activities/index.ts +23 -0
- package/src/op/activities/machine-release.integration.test.ts +113 -0
- package/src/op/activities/machine-release.test.ts +138 -0
- package/src/op/activities/machine-release.ts +407 -0
- package/src/op/activities/machines-contract.docker.integration.test.ts +10 -1
- package/src/op/activities/machines-contract.test.ts +11 -1
- package/src/op/activities/machines-contract.ts +17 -0
- package/src/op/activities/machines-fake.ts +165 -0
- package/src/op/builders.ts +25 -0
- package/src/release-metadata.ts +72 -0
- package/src/release-store.ts +70 -0
- package/src/skills/chant-fly-ops.md +33 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory Fly Machines (flaps) API, as a {@link FlyHttp} (#2736).
|
|
3
|
+
*
|
|
4
|
+
* The twin of ./sprites-fake.ts for the Machines side: the endpoints the
|
|
5
|
+
* applier (./fly-apply.ts) and the release activities (./machine-release.ts)
|
|
6
|
+
* call, answered from memory with the response shapes flaps and mudflaps use.
|
|
7
|
+
* Unit tests inject it where the default client would reach the network; the
|
|
8
|
+
* docker-gated tests run the same activities against the pinned mudflaps.
|
|
9
|
+
*
|
|
10
|
+
* Not an activity: nothing in ./index.ts exports it, so `loadActivities`
|
|
11
|
+
* never binds it.
|
|
12
|
+
*/
|
|
13
|
+
import type { FlyHttp } from "./fly-apply.js";
|
|
14
|
+
/** One Machine the fake holds. */
|
|
15
|
+
export interface FakeMachine {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
state: string;
|
|
19
|
+
instance_id: string;
|
|
20
|
+
config: Record<string, unknown> & {
|
|
21
|
+
metadata?: Record<string, string>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** What an `exec` answers: the Machines API's exec envelope. */
|
|
25
|
+
export interface FakeExecResult {
|
|
26
|
+
exit_code: number;
|
|
27
|
+
stdout?: string;
|
|
28
|
+
stderr?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface MachinesFake {
|
|
31
|
+
http: FlyHttp;
|
|
32
|
+
apps: Set<string>;
|
|
33
|
+
machines: Map<string, FakeMachine[]>;
|
|
34
|
+
/** Every exec, in order: the app, the machine id and the command. */
|
|
35
|
+
execs: Array<{
|
|
36
|
+
app: string;
|
|
37
|
+
id: string;
|
|
38
|
+
command: string[];
|
|
39
|
+
}>;
|
|
40
|
+
/** Every call, as `METHOD path` (no base, no query). */
|
|
41
|
+
calls: string[];
|
|
42
|
+
/** The live machine named `name` in `app`, if any. */
|
|
43
|
+
machine(app: string, name: string): FakeMachine | undefined;
|
|
44
|
+
}
|
|
45
|
+
export interface MachinesFakeOptions {
|
|
46
|
+
/** Answer an exec. Default: exit 0 with no output. */
|
|
47
|
+
exec?: (app: string, machine: FakeMachine, command: string[]) => FakeExecResult;
|
|
48
|
+
/** The state a created or updated machine settles in. Default `started`. */
|
|
49
|
+
settle?: (machine: FakeMachine) => string;
|
|
50
|
+
}
|
|
51
|
+
/** A fresh in-memory flaps. */
|
|
52
|
+
export declare function createMachinesFake(options?: MachinesFakeOptions): MachinesFake;
|
|
53
|
+
//# sourceMappingURL=machines-fake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machines-fake.d.ts","sourceRoot":"","sources":["../../../src/op/activities/machines-fake.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CACzE;AAED,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACrC,qEAAqE;IACrE,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC7D,wDAAwD;IACxD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sDAAsD;IACtD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;CAC7D;AAED,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,cAAc,CAAC;IAChF,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;CAC3C;AAOD,+BAA+B;AAC/B,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CA4GlF"}
|
package/dist/op/builders.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { SpriteApplyNetworkPolicyArgs, SpriteApplyServicesArgs } from "./ac
|
|
|
33
33
|
import type { SpriteServiceCreateArgs, SpriteServiceGetArgs, SpriteServiceListArgs, SpriteServiceStartArgs, SpriteServiceStopArgs, SpriteServiceDeleteArgs, SpriteServiceLogsArgs } from "./activities/sprite-services.js";
|
|
34
34
|
import type { SpriteTaskCreateArgs, SpriteTaskRefreshArgs, SpriteTaskReleaseArgs } from "./activities/sprite-tasks.js";
|
|
35
35
|
import type { SpritesUpArgs, SpritesDownArgs } from "./activities/sprites-emulator.js";
|
|
36
|
+
import type { FlyMachineReleaseArgs, FlyMachineExecArgs, FlyMachineStateArgs, FlyMachineVerifyArgs, FlyMachineRestoreArgs } from "./activities/machine-release.js";
|
|
36
37
|
type StepOpts = {
|
|
37
38
|
profile?: ActivityStep["profile"];
|
|
38
39
|
};
|
|
@@ -88,5 +89,17 @@ export declare const spriteTaskRelease: (args: WithStepRefs<SpriteTaskReleaseArg
|
|
|
88
89
|
export declare const spritesUp: (args?: WithStepRefs<SpritesUpArgs> & StepOpts) => NamedActivityStep;
|
|
89
90
|
/** Stop and remove the local spritzer container — the fully typed twin of core's `spritesDown`. Defaults to the `fastIdempotent` profile. */
|
|
90
91
|
export declare const spritesDown: (args?: WithStepRefs<SpritesDownArgs> & StepOpts) => NamedActivityStep;
|
|
92
|
+
/** Apply the plan with the Machine serving a release (its digest in the Machine's metadata). Defaults to the `longInfra` profile. */
|
|
93
|
+
export declare const flyMachineRelease: (args: WithStepRefs<FlyMachineReleaseArgs> & StepOpts) => NamedActivityStep;
|
|
94
|
+
/** Run a command inside a Machine (a migration). Defaults to the `atMostOnce` profile: chant cannot know the command is safe to repeat. */
|
|
95
|
+
export declare const flyMachineExec: (args: WithStepRefs<FlyMachineExecArgs> & StepOpts) => NamedActivityStep;
|
|
96
|
+
/** Restart a Machine under a lease and wait for it to be started. Defaults to the `longInfra` profile. */
|
|
97
|
+
export declare const flyMachineRestart: (args: WithStepRefs<FlyMachineStateArgs> & StepOpts) => NamedActivityStep;
|
|
98
|
+
/** Stop a Machine under a lease. Defaults to the `longInfra` profile. */
|
|
99
|
+
export declare const flyMachineStop: (args: WithStepRefs<FlyMachineStateArgs> & StepOpts) => NamedActivityStep;
|
|
100
|
+
/** Check a Machine is started with a release, and its health endpoint answers. Defaults to the `longInfra` profile. */
|
|
101
|
+
export declare const flyMachineVerify: (args: WithStepRefs<FlyMachineVerifyArgs> & StepOpts) => NamedActivityStep;
|
|
102
|
+
/** Put a recorded Machine config back (restore, rollback). Defaults to the `longInfra` profile. */
|
|
103
|
+
export declare const flyMachineRestore: (args: WithStepRefs<FlyMachineRestoreArgs> & StepOpts) => NamedActivityStep;
|
|
91
104
|
export {};
|
|
92
105
|
//# sourceMappingURL=builders.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/op/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAY,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAoB,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC/L,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC3H,OAAO,KAAK,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACxG,OAAO,KAAK,EACV,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACpH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/op/builders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release a Fly Machine serves, as its metadata records it (#2736, ws-056).
|
|
3
|
+
*
|
|
4
|
+
* Pure: the keys, a reader, and a writer over a Machine config. The Machines
|
|
5
|
+
* activities (./op/activities/machine-release.ts) stamp these on a release,
|
|
6
|
+
* `describeResources` (./describe-resources.ts) reads them back, and
|
|
7
|
+
* `chant components status --live` compares the digest with the release
|
|
8
|
+
* ledger.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The Machine metadata keys that say which release it serves. Fly metadata is
|
|
12
|
+
* a string map, so each field is its own key.
|
|
13
|
+
*
|
|
14
|
+
* - `digest`: the release's digest, the release ledger's join key.
|
|
15
|
+
* - `gitSha`: the commit the release was built from.
|
|
16
|
+
* - `release`: an optional human label (a tag, a release id).
|
|
17
|
+
* - `previousDigest`: the release this one replaced when it first shipped, so
|
|
18
|
+
* a rollback knows where to go back to with no history of its own.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RELEASE_METADATA_KEYS: {
|
|
21
|
+
readonly digest: "chant-release-digest";
|
|
22
|
+
readonly gitSha: "chant-release-git-sha";
|
|
23
|
+
readonly release: "chant-release";
|
|
24
|
+
readonly previousDigest: "chant-release-previous-digest";
|
|
25
|
+
};
|
|
26
|
+
/** A release, as a Machine's metadata records it. */
|
|
27
|
+
export interface MachineRelease {
|
|
28
|
+
/** The release's digest (`sha256:...`): what the release ledger records. */
|
|
29
|
+
digest: string;
|
|
30
|
+
/** The commit it was built from. */
|
|
31
|
+
gitSha?: string;
|
|
32
|
+
/** A human label for it. */
|
|
33
|
+
release?: string;
|
|
34
|
+
/** The digest of the release it replaced, when there was one. */
|
|
35
|
+
previousDigest?: string;
|
|
36
|
+
}
|
|
37
|
+
export type MachineConfig = Record<string, unknown> & {
|
|
38
|
+
metadata?: Record<string, string>;
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
};
|
|
41
|
+
/** The release a Machine's metadata names, or undefined when it names none. Pure. */
|
|
42
|
+
export declare function readMachineRelease(metadata: Record<string, string> | null | undefined): MachineRelease | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* A Machine config serving `release`: a copy of `config` with the release's
|
|
45
|
+
* metadata keys set (and any it does not carry removed). Pure.
|
|
46
|
+
*/
|
|
47
|
+
export declare function withReleaseMetadata(config: MachineConfig | undefined, release: MachineRelease): MachineConfig;
|
|
48
|
+
//# sourceMappingURL=release-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-metadata.d.ts","sourceRoot":"","sources":["../src/release-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;;;;CAKxB,CAAC;AAEX,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAE1H,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAYlH;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,aAAa,CAU7G"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Fly release's Machine config is kept, so a rollback can put it back
|
|
3
|
+
* (#2736, ws-056).
|
|
4
|
+
*
|
|
5
|
+
* The Machine's metadata says which release it serves and which one it
|
|
6
|
+
* replaced; the config that earlier release applied is kept on the
|
|
7
|
+
* `chant/lifecycle` branch beside the release ledger, one file per release at
|
|
8
|
+
* `<env>/fly/<app>/<machine>/<digest>.json`. It is written through core's
|
|
9
|
+
* ledger plumbing, so a workspace member's copy lands under its own prefix,
|
|
10
|
+
* and every checkout that fetches the branch can roll back.
|
|
11
|
+
*
|
|
12
|
+
* Not exported from the package entry point: it spawns git, which stays off
|
|
13
|
+
* the build path. The `fly-release` and `fly-rollback` capabilities import it.
|
|
14
|
+
*/
|
|
15
|
+
/** Which Machine and release a config belongs to. */
|
|
16
|
+
export interface MachineConfigRef {
|
|
17
|
+
app: string;
|
|
18
|
+
machine: string;
|
|
19
|
+
digest: string;
|
|
20
|
+
}
|
|
21
|
+
/** A store of the Machine config each release applied. */
|
|
22
|
+
export interface MachineConfigStore {
|
|
23
|
+
read(ref: MachineConfigRef): Promise<Record<string, unknown> | undefined>;
|
|
24
|
+
write(ref: MachineConfigRef, config: Record<string, unknown>): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/** The file a release's Machine config is kept in, under the environment's ledger directory. */
|
|
27
|
+
export declare function machineConfigFile(ref: MachineConfigRef): string;
|
|
28
|
+
/** The lifecycle-branch store for `environment`. */
|
|
29
|
+
export declare function lifecycleMachineConfigStore(environment: string, opts?: {
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}): MachineConfigStore;
|
|
32
|
+
/** An in-memory store, for tests and for a caller with nowhere to keep configs. */
|
|
33
|
+
export declare function memoryMachineConfigStore(): MachineConfigStore & {
|
|
34
|
+
entries: Map<string, Record<string, unknown>>;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=release-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-store.d.ts","sourceRoot":"","sources":["../src/release-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1E,KAAK,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9E;AAID,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE/D;AAED,oDAAoD;AACpD,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,kBAAkB,CAgB5G;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,IAAI,kBAAkB,GAAG;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAAE,CAYjH"}
|
|
@@ -53,3 +53,36 @@ Each prune logs the resource and endpoint it removed, so a prune run is auditabl
|
|
|
53
53
|
## Re-applying is safe
|
|
54
54
|
|
|
55
55
|
A re-apply of an unchanged stack is a no-op per resource: machines whose config is structurally equal to live are skipped, volumes and certificates that already exist are skipped, and an IP of an already-present family is skipped. Only apply-only secrets are always re-set, because flaps exposes no value to diff against.
|
|
56
|
+
|
|
57
|
+
## Releases on a Machine
|
|
58
|
+
|
|
59
|
+
A component can deploy to Fly with this lexicon alone. It declares an `App` and the `Machine` that serves, builds them with `chant build --lexicon fly -o dist/fly.json`, and deploys with the `fly-release` step. Here a `Publish` phase (`publish-image`, elided) pushes the image, and the release serves it by digest:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { phase, type Component } from "@intentius/chant/components";
|
|
63
|
+
|
|
64
|
+
export const web: Component = {
|
|
65
|
+
name: "web",
|
|
66
|
+
deploy: [
|
|
67
|
+
phase("Publish", [/* publish-image */]),
|
|
68
|
+
phase("Release", [
|
|
69
|
+
{
|
|
70
|
+
kind: "fly-release",
|
|
71
|
+
plan: "dist/fly.json",
|
|
72
|
+
digest: "@Publish.digest",
|
|
73
|
+
image: "@Publish.uri",
|
|
74
|
+
migrations: [{ name: "001_init.sql", command: "node migrate.js 001_init.sql" }],
|
|
75
|
+
verify: { url: "https://web.example.com", healthPath: "/health" },
|
|
76
|
+
},
|
|
77
|
+
]),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The step updates the Machine in place with the release in its `config.metadata`: `chant-release-digest`, `chant-release-git-sha` (the commit, `git rev-parse HEAD` unless given), and `chant-release-previous-digest` (the release it replaced). Each migration then runs inside the Machine through the Machines API's exec, once per environment: its receipt is kept on `chant/lifecycle` at `<env>/receipts/`, and a migration fires again only when its `sha` (or command) changes. After a migration fires, the Machine restarts, and the step checks that it is started with this release and, given a `url`, that its health endpoint answers with this commit or digest. If anything fails after the Machine changed, the step puts back the config the Machine served before (on a first release, it stops the Machine) and fails.
|
|
83
|
+
|
|
84
|
+
The step's output carries `uri` and `digest`, so `chant run --components web --env prod` records the release in the ledger. `chant components status prod --live` reads the Machine's metadata back and compares its digest with the ledger's: the row is `reconciled` when they agree and `drifted` when the Machine serves something the ledger does not name. The component joins the Machine by name: name the component after the Machine entity, or list it in `liveNames`.
|
|
85
|
+
|
|
86
|
+
Each release's Machine config is kept on `chant/lifecycle` at `<env>/fly/<app>/<machine>/<digest>.json`. `fly-rollback` puts back the config of the release the serving one replaced (or the digest given as `to`), checks it, and outputs that digest so the ledger records it again. `fly-release`'s own saga compensation does the same.
|
|
87
|
+
|
|
88
|
+
The steps are also Op activities, for an Op that composes them itself: `flyMachineRelease`, `flyMachineExec`, `flyMachineRestart`, `flyMachineStop`, `flyMachineVerify` and `flyMachineRestore`. Wrap a migration's `flyMachineExec` in `effect()` so it fires once.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"typescript": "^5.9.3"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@intentius/chant": "^0.
|
|
66
|
+
"@intentius/chant": "^0.91.0",
|
|
67
67
|
"typescript": "^5.9.3"
|
|
68
68
|
},
|
|
69
69
|
"description": "Fly.io Machines lexicon for chant — declarative IaC in TypeScript",
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `flyCapabilityPlugin` — the fly lexicon's capability plugin (#1942, epic
|
|
3
3
|
* #1564 phase 2).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (`./run-agent.ts`)
|
|
5
|
+
* Leaves: `run-agent`, the sprite-lifecycle-backed agent-turn verb
|
|
6
|
+
* (`./run-agent.ts`), and `fly-release` / `fly-rollback`, a release served by
|
|
7
|
+
* a Fly Machine (`./fly-release.ts`, #2736). Contributed through core's `CapabilityPlugin` contract
|
|
7
8
|
* the same way aws contributes `cfn-deploy` and helm contributes
|
|
8
9
|
* `helm-upgrade` (docs/components/cloud-boundary) — a project declaring
|
|
9
10
|
* `lexicons: ["fly"]` gets `run-agent` registered automatically when it runs
|
|
@@ -15,9 +16,12 @@
|
|
|
15
16
|
import type { Capability } from "@intentius/chant/components/capability";
|
|
16
17
|
import { ownPackageVersion, type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
17
18
|
import { flyRunAgentCapability } from "./run-agent";
|
|
19
|
+
import { flyReleaseCapability, flyRollbackCapability } from "./fly-release";
|
|
18
20
|
|
|
19
21
|
export const FLY_VERB_FAMILIES = {
|
|
20
22
|
agentExecution: ["run-agent"],
|
|
23
|
+
// A release served by a Fly Machine, and going back to an earlier one (#2736).
|
|
24
|
+
apply: ["fly-release", "fly-rollback"],
|
|
21
25
|
} as const;
|
|
22
26
|
|
|
23
27
|
export const flyCapabilityPlugin: CapabilityPlugin = {
|
|
@@ -28,7 +32,11 @@ export const flyCapabilityPlugin: CapabilityPlugin = {
|
|
|
28
32
|
return ownPackageVersion(import.meta.url);
|
|
29
33
|
},
|
|
30
34
|
capabilities(): Array<Capability<never, unknown>> {
|
|
31
|
-
return [
|
|
35
|
+
return [
|
|
36
|
+
flyRunAgentCapability as Capability<never, unknown>,
|
|
37
|
+
flyReleaseCapability as Capability<never, unknown>,
|
|
38
|
+
flyRollbackCapability as Capability<never, unknown>,
|
|
39
|
+
];
|
|
32
40
|
},
|
|
33
41
|
families(): Record<string, readonly string[]> {
|
|
34
42
|
return FLY_VERB_FAMILIES;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach } from "vitest";
|
|
6
|
+
import type { DeployContext } from "@intentius/chant/components/capability";
|
|
7
|
+
import type { EffectReceiptRef, ReceiptStore } from "@intentius/chant/op/receipt-store";
|
|
8
|
+
import { buildChangeSet } from "@intentius/chant/lifecycle/change-set";
|
|
9
|
+
import { liveEvidenceFromChangeSet, reconcileStatus, type EntityLiveRead } from "@intentius/chant/lifecycle/status";
|
|
10
|
+
import type { ReleaseRecord } from "@intentius/chant/lifecycle/release-ledger";
|
|
11
|
+
import { normalizeObservation } from "@intentius/chant/observation";
|
|
12
|
+
import { createFlyReleaseCapability, createFlyRollbackCapability, migrationReceipt, type FlyReleaseDeps } from "./fly-release";
|
|
13
|
+
import { flyCapabilityPlugin } from "./capability-plugin";
|
|
14
|
+
import { createMachinesFake, type MachinesFake } from "../op/activities/machines-fake";
|
|
15
|
+
import { memoryMachineConfigStore } from "../release-store";
|
|
16
|
+
import { readMachineRelease } from "../release-metadata";
|
|
17
|
+
import { describeResources } from "../describe-resources";
|
|
18
|
+
|
|
19
|
+
const ENDPOINT = "http://flaps.test";
|
|
20
|
+
const NO_WAIT = { intervalMs: 0, deadlineMs: 2_000 };
|
|
21
|
+
const CTX: DeployContext = { env: "prod", component: "shop" };
|
|
22
|
+
|
|
23
|
+
const PLAN = {
|
|
24
|
+
shop: { endpoint: "/v1/apps", method: "POST", body: { app_name: "shop", org_slug: "personal" } },
|
|
25
|
+
web: {
|
|
26
|
+
endpoint: "/v1/apps/shop/machines",
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: { name: "web", config: { image: "shop:declared", metadata: { "managed-by": "chant" } } },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function memoryReceipts(): ReceiptStore & { values: Map<string, string> } {
|
|
33
|
+
const values = new Map<string, string>();
|
|
34
|
+
return {
|
|
35
|
+
values,
|
|
36
|
+
read: async (r: EffectReceiptRef) => values.get(r.name),
|
|
37
|
+
write: async (r: EffectReceiptRef, v: string) => void values.set(r.name, v),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let dir: string;
|
|
42
|
+
let planPath: string;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
dir = mkdtempSync(join(tmpdir(), "fly-release-"));
|
|
45
|
+
planPath = join(dir, "fly.json");
|
|
46
|
+
writeFileSync(planPath, JSON.stringify(PLAN));
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
49
|
+
|
|
50
|
+
function setup(fake: MachinesFake = createMachinesFake()) {
|
|
51
|
+
const configs = memoryMachineConfigStore();
|
|
52
|
+
const receipts = memoryReceipts();
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
const deps: FlyReleaseDeps = {
|
|
55
|
+
http: fake.http,
|
|
56
|
+
configStore: () => configs,
|
|
57
|
+
receiptStore: () => receipts,
|
|
58
|
+
headCommit: async () => "0000000000000000000000000000000000000000",
|
|
59
|
+
log: (l) => lines.push(l),
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
fake,
|
|
63
|
+
configs,
|
|
64
|
+
receipts,
|
|
65
|
+
lines,
|
|
66
|
+
release: createFlyReleaseCapability(deps),
|
|
67
|
+
rollback: createFlyRollbackCapability(deps),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const input = (digest: string, extra: Record<string, unknown> = {}) => ({
|
|
72
|
+
plan: planPath,
|
|
73
|
+
digest,
|
|
74
|
+
gitSha: `${digest.slice(7, 14)}`,
|
|
75
|
+
endpoint: ENDPOINT,
|
|
76
|
+
wait: NO_WAIT,
|
|
77
|
+
verify: { intervalMs: 1, timeoutMs: 20 },
|
|
78
|
+
...extra,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("fly-release", () => {
|
|
82
|
+
test("is registered by the fly capability plugin, with a native rollback", () => {
|
|
83
|
+
const kinds = flyCapabilityPlugin.capabilities().map((c) => c.kind);
|
|
84
|
+
expect(kinds).toEqual(expect.arrayContaining(["fly-release", "fly-rollback"]));
|
|
85
|
+
const release = flyCapabilityPlugin.capabilities().find((c) => c.kind === "fly-release");
|
|
86
|
+
expect(release?.rollbackPolicy).toBe("native");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("serves the release, records it on the Machine, and returns what the ledger records", async () => {
|
|
90
|
+
const s = setup();
|
|
91
|
+
const out = await s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { image: "shop@sha256:img" }));
|
|
92
|
+
expect(out).toMatchObject({ uri: "fly://shop/web", digest: "sha256:aaaaaaaaaaaa", app: "shop", previous: null, migrations: [] });
|
|
93
|
+
const m = s.fake.machine("shop", "web")!;
|
|
94
|
+
expect(m.config.image).toBe("shop@sha256:img");
|
|
95
|
+
expect(readMachineRelease(m.config.metadata)).toEqual({ digest: "sha256:aaaaaaaaaaaa", gitSha: "aaaaaaa" });
|
|
96
|
+
// The config it applied is kept for a later rollback.
|
|
97
|
+
expect(await s.configs.read({ app: "shop", machine: "web", digest: "sha256:aaaaaaaaaaaa" })).toEqual(m.config);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("the commit defaults to HEAD, as the ledger records it", async () => {
|
|
101
|
+
const s = setup();
|
|
102
|
+
const out = await s.release.run(CTX, { ...input("sha256:aaaaaaaaaaaa"), gitSha: undefined });
|
|
103
|
+
expect(out.gitSha).toBe("0000000000000000000000000000000000000000");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("each migration fires once per environment, then the Machine restarts", async () => {
|
|
107
|
+
const s = setup();
|
|
108
|
+
const migrations = [
|
|
109
|
+
{ name: "001_init.sql", command: "node migrate.js 001_init.sql", sha: "sha256:m1" },
|
|
110
|
+
{ name: "002_add.sql", command: ["node", "migrate.js", "002_add.sql"] },
|
|
111
|
+
];
|
|
112
|
+
const first = await s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { migrations }));
|
|
113
|
+
expect(first.migrations).toEqual([
|
|
114
|
+
{ name: "001_init.sql", fired: true },
|
|
115
|
+
{ name: "002_add.sql", fired: true },
|
|
116
|
+
]);
|
|
117
|
+
expect(s.fake.execs.map((e) => e.command)).toEqual([
|
|
118
|
+
["sh", "-c", "node migrate.js 001_init.sql"],
|
|
119
|
+
["node", "migrate.js", "002_add.sql"],
|
|
120
|
+
]);
|
|
121
|
+
expect(s.fake.calls.some((c) => c.endsWith("/restart"))).toBe(true);
|
|
122
|
+
expect(s.receipts.values.get("fly-migration:shop/001_init.sql")).toBe("sha256:m1");
|
|
123
|
+
|
|
124
|
+
const second = await s.release.run(CTX, input("sha256:bbbbbbbbbbbb", { migrations: [...migrations, { name: "003.sql", command: "m 3" }] }));
|
|
125
|
+
expect(second.migrations).toEqual([
|
|
126
|
+
{ name: "001_init.sql", fired: false },
|
|
127
|
+
{ name: "002_add.sql", fired: false },
|
|
128
|
+
{ name: "003.sql", fired: true },
|
|
129
|
+
]);
|
|
130
|
+
expect(s.fake.execs).toHaveLength(3);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a failed migration restores the previous release's config, leaves its receipt unwritten, and fails", async () => {
|
|
134
|
+
const fake = createMachinesFake({ exec: (_a, _m, cmd) => (cmd.join(" ").includes("boom") ? { exit_code: 1, stderr: "boom" } : { exit_code: 0 }) });
|
|
135
|
+
const s = setup(fake);
|
|
136
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { image: "shop:1" }));
|
|
137
|
+
const before = structuredClone(fake.machine("shop", "web")!.config);
|
|
138
|
+
await expect(
|
|
139
|
+
s.release.run(CTX, input("sha256:bbbbbbbbbbbb", { image: "shop:2", migrations: [{ name: "004.sql", command: "boom" }] })),
|
|
140
|
+
).rejects.toThrow(/exited 1/);
|
|
141
|
+
expect(fake.machine("shop", "web")!.config).toEqual(before);
|
|
142
|
+
expect(s.receipts.values.has("fly-migration:shop/004.sql")).toBe(false);
|
|
143
|
+
expect(await s.configs.read({ app: "shop", machine: "web", digest: "sha256:bbbbbbbbbbbb" })).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a failed first release stops the Machine: nothing was served before", async () => {
|
|
147
|
+
const fake = createMachinesFake({ exec: () => ({ exit_code: 2 }) });
|
|
148
|
+
const s = setup(fake);
|
|
149
|
+
await expect(s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { migrations: [{ name: "1", command: "x" }] }))).rejects.toThrow();
|
|
150
|
+
expect(fake.machine("shop", "web")!.state).toBe("stopped");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("its saga rollback puts back the previous release's Machine config", async () => {
|
|
154
|
+
const s = setup();
|
|
155
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { image: "shop:1" }));
|
|
156
|
+
const one = structuredClone(s.fake.machine("shop", "web")!.config);
|
|
157
|
+
const i2 = input("sha256:bbbbbbbbbbbb", { image: "shop:2" });
|
|
158
|
+
const out = await s.release.run(CTX, i2);
|
|
159
|
+
expect(out.previous?.digest).toBe("sha256:aaaaaaaaaaaa");
|
|
160
|
+
await s.release.rollback!(CTX, i2, out);
|
|
161
|
+
expect(s.fake.machine("shop", "web")!.config).toEqual(one);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a Machine released before configs were kept can still be rolled back to", async () => {
|
|
165
|
+
const s = setup();
|
|
166
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { image: "shop:1" }));
|
|
167
|
+
s.configs.entries.clear();
|
|
168
|
+
await s.release.run(CTX, input("sha256:bbbbbbbbbbbb", { image: "shop:2" }));
|
|
169
|
+
expect(await s.configs.read({ app: "shop", machine: "web", digest: "sha256:aaaaaaaaaaaa" })).toBeDefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("fly-rollback", () => {
|
|
174
|
+
test("restores the previous release's Machine config, and names it for the ledger", async () => {
|
|
175
|
+
const s = setup();
|
|
176
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa", { image: "shop:1" }));
|
|
177
|
+
const one = structuredClone(s.fake.machine("shop", "web")!.config);
|
|
178
|
+
await s.release.run(CTX, input("sha256:bbbbbbbbbbbb", { image: "shop:2" }));
|
|
179
|
+
const back = await s.rollback.run(CTX, { plan: planPath, endpoint: ENDPOINT, wait: NO_WAIT, verify: { intervalMs: 1, timeoutMs: 20 } });
|
|
180
|
+
expect(back).toMatchObject({ uri: "fly://shop/web", digest: "sha256:aaaaaaaaaaaa", gitSha: "aaaaaaa", previous: { digest: "sha256:bbbbbbbbbbbb" } });
|
|
181
|
+
expect(s.fake.machine("shop", "web")!.config).toEqual(one);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("with no release before the serving one, it refuses", async () => {
|
|
185
|
+
const s = setup();
|
|
186
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa"));
|
|
187
|
+
await expect(s.rollback.run(CTX, { plan: planPath, endpoint: ENDPOINT })).rejects.toThrow(/no release before it/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("to a digest with no recorded config, it refuses", async () => {
|
|
191
|
+
const s = setup();
|
|
192
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa"));
|
|
193
|
+
await expect(s.rollback.run(CTX, { plan: planPath, endpoint: ENDPOINT, to: "sha256:zzz" })).rejects.toThrow(/no recorded Machine config/);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("components status --live over a Fly release", () => {
|
|
198
|
+
const record = (digest: string): ReleaseRecord => ({
|
|
199
|
+
version: 1,
|
|
200
|
+
component: "web",
|
|
201
|
+
env: "prod",
|
|
202
|
+
digest,
|
|
203
|
+
gitSha: "0000000",
|
|
204
|
+
runId: "run-1",
|
|
205
|
+
timestamp: "2026-09-25T00:00:00.000Z",
|
|
206
|
+
actor: "lex00",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
async function status(fake: MachinesFake, recorded: string) {
|
|
210
|
+
const entities = new Map([
|
|
211
|
+
["shop", { entityType: "Fly::Machines::App", props: { name: "shop" } as Record<string, unknown> }],
|
|
212
|
+
["web", { entityType: "Fly::Machines::Machine", props: { name: "web" } as Record<string, unknown> }],
|
|
213
|
+
]);
|
|
214
|
+
const observed = normalizeObservation(
|
|
215
|
+
await describeResources({ environment: "prod", buildOutput: "", entityNames: ["shop", "web"], entities, endpoint: ENDPOINT }, fake.http),
|
|
216
|
+
);
|
|
217
|
+
const cs = buildChangeSet("prod", { declared: new Set(entities.keys()), observedNow: observed.resources, observedThen: undefined, unobserved: observed.unobserved }, { lexicon: "fly" });
|
|
218
|
+
const observations = new Map<string, EntityLiveRead>(cs.entries.map((e) => [e.name, { now: observed.resources[e.name] }]));
|
|
219
|
+
const evidence = liveEvidenceFromChangeSet(cs, undefined, { observations });
|
|
220
|
+
return reconcileStatus("prod", [record(recorded)], { liveEvidence: evidence }).find((r) => r.component === "web")!;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test("the Machine's release is compared with the ledger: reconciled when they agree, drifted when not", async () => {
|
|
224
|
+
const s = setup();
|
|
225
|
+
await s.release.run(CTX, input("sha256:aaaaaaaaaaaa"));
|
|
226
|
+
await s.release.run(CTX, input("sha256:bbbbbbbbbbbb"));
|
|
227
|
+
|
|
228
|
+
const agreed = await status(s.fake, "sha256:bbbbbbbbbbbb");
|
|
229
|
+
expect(agreed.reconciliation).toBe("reconciled");
|
|
230
|
+
|
|
231
|
+
// The Machine rolled back with no ledger record: live and recorded disagree.
|
|
232
|
+
await s.rollback.run(CTX, { plan: planPath, endpoint: ENDPOINT, wait: NO_WAIT, verify: { intervalMs: 1, timeoutMs: 20 } });
|
|
233
|
+
const drifted = await status(s.fake, "sha256:bbbbbbbbbbbb");
|
|
234
|
+
expect(drifted.reconciliation).toBe("drifted");
|
|
235
|
+
expect(drifted.detail).toContain("reports digest sha256:aaaaaaaaaaaa");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("the migration receipt is named for the app and migration", () => {
|
|
239
|
+
expect(migrationReceipt("shop", { name: "001.sql", command: "x", sha: "sha256:1" })).toMatchObject({
|
|
240
|
+
ref: { name: "fly-migration:shop/001.sql", effect: "fly-migrate", flavor: "hash" },
|
|
241
|
+
expectation: "sha256:1",
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|