@avi2dg/checks 0.20.0 → 0.21.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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
4
4
 
5
+ ## 0.21.0
6
+
7
+ Released 2026-09-26.
8
+
9
+ ### Features
10
+
11
+ - **scripts:** let quality.json set the runs-on of generated workflows (#66)
12
+
5
13
  ## 0.20.0
6
14
 
7
15
  Released 2026-09-26.
@@ -153,6 +153,10 @@ var Gates = Schema3.Struct({
153
153
  scheduled: Schema3.optionalKey(Schema3.Array(Command).annotate({ description: "The commands a cron-scheduled workflow runs" })),
154
154
  lint: Schema3.optionalKey(LintGates)
155
155
  });
156
+ var RunsOn = Schema3.NonEmptyArray(Schema3.NonEmptyString).annotate({
157
+ identifier: "RunsOn",
158
+ description: "The runner labels every job the kit generates runs on; ubuntu-latest when absent"
159
+ });
156
160
  var EffectSources = Schema3.Struct({
157
161
  paths: Schema3.NonEmptyArray(PathGlob).annotate({
158
162
  description: "Where source is written in Effect, held to the Effect rules of oxlint and the language service"
@@ -233,6 +237,7 @@ var Quality = Schema3.Struct({
233
237
  $schema: Schema3.optionalKey(Schema3.String),
234
238
  defaultBranch: Schema3.optionalKey(Schema3.NonEmptyString.annotate({ description: "The branch pull requests merge into; main when absent" })),
235
239
  gates: Schema3.optionalKey(Gates),
240
+ runsOn: Schema3.optionalKey(RunsOn),
236
241
  commitIdentity: Schema3.optionalKey(CommitIdentity),
237
242
  sources: Schema3.optionalKey(Sources),
238
243
  size: Schema3.optionalKey(Size),
@@ -14,6 +14,7 @@ The kit's bins find the file at the git root and read it there:
14
14
  "ci": ["bun run lint", "bun run typecheck", "bun run test"],
15
15
  "scheduled": ["bunx checks-flake --runs 10 --report flake-report.json"]
16
16
  },
17
+ "runsOn": ["self-hosted", "Linux", "X64", "winbox"],
17
18
  "commitIdentity": { "authors": [{ "name": "avi2d", "email": "avi2dg@gmail.com" }] },
18
19
  "sources": {
19
20
  "production": ["src/**/*.ts"],
@@ -43,6 +44,7 @@ The kit's bins find the file at the git root and read it there:
43
44
  | `gates.ci` | `checks-ci-wiring`, `checks-quality` | the commands CI runs on every pull request, as [checks-ci-wiring](../gates/checks-ci-wiring.md) says |
44
45
  | `gates.scheduled` | `checks-ci-wiring` | the commands a schedule runs |
45
46
  | `gates.lint` | `checks-lint`, `checks-ci-wiring` | the gates `checks-lint` runs when not all apply, as [Gate selection](../gates/checks-lint.md#gate-selection) says |
47
+ | `runsOn` | `checks-quality` | the runner labels every job the ci and commitlint workflows run on, `ubuntu-latest` when absent |
46
48
  | `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit, as [checks-commit-identity](../gates/checks-commit-identity.md) says |
47
49
  | `sources.production` | `checks-size-budget`, `checks-repetition`, `checks-quality` | the source the repository ships, as [checks-size-budget](../gates/checks-size-budget.md) and [checks-repetition](../gates/checks-repetition.md) say |
48
50
  | `sources.effect` | `checks-quality` | the paths held to the Effect rules, and the files under them that are not, as [The Effect rules](effect-rules.md) says |
@@ -32,6 +32,8 @@ A rule only this repository needs stays in its own `.oxlintrc.json`, whose overr
32
32
  GitHub Actions reads its own YAML and nothing else, so `checks-quality generate` also writes the kit recipe workflows whole.
33
33
  `.github/workflows/ci.yml` runs every `gates.ci` command but the title lint as its own step after a frozen install.
34
34
  `.github/workflows/commitlint.yml` lints the pull request title with the installed kit config.
35
+ Both jobs run on the runner labels `quality.json` `runsOn` lists, or on `ubuntu-latest` when the key is absent.
36
+ A self-hosted runner in a public repository runs the code of any pull request from a fork.
35
37
  A step one repository alone needs lives in another workflow file, never in the recipe.
36
38
  All generated workflows are committed.
37
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Deterministic checks shared across the captain's TypeScript repos",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -88,6 +88,9 @@
88
88
  },
89
89
  "additionalProperties": false
90
90
  },
91
+ "runsOn": {
92
+ "$ref": "#/$defs/RunsOn"
93
+ },
91
94
  "commitIdentity": {
92
95
  "type": "object",
93
96
  "properties": {
@@ -406,6 +409,21 @@
406
409
  "type": "string",
407
410
  "minLength": 1
408
411
  },
412
+ "RunsOn": {
413
+ "type": "array",
414
+ "prefixItems": [
415
+ {
416
+ "type": "string",
417
+ "minLength": 1
418
+ }
419
+ ],
420
+ "minItems": 1,
421
+ "items": {
422
+ "type": "string",
423
+ "minLength": 1
424
+ },
425
+ "description": "The runner labels every job the kit generates runs on; ubuntu-latest when absent"
426
+ },
409
427
  "Identity": {
410
428
  "type": "object",
411
429
  "properties": {
@@ -82,6 +82,11 @@ const Gates = Schema.Struct({
82
82
  lint: Schema.optionalKey(LintGates),
83
83
  });
84
84
 
85
+ const RunsOn = Schema.NonEmptyArray(Schema.NonEmptyString).annotate({
86
+ identifier: "RunsOn",
87
+ description: "The runner labels every job the kit generates runs on; ubuntu-latest when absent",
88
+ });
89
+
85
90
  const EffectSources = Schema.Struct({
86
91
  paths: Schema.NonEmptyArray(PathGlob).annotate({
87
92
  description: "Where source is written in Effect, held to the Effect rules of oxlint and the language service",
@@ -205,6 +210,7 @@ export const Quality = Schema.Struct({
205
210
  Schema.NonEmptyString.annotate({ description: "The branch pull requests merge into; main when absent" }),
206
211
  ),
207
212
  gates: Schema.optionalKey(Gates),
213
+ runsOn: Schema.optionalKey(RunsOn),
208
214
  commitIdentity: Schema.optionalKey(CommitIdentity),
209
215
  sources: Schema.optionalKey(Sources),
210
216
  size: Schema.optionalKey(Size),
@@ -105,11 +105,26 @@ function runsInTitleLint(gate: string, commitlintConfig: string): boolean {
105
105
  return words !== undefined && invokes(plainCommand(titleLint(commitlintConfig)), words);
106
106
  }
107
107
 
108
+ // YAML 1.2's core schema resolves these plain scalars to a null, a boolean or a number, and YAML 1.1 parsers also read yes, no, on and off as booleans.
109
+ const NON_STRING_SCALAR =
110
+ /^(?:~|null|Null|NULL|true|True|TRUE|false|False|FALSE|y|Y|yes|Yes|YES|n|N|no|No|NO|on|On|ON|off|Off|OFF|[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?|0o[0-7]+|0x[0-9a-fA-F]+|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/;
111
+
108
112
  function runScalar(command: string): string {
109
- return /(^[\s#&*!|>@`'"%{[-])|:\s|\s#|:$|[\n\\]/.test(command) ? JSON.stringify(command) : command;
113
+ return /(^[\s#&*!|>@`'"%{[-])|:\s|\s#|:$|[\n\\]/.test(command) || NON_STRING_SCALAR.test(command)
114
+ ? JSON.stringify(command)
115
+ : command;
116
+ }
117
+
118
+ function flowScalar(label: string): string {
119
+ return /[,[\]{}]/.test(label) ? JSON.stringify(label) : runScalar(label);
120
+ }
121
+
122
+ function runsOnLine(runsOn: Quality["runsOn"]): string {
123
+ if (runsOn === undefined) return "ubuntu-latest";
124
+ return `[${runsOn.map(flowScalar).join(", ")}]`;
110
125
  }
111
126
 
112
- export function commitlintWorkflow(commitlintConfig: string): string {
127
+ export function commitlintWorkflow(commitlintConfig: string, runsOn: Quality["runsOn"]): string {
113
128
  return `permissions:
114
129
  contents: read
115
130
  on:
@@ -117,7 +132,7 @@ on:
117
132
  types: [opened, edited, synchronize, reopened]
118
133
  jobs:
119
134
  commitlint:
120
- runs-on: ubuntu-latest
135
+ runs-on: ${runsOnLine(runsOn)}
121
136
  steps:
122
137
  - uses: actions/checkout@v5
123
138
  - uses: oven-sh/setup-bun@v2
@@ -135,7 +150,13 @@ jobs:
135
150
  `;
136
151
  }
137
152
 
138
- export function suiteWorkflow(defaultBranch: string, gates: readonly string[], bunVersionFile: boolean, nodeVersionFile: boolean): string {
153
+ export function suiteWorkflow(
154
+ defaultBranch: string,
155
+ gates: readonly string[],
156
+ bunVersionFile: boolean,
157
+ nodeVersionFile: boolean,
158
+ runsOn: Quality["runsOn"],
159
+ ): string {
139
160
  const node = nodeVersionFile
140
161
  ? " - uses: actions/setup-node@v5\n with:\n node-version-file: .node-version\n"
141
162
  : "";
@@ -143,20 +164,20 @@ export function suiteWorkflow(defaultBranch: string, gates: readonly string[], b
143
164
  ? " - uses: oven-sh/setup-bun@v2\n with:\n bun-version-file: .bun-version\n"
144
165
  : " - uses: oven-sh/setup-bun@v2\n";
145
166
  const steps = gates.map((gate) => ` - run: ${runScalar(gate)}\n`).join("");
146
- return `name: ci\non:\n push:\n branches: [${defaultBranch}]\n pull_request:\n types: [opened, edited, synchronize, reopened]\njobs:\n checks:\n runs-on: ubuntu-latest\n steps:\n # The head, not GitHub's merge ref, so the tree the gates read is the commit the range ends at.\n # The whole history, since the range starts where the head branched from the base branch.\n - uses: actions/checkout@v5\n with:\n ref: \${{ github.event.pull_request.head.sha || github.sha }}\n fetch-depth: 0\n${node}${setup} - run: bun install --frozen-lockfile\n${steps}`;
167
+ return `name: ci\non:\n push:\n branches: [${defaultBranch}]\n pull_request:\n types: [opened, edited, synchronize, reopened]\njobs:\n checks:\n runs-on: ${runsOnLine(runsOn)}\n steps:\n # The head, not GitHub's merge ref, so the tree the gates read is the commit the range ends at.\n # The whole history, since the range starts where the head branched from the base branch.\n - uses: actions/checkout@v5\n with:\n ref: \${{ github.event.pull_request.head.sha || github.sha }}\n fetch-depth: 0\n${node}${setup} - run: bun install --frozen-lockfile\n${steps}`;
147
168
  }
148
169
 
149
170
  export function workflowsFor(quality: Quality, recipe: WorkflowRecipe): readonly GeneratedWorkflow[] {
150
171
  const commitlint: GeneratedWorkflow = {
151
172
  file: COMMITLINT_WORKFLOW,
152
- content: commitlintWorkflow(recipe.commitlintConfig),
173
+ content: commitlintWorkflow(recipe.commitlintConfig, quality.runsOn),
153
174
  };
154
175
  if (quality.gates?.ci === undefined) return [commitlint];
155
176
  const suite = quality.gates.ci.filter((gate) => !runsInTitleLint(gate, recipe.commitlintConfig));
156
177
  return [
157
178
  {
158
179
  file: SUITE_WORKFLOW,
159
- content: suiteWorkflow(quality.defaultBranch ?? DEFAULT_BRANCH, suite, recipe.bunVersionFile, recipe.nodeVersionFile),
180
+ content: suiteWorkflow(quality.defaultBranch ?? DEFAULT_BRANCH, suite, recipe.bunVersionFile, recipe.nodeVersionFile, quality.runsOn),
160
181
  },
161
182
  commitlint,
162
183
  ];