@intentius/chant-lexicon-render 0.53.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/README.md +89 -0
- package/dist/catalog.d.ts +100 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +21 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +6 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-lexicon.d.ts +18 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -0
- package/dist/codegen/generate-typescript.d.ts +12 -0
- package/dist/codegen/generate-typescript.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +15 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +14 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +17 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/render-deploy.d.ts +88 -0
- package/dist/composites/render-deploy.d.ts.map +1 -0
- package/dist/coverage.d.ts +15 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +60 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +12 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/generated/index.d.ts +38 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +5 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +18 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts +12 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts.map +1 -0
- package/dist/lint/post-synth/ren011-service-requires-source.d.ts +12 -0
- package/dist/lint/post-synth/ren011-service-requires-source.d.ts.map +1 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts +11 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts.map +1 -0
- package/dist/lint/post-synth/render-helpers.d.ts +19 -0
- package/dist/lint/post-synth/render-helpers.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +7 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/no-secret-literals.d.ts +13 -0
- package/dist/lint/rules/no-secret-literals.d.ts.map +1 -0
- package/dist/lint/rules/valid-cron-schedule.d.ts +12 -0
- package/dist/lint/rules/valid-cron-schedule.d.ts.map +1 -0
- package/dist/lint/rules/valid-region.d.ts +16 -0
- package/dist/lint/rules/valid-region.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +8 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +6 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +304 -0
- package/dist/okf/index.md +29 -0
- package/dist/okf/rules/REN001.md +11 -0
- package/dist/okf/rules/REN002.md +15 -0
- package/dist/okf/rules/REN003.md +15 -0
- package/dist/okf/rules/REN010.md +11 -0
- package/dist/okf/rules/REN011.md +11 -0
- package/dist/okf/rules/REN012.md +11 -0
- package/dist/okf/types/BackgroundWorker.md +24 -0
- package/dist/okf/types/CronJob.md +28 -0
- package/dist/okf/types/CustomDomain.md +14 -0
- package/dist/okf/types/Disk.md +16 -0
- package/dist/okf/types/EnvGroup.md +22 -0
- package/dist/okf/types/Environment.md +17 -0
- package/dist/okf/types/KeyValue.md +20 -0
- package/dist/okf/types/Postgres.md +29 -0
- package/dist/okf/types/PrivateService.md +24 -0
- package/dist/okf/types/Project.md +15 -0
- package/dist/okf/types/RegistryCredential.md +17 -0
- package/dist/okf/types/StaticSite.md +24 -0
- package/dist/okf/types/WebService.md +24 -0
- package/dist/okf/types/Webhook.md +17 -0
- package/dist/op/activities/fake-render.d.ts +52 -0
- package/dist/op/activities/fake-render.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/render-apply.d.ts +229 -0
- package/dist/op/activities/render-apply.d.ts.map +1 -0
- package/dist/ownership.d.ts +32 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pseudo.d.ts +21 -0
- package/dist/pseudo.d.ts.map +1 -0
- package/dist/rules/no-secret-literals.ts +73 -0
- package/dist/rules/ren010-native-runtime-requires-commands.ts +47 -0
- package/dist/rules/ren011-service-requires-source.ts +50 -0
- package/dist/rules/ren012-free-plan-cannot-scale.ts +43 -0
- package/dist/rules/render-helpers.ts +38 -0
- package/dist/rules/valid-cron-schedule.ts +51 -0
- package/dist/rules/valid-region.ts +49 -0
- package/dist/serializer.d.ts +99 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-render-patterns.md +126 -0
- package/dist/skills/chant-render.md +112 -0
- package/dist/spec/fetch.d.ts +21 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +120 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/types/index.d.ts +573 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +73 -0
- package/src/catalog.ts +286 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +120 -0
- package/src/codegen/generate-cli.ts +36 -0
- package/src/codegen/generate-lexicon.ts +52 -0
- package/src/codegen/generate-typescript.ts +70 -0
- package/src/codegen/generate.ts +106 -0
- package/src/codegen/naming.ts +55 -0
- package/src/codegen/package.ts +30 -0
- package/src/composites/render-deploy.ts +133 -0
- package/src/coverage.ts +49 -0
- package/src/describe-resources.test.ts +143 -0
- package/src/describe-resources.ts +366 -0
- package/src/detect.ts +29 -0
- package/src/generated/index.d.ts +573 -0
- package/src/generated/index.ts +43 -0
- package/src/generated/lexicon-render.json +304 -0
- package/src/generated/runtime.ts +4 -0
- package/src/index.ts +30 -0
- package/src/lint/audit-catalog.ts +47 -0
- package/src/lint/post-synth/index.ts +11 -0
- package/src/lint/post-synth/post-synth.test.ts +151 -0
- package/src/lint/post-synth/ren010-native-runtime-requires-commands.ts +47 -0
- package/src/lint/post-synth/ren011-service-requires-source.ts +50 -0
- package/src/lint/post-synth/ren012-free-plan-cannot-scale.ts +43 -0
- package/src/lint/post-synth/render-helpers.ts +38 -0
- package/src/lint/rules/index.ts +11 -0
- package/src/lint/rules/no-secret-literals.ts +73 -0
- package/src/lint/rules/rules.test.ts +73 -0
- package/src/lint/rules/valid-cron-schedule.ts +51 -0
- package/src/lint/rules/valid-region.ts +49 -0
- package/src/lsp/completions.test.ts +28 -0
- package/src/lsp/completions.ts +22 -0
- package/src/lsp/hover.test.ts +32 -0
- package/src/lsp/hover.ts +50 -0
- package/src/op/activities/fake-render.ts +275 -0
- package/src/op/activities/index.ts +51 -0
- package/src/op/activities/render-apply.test.ts +403 -0
- package/src/op/activities/render-apply.ts +1042 -0
- package/src/ownership.ts +44 -0
- package/src/package-cli.ts +23 -0
- package/src/plugin.test.ts +54 -0
- package/src/plugin.ts +258 -0
- package/src/pseudo.ts +26 -0
- package/src/serializer.test.ts +221 -0
- package/src/serializer.ts +286 -0
- package/src/skills/chant-render-patterns.md +126 -0
- package/src/skills/chant-render.md +112 -0
- package/src/spec/fetch.ts +33 -0
- package/src/spec/parse.ts +721 -0
- package/src/validate-cli.ts +5 -0
- package/src/validate.ts +47 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render naming strategy — maps Render::{Group}::{Kind} type names to
|
|
3
|
+
* user-friendly TypeScript class names.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
NamingStrategy as CoreNamingStrategy,
|
|
8
|
+
type NamingConfig,
|
|
9
|
+
type NamingInput,
|
|
10
|
+
} from "@intentius/chant/codegen/naming";
|
|
11
|
+
import { renderShortName, renderServiceName, type RenderParseResult } from "../spec/parse";
|
|
12
|
+
|
|
13
|
+
export { propertyTypeName, extractDefName } from "@intentius/chant/codegen/naming";
|
|
14
|
+
|
|
15
|
+
const renderNamingConfig: NamingConfig = {
|
|
16
|
+
priorityNames: {
|
|
17
|
+
"Render::Services::WebService": "WebService",
|
|
18
|
+
"Render::Services::StaticSite": "StaticSite",
|
|
19
|
+
"Render::Services::PrivateService": "PrivateService",
|
|
20
|
+
"Render::Services::BackgroundWorker": "BackgroundWorker",
|
|
21
|
+
"Render::Services::CronJob": "CronJob",
|
|
22
|
+
"Render::Datastores::Postgres": "Postgres",
|
|
23
|
+
"Render::Datastores::KeyValue": "KeyValue",
|
|
24
|
+
"Render::Config::EnvGroup": "EnvGroup",
|
|
25
|
+
"Render::Projects::Project": "Project",
|
|
26
|
+
"Render::Projects::Environment": "Environment",
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
priorityAliases: {},
|
|
30
|
+
|
|
31
|
+
priorityPropertyAliases: {},
|
|
32
|
+
|
|
33
|
+
serviceAbbreviations: {
|
|
34
|
+
Services: "Services",
|
|
35
|
+
Datastores: "Datastores",
|
|
36
|
+
Config: "Config",
|
|
37
|
+
Projects: "Projects",
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
shortName: renderShortName,
|
|
41
|
+
serviceName: renderServiceName,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render-specific naming strategy. Extends the core strategy with the render config.
|
|
46
|
+
*/
|
|
47
|
+
export class NamingStrategy extends CoreNamingStrategy {
|
|
48
|
+
constructor(results: RenderParseResult[]) {
|
|
49
|
+
const inputs: NamingInput[] = results.map((r) => ({
|
|
50
|
+
typeName: r.resource.typeName,
|
|
51
|
+
propertyTypes: r.propertyTypes,
|
|
52
|
+
}));
|
|
53
|
+
super(inputs, renderNamingConfig);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
|
|
2
|
+
import { generate } from "./generate";
|
|
3
|
+
import { renderPlugin } from "../plugin";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Package the render lexicon for distribution.
|
|
10
|
+
*/
|
|
11
|
+
export async function packageLexicon(options?: { verbose?: boolean; force?: boolean }) {
|
|
12
|
+
// This file is src/codegen/package.ts — rules and skills are collected
|
|
13
|
+
// relative to src/, so srcDir must be the parent of this directory.
|
|
14
|
+
const srcDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
|
|
16
|
+
|
|
17
|
+
const { spec, stats } = await packagePipeline({
|
|
18
|
+
generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
|
|
19
|
+
buildManifest: (_genResult) => ({
|
|
20
|
+
name: "render",
|
|
21
|
+
version: pkgJson.version ?? "0.0.0",
|
|
22
|
+
chantVersion: ">=0.1.0",
|
|
23
|
+
namespace: "Render",
|
|
24
|
+
}),
|
|
25
|
+
srcDir,
|
|
26
|
+
collectSkills: () => collectSkills(renderPlugin.skills?.() ?? []),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return { spec, stats };
|
|
30
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render deploy Op — `build → renderApply [→ verify]` on the local executor.
|
|
3
|
+
* The peer of `flyDeploy`, for a platform with no local emulator: it targets a
|
|
4
|
+
* real Render workspace with a `RENDER_API_KEY`.
|
|
5
|
+
*
|
|
6
|
+
* The phases compose modeled activities, no raw shell:
|
|
7
|
+
* Build run the project's `build:render` npm script → the serialized plan
|
|
8
|
+
* Apply `renderApply` the plan straight to the Render Public API (creates
|
|
9
|
+
* or PATCHes each resource by name, waits each created service's
|
|
10
|
+
* deploy to `live`, prunes owned-only when asked)
|
|
11
|
+
* Verify optional: GET a public URL and assert on its body
|
|
12
|
+
*
|
|
13
|
+
* The activity steps are the generic `activity(fn, args)` builder tagged with
|
|
14
|
+
* the exported activity function names the render lexicon contributes
|
|
15
|
+
* (`renderApply`/`renderDelete` from `src/op/activities`). The core activity
|
|
16
|
+
* registry resolves them by name when a project lists the `render` lexicon.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Op, phase, build, activity, httpCheck } from "@intentius/chant/op";
|
|
20
|
+
import type { OpResource } from "@intentius/chant/op";
|
|
21
|
+
import type { ActivityStep } from "@intentius/chant/op";
|
|
22
|
+
|
|
23
|
+
/** Options for the render apply step (mirror `RenderApplyArgs`, minus `planPath`). */
|
|
24
|
+
export interface RenderApplyStepOpts {
|
|
25
|
+
/** API base URL override. Default: `RENDER_API_BASE_URL` env, else api.render.com. */
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
/** Bearer token. Default: `RENDER_API_KEY`. */
|
|
28
|
+
token?: string;
|
|
29
|
+
/** Workspace id for `$owner` markers. Default: `RENDER_OWNER_ID`, else the sole visible owner. */
|
|
30
|
+
ownerId?: string;
|
|
31
|
+
/** Prune chant-owned services/env groups the plan no longer declares. Destructive — off by default. */
|
|
32
|
+
prune?: boolean;
|
|
33
|
+
/** Deploy wait tuning. */
|
|
34
|
+
wait?: Record<string, unknown>;
|
|
35
|
+
/** Activity profile override. */
|
|
36
|
+
profile?: ActivityStep["profile"];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply the serialized render plan straight to the Render Public API — the
|
|
41
|
+
* typed twin of `flyApplyStep`. Resolves to the `renderApply` activity.
|
|
42
|
+
* Defaults to the `longInfra` profile (a first deploy builds and can take
|
|
43
|
+
* minutes); override via `opts.profile`.
|
|
44
|
+
*/
|
|
45
|
+
export const renderApplyStep = (planPath: string, opts?: RenderApplyStepOpts): ActivityStep => {
|
|
46
|
+
const { profile, ...rest } = opts ?? {};
|
|
47
|
+
return activity("renderApply", { planPath, ...(rest as Record<string, unknown>) }, profile ?? "longInfra");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Delete everything the plan declares, in reverse order — the teardown twin.
|
|
52
|
+
* Resolves to the `renderDelete` activity.
|
|
53
|
+
*/
|
|
54
|
+
export const renderDeleteStep = (planPath: string, opts?: RenderApplyStepOpts): ActivityStep => {
|
|
55
|
+
const { profile, ...rest } = opts ?? {};
|
|
56
|
+
return activity("renderDelete", { planPath, ...(rest as Record<string, unknown>) }, profile ?? "longInfra");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Options for {@link renderDeploy}. */
|
|
60
|
+
export interface RenderDeployOpts {
|
|
61
|
+
/** Op name (the `chant run <name>` target). Default: `render`. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Op overview line. */
|
|
64
|
+
overview?: string;
|
|
65
|
+
/** Temporal task queue (unused by the local executor). Default: `local-render`. */
|
|
66
|
+
taskQueue?: string;
|
|
67
|
+
/** Directory the `build:render` script and plan path are relative to. Default: `.`. */
|
|
68
|
+
path?: string;
|
|
69
|
+
/** npm build script to run. Default: `build:render`. */
|
|
70
|
+
buildScript?: string;
|
|
71
|
+
/** Path to the serialized render plan the build writes. Default: `dist/render.json`. */
|
|
72
|
+
planPath?: string;
|
|
73
|
+
/** API base URL override (a local stand-in). Default: real Render via `RENDER_API_BASE_URL` / `RENDER_API_KEY`. */
|
|
74
|
+
endpoint?: string;
|
|
75
|
+
/** Workspace id. Default: `RENDER_OWNER_ID`, else the sole visible owner. */
|
|
76
|
+
ownerId?: string;
|
|
77
|
+
/** Prune declared-then-removed owned resources on apply. Off by default. */
|
|
78
|
+
prune?: boolean;
|
|
79
|
+
/** Deploy wait tuning passed through to `renderApply`. */
|
|
80
|
+
wait?: Record<string, unknown>;
|
|
81
|
+
/**
|
|
82
|
+
* A public URL to GET after apply, with the text it must contain. When set,
|
|
83
|
+
* the Op adds a Verify phase. Omitted → no Verify (the Apply step still
|
|
84
|
+
* waits each created service's deploy to `live`).
|
|
85
|
+
*/
|
|
86
|
+
verify?: { url: string; contains?: string; retries?: number; intervalMs?: number };
|
|
87
|
+
/** Add a Teardown phase that deletes what the plan declares. Off by default. */
|
|
88
|
+
teardown?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a render deploy Op: build the plan, apply it to the workspace, verify
|
|
93
|
+
* a URL when asked, tear down when asked. See the module docstring.
|
|
94
|
+
*/
|
|
95
|
+
export function renderDeploy(opts: RenderDeployOpts = {}): InstanceType<typeof OpResource> {
|
|
96
|
+
const path = opts.path ?? ".";
|
|
97
|
+
const planPath = opts.planPath ?? "dist/render.json";
|
|
98
|
+
|
|
99
|
+
const applyOpts: RenderApplyStepOpts = {
|
|
100
|
+
...(opts.endpoint ? { endpoint: opts.endpoint } : {}),
|
|
101
|
+
...(opts.ownerId ? { ownerId: opts.ownerId } : {}),
|
|
102
|
+
...(opts.prune ? { prune: true } : {}),
|
|
103
|
+
...(opts.wait ? { wait: opts.wait } : {}),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const phases = [
|
|
107
|
+
phase("Build", [build(path, { script: opts.buildScript ?? "build:render" })]),
|
|
108
|
+
phase("Apply", [renderApplyStep(planPath, applyOpts)]),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (opts.verify) {
|
|
112
|
+
phases.push(
|
|
113
|
+
phase("Verify", [
|
|
114
|
+
httpCheck(opts.verify.url, {
|
|
115
|
+
...(opts.verify.contains ? { contains: opts.verify.contains } : {}),
|
|
116
|
+
retries: opts.verify.retries ?? 30,
|
|
117
|
+
intervalMs: opts.verify.intervalMs ?? 5000,
|
|
118
|
+
}),
|
|
119
|
+
]),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (opts.teardown) {
|
|
124
|
+
phases.push(phase("Teardown", [renderDeleteStep(planPath, applyOpts)]));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Op({
|
|
128
|
+
name: opts.name ?? "render",
|
|
129
|
+
overview: opts.overview ?? "Render: build the plan, apply it to the workspace over the Public API, verify",
|
|
130
|
+
taskQueue: opts.taskQueue ?? "local-render",
|
|
131
|
+
phases,
|
|
132
|
+
});
|
|
133
|
+
}
|
package/src/coverage.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage analysis for the render lexicon — how much of the generated Render
|
|
3
|
+
* API surface the lexicon's typed resources cover. Thin wrapper over the shared
|
|
4
|
+
* `computeCoverage`, mirroring the fly/gcp peers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import {
|
|
11
|
+
computeCoverage,
|
|
12
|
+
overallPct,
|
|
13
|
+
formatSummary,
|
|
14
|
+
formatVerbose,
|
|
15
|
+
checkThresholds,
|
|
16
|
+
type CoverageReport,
|
|
17
|
+
type CoverageThresholds,
|
|
18
|
+
} from "@intentius/chant/codegen/coverage";
|
|
19
|
+
|
|
20
|
+
export type { CoverageReport, CoverageThresholds };
|
|
21
|
+
export { computeCoverage, overallPct, formatSummary, formatVerbose, checkThresholds };
|
|
22
|
+
|
|
23
|
+
/** Run coverage analysis for the render lexicon. */
|
|
24
|
+
export async function analyzeRenderCoverage(opts?: {
|
|
25
|
+
basePath?: string;
|
|
26
|
+
verbose?: boolean;
|
|
27
|
+
minOverall?: number;
|
|
28
|
+
}): Promise<CoverageReport> {
|
|
29
|
+
const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
30
|
+
const lexiconPath = join(basePath, "src", "generated", "lexicon-render.json");
|
|
31
|
+
|
|
32
|
+
if (!existsSync(lexiconPath)) {
|
|
33
|
+
throw new Error(`Generated lexicon not found at ${lexiconPath}. Run "chant dev generate" first.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const report = computeCoverage(readFileSync(lexiconPath, "utf-8"));
|
|
37
|
+
|
|
38
|
+
console.error(opts?.verbose ? formatVerbose(report) : formatSummary(report));
|
|
39
|
+
|
|
40
|
+
if (typeof opts?.minOverall === "number") {
|
|
41
|
+
const result = checkThresholds(report, { minOverallPct: opts.minOverall });
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
for (const v of result.violations) console.error(` FAIL: ${v}`);
|
|
44
|
+
throw new Error("Coverage below threshold");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return report;
|
|
49
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import type { Declarable } from "@intentius/chant";
|
|
3
|
+
import { resolveAttrRefs } from "@intentius/chant/discovery/resolve";
|
|
4
|
+
import { serializeRender } from "./serializer";
|
|
5
|
+
import { describeResources } from "./describe-resources";
|
|
6
|
+
import { FakeRender } from "./op/activities/fake-render";
|
|
7
|
+
import { renderApply } from "./op/activities/render-apply";
|
|
8
|
+
import { WebService, WebServiceDetails, Postgres, Disk, CustomDomain, EnvGroup } from "./generated/index";
|
|
9
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
function stack(...entries: Array<[string, unknown]>): Map<string, Declarable> {
|
|
14
|
+
return new Map(entries as Array<[string, Declarable]>);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function build(entities: Map<string, Declarable>): { buildOutput: string; entities: Map<string, { entityType: string; props: Record<string, unknown> }> } {
|
|
18
|
+
resolveAttrRefs(entities);
|
|
19
|
+
const buildOutput = serializeRender(entities);
|
|
20
|
+
const meta = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
21
|
+
for (const [name, e] of entities) {
|
|
22
|
+
meta.set(name, { entityType: (e as unknown as { entityType: string }).entityType, props: (e as unknown as { props: Record<string, unknown> }).props ?? {} });
|
|
23
|
+
}
|
|
24
|
+
return { buildOutput, entities: meta };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const base = (b: ReturnType<typeof build>) => ({
|
|
28
|
+
environment: "dev",
|
|
29
|
+
buildOutput: b.buildOutput,
|
|
30
|
+
entityNames: [...b.entities.keys()],
|
|
31
|
+
entities: b.entities,
|
|
32
|
+
endpoint: "http://fake/v1",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("render describeResources", () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
delete process.env.RENDER_OWNER_ID;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reports absent before apply and present (with verdicts) after", async () => {
|
|
41
|
+
process.env.RENDER_OWNER_ID = "tea-1";
|
|
42
|
+
const fake = new FakeRender();
|
|
43
|
+
const w = new WebService({ name: "web", serviceDetails: new WebServiceDetails({ runtime: "docker" }) });
|
|
44
|
+
const b = build(stack(["web", w], ["db", new Postgres({ name: "db", plan: "free", version: "16" })], ["disk", new Disk({ name: "data", sizeGB: 1, mountPath: "/d", serviceId: w })]));
|
|
45
|
+
|
|
46
|
+
const before = await describeResources(base(b), fake.http());
|
|
47
|
+
expect(before.observation).toBe("v1");
|
|
48
|
+
expect(before.resources).toEqual({});
|
|
49
|
+
expect(before.unobserved).toBeUndefined();
|
|
50
|
+
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), "render-obs-"));
|
|
52
|
+
const planPath = join(dir, "render.json");
|
|
53
|
+
writeFileSync(planPath, b.buildOutput);
|
|
54
|
+
await renderApply({ planPath, endpoint: "http://fake/v1", wait: { intervalMs: 0 } }, undefined, fake.http());
|
|
55
|
+
|
|
56
|
+
const after = await describeResources(base(b), fake.http());
|
|
57
|
+
expect(Object.keys(after.resources).sort()).toEqual(["db", "disk", "web"]);
|
|
58
|
+
expect(after.resources.web.ownership).toBe("owned");
|
|
59
|
+
expect(after.resources.web.type).toBe("Render::Services::WebService");
|
|
60
|
+
expect(after.resources.web.physicalId).toMatch(/^srv-/);
|
|
61
|
+
expect(after.resources.db.ownership).toBe("unknown");
|
|
62
|
+
// Service boundary: the disk inherits its owned parent's verdict.
|
|
63
|
+
expect(after.resources.disk.ownership).toBe("owned");
|
|
64
|
+
expect(after.resources.disk.attributes?.serviceId).toBe(after.resources.web.physicalId);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("disks and domains inherit a foreign parent's verdict, and undeclared ones under an owned service are owned orphans", async () => {
|
|
68
|
+
process.env.RENDER_OWNER_ID = "tea-1";
|
|
69
|
+
const fake = new FakeRender();
|
|
70
|
+
const foreign = fake.seed("/services", { name: "theirs", type: "web_service", ownerId: "tea-1", envVars: [] });
|
|
71
|
+
fake.seed("/disks", { name: "fd", sizeGB: 1, mountPath: "/d", serviceId: foreign.id });
|
|
72
|
+
const w = new WebService({ name: "web", serviceDetails: new WebServiceDetails({ runtime: "docker" }) });
|
|
73
|
+
const b = build(
|
|
74
|
+
stack(
|
|
75
|
+
["web", w],
|
|
76
|
+
["disk", new Disk({ name: "data", sizeGB: 1, mountPath: "/d", serviceId: w })],
|
|
77
|
+
["fdisk", new Disk({ name: "fd", sizeGB: 1, mountPath: "/d", serviceId: foreign.id as string })],
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const dir = mkdtempSync(join(tmpdir(), "render-obs-"));
|
|
81
|
+
const planPath = join(dir, "render.json");
|
|
82
|
+
writeFileSync(planPath, b.buildOutput);
|
|
83
|
+
await renderApply({ planPath, endpoint: "http://fake/v1", wait: { intervalMs: 0 } }, undefined, fake.http());
|
|
84
|
+
const webId = fake.service("web")!.id as string;
|
|
85
|
+
fake.seed("/disks", { name: "stray", sizeGB: 1, mountPath: "/s", serviceId: webId });
|
|
86
|
+
fake.domains.set(webId, [{ id: "cd-1", name: "stray.example.com", verificationStatus: "unverified" }]);
|
|
87
|
+
|
|
88
|
+
const out = await describeResources(base(b), fake.http());
|
|
89
|
+
expect(out.resources.disk.ownership).toBe("owned");
|
|
90
|
+
expect(out.resources.fdisk.ownership).toBe("foreign");
|
|
91
|
+
expect(out.resources["Disk/stray"]).toMatchObject({ ownership: "owned", type: "Render::Services::Disk" });
|
|
92
|
+
expect(out.resources["CustomDomain/stray.example.com"]).toMatchObject({ ownership: "owned", status: "unverified" });
|
|
93
|
+
|
|
94
|
+
const owned = await describeResources({ ...base(b), owned: true }, fake.http());
|
|
95
|
+
expect(owned.resources.fdisk).toBeUndefined();
|
|
96
|
+
expect(owned.unobserved?.fdisk.reason).toBe("filtered");
|
|
97
|
+
expect(owned.resources.disk.ownership).toBe("owned");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("marks an unmarked live service foreign, and withholds it under --owned", async () => {
|
|
101
|
+
process.env.RENDER_OWNER_ID = "tea-1";
|
|
102
|
+
const fake = new FakeRender();
|
|
103
|
+
fake.seed("/services", { name: "web", type: "web_service", ownerId: "tea-1", envVars: [{ key: "A", value: "1" }] });
|
|
104
|
+
const b = build(stack(["web", new WebService({ name: "web", serviceDetails: new WebServiceDetails({ runtime: "docker" }) })]));
|
|
105
|
+
|
|
106
|
+
const all = await describeResources(base(b), fake.http());
|
|
107
|
+
expect(all.resources.web.ownership).toBe("foreign");
|
|
108
|
+
|
|
109
|
+
const owned = await describeResources({ ...base(b), owned: true }, fake.http());
|
|
110
|
+
expect(owned.resources.web).toBeUndefined();
|
|
111
|
+
expect(owned.unobserved?.web.reason).toBe("filtered");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns undeclared chant-marked services and env groups as owned orphans", async () => {
|
|
115
|
+
process.env.RENDER_OWNER_ID = "tea-1";
|
|
116
|
+
const fake = new FakeRender();
|
|
117
|
+
fake.seed("/services", { name: "stale", type: "cron_job", ownerId: "tea-1", envVars: [{ key: "CHANT_MANAGED_BY", value: "chant" }] });
|
|
118
|
+
fake.seed("/services", { name: "theirs", type: "web_service", ownerId: "tea-1", envVars: [] });
|
|
119
|
+
fake.seed("/env-groups", { name: "old", ownerId: "tea-1", envVars: [{ key: "CHANT_MANAGED_BY", value: "chant" }] });
|
|
120
|
+
const b = build(stack(["g", new EnvGroup({ name: "g", envVars: [] })]));
|
|
121
|
+
|
|
122
|
+
const out = await describeResources(base(b), fake.http());
|
|
123
|
+
expect(Object.keys(out.resources).sort()).toEqual(["CronJob/stale", "EnvGroup/old"]);
|
|
124
|
+
expect(out.resources["CronJob/stale"].ownership).toBe("owned");
|
|
125
|
+
expect(out.resources["CronJob/stale"].type).toBe("Render::Services::CronJob");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("with no build output, every declared entity is unobserved (read-failed), not absent", async () => {
|
|
129
|
+
const fake = new FakeRender();
|
|
130
|
+
const b = build(stack(["web", new WebService({ name: "web", serviceDetails: new WebServiceDetails({ runtime: "docker" }) })]));
|
|
131
|
+
const out = await describeResources({ ...base(b), buildOutput: "" }, fake.http());
|
|
132
|
+
expect(out.resources).toEqual({});
|
|
133
|
+
expect(out.unobserved?.web.reason).toBe("read-failed");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("an owner that cannot be resolved is no-credentials for every entity", async () => {
|
|
137
|
+
const fake = new FakeRender();
|
|
138
|
+
fake.owners.push({ id: "tea-2", name: "B", email: "", type: "team" });
|
|
139
|
+
const b = build(stack(["web", new WebService({ name: "web", serviceDetails: new WebServiceDetails({ runtime: "docker" }) })]));
|
|
140
|
+
const out = await describeResources(base(b), fake.http());
|
|
141
|
+
expect(out.unobserved?.web.reason).toBe("no-credentials");
|
|
142
|
+
});
|
|
143
|
+
});
|