@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The render lexicon's ownership convention, declared here so core carries no
|
|
3
|
+
* render-specific branch — the #686 seam: each lexicon owns its key names; core
|
|
4
|
+
* keeps only the generic stamp/detect logic (@intentius/chant/ownership).
|
|
5
|
+
*
|
|
6
|
+
* Render has no tags or labels on any resource. What it does have, on the two
|
|
7
|
+
* kinds chant creates most and prunes — services and env groups — is a
|
|
8
|
+
* key/value store the API writes and reads back verbatim: environment
|
|
9
|
+
* variables. chant stamps `CHANT_MANAGED_BY=chant` (+ `CHANT_STACK` /
|
|
10
|
+
* `CHANT_ENV`) into a service's or env group's env vars at serialize time and
|
|
11
|
+
* reads them back through `GET .../env-vars`; that is the primary marker and
|
|
12
|
+
* the basis for owned-only prune. A service without it is never modified or
|
|
13
|
+
* deleted by a prune. The keys are upper-snake because that is what an env var
|
|
14
|
+
* is; the values are exactly what every other lexicon stamps.
|
|
15
|
+
*
|
|
16
|
+
* Disks and custom domains carry no marker either, but they hang off a
|
|
17
|
+
* service, so they inherit the parent's verdict — the service boundary, one
|
|
18
|
+
* level down from fly's app boundary. An undeclared disk under a chant-owned
|
|
19
|
+
* service is chant's and is pruned; one under a foreign service is foreign.
|
|
20
|
+
*
|
|
21
|
+
* Datastores, projects, environments, registry credentials, and webhooks
|
|
22
|
+
* carry no channel and no boundary. Their verdict is `unknown` (never
|
|
23
|
+
* `foreign`, never `owned`): the change set never escalates `unknown` to a
|
|
24
|
+
* delete, so an undeclared Postgres is never pruned. Removing one is an
|
|
25
|
+
* explicit `renderDelete` of a plan that names it.
|
|
26
|
+
*/
|
|
27
|
+
import type { ChannelKeys } from "@intentius/chant/ownership";
|
|
28
|
+
/** Env-var keys for chant's ownership markers on services and env groups. */
|
|
29
|
+
export declare const RENDER_ENV_OWNERSHIP_KEYS: ChannelKeys;
|
|
30
|
+
/** True when a key is one of chant's marker env vars. */
|
|
31
|
+
export declare function isOwnershipKey(key: string): boolean;
|
|
32
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../src/ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D,6EAA6E;AAC7E,eAAO,MAAM,yBAAyB,EAAE,WAIvC,CAAC;AAEF,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMnD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-cli.d.ts","sourceRoot":"","sources":["../src/package-cli.ts"],"names":[],"mappings":""}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,0BAA0B,CAAC;AAY/E;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,aAgP1B,CAAC"}
|
package/dist/pseudo.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PseudoParameter } from "@intentius/chant/pseudo-parameter";
|
|
2
|
+
export { PseudoParameter };
|
|
3
|
+
/**
|
|
4
|
+
* Render pseudo-parameters — environment-resolved values a project references
|
|
5
|
+
* without hard-coding. Each serializes to a `{ Ref: "Render::..." }` marker
|
|
6
|
+
* that the serializer resolves from an environment variable at build time (see
|
|
7
|
+
* PSEUDO_ENV_MAP in ./serializer.ts), mirroring fly's `Fly.OrgSlug`.
|
|
8
|
+
*
|
|
9
|
+
* - OwnerId → the workspace (team or personal) id (RENDER_OWNER_ID), e.g. `tea-…`.
|
|
10
|
+
* Every ownerId chant fills in defaults to this, so a stack rarely names it.
|
|
11
|
+
* - Region → the target region (RENDER_REGION), default `oregon`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const OwnerId: PseudoParameter, Region: PseudoParameter;
|
|
14
|
+
/**
|
|
15
|
+
* Render namespace containing all pseudo-parameters.
|
|
16
|
+
*/
|
|
17
|
+
export declare const Render: {
|
|
18
|
+
readonly OwnerId: PseudoParameter;
|
|
19
|
+
readonly Region: PseudoParameter;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=pseudo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pseudo.d.ts","sourceRoot":"","sources":["../src/pseudo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA0B,MAAM,mCAAmC,CAAC;AAE5F,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B;;;;;;;;;GASG;AACH,eAAO,MAAQ,OAAO,mBAAE,MAAM,iBAG5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,MAAM;;;CAGT,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Env-var keys that carry a credential. Kept conservative on purpose — an
|
|
6
|
+
* inline value under one of these keys is very likely a real secret.
|
|
7
|
+
*/
|
|
8
|
+
const SECRET_KEY_PATTERN = /(?:^|[_-])(?:password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?token)(?:$|[_-])/i;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Values that are references or placeholders, not literal secrets: shell/
|
|
12
|
+
* interpolation references ($FOO, ${FOO}) and secret-manager style references.
|
|
13
|
+
*/
|
|
14
|
+
const REFERENCE_VALUE_PATTERN = /^(?:\$\{?[A-Za-z0-9_]+\}?|(?:secret|ref|env|vault):\S+)$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* REN002: No secret literals in env vars
|
|
18
|
+
*
|
|
19
|
+
* Flags an `envVars` (or `secretFiles`) entry whose `key` looks like a
|
|
20
|
+
* credential and whose `value` is a plain string literal. Render can generate
|
|
21
|
+
* the value (`generateValue: true`), it can come from an EnvGroup or from
|
|
22
|
+
* another resource's attribute (`db.internalConnectionString`), or from the
|
|
23
|
+
* process environment at build time — anything but a literal in source that
|
|
24
|
+
* ends up in git and in the build output.
|
|
25
|
+
*/
|
|
26
|
+
export const noSecretLiteralsRule: LintRule = {
|
|
27
|
+
id: "REN002",
|
|
28
|
+
severity: "warning",
|
|
29
|
+
category: "security",
|
|
30
|
+
description: "Secret env-var values must not be written inline — use generateValue, an EnvGroup, or a reference",
|
|
31
|
+
|
|
32
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
33
|
+
const { sourceFile } = context;
|
|
34
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
35
|
+
|
|
36
|
+
function literalOf(init: ts.Expression): string | undefined {
|
|
37
|
+
return ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init) ? init.text : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function visit(node: ts.Node): void {
|
|
41
|
+
// `{ key: "DB_PASSWORD", value: "hunter2" }` — an object literal with a
|
|
42
|
+
// secret-looking `key` and a literal `value`.
|
|
43
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
44
|
+
let key: string | undefined;
|
|
45
|
+
let valueNode: ts.Expression | undefined;
|
|
46
|
+
for (const prop of node.properties) {
|
|
47
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
48
|
+
const name = prop.name.getText(sourceFile).replace(/^["']|["']$/g, "");
|
|
49
|
+
if (name === "key") key = literalOf(prop.initializer);
|
|
50
|
+
if (name === "value") valueNode = prop.initializer;
|
|
51
|
+
}
|
|
52
|
+
if (key && valueNode) {
|
|
53
|
+
const value = literalOf(valueNode);
|
|
54
|
+
if (value !== undefined && value.length >= 4 && SECRET_KEY_PATTERN.test(key) && !REFERENCE_VALUE_PATTERN.test(value.trim())) {
|
|
55
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(valueNode.getStart(sourceFile));
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
file: sourceFile.fileName,
|
|
58
|
+
line: line + 1,
|
|
59
|
+
column: character + 1,
|
|
60
|
+
ruleId: "REN002",
|
|
61
|
+
severity: "warning",
|
|
62
|
+
message: `Possible inline secret for env var "${key}". Use \`generateValue: true\`, an EnvGroup, a resource attribute (e.g. db.internalConnectionString), or process.env — not a literal.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
ts.forEachChild(node, visit);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
visit(sourceFile);
|
|
71
|
+
return diagnostics;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REN010: A native-runtime service must say how to build and start.
|
|
3
|
+
*
|
|
4
|
+
* Render's native runtimes (node, python, ruby, go, rust, elixir) need a
|
|
5
|
+
* `buildCommand` and a `startCommand` in `envSpecificDetails`; without them the
|
|
6
|
+
* API rejects the create (or, on some paths, accepts it and the first deploy
|
|
7
|
+
* fails). `docker` reads a Dockerfile and `image` pulls a prebuilt image, so
|
|
8
|
+
* neither needs commands. Static sites have no runtime.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
import { ENTITY_TYPES } from "../../catalog";
|
|
13
|
+
import { readProps, entityTypeOf, isService, kindOf } from "./render-helpers";
|
|
14
|
+
|
|
15
|
+
const NATIVE_RUNTIMES = new Set(["node", "python", "ruby", "go", "rust", "elixir"]);
|
|
16
|
+
|
|
17
|
+
export const ren010: PostSynthCheck = {
|
|
18
|
+
id: "REN010",
|
|
19
|
+
description: "A native-runtime service must set envSpecificDetails.buildCommand and startCommand",
|
|
20
|
+
|
|
21
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
23
|
+
|
|
24
|
+
for (const [name, entity] of ctx.entities) {
|
|
25
|
+
if (!isService(entity) || entityTypeOf(entity) === ENTITY_TYPES.staticSite) continue;
|
|
26
|
+
const details = readProps(readProps(entity).serviceDetails);
|
|
27
|
+
const runtime = details.runtime;
|
|
28
|
+
if (typeof runtime !== "string" || !NATIVE_RUNTIMES.has(runtime)) continue;
|
|
29
|
+
|
|
30
|
+
const env = readProps(details.envSpecificDetails);
|
|
31
|
+
const missing: string[] = [];
|
|
32
|
+
if (typeof env.buildCommand !== "string" || env.buildCommand.length === 0) missing.push("buildCommand");
|
|
33
|
+
if (typeof env.startCommand !== "string" || env.startCommand.length === 0) missing.push("startCommand");
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
checkId: "REN010",
|
|
37
|
+
severity: "error",
|
|
38
|
+
message: `${kindOf(entity)} "${name}" uses the ${runtime} runtime but sets no ${missing.join(" or ")} — add envSpecificDetails: new NativeEnvironmentDetails({ buildCommand, startCommand })`,
|
|
39
|
+
entity: name,
|
|
40
|
+
lexicon: "render",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return diagnostics;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REN011: A service needs a source — a repo or an image.
|
|
3
|
+
*
|
|
4
|
+
* Every Render service is either git-backed (`repo`, built on Render) or
|
|
5
|
+
* image-backed (`image.imagePath`, pulled from a registry). One that names
|
|
6
|
+
* neither has nothing to deploy and is rejected at create time; one that uses
|
|
7
|
+
* the `image` runtime without an `image` is the same mistake spelled
|
|
8
|
+
* differently. Static sites are always git-backed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
import { readProps, isService, kindOf } from "./render-helpers";
|
|
13
|
+
|
|
14
|
+
export const ren011: PostSynthCheck = {
|
|
15
|
+
id: "REN011",
|
|
16
|
+
description: "A service must have a repo (git-backed) or an image (image-backed)",
|
|
17
|
+
|
|
18
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [name, entity] of ctx.entities) {
|
|
22
|
+
if (!isService(entity)) continue;
|
|
23
|
+
const props = readProps(entity);
|
|
24
|
+
const repo = typeof props.repo === "string" && props.repo.length > 0;
|
|
25
|
+
const imagePath = readProps(props.image).imagePath;
|
|
26
|
+
const image = typeof imagePath === "string" && imagePath.length > 0;
|
|
27
|
+
const runtime = readProps(props.serviceDetails).runtime;
|
|
28
|
+
|
|
29
|
+
if (!repo && !image) {
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
checkId: "REN011",
|
|
32
|
+
severity: "error",
|
|
33
|
+
message: `${kindOf(entity)} "${name}" has neither a repo nor an image — set repo: "https://github.com/…" or image: new Image({ imagePath: "docker.io/…" })`,
|
|
34
|
+
entity: name,
|
|
35
|
+
lexicon: "render",
|
|
36
|
+
});
|
|
37
|
+
} else if (runtime === "image" && !image) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "REN011",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `${kindOf(entity)} "${name}" uses runtime "image" but sets no image.imagePath`,
|
|
42
|
+
entity: name,
|
|
43
|
+
lexicon: "render",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return diagnostics;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REN012: The free instance type cannot scale.
|
|
3
|
+
*
|
|
4
|
+
* Render's `free` plan runs a single instance and cannot autoscale; a
|
|
5
|
+
* `numInstances > 1` or an enabled `autoscaling` block on a free service is
|
|
6
|
+
* rejected at create/patch time. Persistent disks are also unavailable on
|
|
7
|
+
* free. Catch all three at synth time with a message that says which.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { readProps, isService, kindOf } from "./render-helpers";
|
|
12
|
+
|
|
13
|
+
export const ren012: PostSynthCheck = {
|
|
14
|
+
id: "REN012",
|
|
15
|
+
description: "A free-plan service cannot set numInstances > 1, autoscaling, or a disk",
|
|
16
|
+
|
|
17
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
18
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [name, entity] of ctx.entities) {
|
|
21
|
+
if (!isService(entity)) continue;
|
|
22
|
+
const details = readProps(readProps(entity).serviceDetails);
|
|
23
|
+
if (details.plan !== "free") continue;
|
|
24
|
+
|
|
25
|
+
const problems: string[] = [];
|
|
26
|
+
if (typeof details.numInstances === "number" && details.numInstances > 1) problems.push(`numInstances: ${details.numInstances}`);
|
|
27
|
+
const autoscaling = readProps(details.autoscaling);
|
|
28
|
+
if (autoscaling.enabled === true) problems.push("autoscaling");
|
|
29
|
+
if (details.disk !== undefined) problems.push("a disk");
|
|
30
|
+
if (problems.length > 0) {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
checkId: "REN012",
|
|
33
|
+
severity: "error",
|
|
34
|
+
message: `${kindOf(entity)} "${name}" is on the free plan but sets ${problems.join(", ")} — the free instance type runs one instance, cannot autoscale, and has no persistent disk; pick a paid plan (starter or above)`,
|
|
35
|
+
entity: name,
|
|
36
|
+
lexicon: "render",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return diagnostics;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the render post-synth checks. Not a check itself, so the
|
|
3
|
+
* generated barrel skips it (the scanner only picks up exported PostSynthChecks).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isServiceEntityType } from "../../catalog";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read an entity or property's constructor props. Nested Declarables
|
|
10
|
+
* (WebServiceDetails, Image, ServiceDisk) stash their args under a
|
|
11
|
+
* non-enumerable `props`; a plain inline object carries them directly. Handle
|
|
12
|
+
* both so a check behaves the same whether the user authored
|
|
13
|
+
* `serviceDetails: { runtime }` or `serviceDetails: new WebServiceDetails({ runtime })`.
|
|
14
|
+
*/
|
|
15
|
+
export function readProps(value: unknown): Record<string, unknown> {
|
|
16
|
+
if (value && typeof value === "object") {
|
|
17
|
+
const nested = (value as { props?: unknown }).props;
|
|
18
|
+
if (nested && typeof nested === "object") return nested as Record<string, unknown>;
|
|
19
|
+
return value as Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The entityType of a declarable, or undefined. */
|
|
25
|
+
export function entityTypeOf(value: unknown): string | undefined {
|
|
26
|
+
return (value as { entityType?: string } | undefined)?.entityType;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when the entity is one of the five service kinds. */
|
|
30
|
+
export function isService(value: unknown): boolean {
|
|
31
|
+
return isServiceEntityType(entityTypeOf(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The short kind (`WebService`) from an entity type, for messages. */
|
|
35
|
+
export function kindOf(value: unknown): string {
|
|
36
|
+
const t = entityTypeOf(value) ?? "";
|
|
37
|
+
return t.slice(t.lastIndexOf("::") + 2);
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/** One cron field: `*`, a number, a range, a list, or any of those with a `/step`. */
|
|
5
|
+
const FIELD = String.raw`(?:\*|\d+(?:-\d+)?)(?:,(?:\*|\d+(?:-\d+)?))*(?:\/\d+)?`;
|
|
6
|
+
const CRON_5 = new RegExp(`^${FIELD}(?:\\s+${FIELD}){4}$`);
|
|
7
|
+
|
|
8
|
+
/** True for a standard five-field cron expression (Render accepts no seconds field or macros). */
|
|
9
|
+
export function isValidCronSchedule(s: string): boolean {
|
|
10
|
+
return CRON_5.test(s.trim());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* REN003: Valid cron schedule
|
|
15
|
+
*
|
|
16
|
+
* A CronJob's `schedule` must be a standard five-field cron expression
|
|
17
|
+
* (`minute hour day-of-month month day-of-week`). Render rejects `@hourly`-style
|
|
18
|
+
* macros and six-field (seconds) expressions at create time; catch them at build.
|
|
19
|
+
*/
|
|
20
|
+
export const validCronScheduleRule: LintRule = {
|
|
21
|
+
id: "REN003",
|
|
22
|
+
severity: "error",
|
|
23
|
+
category: "correctness",
|
|
24
|
+
description: "A cron job schedule must be a five-field cron expression",
|
|
25
|
+
|
|
26
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
27
|
+
const { sourceFile } = context;
|
|
28
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
29
|
+
|
|
30
|
+
function visit(node: ts.Node): void {
|
|
31
|
+
if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "schedule") {
|
|
32
|
+
const init = node.initializer;
|
|
33
|
+
if (ts.isStringLiteral(init) && !isValidCronSchedule(init.text)) {
|
|
34
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
file: sourceFile.fileName,
|
|
37
|
+
line: line + 1,
|
|
38
|
+
column: character + 1,
|
|
39
|
+
ruleId: "REN003",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `Invalid cron schedule "${init.text}". Use five fields, e.g. "0 * * * *" (hourly) or "*/15 * * * *" (every 15 minutes); Render accepts no @macros or seconds field.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
ts.forEachChild(node, visit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
visit(sourceFile);
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render's regions, per the `region` enum in the Public API spec. The generated
|
|
6
|
+
* types already narrow `region` to this union, so a typo is a compile error in
|
|
7
|
+
* a typed project; this rule gives the same answer with a clearer message, and
|
|
8
|
+
* fires in contexts the type checker does not reach (a `.js` stack, a value
|
|
9
|
+
* that has been widened to `string`).
|
|
10
|
+
*/
|
|
11
|
+
export const KNOWN_REGIONS = new Set<string>(["frankfurt", "oregon", "ohio", "singapore", "virginia"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* REN001: Valid Render region
|
|
15
|
+
*
|
|
16
|
+
* A service, datastore, or Key Value `region` must be a Render region.
|
|
17
|
+
*/
|
|
18
|
+
export const validRegionRule: LintRule = {
|
|
19
|
+
id: "REN001",
|
|
20
|
+
severity: "error",
|
|
21
|
+
category: "correctness",
|
|
22
|
+
description: "A region must be a Render region (frankfurt, oregon, ohio, singapore, virginia)",
|
|
23
|
+
|
|
24
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
25
|
+
const { sourceFile } = context;
|
|
26
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
27
|
+
|
|
28
|
+
function visit(node: ts.Node): void {
|
|
29
|
+
if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "region") {
|
|
30
|
+
const init = node.initializer;
|
|
31
|
+
if (ts.isStringLiteral(init) && init.text.length > 0 && !KNOWN_REGIONS.has(init.text)) {
|
|
32
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
file: sourceFile.fileName,
|
|
35
|
+
line: line + 1,
|
|
36
|
+
column: character + 1,
|
|
37
|
+
ruleId: "REN001",
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: `Unknown Render region "${init.text}". Use one of: ${[...KNOWN_REGIONS].join(", ")}.`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
ts.forEachChild(node, visit);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
visit(sourceFile);
|
|
47
|
+
return diagnostics;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render serializer.
|
|
3
|
+
*
|
|
4
|
+
* Turns declared `WebService`, `Postgres`, `EnvGroup`, `Project`, ... resources
|
|
5
|
+
* into the JSON create bodies the Render Public API accepts, so the applier can
|
|
6
|
+
* POST them straight through.
|
|
7
|
+
*
|
|
8
|
+
* Output shape — a JSON object keyed by entity name. Each value is a single
|
|
9
|
+
* Render request plus the identity the applier reconciles by:
|
|
10
|
+
*
|
|
11
|
+
* {
|
|
12
|
+
* "<entityName>": {
|
|
13
|
+
* "kind": "WebService", // generated class name
|
|
14
|
+
* "entityType": "Render::Services::WebService",
|
|
15
|
+
* "endpoint": "/services", // create collection
|
|
16
|
+
* "method": "POST",
|
|
17
|
+
* "name": "my-web", // reconcile key (Render has no client-chosen ids)
|
|
18
|
+
* "body": { ... }, // the create body
|
|
19
|
+
* "pathParams": { "serviceId": { "$ref": "web" } } // for child collections
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Cross-resource references. Render identifies resources by server-assigned
|
|
24
|
+
* ids, so a Disk's `serviceId` or an Environment's `projectId` cannot be known
|
|
25
|
+
* at build time. A declared resource passed where an id is expected serializes
|
|
26
|
+
* to `{ "$ref": "<entityName>" }`; an attribute read (`db.internalConnectionString`)
|
|
27
|
+
* to `{ "$attr": { "entity": "<entityName>", "attribute": "..." } }`. The applier
|
|
28
|
+
* resolves both once the target exists.
|
|
29
|
+
*
|
|
30
|
+
* Ownership. Every serialized service and env group carries the
|
|
31
|
+
* `CHANT_MANAGED_BY=chant` marker (plus the stack/env identity from
|
|
32
|
+
* `context.ownership`) merged into `envVars`, even when the author supplied
|
|
33
|
+
* none. See ./ownership.ts for why env vars.
|
|
34
|
+
*
|
|
35
|
+
* `ownerId`. Every owner-scoped body gets `ownerId` filled from `Render.OwnerId`
|
|
36
|
+
* (`RENDER_OWNER_ID`) when the author omitted it. When that is unset too, an
|
|
37
|
+
* `{ "$owner": true }` marker is left for the applier, which resolves it from
|
|
38
|
+
* `GET /owners` when the token sees exactly one workspace.
|
|
39
|
+
*/
|
|
40
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
41
|
+
import type { Serializer, SerializeContext } from "@intentius/chant/serializer";
|
|
42
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
43
|
+
/** A cross-resource id reference the applier resolves after the target exists. */
|
|
44
|
+
export interface RefMarker {
|
|
45
|
+
$ref: string;
|
|
46
|
+
}
|
|
47
|
+
/** An attribute read the applier resolves from the live target. */
|
|
48
|
+
export interface AttrMarker {
|
|
49
|
+
$attr: {
|
|
50
|
+
entity: string;
|
|
51
|
+
attribute: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** The workspace placeholder left when neither the author nor `RENDER_OWNER_ID` named one. */
|
|
55
|
+
export interface OwnerMarker {
|
|
56
|
+
$owner: true;
|
|
57
|
+
}
|
|
58
|
+
export declare function isRefMarker(v: unknown): v is RefMarker;
|
|
59
|
+
export declare function isAttrMarker(v: unknown): v is AttrMarker;
|
|
60
|
+
export declare function isOwnerMarker(v: unknown): v is OwnerMarker;
|
|
61
|
+
/** A single Render REST call the applier can issue, plus its reconcile identity. */
|
|
62
|
+
export interface RenderRequest {
|
|
63
|
+
kind: string;
|
|
64
|
+
entityType: string;
|
|
65
|
+
endpoint: string;
|
|
66
|
+
method: "POST";
|
|
67
|
+
/** The human name Render will show and chant reconciles by. */
|
|
68
|
+
name: string;
|
|
69
|
+
body: Record<string, unknown>;
|
|
70
|
+
/** Path placeholders in `endpoint` (child collections). */
|
|
71
|
+
pathParams?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/** The serializer's whole output: entity name → request. */
|
|
74
|
+
export type RenderPlan = Record<string, RenderRequest>;
|
|
75
|
+
/**
|
|
76
|
+
* Recursively replace pseudo-parameters with their resolved environment value.
|
|
77
|
+
* Handles both a raw `PseudoParameter` instance and the already-walked
|
|
78
|
+
* `{ Ref: "Render::X" }` shape. Everything else passes through untouched.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolvePseudoParameters(value: unknown): unknown;
|
|
81
|
+
/**
|
|
82
|
+
* Merge chant's ownership marker into an `envVars` list. The marker wins over
|
|
83
|
+
* any colliding user key so ownership can never be silently unset; the user's
|
|
84
|
+
* ordering is otherwise preserved.
|
|
85
|
+
*/
|
|
86
|
+
export declare function stampOwnership(envVars: unknown, marker: Record<string, string>): unknown[];
|
|
87
|
+
/**
|
|
88
|
+
* Serialize a render partition into the plan document `renderApply` consumes.
|
|
89
|
+
*
|
|
90
|
+
* Split out of the `Serializer` object so callers that need the concrete
|
|
91
|
+
* document — the applier, its tests, the `describeResources` fixtures — get
|
|
92
|
+
* `string` rather than the interface's `string | SerializerResult` union.
|
|
93
|
+
*/
|
|
94
|
+
export declare function serializeRender(entities: Map<string, Declarable>, _outputs?: LexiconOutput[], context?: SerializeContext): string;
|
|
95
|
+
/**
|
|
96
|
+
* Render serializer.
|
|
97
|
+
*/
|
|
98
|
+
export declare const renderSerializer: Serializer;
|
|
99
|
+
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAOrE,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAED,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,CAAC;CACd;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,SAAS,CAEtD;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAGxD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAE1D;AAED,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAkDvD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAuB/D;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAK1F;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,QAAQ,CAAC,EAAE,aAAa,EAAE,EAC1B,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CA4ER;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,UAI9B,CAAC"}
|