model-eol 0.4.0 → 0.5.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 CHANGED
@@ -6,7 +6,7 @@
6
6
  [![feed refresh](https://github.com/thossullivan/model-eol/actions/workflows/feed-refresh.yml/badge.svg)](https://github.com/thossullivan/model-eol/actions/workflows/feed-refresh.yml)
7
7
  ![spec](https://img.shields.io/badge/spec-model--eol%2F0.1-blue)
8
8
  ![deps](https://img.shields.io/badge/runtime%20deps-zero-brightgreen)
9
- ![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
9
+ ![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
10
10
  ![license](https://img.shields.io/badge/license-MIT-blue)
11
11
 
12
12
  **A machine-readable deprecation feed format for AI models, plus the reference
@@ -14,9 +14,34 @@ tooling that turns it into a Dependabot for models.**
14
14
 
15
15
  ![model-eol demo: retired models, distributor clocks, worst-case date](docs/img/demo.gif)
16
16
 
17
+ ## Release line
18
+
19
+ **0.4.1 - trusted migrations.** The 0.4 line made bot migrations independently
20
+ evaluable and bound each result to the exact default-branch commit, plan, eval
21
+ configuration, and feed digests. The workflow resolves the package version once
22
+ and reuses that exact version across planning, evaluation, and publication. It added
23
+ path-scoped policy and lifecycle routing for mixed-provider repositories,
24
+ trusted PR/issue ownership, stale-work reconciliation, and fresh-work behavior
25
+ when a retired model returns. The 0.4.1 patch moved the copy-ready workflows to
26
+ the current Node-backed Actions and fixed Google models-endpoint pagination. The
27
+ bundled feeds contain 313 entries across Amazon, Anthropic, Google, and OpenAI.
28
+
29
+ **0.5.0 - public contract and trust.** This milestone makes the data contract public:
30
+ canonical hosted schemas, a zero-dependency `validate` command, hosted feeds,
31
+ an Atom changelog, and byte-exact refresh receipts whose digests must match the
32
+ published data. It adds status-aware distributor refresh including Bedrock Public
33
+ Extended Access, preserves structured replacement guidance across JSON reports
34
+ and preserves channel-specific lifecycle clocks in CycloneDX,
35
+ preflights multi-file apply plan-wide and rolls back later commit failures,
36
+ fails explicitly on unapproved symlink coverage, and keeps configured eval
37
+ failures as distinct durable issues. It also
38
+ ships a repository-owned eval-harness starter, permanent exact-package consumer
39
+ UAT, and a Node 22 support floor. The hosted URLs become authoritative only after
40
+ the first green Pages deployment.
41
+
17
42
  ## The problem
18
43
 
19
- On July 23, 2026, OpenAI shut down 15 model snapshots on schedule. One of my
44
+ On July 23, 2026, OpenAI shut down a scheduled wave of model snapshots. One of my
20
45
  tuned research workflows returned a model-not-found error that morning even though
21
46
  the retirement had been documented for months. A model ID is the one dependency
22
47
  my toolchain could not see. When an npm package is deprecated, the warning prints
@@ -36,14 +61,17 @@ second, so the existing trackers can converge instead of each scraping alone.
36
61
  - **`feeds/`** - <!-- feeds-status -->Amazon (4 entries), Anthropic (29 entries), Google (86 entries) and OpenAI (194 entries), generated from the providers' live deprecation pages plus the AWS Bedrock and Google Vertex AI lifecycle pages, feed data generated 2026-08-18<!-- /feeds-status -->. Every
37
62
  dated entry carries a source URL.
38
63
  - **`check.mjs`** - zero-dependency CLI: CI gate, PR diff gate, inventory, CycloneDX
39
- ML-BOM export, retirement schedule, alerts and badges, migration plan/apply.
64
+ ML-BOM export, retirement schedule, alerts and badges, migration plan/apply,
65
+ and public document validation.
40
66
  - **`refresh/`** - regenerates the feeds from provider pages, models endpoints, and
41
67
  distributor lifecycle pages, with a semantic diff for human review.
42
68
  - **`bot/`** - the Dependabot part: a cron GitHub workflow that maintains one
43
69
  migration PR or issue per retiring model, published as the `model-eol-bot`
44
70
  binary in the same npm package.
45
- - **`scripts/feed-changelog.mjs`** - the feeds' git history as Atom/markdown, so
46
- "model retirements as they are announced" is a feed you can subscribe to.
71
+ - **Public contract rollout** - the 0.5 Pages workflow publishes versioned
72
+ schemas, hosted feeds, refresh health, and an Atom changelog. Those URLs
73
+ become authoritative only after Pages is enabled and the exact-byte deployment
74
+ check passes.
47
75
 
48
76
  ## Try it
49
77
 
@@ -72,6 +100,9 @@ node check.mjs path/to/your/repo --via aws-bedrock
72
100
  # inventory, and a CycloneDX 1.6 ML-BOM for your SBOM pipeline
73
101
  node check.mjs inventory path/to/your/repo
74
102
  node check.mjs inventory path/to/your/repo --format cyclonedx > model-bom.json
103
+ # the primary CI gate is also a schema-valid public report
104
+ node check.mjs check path/to/your/repo --json > model-eol-check.json
105
+ node check.mjs validate model-eol-check.json
75
106
  # retirement schedule with the repo-level worst case
76
107
  node check.mjs schedule path/to/your/repo
77
108
  # GitHub Actions annotations, Markdown, or a shields.io badge
@@ -80,6 +111,8 @@ node check.mjs alert path/to/your/repo --format badge > model-eol-badge.json
80
111
  # migration plan (only high-confidence direct API refs are patchable) and safe apply
81
112
  node check.mjs plan path/to/your/repo --days 90 > plan.json
82
113
  node check.mjs apply --plan plan.json --dry-run
114
+ # validate provider feeds, repository policy, or model-eol machine reports
115
+ node check.mjs validate feeds/openai.json .model-eol.json plan.json
83
116
  ```
84
117
 
85
118
  </details>
@@ -115,9 +148,13 @@ The same gate as a composite action - no npm install, feeds bundled, pinned to
115
148
  the `v0` line:
116
149
 
117
150
  ```yaml
118
- - uses: actions/checkout@v4
151
+ - uses: actions/checkout@v7
119
152
  with:
120
153
  fetch-depth: 0 # --changed needs history to diff against the base ref
154
+ - uses: actions/setup-node@v7
155
+ with:
156
+ node-version: 22
157
+ package-manager-cache: false
121
158
  - uses: thossullivan/model-eol@v0
122
159
  with:
123
160
  command: check
@@ -131,10 +168,16 @@ or retiring within the threshold. Add `via: aws-bedrock` (or `azure-ai-foundry`)
131
168
  to judge by a distributor's clock. The copy-paste version with both gates - PR
132
169
  diff plus a weekly full-repository check - is
133
170
  [`examples/workflows/model-eol.yml`](examples/workflows/model-eol.yml). The
134
- Action also exposes `inventory`, `schedule`, `alert`, and `plan`. Use
171
+ Action also exposes `inventory`, `schedule`, `alert`, `plan`, and `validate`. Use
135
172
  `format: cyclonedx` for an ML-BOM, or `output-file` to retain any report as a
136
173
  workflow artifact while keeping the same output in the job log.
137
174
 
175
+ CycloneDX exports use one machine-learning component per canonical model and
176
+ lifecycle channel. Direct publisher (`publisher-direct`), Azure, Bedrock, and
177
+ other routed references therefore retain their own status and shutdown clock
178
+ under deterministic, channel-qualified `bom-ref` values; occurrences are sorted
179
+ and scoped to the component whose clock they used.
180
+
138
181
  ## One repository policy
139
182
 
140
183
  The CLI, Action, and bot all discover a strict `.model-eol.json` at the target
@@ -151,7 +194,7 @@ Git repository root. CLI flags or Action inputs override configured values, and
151
194
  "paths": ["test/fixtures/**", "vendor"]
152
195
  },
153
196
  "issues": { "enabled": true },
154
- "eval": { "command": "npm test" }
197
+ "eval": { "command": "node scripts/model-eol-eval.mjs" }
155
198
  }
156
199
  ```
157
200
 
@@ -160,8 +203,28 @@ in `ignore.models` suppresses the whole alias family; `ignore.paths` uses
160
203
  repo-relative `*`, `**`, and `?` globs and excludes matching files before scan
161
204
  coverage limits are counted. With no config the CLI keeps its historical
162
205
  `scope: all`; once a config exists, omitted values use the shared bot defaults
163
- (`days: 90`, `scope: direct`, and the publisher clock). The complete contract is
164
- [`schema/model-eol.bot-config.schema.json`](schema/model-eol.bot-config.schema.json).
206
+ (`days: 90`, `scope: direct`, and the publisher clock). Portable structure is
207
+ defined by
208
+ [`schema/model-eol.bot-config.schema.json`](schema/model-eol.bot-config.schema.json),
209
+ while `model-eol validate` also enforces runtime semantic and safety checks that
210
+ Draft-07 cannot express cleanly.
211
+
212
+ For content-heavy repositories, start with `inventory`, then exclude caches,
213
+ generated catalogs, archived fixtures, and documentation that are not live model
214
+ dependencies. Published-repository UAT deliberately verifies both outcomes: the
215
+ unconfigured scan reports evidence, while an explicit path policy produces a
216
+ clean check and empty bot decision table without changing repository content.
217
+
218
+ Parent-repository scans do not recurse into tracked Git submodules. A detected
219
+ gitlink is reported as incomplete coverage, so `check` and `plan` fail closed
220
+ unless the path is explicitly ignored or `--allow-incomplete` is chosen. To
221
+ inspect submodule contents, run model-eol against the checked-out submodule as a
222
+ separate target/repository policy.
223
+
224
+ Untracked nested Git repositories are handled the same way: the parent reports
225
+ `nested-repository-skipped` and never recurses into the nested checkout. Ignore
226
+ an intentional nested repository explicitly, allow incomplete coverage, or scan
227
+ it separately with its own repository policy.
165
228
 
166
229
  Mixed-provider monorepos can keep those top-level values as repository defaults,
167
230
  then apply path policies and a lifecycle channel per reference:
@@ -250,8 +313,9 @@ human. The workflow resolves `model-eol@0` from npm once and reuses that exact
250
313
  version across its plan, read-only evaluation, and write-token publication jobs;
251
314
  consumer repositories do not copy `check.mjs` or the `bot/` source directory.
252
315
  Dismissals are respected, reintroduced retired models create fresh work, and
253
- bot-owned PRs/issues close when their finding disappears. Preview everything with
254
- no GitHub calls and without adding a project dependency:
316
+ bot-owned PRs/issues close when their finding disappears. In a repository without
317
+ a configured eval, preview everything with no GitHub calls and without adding a
318
+ project dependency:
255
319
 
256
320
  ```sh
257
321
  npx --yes --package=model-eol@0 model-eol-bot --dry-run --target-dir . --repo OWNER/REPO
@@ -262,31 +326,106 @@ it uses the bot defaults of 90 days and direct references. `eval.command` also
262
326
  comes from the config unless the `MODEL_EOL_EVAL_COMMAND` repository variable is
263
327
  set as an explicit workflow override. Each patchable model is applied in an
264
328
  isolated checkout and evaluated independently with its own old/new IDs, timeout,
265
- bounded report, and explicitly allowed environment. The content-bound result
329
+ bounded report, and explicitly forwarded environment names. The content-bound result
266
330
  manifest is also pinned to the evaluated Git commit, letting passing migrations
267
331
  proceed while a failing peer remains blocked without publishing against a newer,
268
332
  unevaluated default branch.
269
333
 
334
+ ### Prove the swap works in your repository
335
+
336
+ Model behavior is repository-specific, so model-eol never guesses which test is
337
+ good enough to authorize a migration. Check a harness into the repository and
338
+ name it explicitly in `.model-eol.json`. A non-empty
339
+ `MODEL_EOL_EVAL_COMMAND` repository variable overrides that command; otherwise
340
+ the config is authoritative. With neither configured, the result manifest says
341
+ that evaluation was not configured.
342
+
343
+ Start by copying [`examples/model-eol-eval.mjs`](examples/model-eol-eval.mjs) to
344
+ `scripts/model-eol-eval.mjs`, then replace its `VERIFY` command with the smallest
345
+ deterministic test that proves the behavior your application relies on. A useful
346
+ harness checks response or tool-call shape, structured-output parsing, required
347
+ capabilities, and a small golden task set. A generic build passing is supporting
348
+ evidence, but it does not by itself prove model behavior. The starter invokes a
349
+ dedicated `npm run eval:model-eol` script so adopting it requires an explicit
350
+ repository test rather than accidentally treating an unrelated unit suite as an
351
+ LLM-behavior eval.
352
+ Point `eval:model-eol` at the underlying behavior test, not back at the harness
353
+ itself.
354
+
355
+ The harness runs after the proposed migration in an isolated checkout and gets:
356
+
357
+ - `MODEL_EOL_OLD_ID` and `MODEL_EOL_NEW_ID` for the current migration.
358
+ - `MODEL_EOL_PLAN`, a one-model plan containing the exact changed references.
359
+ - `MODEL_EOL_REPORT`, the required path for a bounded, publishable Markdown receipt.
360
+
361
+ Exit zero plus a regular report file means pass; any other exit, timeout, missing
362
+ report, tracked workspace mutation, or checkout drift fails that migration. Test
363
+ stdout and stderr are intentionally not published. Put only non-secret evidence
364
+ in the report, and forward only credentials required by trusted eval code through
365
+ `eval.pass_env`. That allowlist controls normal subprocess environment forwarding;
366
+ it is not an OS sandbox and does not isolate code running as the same user. The
367
+ workflow's security boundary is that this job has no repository write token and
368
+ the publication job never executes repository-owned eval code.
369
+
370
+ Before enabling the scheduled bot, commit the harness and policy on a clean
371
+ branch and exercise the same isolated evaluator locally:
372
+
373
+ ```bash
374
+ set -euo pipefail
375
+ repo="$(pwd -P)"
376
+ version="$(npm view model-eol@0 version --json | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s);const r=Array.isArray(v)?v.at(-1):v;if(typeof r!=="string")process.exit(1);process.stdout.write(r)})')"
377
+ tmp="$(mktemp -d)"
378
+ trap 'rm -rf -- "$tmp"' EXIT
379
+ cd "$tmp"
380
+ if [ -f "$repo/.model-eol.json" ]; then
381
+ MODEL_EOL_UAT_REPO="$repo" npx --yes --package="model-eol@$version" \
382
+ -c 'cd "$MODEL_EOL_UAT_REPO" && model-eol plan .' > "$tmp/plan.json"
383
+ else
384
+ MODEL_EOL_UAT_REPO="$repo" npx --yes --package="model-eol@$version" \
385
+ -c 'cd "$MODEL_EOL_UAT_REPO" && model-eol plan . --days 90 --scope direct' > "$tmp/plan.json"
386
+ fi
387
+ npx --yes --package="model-eol@$version" model-eol-bot evaluate \
388
+ --target-dir "$repo" \
389
+ --plan-file "$tmp/plan.json" \
390
+ --output-file "$tmp/eval.json"
391
+ node -e 'const r=require(process.argv[1]); console.log(r.results)' "$tmp/eval.json"
392
+ MODEL_EOL_EVAL_RESULTS_FILE="$tmp/eval.json" \
393
+ npx --yes --package="model-eol@$version" model-eol-bot \
394
+ --dry-run --target-dir "$repo" --repo OWNER/REPO
395
+ ```
396
+
397
+ Each planned model is patched and evaluated independently. The cleanup trap
398
+ removes the temporary artifacts when this shell exits; copy `eval.json` elsewhere
399
+ after the dry run if you want to retain the receipt as UAT evidence. Then let
400
+ `bot.yml.example` run the same contract with any explicitly opted-in provider
401
+ keys available to its read-only evaluate job, not its write-capable publish job.
402
+
403
+ Starting in 0.5, inline `model-eol-bot --eval` and the legacy unbound report/status
404
+ artifacts are refused. A configured publication must consume the commit-bound
405
+ manifest from `model-eol-bot evaluate`; missing results fail before GitHub API
406
+ access.
407
+
270
408
  On its first actionable run, the bot creates the `model-eol` label. If repository
271
409
  policy prevents label creation or assignment, it fails closed rather than publish
272
410
  work whose ownership cannot be authenticated. Existing work is trusted only when
273
411
  the label, metadata, deterministic branch, repository, base, and Git lease agree.
274
- For HTTPS remotes—including private repositories—the bot passes `GITHUB_TOKEN` to
412
+ For HTTPS remotes - including private repositories - the bot passes `GITHUB_TOKEN` to
275
413
  temporary Git processes as a host-scoped authorization header and never places it
276
414
  in a remote URL.
277
415
 
278
416
  Two operational notes the hard way teaches: PRs created with `GITHUB_TOKEN` do not
279
417
  trigger `pull_request` workflows (use a fine-grained PAT or GitHub App token when
280
418
  checks must run, stored as the optional `MODEL_EOL_BOT_TOKEN` secret), and the
281
- workflow splits privileges—provider keys live only in the read-only evaluation
282
- job, while write tokens live only in the reconciliation job.
419
+ workflow splits privileges - only provider secrets explicitly added for the
420
+ trusted eval live in the read-only evaluation job, while write tokens live only
421
+ in the reconciliation job.
283
422
 
284
423
  ## Keeping the feeds honest
285
424
 
286
425
  ```sh
287
426
  node refresh/refresh.mjs --check # semantic diff vs live pages; exit 3 = PR-worthy
288
427
  node refresh/refresh.mjs --distributor aws-bedrock,vertex-ai # distributor lifecycle clocks
289
- node scripts/feed-changelog.mjs # feeds' git history as an Atom feed
428
+ node scripts/feed-changelog.mjs # local rendering of the hosted Atom feed
290
429
  ```
291
430
 
292
431
  Parse failures fail loudly and never emit a guessed feed. This runs automatically:
@@ -297,6 +436,14 @@ their keep: they caught the hand-compiled feeds drifting from Anthropic's
297
436
  recommendations within one week, and corrected a hand-compiled Bedrock date that
298
437
  was three months wrong.
299
438
 
439
+ Every successful live refresh emits a byte-exact receipt. The Pages workflow
440
+ publishes the [public contract](https://thossullivan.github.io/model-eol/) only
441
+ when that receipt hashes the exact feeds on `main`: a no-change check can advance
442
+ `last_checked` immediately, while a material-change run waits for its generated
443
+ feed PR to merge. Feed `generated` remains semantic - it changes only when source
444
+ data changes. The hosted contract contains canonical Draft-07 schemas, feeds
445
+ with SHA-256 receipts, and the Atom changelog.
446
+
300
447
  The refresh also travels in the other direction. On August 3, Google removed the
301
448
  previously listed October 16 earliest shutdown dates for the Gemini 2.5 Pro,
302
449
  Flash, and Flash-Lite GA models. The pipeline opened
@@ -322,6 +469,15 @@ move any of us can make here.
322
469
  CI runs the full suite on every push and PR. The copy-ready bot workflow ships
323
470
  as `bot.yml.example` and consumes the published package rather than local copies
324
471
  of the tooling.
472
+ - The 0.5 Pages workflow publishes canonical schemas, feeds, refresh health, and
473
+ Atom at `thossullivan.github.io/model-eol`. Treat that host as authoritative
474
+ only after repository Pages is enabled and its first deployment passes the
475
+ exact-byte live check. `model-eol validate` checks feeds, repository policy,
476
+ check reports, inventories, schedules, alerts, and plans against the same zero-dependency
477
+ runtime contracts.
478
+ Maintainer rollout: set Pages source to GitHub Actions, dispatch `feed-refresh`
479
+ from `main`, and require the resulting `public-contract` deployment to pass
480
+ before releasing 0.5.
325
481
  - Current-model entries (and therefore policy-floor horizons) populate only when
326
482
  refresh runs with provider API keys for the models endpoints.
327
483
  - The checker matches known IDs only - it will not discover models absent from the
@@ -333,7 +489,7 @@ move any of us can make here.
333
489
  changes republish automatically as patch versions (trusted publishing with
334
490
  provenance), while manual code releases require an explicit stable version, so
335
491
  `npx model-eol` always checks against current dates.
336
- - Not yet: feed signing, gateway route resolvers.
492
+ - Not yet: feed signing, authenticated account-level gateway/cloud resolvers.
337
493
 
338
494
  ## Contributing
339
495
 
package/SPEC.md CHANGED
@@ -1,6 +1,6 @@
1
- # model-eol - a machine-readable model deprecation feed (draft spec v0.1)
1
+ # model-eol - a machine-readable model deprecation feed (public draft v0.1)
2
2
 
3
- > Status: sketch, 2026-07-25. The problem: model retirement dates live in HTML docs
3
+ > Status: public draft, updated 2026-08-18. The problem: model retirement dates live in HTML docs
4
4
  > and emails, so every deprecation checker is a scraper with a private registry
5
5
  > format. Ordinary software solved this layer - endoflife.date for EOL data, OSV for
6
6
  > vulnerability feeds. This is the model version: small enough that a provider could
@@ -57,10 +57,20 @@ is data, which is exactly what scraping HTML can never give you.
57
57
 
58
58
  `distributions[].status`, when present, is one of `active`, `legacy`,
59
59
  `extended-access`, or `retired`. `legacy` means no new consumers but existing
60
- usage works; `extended-access` means availability continues past the stated
61
- `shutdown` under a paid or opt-in program (Bedrock offers this for some models) -
62
- the `shutdown` date stays the base retirement, and the status flags that a
63
- negotiated tail exists.
60
+ usage works. `extended-access` means the distributor has announced or entered a
61
+ distinct extended-access portion of that lifecycle; it does not imply availability
62
+ after `shutdown`. On Amazon Bedrock, public extended access is a potentially
63
+ higher-priced portion of the Legacy period for active users and ends at EOL, so
64
+ `shutdown` remains the Bedrock EOL date. Bedrock still calls the encompassing
65
+ state Legacy; `extended-access` is this feed's finer-grained channel signal.
66
+ Private arrangements after EOL are not represented by this status. `retired`
67
+ means the distributor explicitly reports the model as no longer generally
68
+ available even when it does not publish an exact shutdown date.
69
+
70
+ A model may contain at most one distribution for each `via`. Duplicate channel
71
+ records are ambiguous and fail runtime semantic validation. Draft-07 can enforce
72
+ whole-object uniqueness, but cannot express uniqueness keyed by the `via` field,
73
+ so `model-eol validate` and feed loading enforce this constraint.
64
74
 
65
75
  ### Structured replacement contract
66
76
 
@@ -119,6 +129,11 @@ months early - or tell an OpenAI-direct shop they're fine because Azure says so.
119
129
  - **Consumers**: anything that can read JSON - a CI step, a dashboard, a Dependabot
120
130
  clone that opens migration PRs when `shutdown - today < threshold`.
121
131
 
132
+ Repository tools that export CycloneDX SHOULD represent a canonical model once
133
+ per lifecycle channel, with a deterministic channel-qualified `bom-ref`. This
134
+ keeps direct publisher and distributor clocks distinct and binds each source
135
+ occurrence to the lifecycle decision that evaluated it.
136
+
122
137
  ## Non-goals (v0.1)
123
138
 
124
139
  Pricing, capabilities, context windows (models.dev and friends already do this);
package/action.yml CHANGED
@@ -5,7 +5,7 @@ branding:
5
5
  color: 'red'
6
6
  inputs:
7
7
  command:
8
- description: 'Command to run: check, inventory, schedule, alert, or plan'
8
+ description: 'Command to run: check, inventory, schedule, alert, plan, or validate'
9
9
  required: false
10
10
  default: 'check'
11
11
  paths:
@@ -40,6 +40,10 @@ inputs:
40
40
  description: 'Path to a model-eol JSON config (defaults to repository config discovery)'
41
41
  required: false
42
42
  default: ''
43
+ document-type:
44
+ description: 'Optional validate document type: feed, config, check, inventory, schedule, alert, or plan'
45
+ required: false
46
+ default: ''
43
47
  include-docs:
44
48
  description: 'Scan documentation files in addition to the default source and config files'
45
49
  required: false
@@ -72,6 +76,7 @@ runs:
72
76
  MODEL_EOL_FORMAT: ${{ inputs.format }}
73
77
  MODEL_EOL_FEEDS: ${{ inputs.feeds }}
74
78
  MODEL_EOL_CONFIG: ${{ inputs.config }}
79
+ MODEL_EOL_DOCUMENT_TYPE: ${{ inputs['document-type'] }}
75
80
  MODEL_EOL_INCLUDE_DOCS: ${{ inputs['include-docs'] }}
76
81
  MODEL_EOL_ALLOW_INCOMPLETE: ${{ inputs['allow-incomplete'] }}
77
82
  MODEL_EOL_JSON: ${{ inputs.json }}
@@ -79,6 +84,12 @@ runs:
79
84
  run: |
80
85
  set -euo pipefail
81
86
 
87
+ NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
88
+ if ! [[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || [ "$NODE_MAJOR" -lt 22 ]; then
89
+ echo "model-eol action: Node 22 or newer is required; found $(node --version 2>/dev/null || echo unknown)" >&2
90
+ exit 2
91
+ fi
92
+
82
93
  split_model_eol_paths() {
83
94
  PATH_ARGS=()
84
95
  if [[ "$MODEL_EOL_PATHS" == *$'\n'* ]]; then
@@ -92,9 +103,9 @@ runs:
92
103
  split_model_eol_paths
93
104
 
94
105
  case "$MODEL_EOL_COMMAND" in
95
- check|inventory|schedule|alert|plan) ;;
106
+ check|inventory|schedule|alert|plan|validate) ;;
96
107
  *)
97
- echo "model-eol action: command must be check, inventory, schedule, alert, or plan" >&2
108
+ echo "model-eol action: command must be check, inventory, schedule, alert, plan, or validate" >&2
98
109
  exit 2
99
110
  ;;
100
111
  esac
@@ -106,8 +117,12 @@ runs:
106
117
  echo "model-eol action: format is only supported by the inventory and alert commands" >&2
107
118
  exit 2
108
119
  fi
120
+ if [ -n "$MODEL_EOL_DOCUMENT_TYPE" ] && [ "$MODEL_EOL_COMMAND" != 'validate' ]; then
121
+ echo "model-eol action: document-type is only supported by the validate command" >&2
122
+ exit 2
123
+ fi
109
124
 
110
- ARGS=("$MODEL_EOL_COMMAND" "${PATH_ARGS[@]}")
125
+ ARGS=("$MODEL_EOL_COMMAND")
111
126
  if [ -n "$MODEL_EOL_DAYS" ]; then ARGS+=(--days "$MODEL_EOL_DAYS"); fi
112
127
  if [ -n "$MODEL_EOL_VIA" ]; then ARGS+=(--via "$MODEL_EOL_VIA"); fi
113
128
  if [ -n "$MODEL_EOL_CHANGED" ]; then ARGS+=(--changed "$MODEL_EOL_CHANGED"); fi
@@ -115,6 +130,7 @@ runs:
115
130
  if [ -n "$MODEL_EOL_FORMAT" ]; then ARGS+=(--format "$MODEL_EOL_FORMAT"); fi
116
131
  if [ -n "$MODEL_EOL_FEEDS" ]; then ARGS+=(--feeds "$MODEL_EOL_FEEDS"); fi
117
132
  if [ -n "$MODEL_EOL_CONFIG" ]; then ARGS+=(--config "$MODEL_EOL_CONFIG"); fi
133
+ if [ -n "$MODEL_EOL_DOCUMENT_TYPE" ]; then ARGS+=(--type "$MODEL_EOL_DOCUMENT_TYPE"); fi
118
134
 
119
135
  append_boolean_flag() {
120
136
  local value="$1"
@@ -146,11 +162,13 @@ runs:
146
162
  esac
147
163
  case "$MODEL_EOL_JSON" in
148
164
  true)
149
- if [ "$MODEL_EOL_COMMAND" = 'plan' ]; then
150
- echo "model-eol action: json is only supported by the check, inventory, schedule, and alert commands" >&2
151
- exit 2
152
- fi
153
- ARGS+=(--json)
165
+ case "$MODEL_EOL_COMMAND" in
166
+ check|inventory|schedule|alert) ARGS+=(--json) ;;
167
+ *)
168
+ echo "model-eol action: json is only supported by the check, inventory, schedule, and alert commands" >&2
169
+ exit 2
170
+ ;;
171
+ esac
154
172
  ;;
155
173
  false|'') ;;
156
174
  *)
@@ -159,6 +177,9 @@ runs:
159
177
  ;;
160
178
  esac
161
179
 
180
+ # Keep every consumer-controlled path after the option terminator.
181
+ ARGS+=(-- "${PATH_ARGS[@]}")
182
+
162
183
  if [ -z "$MODEL_EOL_OUTPUT_FILE" ]; then
163
184
  exec node "$MODEL_EOL_ACTION_PATH/check.mjs" "${ARGS[@]}"
164
185
  fi