@intentius/chant-lexicon-fly 0.37.2 → 0.39.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "de4e2ecb20f4ab172c08473322d96e3c51f35ac76d90c5e5f54d27216f14329e",
4
+ "manifest.json": "70ff6f13c27e82785b87f22342a1cfd59fbb9e02dce1e59c70cd9696d8e0d364",
5
5
  "meta.json": "e46e65ba7544fafc6dbb3fd48b841904b5ae6f7cb4d517a940ed68b01626c097",
6
6
  "types/index.d.ts": "07f5d04d0d3831cae053b1f0f5dd661c58e9708f45067a0fc85dd94079ac144b",
7
7
  "rules/guest-sizing.ts": "bc749d218df8f3cc3d8fbe388205ae96e6b204ae0d6d77e8bf82cb936cc53ff6",
@@ -15,5 +15,5 @@
15
15
  "skills/chant-fly-ops.md": "b8eef6504e203133e6a35c83aabb08404eec95060705395515a0046d42269cf1",
16
16
  "skills/chant-fly-sprites.md": "89ba52db63a71a17eab483d99233a4ae67f5d34920445010e750f81c95200b4e"
17
17
  },
18
- "composite": "d39afaec5eb09bc53115953328ddc1b0c572ce6e265a10b31193a8fe32cba9e6"
18
+ "composite": "0452703745228834870dff925469a260ce4eadf276f59bc0d4618e78459f9b25"
19
19
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The fly lexicon's chant audit catalog — metadata for the FLY post-synth
3
+ * checks, contributed via `flyPlugin.auditCatalog()` (#687).
4
+ *
5
+ * Both entries carry `yamlBased: false`: each check reads the chant model
6
+ * (`ctx.entities`) rather than the emitted manifest, because what they need —
7
+ * whether a mount's volume is declared in the same stack — lives in the typed
8
+ * graph and is gone by the time `fly.toml` exists. So they fire on
9
+ * `chant build` and cannot fire on an audit of standalone Fly config.
10
+ * `auditRule()` hardcodes `yamlBased: true`, so these are constructed directly
11
+ * (the same reasoning as fountain's catalog).
12
+ *
13
+ * Without this the two checks reached `chant audit` with no title, tier, fix
14
+ * kind, or category at all — `resolveAuditCatalog` contributes nothing for a
15
+ * lexicon that omits the method, silently (#1346).
16
+ */
17
+ import type { RuleMeta } from "@intentius/chant/audit/catalog";
18
+ export declare const flyAuditCatalog: Record<string, RuleMeta>;
19
+ //# sourceMappingURL=audit-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAa/D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAepD,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fly",
3
- "version": "0.37.2",
3
+ "version": "0.39.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Fly"
6
6
  }
@@ -1,3 +1,4 @@
1
+ import { type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
1
2
  export interface FlapsUpArgs {
2
3
  /** Container name. Default: `chant-mudflaps`. */
3
4
  name?: string;
@@ -14,6 +15,15 @@ export interface FlapsDownArgs {
14
15
  /** Container name to remove. Default: `chant-mudflaps`. */
15
16
  name?: string;
16
17
  }
18
+ export declare const MUDFLAPS_SPEC: EmulatorSpec;
19
+ /**
20
+ * The Machines half of fly's emulator capability (#1345).
21
+ *
22
+ * fly ships two emulators, and `LexiconPlugin.emulator` held exactly one until
23
+ * #1345 widened it — so neither was declared and `chant emulator` reported fly
24
+ * as having none, while the tutorials booted mudflaps through `chant run`.
25
+ */
26
+ export declare const MUDFLAPS_EMULATOR: EmulatorCapability;
17
27
  export declare const flapsExistsCommand: (name: string) => string;
18
28
  export declare const flapsRmCommand: (name: string) => string;
19
29
  export declare const flapsHealthUrl: (port: number) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"flaps.d.ts","sourceRoot":"","sources":["../../../src/op/activities/flaps.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAYD,eAAO,MAAM,kBAAkB,0BAAsB,CAAC;AACtD,eAAO,MAAM,cAAc,0BAAkB,CAAC;AAC9C,eAAO,MAAM,cAAc,0BAAkB,CAAC;AAC9C,4FAA4F;AAC5F,eAAO,MAAM,aAAa,0BAAiB,CAAC;AAC5C,eAAO,MAAM,eAAe,GAAI,OAAM,WAAgB,KAAG,MAAgC,CAAC;AAE1F,2FAA2F;AAC3F,eAAO,MAAM,OAAO,GAAI,OAAM,WAAgB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAC3E,CAAC;AAEzB,4EAA4E;AAC5E,eAAO,MAAM,SAAS,GAAI,OAAM,aAAkB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CAC7D,CAAC"}
1
+ {"version":3,"file":"flaps.d.ts","sourceRoot":"","sources":["../../../src/op/activities/flaps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGpG,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAKD,eAAO,MAAM,aAAa,EAAE,YAM3B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC;AAIF,eAAO,MAAM,kBAAkB,0BAAsB,CAAC;AACtD,eAAO,MAAM,cAAc,0BAAkB,CAAC;AAC9C,eAAO,MAAM,cAAc,0BAAkB,CAAC;AAC9C,4FAA4F;AAC5F,eAAO,MAAM,aAAa,0BAAiB,CAAC;AAC5C,eAAO,MAAM,eAAe,GAAI,OAAM,WAAgB,KAAG,MAAgC,CAAC;AAE1F,2FAA2F;AAC3F,eAAO,MAAM,OAAO,GAAI,OAAM,WAAgB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAC3E,CAAC;AAEzB,4EAA4E;AAC5E,eAAO,MAAM,SAAS,GAAI,OAAM,aAAkB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CAC7D,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
1
2
  export interface SpritesUpArgs {
2
3
  /** Container name. Default: `chant-spritzer`. */
3
4
  name?: string;
@@ -14,6 +15,9 @@ export interface SpritesDownArgs {
14
15
  /** Container name to remove. Default: `chant-spritzer`. */
15
16
  name?: string;
16
17
  }
18
+ export declare const SPRITZER_SPEC: EmulatorSpec;
19
+ /** The Sprites half of fly's emulator capability (#1345). */
20
+ export declare const SPRITZER_EMULATOR: EmulatorCapability;
17
21
  export declare const spritesExistsCommand: (name: string) => string;
18
22
  export declare const spritesRmCommand: (name: string) => string;
19
23
  export declare const spritesHealthUrl: (port: number) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"sprites-emulator.d.ts","sourceRoot":"","sources":["../../../src/op/activities/sprites-emulator.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAaD,eAAO,MAAM,oBAAoB,0BAAyB,CAAC;AAC3D,eAAO,MAAM,gBAAgB,0BAAqB,CAAC;AACnD,eAAO,MAAM,gBAAgB,0BAAqB,CAAC;AACnD,sGAAsG;AACtG,eAAO,MAAM,eAAe,0BAAoB,CAAC;AACjD,eAAO,MAAM,iBAAiB,GAAI,OAAM,aAAkB,KAAG,MAAmC,CAAC;AAEjG,0FAA0F;AAC1F,eAAO,MAAM,SAAS,GAAI,OAAM,aAAkB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAC5E,CAAC;AAE5B,4EAA4E;AAC5E,eAAO,MAAM,WAAW,GAAI,OAAM,eAAoB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CAC9D,CAAC"}
1
+ {"version":3,"file":"sprites-emulator.d.ts","sourceRoot":"","sources":["../../../src/op/activities/sprites-emulator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGpG,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,eAAO,MAAM,aAAa,EAAE,YAM3B,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC;AAIF,eAAO,MAAM,oBAAoB,0BAAyB,CAAC;AAC3D,eAAO,MAAM,gBAAgB,0BAAqB,CAAC;AACnD,eAAO,MAAM,gBAAgB,0BAAqB,CAAC;AACnD,sGAAsG;AACtG,eAAO,MAAM,eAAe,0BAAoB,CAAC;AACjD,eAAO,MAAM,iBAAiB,GAAI,OAAM,aAAkB,KAAG,MAAmC,CAAC;AAEjG,0FAA0F;AAC1F,eAAO,MAAM,SAAS,GAAI,OAAM,aAAkB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAC5E,CAAC;AAE5B,4EAA4E;AAC5E,eAAO,MAAM,WAAW,GAAI,OAAM,eAAoB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CAC9D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAkD,MAAM,0BAA0B,CAAC;AAmB9G;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aA2RvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAkD,MAAM,0BAA0B,CAAC;AAuB9G;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aA+RvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-fly",
3
- "version": "0.37.2",
3
+ "version": "0.39.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -57,7 +57,7 @@
57
57
  "typescript": "^5.9.3"
58
58
  },
59
59
  "peerDependencies": {
60
- "@intentius/chant": "^0.37.2",
60
+ "@intentius/chant": "^0.39.0",
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "description": "Fly.io Machines lexicon for chant — declarative IaC in TypeScript",
@@ -2,6 +2,7 @@ import { describe, test, expect } from "vitest";
2
2
  import { describeObservationConformance } from "@intentius/chant-test-utils";
3
3
  import { describeResources, flyPlan } from "./describe-resources";
4
4
  import type { FlyHttp } from "./op/activities/fly-apply";
5
+ import { FLY_METADATA_OWNERSHIP_KEYS } from "./ownership";
5
6
 
6
7
  // Injected-HTTP unit tests, mirroring fly-apply.test.ts: a scripted flaps answers
7
8
  // list/get reads, and describeResources maps them to ResourceMetadata with the
@@ -219,6 +220,7 @@ describe("flyPlan: declared-vs-live change set (create/noop/delete/adopt)", () =
219
220
  // The shared conformance suite (#1089).
220
221
  describeObservationConformance({
221
222
  lexicon: "fly",
223
+ ownershipChannel: { keys: FLY_METADATA_OWNERSHIP_KEYS, reads: ["describeResources"] },
222
224
  scenarios: [
223
225
  {
224
226
  name: "a build output that is not a readable fly plan",
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The fly lexicon's chant audit catalog — metadata for the FLY post-synth
3
+ * checks, contributed via `flyPlugin.auditCatalog()` (#687).
4
+ *
5
+ * Both entries carry `yamlBased: false`: each check reads the chant model
6
+ * (`ctx.entities`) rather than the emitted manifest, because what they need —
7
+ * whether a mount's volume is declared in the same stack — lives in the typed
8
+ * graph and is gone by the time `fly.toml` exists. So they fire on
9
+ * `chant build` and cannot fire on an audit of standalone Fly config.
10
+ * `auditRule()` hardcodes `yamlBased: true`, so these are constructed directly
11
+ * (the same reasoning as fountain's catalog).
12
+ *
13
+ * Without this the two checks reached `chant audit` with no title, tier, fix
14
+ * kind, or category at all — `resolveAuditCatalog` contributes nothing for a
15
+ * lexicon that omits the method, silently (#1346).
16
+ */
17
+
18
+ import type { RuleMeta } from "@intentius/chant/audit/catalog";
19
+
20
+ /** Entity-based rule: everything fly ships. */
21
+ function rule(
22
+ id: string,
23
+ tier: RuleMeta["tier"],
24
+ category: RuleMeta["category"],
25
+ title: string,
26
+ remediation: string,
27
+ ): RuleMeta {
28
+ return { id, tier, fixKind: "guidance", category, title, remediation, yamlBased: false };
29
+ }
30
+
31
+ export const flyAuditCatalog: Record<string, RuleMeta> = {
32
+ FLY010: rule(
33
+ "FLY010",
34
+ "merge-worthy",
35
+ "correctness",
36
+ "Machine config has no image",
37
+ "Set `config.image` on the Machine — a Machine cannot boot without one.",
38
+ ),
39
+ FLY011: rule(
40
+ "FLY011",
41
+ "merge-worthy",
42
+ "correctness",
43
+ "Machine mount references an undeclared volume",
44
+ "Declare the Volume in the same stack, or point the mount at one that is.",
45
+ ),
46
+ };
@@ -1,4 +1,4 @@
1
- import { emulatorLifecycle } from "@intentius/chant/op";
1
+ import { emulatorLifecycle, type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
2
2
  import { MUDFLAPS_IMAGE } from "./emulator-images";
3
3
 
4
4
  export interface FlapsUpArgs {
@@ -22,12 +22,27 @@ export interface FlapsDownArgs {
22
22
  // mudflaps is a stateful fake of the Fly Machines API (flaps) — a plain 200 on
23
23
  // its health endpoint means ready. The local target for flyApply; point it there
24
24
  // with FLY_FLAPS_BASE_URL. Shared lifecycle: emulatorLifecycle (#746).
25
- const flaps = emulatorLifecycle({
25
+ export const MUDFLAPS_SPEC: EmulatorSpec = {
26
26
  name: "chant-mudflaps",
27
27
  image: MUDFLAPS_IMAGE,
28
28
  containerPort: 4280,
29
29
  healthPath: "/_mudflaps/health",
30
- });
30
+ upstream: { repo: "intentius/mudflaps" },
31
+ };
32
+
33
+ /**
34
+ * The Machines half of fly's emulator capability (#1345).
35
+ *
36
+ * fly ships two emulators, and `LexiconPlugin.emulator` held exactly one until
37
+ * #1345 widened it — so neither was declared and `chant emulator` reported fly
38
+ * as having none, while the tutorials booted mudflaps through `chant run`.
39
+ */
40
+ export const MUDFLAPS_EMULATOR: EmulatorCapability = {
41
+ spec: MUDFLAPS_SPEC,
42
+ env: (endpoint) => ({ FLY_FLAPS_BASE_URL: endpoint }),
43
+ };
44
+
45
+ const flaps = emulatorLifecycle(MUDFLAPS_SPEC);
31
46
 
32
47
  export const flapsExistsCommand = flaps.existsCommand;
33
48
  export const flapsRmCommand = flaps.rmCommand;
@@ -1,4 +1,4 @@
1
- import { emulatorLifecycle } from "@intentius/chant/op";
1
+ import { emulatorLifecycle, type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
2
2
  import { SPRITZER_IMAGE } from "./emulator-images";
3
3
 
4
4
  export interface SpritesUpArgs {
@@ -23,12 +23,21 @@ export interface SpritesDownArgs {
23
23
  // endpoint means ready. The local target for the sprite activities; point them
24
24
  // there with SPRITES_BASE_URL. Shared lifecycle: emulatorLifecycle (the same
25
25
  // helper that boots mudflaps for fly).
26
- const spritzer = emulatorLifecycle({
26
+ export const SPRITZER_SPEC: EmulatorSpec = {
27
27
  name: "chant-spritzer",
28
28
  image: SPRITZER_IMAGE,
29
29
  containerPort: 4290,
30
30
  healthPath: "/_spritzer/health",
31
- });
31
+ upstream: { repo: "intentius/spritzer" },
32
+ };
33
+
34
+ /** The Sprites half of fly's emulator capability (#1345). */
35
+ export const SPRITZER_EMULATOR: EmulatorCapability = {
36
+ spec: SPRITZER_SPEC,
37
+ env: (endpoint) => ({ SPRITES_BASE_URL: endpoint }),
38
+ };
39
+
40
+ const spritzer = emulatorLifecycle(SPRITZER_SPEC);
32
41
 
33
42
  export const spritesExistsCommand = spritzer.existsCommand;
34
43
  export const spritesRmCommand = spritzer.rmCommand;
package/src/plugin.ts CHANGED
@@ -4,6 +4,9 @@ import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
4
4
  import type { CompletionContext, CompletionItem, HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
5
5
  import type { McpToolContribution, McpResourceContribution } from "@intentius/chant/mcp/types";
6
6
  import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
7
+ import { MUDFLAPS_EMULATOR } from "./op/activities/flaps";
8
+ import { SPRITZER_EMULATOR } from "./op/activities/sprites-emulator";
9
+ import { flyAuditCatalog } from "./lint/audit-catalog";
7
10
  import type { TemplateParser } from "@intentius/chant/import/parser";
8
11
  import type { TypeScriptGenerator } from "@intentius/chant/import/generator";
9
12
  import { flySerializer } from "./serializer";
@@ -16,6 +19,7 @@ import { FlyParser } from "./import/parser";
16
19
  import { FlyGenerator } from "./import/generator";
17
20
  import { flyReferenceCatalog } from "./reference-catalog";
18
21
  import { flyContextTools } from "./mcp/context-tools";
22
+ import { FLY_METADATA_OWNERSHIP_KEYS } from "./ownership";
19
23
 
20
24
  /**
21
25
  * fly lexicon plugin.
@@ -24,6 +28,10 @@ import { flyContextTools } from "./mcp/context-tools";
24
28
  */
25
29
  export const flyPlugin: LexiconPlugin = {
26
30
  name: "fly",
31
+ ownershipChannel: { keys: FLY_METADATA_OWNERSHIP_KEYS, reads: ["describeResources"] },
32
+ // Two: the Machines API and Sprites (#1345).
33
+ emulator: [MUDFLAPS_EMULATOR, SPRITZER_EMULATOR],
34
+ auditCatalog: () => flyAuditCatalog,
27
35
  serializer: flySerializer,
28
36
 
29
37
  // ── Required lifecycle methods ────────────────────────────────
@@ -1,11 +0,0 @@
1
- /**
2
- * CLI for the emulator-freshness check (#808 T2). Fetches the latest mudflaps /
3
- * spritzer releases, compares to the pinned tags, prints a report, and — when
4
- * run in CI — writes `behind` + a Markdown `body` to `$GITHUB_OUTPUT` so the
5
- * weekly workflow can open/refresh a single "N releases behind" notice issue.
6
- *
7
- * Advisory only: exits 0 whether or not a pin is behind (the bump is a human
8
- * decision per the #808 policy). A hard failure (network/API) exits 1.
9
- */
10
- export {};
11
- //# sourceMappingURL=emulator-freshness-cli.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"emulator-freshness-cli.d.ts","sourceRoot":"","sources":["../src/emulator-freshness-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -1,38 +0,0 @@
1
- /**
2
- * Emulator-freshness check (#808 T2, second half).
3
- *
4
- * mudflaps (Machines) and spritzer (Sprites) are pinned to a single source
5
- * (./op/activities/emulator-images.ts). Their upstream repos cut GitHub releases,
6
- * so this compares each pinned tag against the latest release and reports how far
7
- * behind it is. A weekly workflow surfaces a "N releases behind" notice — never
8
- * an auto-bump. Per the bump policy (#808), the tag moves only when a consuming
9
- * test needs the newer emulator, so this is advisory, not gating on the pin.
10
- */
11
- export interface EmulatorPin {
12
- /** Short name, e.g. "mudflaps". */
13
- name: string;
14
- /** GitHub repo "owner/name" whose releases publish the emulator. */
15
- repo: string;
16
- /** Pinned version, no leading "v" (e.g. "0.3.1"). */
17
- pinned: string;
18
- }
19
- export interface FreshnessResult {
20
- name: string;
21
- pinned: string;
22
- latest: string;
23
- /** True when the latest release is newer than the pinned version. */
24
- behind: boolean;
25
- }
26
- /** Parse the version tag from a ghcr image ref (".../mudflaps:0.3.1" → "0.3.1"). */
27
- export declare function parseVersion(image: string): string;
28
- /** Compare pinned vs latest as dotted numeric versions; `behind` when latest > pinned. */
29
- export declare function compare(name: string, pinned: string, latest: string): FreshnessResult;
30
- /** The emulator pins to check, read from the single-source image constants. */
31
- export declare const EMULATOR_PINS: readonly EmulatorPin[];
32
- /** Fetch the latest release tag for a repo via the GitHub REST API. */
33
- export declare function latestRelease(repo: string, fetchImpl?: typeof fetch): Promise<string>;
34
- /** Check every emulator pin against its latest release. */
35
- export declare function checkFreshness(fetchImpl?: typeof fetch): Promise<FreshnessResult[]>;
36
- /** One-line human summary of a result. */
37
- export declare function formatResult(r: FreshnessResult): string;
38
- //# sourceMappingURL=emulator-freshness.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"emulator-freshness.d.ts","sourceRoot":"","sources":["../src/emulator-freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGlD;AAMD,0FAA0F;AAC1F,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,CAcrF;AAED,+EAA+E;AAC/E,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAGtC,CAAC;AAEX,uEAAuE;AACvE,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAQlG;AAED,2DAA2D;AAC3D,wBAAsB,cAAc,CAAC,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAIhG;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAIvD"}
@@ -1,49 +0,0 @@
1
- /**
2
- * CLI for the emulator-freshness check (#808 T2). Fetches the latest mudflaps /
3
- * spritzer releases, compares to the pinned tags, prints a report, and — when
4
- * run in CI — writes `behind` + a Markdown `body` to `$GITHUB_OUTPUT` so the
5
- * weekly workflow can open/refresh a single "N releases behind" notice issue.
6
- *
7
- * Advisory only: exits 0 whether or not a pin is behind (the bump is a human
8
- * decision per the #808 policy). A hard failure (network/API) exits 1.
9
- */
10
-
11
- import { appendFileSync } from "node:fs";
12
- import { checkFreshness, formatResult, type FreshnessResult } from "./emulator-freshness";
13
-
14
- function issueBody(behind: FreshnessResult[]): string {
15
- const rows = behind.map((r) => `- **${r.name}** — pinned \`${r.pinned}\`, latest \`${r.latest}\``).join("\n");
16
- return [
17
- "The pinned Fly emulator image(s) are behind their latest upstream release:",
18
- "",
19
- rows,
20
- "",
21
- "The tag lives in `lexicons/fly/src/op/activities/emulator-images.ts` (single source).",
22
- "",
23
- "Per the #808 bump policy this is **advisory** — move the pin only when a consuming",
24
- "test needs the newer emulator (a fidelity fix the fly activities exercise), not on",
25
- "every release. Close this issue once reviewed or bumped.",
26
- ].join("\n");
27
- }
28
-
29
- async function main(): Promise<void> {
30
- const results = await checkFreshness();
31
- for (const r of results) console.error(formatResult(r));
32
-
33
- const behind = results.filter((r) => r.behind);
34
- console.log(JSON.stringify({ behind: behind.length > 0, results }, null, 2));
35
-
36
- const out = process.env.GITHUB_OUTPUT;
37
- if (out) {
38
- appendFileSync(out, `behind=${behind.length > 0}\n`);
39
- if (behind.length > 0) {
40
- // Multiline output via the GITHUB_OUTPUT heredoc form.
41
- appendFileSync(out, `body<<FRESHNESS_EOF\n${issueBody(behind)}\nFRESHNESS_EOF\n`);
42
- }
43
- }
44
- }
45
-
46
- main().catch((err: unknown) => {
47
- console.error(err instanceof Error ? err.message : String(err));
48
- process.exit(1);
49
- });
@@ -1,86 +0,0 @@
1
- import { describe, test, expect } from "vitest";
2
- import {
3
- parseVersion,
4
- compare,
5
- latestRelease,
6
- checkFreshness,
7
- formatResult,
8
- EMULATOR_PINS,
9
- } from "./emulator-freshness";
10
-
11
- describe("parseVersion", () => {
12
- test("extracts the version from a ghcr image ref, stripping a leading v", () => {
13
- expect(parseVersion("ghcr.io/intentius/mudflaps:0.3.1")).toBe("0.3.1");
14
- expect(parseVersion("ghcr.io/intentius/spritzer:v1.2.0")).toBe("1.2.0");
15
- });
16
- });
17
-
18
- describe("compare", () => {
19
- test("not behind when pinned equals latest", () => {
20
- expect(compare("mudflaps", "0.3.1", "v0.3.1").behind).toBe(false);
21
- });
22
- test("behind when latest is a newer patch/minor/major", () => {
23
- expect(compare("m", "0.3.1", "v0.3.2").behind).toBe(true);
24
- expect(compare("m", "0.3.1", "v0.4.0").behind).toBe(true);
25
- expect(compare("m", "0.3.1", "v1.0.0").behind).toBe(true);
26
- });
27
- test("not behind when pinned is ahead of latest", () => {
28
- expect(compare("m", "0.4.0", "v0.3.9").behind).toBe(false);
29
- });
30
- test("normalizes the leading v out of the reported versions", () => {
31
- const r = compare("m", "0.3.1", "v0.3.2");
32
- expect(r.pinned).toBe("0.3.1");
33
- expect(r.latest).toBe("0.3.2");
34
- });
35
- });
36
-
37
- describe("EMULATOR_PINS", () => {
38
- test("tracks mudflaps and spritzer from the single-source image constants", () => {
39
- expect(EMULATOR_PINS.map((p) => p.name).sort()).toEqual(["mudflaps", "spritzer"]);
40
- for (const p of EMULATOR_PINS) {
41
- expect(p.repo).toMatch(/^intentius\//);
42
- expect(p.pinned).toMatch(/^\d+\.\d+\.\d+$/);
43
- }
44
- });
45
- });
46
-
47
- describe("latestRelease + checkFreshness (mocked fetch)", () => {
48
- const mkFetch = (tags: Record<string, string>): typeof fetch =>
49
- (async (url: string | URL | Request) => {
50
- const u = String(url);
51
- const repo = u.match(/repos\/([^/]+\/[^/]+)\/releases/)?.[1] ?? "";
52
- return { ok: true, status: 200, json: async () => ({ tag_name: tags[repo] }) } as Response;
53
- }) as unknown as typeof fetch;
54
-
55
- test("latestRelease returns the tag_name", async () => {
56
- const f = mkFetch({ "intentius/mudflaps": "v0.9.0" });
57
- expect(await latestRelease("intentius/mudflaps", f)).toBe("v0.9.0");
58
- });
59
-
60
- test("checkFreshness flags a pin behind its latest release", async () => {
61
- // Force both upstreams to a high version so the check reports behind
62
- // regardless of the currently-pinned tag.
63
- const f = mkFetch({ "intentius/mudflaps": "v99.0.0", "intentius/spritzer": "v99.0.0" });
64
- const results = await checkFreshness(f);
65
- expect(results).toHaveLength(2);
66
- expect(results.every((r) => r.behind)).toBe(true);
67
- });
68
-
69
- test("checkFreshness reports current when latest matches the pin", async () => {
70
- const pins = Object.fromEntries(EMULATOR_PINS.map((p) => [p.repo, `v${p.pinned}`]));
71
- const results = await checkFreshness(mkFetch(pins));
72
- expect(results.every((r) => !r.behind)).toBe(true);
73
- });
74
-
75
- test("latestRelease throws on a non-ok response", async () => {
76
- const f = (async () => ({ ok: false, status: 404 }) as Response) as unknown as typeof fetch;
77
- await expect(latestRelease("intentius/nope", f)).rejects.toThrow(/HTTP 404/);
78
- });
79
- });
80
-
81
- describe("formatResult", () => {
82
- test("marks behind vs current distinctly", () => {
83
- expect(formatResult({ name: "m", pinned: "0.3.1", latest: "0.4.0", behind: true })).toMatch(/behind/);
84
- expect(formatResult({ name: "m", pinned: "0.3.1", latest: "0.3.1", behind: false })).toMatch(/current/);
85
- });
86
- });
@@ -1,87 +0,0 @@
1
- /**
2
- * Emulator-freshness check (#808 T2, second half).
3
- *
4
- * mudflaps (Machines) and spritzer (Sprites) are pinned to a single source
5
- * (./op/activities/emulator-images.ts). Their upstream repos cut GitHub releases,
6
- * so this compares each pinned tag against the latest release and reports how far
7
- * behind it is. A weekly workflow surfaces a "N releases behind" notice — never
8
- * an auto-bump. Per the bump policy (#808), the tag moves only when a consuming
9
- * test needs the newer emulator, so this is advisory, not gating on the pin.
10
- */
11
-
12
- import { MUDFLAPS_IMAGE, SPRITZER_IMAGE } from "./op/activities/emulator-images";
13
-
14
- export interface EmulatorPin {
15
- /** Short name, e.g. "mudflaps". */
16
- name: string;
17
- /** GitHub repo "owner/name" whose releases publish the emulator. */
18
- repo: string;
19
- /** Pinned version, no leading "v" (e.g. "0.3.1"). */
20
- pinned: string;
21
- }
22
-
23
- export interface FreshnessResult {
24
- name: string;
25
- pinned: string;
26
- latest: string;
27
- /** True when the latest release is newer than the pinned version. */
28
- behind: boolean;
29
- }
30
-
31
- /** Parse the version tag from a ghcr image ref (".../mudflaps:0.3.1" → "0.3.1"). */
32
- export function parseVersion(image: string): string {
33
- const tag = image.slice(image.lastIndexOf(":") + 1);
34
- return tag.replace(/^v/, "");
35
- }
36
-
37
- function toParts(v: string): number[] {
38
- return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
39
- }
40
-
41
- /** Compare pinned vs latest as dotted numeric versions; `behind` when latest > pinned. */
42
- export function compare(name: string, pinned: string, latest: string): FreshnessResult {
43
- const p = toParts(pinned);
44
- const l = toParts(latest);
45
- let behind = false;
46
- for (let i = 0; i < Math.max(p.length, l.length); i++) {
47
- const a = p[i] ?? 0;
48
- const b = l[i] ?? 0;
49
- if (b > a) {
50
- behind = true;
51
- break;
52
- }
53
- if (b < a) break;
54
- }
55
- return { name, pinned: pinned.replace(/^v/, ""), latest: latest.replace(/^v/, ""), behind };
56
- }
57
-
58
- /** The emulator pins to check, read from the single-source image constants. */
59
- export const EMULATOR_PINS: readonly EmulatorPin[] = [
60
- { name: "mudflaps", repo: "intentius/mudflaps", pinned: parseVersion(MUDFLAPS_IMAGE) },
61
- { name: "spritzer", repo: "intentius/spritzer", pinned: parseVersion(SPRITZER_IMAGE) },
62
- ] as const;
63
-
64
- /** Fetch the latest release tag for a repo via the GitHub REST API. */
65
- export async function latestRelease(repo: string, fetchImpl: typeof fetch = fetch): Promise<string> {
66
- const headers: Record<string, string> = { Accept: "application/vnd.github+json" };
67
- if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
68
- const res = await fetchImpl(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
69
- if (!res.ok) throw new Error(`releases/latest ${repo}: HTTP ${res.status}`);
70
- const body = (await res.json()) as { tag_name?: string };
71
- if (!body.tag_name) throw new Error(`releases/latest ${repo}: no tag_name`);
72
- return body.tag_name;
73
- }
74
-
75
- /** Check every emulator pin against its latest release. */
76
- export async function checkFreshness(fetchImpl: typeof fetch = fetch): Promise<FreshnessResult[]> {
77
- return Promise.all(
78
- EMULATOR_PINS.map(async (p) => compare(p.name, p.pinned, await latestRelease(p.repo, fetchImpl))),
79
- );
80
- }
81
-
82
- /** One-line human summary of a result. */
83
- export function formatResult(r: FreshnessResult): string {
84
- return r.behind
85
- ? `⚠ ${r.name}: pinned ${r.pinned}, latest ${r.latest} — behind`
86
- : `✓ ${r.name}: pinned ${r.pinned} is current (latest ${r.latest})`;
87
- }