@intentius/chant-lexicon-github 0.118.1 → 0.120.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/parse.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/okf/types/Job.md +1 -0
- package/dist/okf/types/ReusableWorkflowCallJob.md +1 -0
- package/dist/okf/types/Workflow.md +1 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/types/index.d.ts +14 -5
- package/package.json +2 -2
- package/src/codegen/docs.ts +37 -0
- package/src/codegen/parse.ts +29 -5
- package/src/generated/index.d.ts +14 -5
- package/src/generated-types.test.ts +45 -0
- package/src/serializer.test.ts +80 -0
- package/src/serializer.ts +124 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;GAEG;AAmHH;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B9E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/codegen/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAEL,kBAAkB,IAAI,sBAAsB,EAG7C,MAAM,sCAAsC,CAAC;AAI9C,YAAY,EAAE,mBAAmB,EAAE,CAAC;AACpC,OAAO,EAAE,sBAAsB,IAAI,kBAAkB,EAAE,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAyFD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,EAAE,CAkB9E;
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/codegen/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAEL,kBAAkB,IAAI,sBAAsB,EAG7C,MAAM,sCAAsC,CAAC;AAI9C,YAAY,EAAE,mBAAmB,EAAE,CAAC;AACpC,OAAO,EAAE,sBAAsB,IAAI,kBAAkB,EAAE,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAyFD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,EAAE,CAkB9E;AAkiBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG1D"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "b90d05876520e11b56b19a89a89e02b3be6ba6410d8220685e5e2c70a8893868",
|
|
5
5
|
"meta.json": "c50e0ef3845c370c3ad2210b8bba455980417c0f818aa6ed46e06d4fb8b1b9f9",
|
|
6
|
-
"types/index.d.ts": "
|
|
6
|
+
"types/index.d.ts": "0ead95536e947ddfb98a5acf0ddb88547913b5a53a342df67711c59266f84b3e",
|
|
7
7
|
"rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
|
|
8
8
|
"rules/detect-secrets.ts": "5829832eb7b43993424971041ed386b196850183bffd34bddd6439f99061e4ae",
|
|
9
9
|
"rules/extract-inline-structs.ts": "8f8147c200de8ffd0620dfdcca2e2cea76a520303ce431479ebecad6aa26aab5",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
|
|
79
79
|
"skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
|
|
80
80
|
},
|
|
81
|
-
"composite": "
|
|
81
|
+
"composite": "9c78bc700120d8f4aa2ea98b522a590fa33de3de88907b30b1080d3cc46a9bbe"
|
|
82
82
|
}
|
package/dist/manifest.json
CHANGED
package/dist/okf/types/Job.md
CHANGED
|
@@ -12,6 +12,7 @@ resource_type: GitHub::Actions::Job
|
|
|
12
12
|
|
|
13
13
|
- `runs-on` (`string | string[]`, required): The type of machine to run the job on. The machine can be either a GitHub-hosted runner, or a self-hosted runner.
|
|
14
14
|
- `cache-mode` (`"read" | "write" | "write-only" | "none"`, optional)
|
|
15
|
+
- `comment` (`string`, optional): YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line
|
|
15
16
|
- `concurrency` (`Concurrency | string`, optional): Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. You can also specify concurrency at the workflow level. When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.
|
|
16
17
|
- `container` (`Container | string`, optional): A container to run any steps in a job that don't already specify a container. If you have steps that use both script and container actions, the container actions will run as sibling containers on the same network with the same volume mounts. If you do not set a container, all steps will run directly on the host specified by runs-on unless a step refers to an action configured to run in a container.
|
|
17
18
|
- `continue-on-error` (`boolean`, optional): Prevents a workflow run from failing when a job fails. Set to true to allow a workflow run to pass when this job fails.
|
|
@@ -12,6 +12,7 @@ resource_type: GitHub::Actions::ReusableWorkflowCallJob
|
|
|
12
12
|
|
|
13
13
|
- `uses` (`string`, required): The location and version of a reusable workflow file to run as a job, of the form './{path/to}/{localfile}.yml' or '{owner}/{repo}/{path}/{filename}@{ref}'. {ref} can be a SHA, a release tag, or a branch name. Using the commit SHA is the safest for stability and security.
|
|
14
14
|
- `cache-mode` (`"read" | "write" | "write-only" | "none"`, optional)
|
|
15
|
+
- `comment` (`string`, optional): YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line
|
|
15
16
|
- `concurrency` (`string | Record<string, any>`, optional): Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. You can also specify concurrency at the workflow level. When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.
|
|
16
17
|
- `if` (`boolean | number | string`, optional): You can use the if conditional to prevent a job from running unless a condition is met. You can use any supported context and expression to create a conditional. Expressions in an if conditional do not require the ${{ }} syntax. For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions.
|
|
17
18
|
- `name` (`string`, optional): The name of the job displayed on GitHub.
|
|
@@ -13,6 +13,7 @@ resource_type: GitHub::Actions::Workflow
|
|
|
13
13
|
- `jobs` (`Record<string, unknown>`, required): A workflow run is made up of one or more jobs. Jobs run in parallel by default. To run jobs sequentially, you can define dependencies on other jobs using the jobs.<job_id>.needs keyword. Each job runs in a fresh instance of the virtual environment specified by runs-on. You can run an unlimited number of jobs as long as you are within the workflow usage limits. For more information, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#usage-limits.
|
|
14
14
|
- `on` (`Record<string, unknown>`, required): The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows.
|
|
15
15
|
- `cache-mode` (`"read" | "write" | "write-only" | "none"`, optional)
|
|
16
|
+
- `comment` (`string`, optional): YAML comment lines written at the top of the file, above `name:`. Not a workflow key; each line of the string becomes a `#` line
|
|
16
17
|
- `concurrency` (`Concurrency | string`, optional): Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. You can also specify concurrency at the workflow level. When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.
|
|
17
18
|
- `defaults` (`Defaults`, optional): A map of default settings that will apply to all jobs in the workflow.
|
|
18
19
|
- `env` (`Record<string, string>`, optional): A map of environment variables that are available to all jobs and steps in the workflow.
|
package/dist/serializer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AAsRhF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,UAwD9B,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export declare class Container {
|
|
|
23
23
|
env?: Record<string, string>;
|
|
24
24
|
/** Docker options */
|
|
25
25
|
options?: string;
|
|
26
|
-
/** Exposed ports */
|
|
27
|
-
ports?: number[];
|
|
26
|
+
/** Exposed ports: a container port (published on a random host port) or "host:container" */
|
|
27
|
+
ports?: (number | string)[];
|
|
28
28
|
/** Volume mounts */
|
|
29
29
|
volumes?: string[];
|
|
30
30
|
});
|
|
@@ -52,6 +52,8 @@ export declare class Job {
|
|
|
52
52
|
/** The type of machine to run the job on. The machine can be either a GitHub-hosted runner, or a self-hosted runner. */
|
|
53
53
|
"runs-on": string | string[];
|
|
54
54
|
"cache-mode"?: "read" | "write" | "write-only" | "none";
|
|
55
|
+
/** YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line */
|
|
56
|
+
comment?: string;
|
|
55
57
|
/** Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context.
|
|
56
58
|
You can also specify concurrency at the workflow level.
|
|
57
59
|
When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. */
|
|
@@ -159,6 +161,8 @@ export declare class ReusableWorkflowCallJob {
|
|
|
159
161
|
/** The location and version of a reusable workflow file to run as a job, of the form './{path/to}/{localfile}.yml' or '{owner}/{repo}/{path}/{filename}@{ref}'. {ref} can be a SHA, a release tag, or a branch name. Using the commit SHA is the safest for stability and security. */
|
|
160
162
|
uses: string;
|
|
161
163
|
"cache-mode"?: "read" | "write" | "write-only" | "none";
|
|
164
|
+
/** YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line */
|
|
165
|
+
comment?: string;
|
|
162
166
|
/** Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context.
|
|
163
167
|
You can also specify concurrency at the workflow level.
|
|
164
168
|
When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. */
|
|
@@ -193,13 +197,16 @@ export declare class Service {
|
|
|
193
197
|
credentials?: Record<string, string>;
|
|
194
198
|
env?: Record<string, string>;
|
|
195
199
|
options?: string;
|
|
196
|
-
ports
|
|
200
|
+
/** Exposed ports: a container port (published on a random host port) or "host:container", which a step on the runner reaches at localhost:<host> */
|
|
201
|
+
ports?: (number | string)[];
|
|
197
202
|
volumes?: string[];
|
|
198
203
|
});
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
export declare class Step {
|
|
202
207
|
constructor(props: {
|
|
208
|
+
/** YAML comment lines written above the step. Not a step key; each line of the string becomes a `#` line */
|
|
209
|
+
comment?: string;
|
|
203
210
|
/** Continue on error */
|
|
204
211
|
"continue-on-error"?: boolean;
|
|
205
212
|
/** Environment variables */
|
|
@@ -218,8 +225,8 @@ export declare class Step {
|
|
|
218
225
|
"timeout-minutes"?: number;
|
|
219
226
|
/** Action reference */
|
|
220
227
|
uses?: string;
|
|
221
|
-
/** Action inputs */
|
|
222
|
-
with?: Record<string, string>;
|
|
228
|
+
/** Action inputs. The runner reads each as a string; a number or boolean renders as written (`fetch-depth: 0`) */
|
|
229
|
+
with?: Record<string, string | number | boolean>;
|
|
223
230
|
/** Working directory */
|
|
224
231
|
"working-directory"?: string;
|
|
225
232
|
});
|
|
@@ -244,6 +251,8 @@ You can run an unlimited number of jobs as long as you are within the workflow u
|
|
|
244
251
|
/** The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows. */
|
|
245
252
|
on: Record<string, unknown>;
|
|
246
253
|
"cache-mode"?: "read" | "write" | "write-only" | "none";
|
|
254
|
+
/** YAML comment lines written at the top of the file, above `name:`. Not a workflow key; each line of the string becomes a `#` line */
|
|
255
|
+
comment?: string;
|
|
247
256
|
/** Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context.
|
|
248
257
|
You can also specify concurrency at the workflow level.
|
|
249
258
|
When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.120.0",
|
|
4
4
|
"description": "GitHub Actions lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@intentius/chant": "^0.
|
|
64
|
+
"@intentius/chant": "^0.120.0",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -57,6 +57,43 @@ The generated file includes:
|
|
|
57
57
|
| \`timeoutMinutes: 15\` | \`timeout-minutes: 15\` | camelCase → kebab-case for job properties |
|
|
58
58
|
| \`new Step({ uses: "actions/checkout@v7" })\` | \`- uses: actions/checkout@v7\` | Steps serialize as sequence entries |
|
|
59
59
|
|
|
60
|
+
## Comments
|
|
61
|
+
|
|
62
|
+
\`Workflow\`, \`Job\` and \`Step\` take a \`comment\`. It is not a workflow key: the serializer writes it as \`#\` lines, one per line of the string, so the reasons a hand-written workflow keeps as YAML comments stay in the file a reader opens on GitHub. A workflow's comment is the file's header, above \`name:\`; a job's goes above its key and a step's above its \`- \`. The Forgejo lexicon renders the same declarations, so its files carry them too.
|
|
63
|
+
|
|
64
|
+
\`\`\`typescript
|
|
65
|
+
export const publish = new Workflow({
|
|
66
|
+
comment: "Publishes on a version tag.\\nKeep the file name: npm's trusted publisher names it.",
|
|
67
|
+
name: "Publish",
|
|
68
|
+
on: { push: { tags: ["v*"] } },
|
|
69
|
+
jobs: {
|
|
70
|
+
publish: new Job({
|
|
71
|
+
"runs-on": "ubuntu-latest",
|
|
72
|
+
steps: [new Step({ comment: "The registry times out now and then; retry once.", run: "npm publish || npm publish" })],
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
\`\`\`yaml
|
|
79
|
+
# Publishes on a version tag.
|
|
80
|
+
# Keep the file name: npm's trusted publisher names it.
|
|
81
|
+
|
|
82
|
+
name: Publish
|
|
83
|
+
|
|
84
|
+
on:
|
|
85
|
+
push:
|
|
86
|
+
tags:
|
|
87
|
+
- v*
|
|
88
|
+
|
|
89
|
+
jobs:
|
|
90
|
+
publish:
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
steps:
|
|
93
|
+
# The registry times out now and then; retry once.
|
|
94
|
+
- run: npm publish || npm publish
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
60
97
|
## Validating locally
|
|
61
98
|
|
|
62
99
|
The output is standard GitHub Actions YAML. Validate locally with \`act\` or push to GitHub:
|
package/src/codegen/parse.ts
CHANGED
|
@@ -175,7 +175,7 @@ function extractEntity(
|
|
|
175
175
|
resource: {
|
|
176
176
|
typeName: entity.typeName,
|
|
177
177
|
description: entity.description,
|
|
178
|
-
properties: buildFallbackProperties(entity.typeName),
|
|
178
|
+
properties: addCommentProperty(entity.typeName, buildFallbackProperties(entity.typeName)),
|
|
179
179
|
attributes: [],
|
|
180
180
|
deprecatedProperties: [],
|
|
181
181
|
},
|
|
@@ -197,6 +197,7 @@ function extractEntity(
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
addCommentProperty(entity.typeName, properties);
|
|
200
201
|
|
|
201
202
|
return {
|
|
202
203
|
resource: {
|
|
@@ -490,7 +491,7 @@ const PROPERTY_OVERRIDES: Record<string, Record<string, string>> = {
|
|
|
490
491
|
},
|
|
491
492
|
"GitHub::Actions::Step": {
|
|
492
493
|
env: "Record<string, string>",
|
|
493
|
-
with: "Record<string, string>",
|
|
494
|
+
with: "Record<string, string | number | boolean>",
|
|
494
495
|
"if": "string",
|
|
495
496
|
"timeout-minutes": "number",
|
|
496
497
|
"continue-on-error": "boolean",
|
|
@@ -500,6 +501,29 @@ const PROPERTY_OVERRIDES: Record<string, Record<string, string>> = {
|
|
|
500
501
|
},
|
|
501
502
|
};
|
|
502
503
|
|
|
504
|
+
// ── Comments ──────────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Entities that carry a `comment`: not a workflow key, but `# ...` lines the
|
|
508
|
+
* serializer writes above the entry (above `name:` for a workflow, the file's
|
|
509
|
+
* header), so the reasons a hand-written workflow keeps as YAML comments
|
|
510
|
+
* reach the rendered file (#3667).
|
|
511
|
+
*/
|
|
512
|
+
const COMMENTED_ENTITIES: Record<string, string> = {
|
|
513
|
+
"GitHub::Actions::Workflow": "YAML comment lines written at the top of the file, above `name:`. Not a workflow key; each line of the string becomes a `#` line",
|
|
514
|
+
"GitHub::Actions::Job": "YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line",
|
|
515
|
+
"GitHub::Actions::ReusableWorkflowCallJob": "YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line",
|
|
516
|
+
"GitHub::Actions::Step": "YAML comment lines written above the step. Not a step key; each line of the string becomes a `#` line",
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
function addCommentProperty(typeName: string, properties: ParsedProperty[]): ParsedProperty[] {
|
|
520
|
+
const description = COMMENTED_ENTITIES[typeName];
|
|
521
|
+
if (description && !properties.some((p) => p.name === "comment")) {
|
|
522
|
+
properties.push({ name: "comment", tsType: "string", required: false, description, constraints: {} });
|
|
523
|
+
}
|
|
524
|
+
return properties;
|
|
525
|
+
}
|
|
526
|
+
|
|
503
527
|
// ── Fallback properties ───────────────────────────────────────────
|
|
504
528
|
|
|
505
529
|
/**
|
|
@@ -548,7 +572,7 @@ function buildFallbackProperties(typeName: string): ParsedProperty[] {
|
|
|
548
572
|
{ name: "name", tsType: "string", required: false, description: "Step display name" },
|
|
549
573
|
{ name: "uses", tsType: "string", required: false, description: "Action reference" },
|
|
550
574
|
{ name: "run", tsType: "string", required: false, description: "Shell command" },
|
|
551
|
-
{ name: "with", tsType: "Record<string, string>", required: false, description: "Action inputs" },
|
|
575
|
+
{ name: "with", tsType: "Record<string, string | number | boolean>", required: false, description: "Action inputs. The runner reads each as a string; a number or boolean renders as written (`fetch-depth: 0`)" },
|
|
552
576
|
{ name: "env", tsType: "Record<string, string>", required: false, description: "Environment variables" },
|
|
553
577
|
{ name: "if", tsType: "string", required: false, description: "Conditional expression" },
|
|
554
578
|
{ name: "id", tsType: "string", required: false, description: "Step ID for output references" },
|
|
@@ -583,7 +607,7 @@ function buildFallbackProperties(typeName: string): ParsedProperty[] {
|
|
|
583
607
|
{ name: "image", tsType: "string", required: true, description: "Docker image" },
|
|
584
608
|
{ name: "credentials", tsType: "Record<string, string>", required: false, description: "Registry credentials" },
|
|
585
609
|
{ name: "env", tsType: "Record<string, string>", required: false, description: "Environment variables" },
|
|
586
|
-
{ name: "ports", tsType: "number[]", required: false, description: "Exposed ports" },
|
|
610
|
+
{ name: "ports", tsType: "(number | string)[]", required: false, description: "Exposed ports: a container port (published on a random host port) or \"host:container\"" },
|
|
587
611
|
{ name: "volumes", tsType: "string[]", required: false, description: "Volume mounts" },
|
|
588
612
|
{ name: "options", tsType: "string", required: false, description: "Docker options" },
|
|
589
613
|
],
|
|
@@ -591,7 +615,7 @@ function buildFallbackProperties(typeName: string): ParsedProperty[] {
|
|
|
591
615
|
{ name: "image", tsType: "string", required: true, description: "Docker image" },
|
|
592
616
|
{ name: "credentials", tsType: "Record<string, string>", required: false },
|
|
593
617
|
{ name: "env", tsType: "Record<string, string>", required: false },
|
|
594
|
-
{ name: "ports", tsType: "number[]", required: false },
|
|
618
|
+
{ name: "ports", tsType: "(number | string)[]", required: false, description: "Exposed ports: a container port (published on a random host port) or \"host:container\", which a step on the runner reaches at localhost:<host>" },
|
|
595
619
|
{ name: "volumes", tsType: "string[]", required: false },
|
|
596
620
|
{ name: "options", tsType: "string", required: false },
|
|
597
621
|
],
|
package/src/generated/index.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export declare class Container {
|
|
|
23
23
|
env?: Record<string, string>;
|
|
24
24
|
/** Docker options */
|
|
25
25
|
options?: string;
|
|
26
|
-
/** Exposed ports */
|
|
27
|
-
ports?: number[];
|
|
26
|
+
/** Exposed ports: a container port (published on a random host port) or "host:container" */
|
|
27
|
+
ports?: (number | string)[];
|
|
28
28
|
/** Volume mounts */
|
|
29
29
|
volumes?: string[];
|
|
30
30
|
});
|
|
@@ -52,6 +52,8 @@ export declare class Job {
|
|
|
52
52
|
/** The type of machine to run the job on. The machine can be either a GitHub-hosted runner, or a self-hosted runner. */
|
|
53
53
|
"runs-on": string | string[];
|
|
54
54
|
"cache-mode"?: "read" | "write" | "write-only" | "none";
|
|
55
|
+
/** YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line */
|
|
56
|
+
comment?: string;
|
|
55
57
|
/** Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context.
|
|
56
58
|
You can also specify concurrency at the workflow level.
|
|
57
59
|
When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. */
|
|
@@ -159,6 +161,8 @@ export declare class ReusableWorkflowCallJob {
|
|
|
159
161
|
/** The location and version of a reusable workflow file to run as a job, of the form './{path/to}/{localfile}.yml' or '{owner}/{repo}/{path}/{filename}@{ref}'. {ref} can be a SHA, a release tag, or a branch name. Using the commit SHA is the safest for stability and security. */
|
|
160
162
|
uses: string;
|
|
161
163
|
"cache-mode"?: "read" | "write" | "write-only" | "none";
|
|
164
|
+
/** YAML comment lines written above the job's key. Not a job key; each line of the string becomes a `#` line */
|
|
165
|
+
comment?: string;
|
|
162
166
|
/** Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context.
|
|
163
167
|
You can also specify concurrency at the workflow level.
|
|
164
168
|
When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. */
|
|
@@ -193,13 +197,16 @@ export declare class Service {
|
|
|
193
197
|
credentials?: Record<string, string>;
|
|
194
198
|
env?: Record<string, string>;
|
|
195
199
|
options?: string;
|
|
196
|
-
ports
|
|
200
|
+
/** Exposed ports: a container port (published on a random host port) or "host:container", which a step on the runner reaches at localhost:<host> */
|
|
201
|
+
ports?: (number | string)[];
|
|
197
202
|
volumes?: string[];
|
|
198
203
|
});
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
export declare class Step {
|
|
202
207
|
constructor(props: {
|
|
208
|
+
/** YAML comment lines written above the step. Not a step key; each line of the string becomes a `#` line */
|
|
209
|
+
comment?: string;
|
|
203
210
|
/** Continue on error */
|
|
204
211
|
"continue-on-error"?: boolean;
|
|
205
212
|
/** Environment variables */
|
|
@@ -218,8 +225,8 @@ export declare class Step {
|
|
|
218
225
|
"timeout-minutes"?: number;
|
|
219
226
|
/** Action reference */
|
|
220
227
|
uses?: string;
|
|
221
|
-
/** Action inputs */
|
|
222
|
-
with?: Record<string, string>;
|
|
228
|
+
/** Action inputs. The runner reads each as a string; a number or boolean renders as written (`fetch-depth: 0`) */
|
|
229
|
+
with?: Record<string, string | number | boolean>;
|
|
223
230
|
/** Working directory */
|
|
224
231
|
"working-directory"?: string;
|
|
225
232
|
});
|
|
@@ -244,6 +251,8 @@ You can run an unlimited number of jobs as long as you are within the workflow u
|
|
|
244
251
|
/** The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows. */
|
|
245
252
|
on: Record<string, unknown>;
|
|
246
253
|
"cache-mode"?: "read" | "write" | "write-only" | "none";
|
|
254
|
+
/** YAML comment lines written at the top of the file, above `name:`. Not a workflow key; each line of the string becomes a `#` line */
|
|
255
|
+
comment?: string;
|
|
247
256
|
/** Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context.
|
|
248
257
|
You can also specify concurrency at the workflow level.
|
|
249
258
|
When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. By default any previously pending job or workflow in the concurrency group will be canceled; this behavior can be changed with `queue`. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true. */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated declarations (`src/generated/index.d.ts`) hold a declaration
|
|
3
|
+
* to what GitHub Actions accepts (#3670). Each snippet is compiled against
|
|
4
|
+
* them in memory, so a type narrower than the workflow syntax fails here.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
|
|
11
|
+
const DTS = readFileSync(join(import.meta.dirname, "generated", "index.d.ts"), "utf-8");
|
|
12
|
+
|
|
13
|
+
/** The diagnostics from compiling `body` against the generated declarations. */
|
|
14
|
+
function diagnostics(body: string): string[] {
|
|
15
|
+
const files: Record<string, string> = {
|
|
16
|
+
"/gh.d.ts": DTS,
|
|
17
|
+
"/main.ts": `import { Container, Service, Step } from "./gh";\n${body}\n`,
|
|
18
|
+
};
|
|
19
|
+
const options: ts.CompilerOptions = { strict: true, noEmit: true, target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, types: [], lib: ["lib.es2022.d.ts"] };
|
|
20
|
+
const host = ts.createCompilerHost(options);
|
|
21
|
+
const read = host.getSourceFile.bind(host);
|
|
22
|
+
host.getSourceFile = (name, lang) => (name in files ? ts.createSourceFile(name, files[name], lang) : read(name, lang));
|
|
23
|
+
host.fileExists = (name) => name in files || ts.sys.fileExists(name);
|
|
24
|
+
host.readFile = (name) => files[name] ?? ts.sys.readFile(name);
|
|
25
|
+
const program = ts.createProgram(["/main.ts"], options, host);
|
|
26
|
+
return ts.getPreEmitDiagnostics(program, program.getSourceFile("/main.ts")).map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("generated types (#3670)", () => {
|
|
30
|
+
test("Service and Container ports take host:container strings as well as numbers", () => {
|
|
31
|
+
expect(diagnostics(`
|
|
32
|
+
new Service({ image: "clickhouse/clickhouse-server:24", ports: ["8123:8123", 9000] });
|
|
33
|
+
new Container({ image: "node:22", ports: ["3000:3000"] });
|
|
34
|
+
`)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("Step with takes numbers and booleans as well as strings", () => {
|
|
38
|
+
expect(diagnostics(`new Step({ uses: "actions/checkout@v4", with: { "fetch-depth": 0, lfs: true, ref: "main" } });`)).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the declarations still reject what the syntax does not take", () => {
|
|
42
|
+
expect(diagnostics(`new Service({ image: "x", ports: [{ port: 1 }] });`)).not.toEqual([]);
|
|
43
|
+
expect(diagnostics(`new Step({ with: { inputs: ["a"] } });`)).not.toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/serializer.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import { githubSerializer } from "./serializer";
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
4
4
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
5
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
5
6
|
|
|
6
7
|
// ── Mock entities ──────────────────────────────────────────────────
|
|
7
8
|
|
|
@@ -407,3 +408,82 @@ describe("YAML key ordering", () => {
|
|
|
407
408
|
expect(envIdx).toBeLessThan(jobsIdx);
|
|
408
409
|
});
|
|
409
410
|
});
|
|
411
|
+
|
|
412
|
+
describe("ports and step inputs render as written (#3670)", () => {
|
|
413
|
+
test("a host:container port stays a mapping and a numeric input stays a number", () => {
|
|
414
|
+
const entities = new Map<string, Declarable>();
|
|
415
|
+
entities.set("workflow", new MockWorkflow({
|
|
416
|
+
name: "CI",
|
|
417
|
+
on: { push: null },
|
|
418
|
+
jobs: {
|
|
419
|
+
test: new MockJob({
|
|
420
|
+
"runs-on": "ubuntu-latest",
|
|
421
|
+
services: { clickhouse: { image: "clickhouse/clickhouse-server:24", ports: ["8123:8123", 9000] } },
|
|
422
|
+
steps: [new MockStep({ uses: "actions/checkout@v4", with: { "fetch-depth": 0, lfs: true } })],
|
|
423
|
+
}),
|
|
424
|
+
},
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
428
|
+
expect(output).toContain(" ports:\n - '8123:8123'\n - 9000");
|
|
429
|
+
expect(output).toContain(" with:\n fetch-depth: 0\n lfs: true\n");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("comments (#3667)", () => {
|
|
434
|
+
const declare = (comments: boolean): Map<string, Declarable> => {
|
|
435
|
+
const c = <T>(v: T): T | undefined => (comments ? v : undefined);
|
|
436
|
+
const entities = new Map<string, Declarable>();
|
|
437
|
+
entities.set("workflow", new MockWorkflow({
|
|
438
|
+
comment: c("What this workflow is for.\n\nKeep the file name: npm's trusted publisher names it."),
|
|
439
|
+
name: "Publish",
|
|
440
|
+
on: { push: { tags: ["v*"] } },
|
|
441
|
+
jobs: {
|
|
442
|
+
build: new MockJob({ "runs-on": "ubuntu-latest", steps: [new MockStep({ run: "npm ci" })] }),
|
|
443
|
+
publish: new MockJob({
|
|
444
|
+
comment: c("Runs after build; holds the id-token."),
|
|
445
|
+
"runs-on": "ubuntu-latest",
|
|
446
|
+
needs: ["build"],
|
|
447
|
+
steps: [
|
|
448
|
+
new MockStep({ uses: "actions/checkout@v4" }),
|
|
449
|
+
new MockStep({ comment: c("The registry is flaky; retry once."), name: "Publish", run: "npm publish || npm publish", env: { NODE_AUTH_TOKEN: "x" } }),
|
|
450
|
+
],
|
|
451
|
+
}),
|
|
452
|
+
},
|
|
453
|
+
}));
|
|
454
|
+
return entities;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
test("a workflow, job and step comment render as # lines above their entries", () => {
|
|
458
|
+
const output = githubSerializer.serialize(declare(true)) as string;
|
|
459
|
+
expect(output.startsWith("# What this workflow is for.\n#\n# Keep the file name: npm's trusted publisher names it.\n\nname: Publish\n")).toBe(true);
|
|
460
|
+
expect(output).toContain(" # Runs after build; holds the id-token.\n publish:\n runs-on: ubuntu-latest\n");
|
|
461
|
+
expect(output).toContain(" - uses: actions/checkout@v4\n # The registry is flaky; retry once.\n - name: Publish\n run: npm publish || npm publish\n");
|
|
462
|
+
expect(output).not.toContain("comment:");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("the comments are the only difference, and the YAML parses to the same document", () => {
|
|
466
|
+
const withComments = githubSerializer.serialize(declare(true)) as string;
|
|
467
|
+
const without = githubSerializer.serialize(declare(false)) as string;
|
|
468
|
+
expect(withComments.split("\n").filter((l) => !/^\s*#/.test(l)).join("\n").replace(/^\n/, "")).toBe(without);
|
|
469
|
+
expect(parseYAML(withComments)).toEqual(parseYAML(without));
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("standalone Job and Step entities carry their comments too", () => {
|
|
473
|
+
const entities = new Map<string, Declarable>();
|
|
474
|
+
entities.set("ci", new MockWorkflow({ comment: "Header", name: "CI", on: { push: null } }));
|
|
475
|
+
entities.set("test", new MockJob({ comment: "Unit tests", "runs-on": "ubuntu-latest", steps: [new MockStep({ comment: "Why", run: "npm test" })] }));
|
|
476
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
477
|
+
expect(output).toContain("# Header\n\nname: CI");
|
|
478
|
+
expect(output).toContain("jobs:\n # Unit tests\n test:\n runs-on: ubuntu-latest\n steps:\n # Why\n - run: npm test\n");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("each file of a multi-workflow build gets its own header", () => {
|
|
482
|
+
const entities = new Map<string, Declarable>();
|
|
483
|
+
entities.set("ci", new MockWorkflow({ comment: "CI header", name: "CI", on: { push: null }, jobs: { a: new MockJob({ "runs-on": "x" }) } }));
|
|
484
|
+
entities.set("release", new MockWorkflow({ comment: "Release header", name: "Release", on: { push: null }, jobs: { b: new MockJob({ "runs-on": "x" }) } }));
|
|
485
|
+
const result = githubSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
486
|
+
expect(result.primary.startsWith("# CI header\n\nname: CI")).toBe(true);
|
|
487
|
+
expect(result.files["release.yml"].startsWith("# Release header\n\nname: Release")).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
});
|
package/src/serializer.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
|
12
12
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
13
13
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
14
14
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
15
|
-
import { emitYAMLEntry } from "@intentius/chant/yaml";
|
|
15
|
+
import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
|
|
16
16
|
|
|
17
17
|
// ── Key conversion ────────────────────────────────────────────────
|
|
18
18
|
|
|
@@ -51,6 +51,62 @@ function isTriggerType(entityType: string): boolean {
|
|
|
51
51
|
return entityType in TRIGGER_TYPE_TO_EVENT;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// ── Comments ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Entity types whose `comment` prop is YAML comment lines, not a key (#3667):
|
|
58
|
+
* the workflow's above `name:`, a job's above its key, a step's above its
|
|
59
|
+
* `- `. The prop never reaches the emitted mapping.
|
|
60
|
+
*/
|
|
61
|
+
const COMMENTED_TYPES = new Set([
|
|
62
|
+
"GitHub::Actions::Workflow",
|
|
63
|
+
"GitHub::Actions::Job",
|
|
64
|
+
"GitHub::Actions::ReusableWorkflowCallJob",
|
|
65
|
+
"GitHub::Actions::Step",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The comment for each serialized job or step mapping, keyed by the object
|
|
70
|
+
* the emitter will see. Attached after key conversion, which builds new
|
|
71
|
+
* objects, and read by `emitJobs`.
|
|
72
|
+
*/
|
|
73
|
+
const COMMENTS = new WeakMap<object, string>();
|
|
74
|
+
|
|
75
|
+
/** The `comment` of a workflow, job or step entity, if it has one. */
|
|
76
|
+
function commentOf(value: unknown): string | undefined {
|
|
77
|
+
if (!value || typeof value !== "object" || !("entityType" in value) || !("props" in value)) return undefined;
|
|
78
|
+
if (!COMMENTED_TYPES.has((value as { entityType: string }).entityType)) return undefined;
|
|
79
|
+
const comment = ((value as { props: unknown }).props as Record<string, unknown> | undefined)?.comment;
|
|
80
|
+
return typeof comment === "string" && comment !== "" ? comment : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `props` without its `comment`, for an entity whose comment is not a key. */
|
|
84
|
+
function withoutComment(props: unknown): unknown {
|
|
85
|
+
if (!props || typeof props !== "object" || !("comment" in props)) return props;
|
|
86
|
+
const { comment: _comment, ...rest } = props as Record<string, unknown>;
|
|
87
|
+
return rest;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A comment as `#` lines at `indent` (two spaces each), one per line of the string. */
|
|
91
|
+
function commentLines(comment: string, indent: number): string {
|
|
92
|
+
const prefix = " ".repeat(indent);
|
|
93
|
+
return comment.split("\n").map((l) => `${prefix}#${l.trimEnd() ? ` ${l.trimEnd()}` : ""}`).join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Record a serialized job's comment and its steps' comments, from the job entity it came from. */
|
|
97
|
+
function attachComments(source: unknown, job: Record<string, unknown> | undefined): void {
|
|
98
|
+
if (!job) return;
|
|
99
|
+
const comment = commentOf(source);
|
|
100
|
+
if (comment) COMMENTS.set(job, comment);
|
|
101
|
+
const rawSteps = (source as { props?: { steps?: unknown } } | undefined)?.props?.steps;
|
|
102
|
+
if (!Array.isArray(rawSteps) || !Array.isArray(job.steps)) return;
|
|
103
|
+
rawSteps.forEach((raw, i) => {
|
|
104
|
+
const stepComment = commentOf(raw);
|
|
105
|
+
const step = (job.steps as unknown[])[i];
|
|
106
|
+
if (stepComment && step && typeof step === "object") COMMENTS.set(step, stepComment);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
// ── Visitor ───────────────────────────────────────────────────────
|
|
55
111
|
|
|
56
112
|
function githubVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
@@ -62,8 +118,10 @@ function githubVisitor(entityNames: Map<Declarable, string>): SerializerVisitor
|
|
|
62
118
|
return undefined;
|
|
63
119
|
}
|
|
64
120
|
const props = entity.props as Record<string, unknown>;
|
|
121
|
+
const commented = COMMENTED_TYPES.has(entity.entityType);
|
|
65
122
|
const result: Record<string, unknown> = {};
|
|
66
123
|
for (const [key, value] of Object.entries(props)) {
|
|
124
|
+
if (commented && key === "comment") continue;
|
|
67
125
|
if (value !== undefined) {
|
|
68
126
|
result[key] = walk(value);
|
|
69
127
|
}
|
|
@@ -134,10 +192,12 @@ function serializeInlineJob(
|
|
|
134
192
|
|
|
135
193
|
if ("entityType" in obj && "props" in obj && JOB_ENTITY_TYPES.has(obj.entityType as string)) {
|
|
136
194
|
// Job entity: serialize its props inline (not as a resource reference)
|
|
137
|
-
const props = toYAMLValue(obj.props, entityNames);
|
|
138
|
-
|
|
195
|
+
const props = toYAMLValue(withoutComment(obj.props), entityNames);
|
|
196
|
+
const job = props && typeof props === "object"
|
|
139
197
|
? convertKeys(props as Record<string, unknown>)
|
|
140
198
|
: undefined;
|
|
199
|
+
attachComments(obj, job);
|
|
200
|
+
return job;
|
|
141
201
|
}
|
|
142
202
|
|
|
143
203
|
// Plain object job definition (JSON-style)
|
|
@@ -173,10 +233,13 @@ function buildStandaloneJobsSection(
|
|
|
173
233
|
const jobsSection: Record<string, unknown> = {};
|
|
174
234
|
for (const [name, job] of jobs) {
|
|
175
235
|
const jProps = toYAMLValue(
|
|
176
|
-
isResourceDeclarable(job) ? job.props : undefined,
|
|
236
|
+
withoutComment(isResourceDeclarable(job) ? job.props : undefined),
|
|
177
237
|
entityNames,
|
|
178
238
|
) as Record<string, unknown> | undefined;
|
|
179
|
-
if (jProps)
|
|
239
|
+
if (!jProps) continue;
|
|
240
|
+
const converted = convertKeys(jProps);
|
|
241
|
+
attachComments(job, converted);
|
|
242
|
+
jobsSection[toKebabCase(name)] = converted;
|
|
180
243
|
}
|
|
181
244
|
return Object.keys(jobsSection).length > 0 ? jobsSection : undefined;
|
|
182
245
|
}
|
|
@@ -358,7 +421,7 @@ function serializeSingleWorkflow(
|
|
|
358
421
|
if (!jobsSection) jobsSection = buildStandaloneJobsSection(jobs, entityNames);
|
|
359
422
|
if (jobsSection) doc.jobs = jobsSection;
|
|
360
423
|
|
|
361
|
-
return emitYAMLDocument(doc);
|
|
424
|
+
return emitYAMLDocument(doc, workflows.length > 0 ? commentOf(workflows[0][1]) : undefined);
|
|
362
425
|
}
|
|
363
426
|
|
|
364
427
|
function serializeMultiWorkflow(
|
|
@@ -409,7 +472,7 @@ function serializeMultiWorkflow(
|
|
|
409
472
|
const jobsSection = inlineJobs ?? (i === 0 ? buildStandaloneJobsSection(jobs, entityNames) : undefined);
|
|
410
473
|
if (jobsSection) doc.jobs = jobsSection;
|
|
411
474
|
|
|
412
|
-
const content = emitYAMLDocument(doc);
|
|
475
|
+
const content = emitYAMLDocument(doc, commentOf(wf));
|
|
413
476
|
if (i === 0) {
|
|
414
477
|
primary = content;
|
|
415
478
|
// primary is written to the -o path by the CLI; don't also add it to files[]
|
|
@@ -443,8 +506,9 @@ function convertTriggerProps(on: unknown): unknown {
|
|
|
443
506
|
/**
|
|
444
507
|
* Emit a complete YAML document from a structured object.
|
|
445
508
|
*/
|
|
446
|
-
function emitYAMLDocument(doc: Record<string, unknown
|
|
509
|
+
function emitYAMLDocument(doc: Record<string, unknown>, header?: string): string {
|
|
447
510
|
const sections: string[] = [];
|
|
511
|
+
if (header) sections.push(commentLines(header, 0));
|
|
448
512
|
|
|
449
513
|
// Emit in canonical order: name, run-name, on, permissions, env, concurrency, defaults, jobs
|
|
450
514
|
const order = ["name", "run-name", "on", "permissions", "env", "concurrency", "defaults", "jobs"];
|
|
@@ -456,6 +520,8 @@ function emitYAMLDocument(doc: Record<string, unknown>): string {
|
|
|
456
520
|
const value = doc[key];
|
|
457
521
|
if (value === null) {
|
|
458
522
|
sections.push(`${key}:`);
|
|
523
|
+
} else if (key === "jobs" && hasComments(value)) {
|
|
524
|
+
sections.push(emitJobs(value as Record<string, unknown>));
|
|
459
525
|
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
460
526
|
sections.push(`${key}: ${yamlScalar(value)}`);
|
|
461
527
|
} else {
|
|
@@ -477,6 +543,56 @@ function emitYAMLDocument(doc: Record<string, unknown>): string {
|
|
|
477
543
|
return unquotePinComments(sections.join("\n\n") + "\n");
|
|
478
544
|
}
|
|
479
545
|
|
|
546
|
+
/** Whether any job in a jobs section, or any of its steps, carries a comment. */
|
|
547
|
+
function hasComments(jobs: unknown): boolean {
|
|
548
|
+
if (!jobs || typeof jobs !== "object") return false;
|
|
549
|
+
return Object.values(jobs as Record<string, unknown>).some((job) =>
|
|
550
|
+
!!job && typeof job === "object" && (COMMENTS.has(job) || stepsOf(job).some((s) => !!s && typeof s === "object" && COMMENTS.has(s))));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const stepsOf = (job: object): unknown[] => {
|
|
554
|
+
const steps = (job as { steps?: unknown }).steps;
|
|
555
|
+
return Array.isArray(steps) ? steps : [];
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* The jobs section with each comment above its job or step. Everything else
|
|
560
|
+
* is what `emitYAMLEntry("jobs", jobs)` writes: the job at indent 1, its keys
|
|
561
|
+
* at 2, its step items at 3.
|
|
562
|
+
*/
|
|
563
|
+
function emitJobs(jobs: Record<string, unknown>): string {
|
|
564
|
+
const lines = ["jobs:"];
|
|
565
|
+
for (const [name, job] of Object.entries(jobs)) {
|
|
566
|
+
if (job === undefined) continue;
|
|
567
|
+
if (!job || typeof job !== "object" || Array.isArray(job)) {
|
|
568
|
+
lines.push(emitYAMLEntry(name, job, 1));
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const comment = COMMENTS.get(job);
|
|
572
|
+
if (comment) lines.push(commentLines(comment, 1));
|
|
573
|
+
const steps = stepsOf(job);
|
|
574
|
+
if (!steps.some((s) => !!s && typeof s === "object" && COMMENTS.has(s))) {
|
|
575
|
+
lines.push(emitYAMLEntry(name, job, 1));
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
lines.push(` ${name}:`);
|
|
579
|
+
for (const [key, value] of Object.entries(job as Record<string, unknown>)) {
|
|
580
|
+
if (value === undefined) continue;
|
|
581
|
+
if (key !== "steps") {
|
|
582
|
+
lines.push(emitYAMLEntry(key, value, 2));
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
lines.push(" steps:");
|
|
586
|
+
for (const step of steps) {
|
|
587
|
+
const stepComment = step && typeof step === "object" ? COMMENTS.get(step) : undefined;
|
|
588
|
+
if (stepComment) lines.push(commentLines(stepComment, 3));
|
|
589
|
+
lines.push(emitYAML([step], 3).slice(1));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return lines.join("\n");
|
|
594
|
+
}
|
|
595
|
+
|
|
480
596
|
/**
|
|
481
597
|
* A SHA-pinned `uses:` carries its version as a trailing comment
|
|
482
598
|
* (`actions/checkout@<sha> # v7.0.1`). The YAML emitter quotes any string with
|