model-eol 0.2.4 → 0.4.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
@@ -1,14 +1,16 @@
1
1
  # model-eol
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/model-eol)
4
+ [![marketplace](https://img.shields.io/badge/marketplace-model--eol%20check-blue?logo=github)](https://github.com/marketplace/actions/model-eol-check)
4
5
  ![CI](https://github.com/thossullivan/model-eol/actions/workflows/ci.yml/badge.svg)
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)
5
7
  ![spec](https://img.shields.io/badge/spec-model--eol%2F0.1-blue)
6
8
  ![deps](https://img.shields.io/badge/runtime%20deps-zero-brightgreen)
7
9
  ![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
8
10
  ![license](https://img.shields.io/badge/license-MIT-blue)
9
11
 
10
12
  **A machine-readable deprecation feed format for AI models, plus the reference
11
- tooling that turns it into a Dependabot for models.** v0.2.3.
13
+ tooling that turns it into a Dependabot for models.**
12
14
 
13
15
  ![model-eol demo: retired models, distributor clocks, worst-case date](docs/img/demo.gif)
14
16
 
@@ -31,14 +33,15 @@ second, so the existing trackers can converge instead of each scraping alone.
31
33
  replacement, `distributions` for per-channel lifecycles, and publisher `policy`
32
34
  floors. Small enough that a provider could serve it at
33
35
  `/.well-known/model-eol.json` in an afternoon.
34
- - **`feeds/`** - <!-- feeds-status -->Amazon (4 entries), Anthropic (29 entries), Google (64 entries) and OpenAI (194 entries), generated from the providers' live deprecation pages plus the AWS Bedrock lifecycle page, last verified 2026-08-03<!-- /feeds-status -->. Every
36
+ - **`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
35
37
  dated entry carries a source URL.
36
38
  - **`check.mjs`** - zero-dependency CLI: CI gate, PR diff gate, inventory, CycloneDX
37
39
  ML-BOM export, retirement schedule, alerts and badges, migration plan/apply.
38
40
  - **`refresh/`** - regenerates the feeds from provider pages, models endpoints, and
39
41
  distributor lifecycle pages, with a semantic diff for human review.
40
42
  - **`bot/`** - the Dependabot part: a cron GitHub workflow that maintains one
41
- migration PR or issue per retiring model.
43
+ migration PR or issue per retiring model, published as the `model-eol-bot`
44
+ binary in the same npm package.
42
45
  - **`scripts/feed-changelog.mjs`** - the feeds' git history as Atom/markdown, so
43
46
  "model retirements as they are announced" is a feed you can subscribe to.
44
47
 
@@ -101,6 +104,11 @@ PRs to trigger checks), and provider API keys enter the picture in exactly two
101
104
  optional places: your own eval hook command, and the feed-refresh models-endpoint
102
105
  coverage.
103
106
 
107
+ Both command-line binaries ship in one zero-dependency npm artifact. The test
108
+ suite packs that artifact, installs it into an empty consumer directory with the
109
+ network disabled, and runs both bins; it also fails if runtime, development,
110
+ peer, or optional npm dependencies are introduced.
111
+
104
112
  ## Use it as a GitHub Action
105
113
 
106
114
  The same gate as a composite action - no npm install, feeds bundled, pinned to
@@ -122,9 +130,69 @@ That is the PR gate: fail only when *this* change adds a model that is retired
122
130
  or retiring within the threshold. Add `via: aws-bedrock` (or `azure-ai-foundry`)
123
131
  to judge by a distributor's clock. The copy-paste version with both gates - PR
124
132
  diff plus a weekly full-repository check - is
125
- [`examples/workflows/model-eol.yml`](examples/workflows/model-eol.yml); the
126
- same inputs drive `inventory` and `schedule` if CI should export an ML-BOM or a
127
- retirement calendar instead.
133
+ [`examples/workflows/model-eol.yml`](examples/workflows/model-eol.yml). The
134
+ Action also exposes `inventory`, `schedule`, `alert`, and `plan`. Use
135
+ `format: cyclonedx` for an ML-BOM, or `output-file` to retain any report as a
136
+ workflow artifact while keeping the same output in the job log.
137
+
138
+ ## One repository policy
139
+
140
+ The CLI, Action, and bot all discover a strict `.model-eol.json` at the target
141
+ Git repository root. CLI flags or Action inputs override configured values, and
142
+ `--config FILE` selects an explicit policy when auto-discovery is ambiguous:
143
+
144
+ ```json
145
+ {
146
+ "days": 90,
147
+ "scope": "direct",
148
+ "via": null,
149
+ "ignore": {
150
+ "models": ["o3-deep-research"],
151
+ "paths": ["test/fixtures/**", "vendor"]
152
+ },
153
+ "issues": { "enabled": true },
154
+ "eval": { "command": "npm test" }
155
+ }
156
+ ```
157
+
158
+ Unknown keys and invalid values fail closed. A canonical model ID or any alias
159
+ in `ignore.models` suppresses the whole alias family; `ignore.paths` uses
160
+ repo-relative `*`, `**`, and `?` globs and excludes matching files before scan
161
+ coverage limits are counted. With no config the CLI keeps its historical
162
+ `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).
165
+
166
+ Mixed-provider monorepos can keep those top-level values as repository defaults,
167
+ then apply path policies and a lifecycle channel per reference:
168
+
169
+ ```json
170
+ {
171
+ "days": 90,
172
+ "scope": "direct",
173
+ "overrides": [
174
+ { "paths": ["services/**"], "scope": "all" },
175
+ { "paths": ["services/bedrock/**"], "days": 180, "via": "aws-bedrock" }
176
+ ],
177
+ "routes": [
178
+ { "paths": ["services/vertex/**"], "via": "vertex-ai" },
179
+ {
180
+ "paths": ["services/bedrock/**"],
181
+ "match": "chat-prod",
182
+ "model": "claude-3-7-sonnet-20250219",
183
+ "via": "aws-bedrock"
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ Matching overrides apply in order: later scalar values win and ignore lists are
190
+ additive. The last matching route wins. Exact `match`/`model` pairs resolve a
191
+ repository deployment alias without making cloud or gateway references patchable.
192
+ Explicit CLI flags and Action inputs still take precedence. JSON inventory and
193
+ plan output records each reference's effective threshold, scope, requested
194
+ channel, and matching rule indexes, so a Bedrock service and a direct publisher
195
+ call in the same repository do not silently share one global clock.
128
196
 
129
197
  ## Same weights, different clocks
130
198
 
@@ -133,9 +201,9 @@ this is wrong in both directions: o3-deep-research died at OpenAI on July 23 but
133
201
  lives on Azure AI Foundry until December 26; claude-opus-4-1 retires at Anthropic on
134
202
  August 5 but lives on Amazon Bedrock until January 8, 2027. `distributions` in the
135
203
  spec carries these per-channel clocks, `--via <distributor>` judges your repo by the
136
- channel you actually call, and the bedrock distributor fetcher keeps the clocks
137
- current from AWS's own lifecycle page. Treat a distributor's later date as runway
138
- for the same migration, not as a destination.
204
+ channel you actually call, and the distributor refresh keeps Bedrock and Vertex
205
+ clocks current from their lifecycle pages. Treat a distributor's later date as
206
+ runway for the same migration, not as a destination.
139
207
 
140
208
  ## Policy floors - a planning floor from absence
141
209
 
@@ -166,33 +234,58 @@ stay visible without generating false alarms.
166
234
  The scanner covers code and configuration files with [`CODE_EXT`](lib/scanner.mjs)
167
235
  as the source of truth - Python, TypeScript/JavaScript, JSON/YAML/TOML, shell,
168
236
  Ruby, Go, Java, C#, Rust, C/C++, PHP, Swift, Kotlin, Terraform, SQL, Scala, and
169
- Objective-C. Terraform files (`.tf`, `.tfvars`) are scanned while `.terraform`
170
- directories stay excluded.
237
+ Objective-C, including `.baml` sources. Publisher-compatible BAML provider/model
238
+ pairs are eligible plan targets; generated BAML clients and conservative
239
+ `DO NOT EDIT`/`@generated` sources are skipped. Model-eol's own JSON reports are
240
+ also skipped on reruns, while third-party CycloneDX files remain scannable unless
241
+ they carry explicit model-eol generator provenance. Terraform files (`.tf`,
242
+ `.tfvars`) are scanned while `.terraform` directories stay excluded.
171
243
 
172
244
  ## The bot
173
245
 
174
246
  Copy `bot.yml.example` to `.github/workflows/model-eol-bot.yml` for a weekly run
175
247
  that maintains exactly one labelled migration PR per retiring model (full feed
176
248
  context in the body, never auto-merged) and one issue per finding that needs a
177
- human. Dismissals are respected; a changed shutdown date reopens. Configure via
178
- `.model-eol.json`: thresholds, scope, ignores, and an optional eval hook that runs
179
- your own regression command against the replacement before the PR opens. Preview
180
- everything with no GitHub calls:
249
+ human. The workflow resolves `model-eol@0` from npm once and reuses that exact
250
+ version across its plan, read-only evaluation, and write-token publication jobs;
251
+ consumer repositories do not copy `check.mjs` or the `bot/` source directory.
252
+ 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:
181
255
 
182
256
  ```sh
183
- node bot/bot.mjs --dry-run --target-dir . --repo OWNER/REPO
257
+ npx --yes --package=model-eol@0 model-eol-bot --dry-run --target-dir . --repo OWNER/REPO
184
258
  ```
185
259
 
260
+ The workflow honors configured `days`, `scope`, and `via` values; without a config
261
+ it uses the bot defaults of 90 days and direct references. `eval.command` also
262
+ comes from the config unless the `MODEL_EOL_EVAL_COMMAND` repository variable is
263
+ set as an explicit workflow override. Each patchable model is applied in an
264
+ isolated checkout and evaluated independently with its own old/new IDs, timeout,
265
+ bounded report, and explicitly allowed environment. The content-bound result
266
+ manifest is also pinned to the evaluated Git commit, letting passing migrations
267
+ proceed while a failing peer remains blocked without publishing against a newer,
268
+ unevaluated default branch.
269
+
270
+ On its first actionable run, the bot creates the `model-eol` label. If repository
271
+ policy prevents label creation or assignment, it fails closed rather than publish
272
+ work whose ownership cannot be authenticated. Existing work is trusted only when
273
+ the label, metadata, deterministic branch, repository, base, and Git lease agree.
274
+ For HTTPS remotes—including private repositories—the bot passes `GITHUB_TOKEN` to
275
+ temporary Git processes as a host-scoped authorization header and never places it
276
+ in a remote URL.
277
+
186
278
  Two operational notes the hard way teaches: PRs created with `GITHUB_TOKEN` do not
187
279
  trigger `pull_request` workflows (use a fine-grained PAT or GitHub App token when
188
- checks must run), and the workflow splits privileges - provider keys live only in
189
- the read-only plan/eval job, write tokens only in the publish job.
280
+ 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.
190
283
 
191
284
  ## Keeping the feeds honest
192
285
 
193
286
  ```sh
194
287
  node refresh/refresh.mjs --check # semantic diff vs live pages; exit 3 = PR-worthy
195
- node refresh/refresh.mjs --distributor aws-bedrock # distributor clocks from AWS's lifecycle page
288
+ node refresh/refresh.mjs --distributor aws-bedrock,vertex-ai # distributor lifecycle clocks
196
289
  node scripts/feed-changelog.mjs # feeds' git history as an Atom feed
197
290
  ```
198
291
 
@@ -226,8 +319,9 @@ move any of us can make here.
226
319
 
227
320
  - Feeds refresh automatically: the weekly feed-refresh workflow is live in this
228
321
  repo (parse failures fail the run; material changes become a reviewed PR), and
229
- CI runs the full suite on every push and PR. The bot workflow still ships as
230
- `bot.yml.example`.
322
+ CI runs the full suite on every push and PR. The copy-ready bot workflow ships
323
+ as `bot.yml.example` and consumes the published package rather than local copies
324
+ of the tooling.
231
325
  - Current-model entries (and therefore policy-floor horizons) populate only when
232
326
  refresh runs with provider API keys for the models endpoints.
233
327
  - The checker matches known IDs only - it will not discover models absent from the
@@ -237,7 +331,8 @@ move any of us can make here.
237
331
  lifecycle fetcher is not built yet.
238
332
  - On npm as [`model-eol`](https://www.npmjs.com/package/model-eol). Material feed
239
333
  changes republish automatically as patch versions (trusted publishing with
240
- provenance), so `npx model-eol` always checks against current dates.
334
+ provenance), while manual code releases require an explicit stable version, so
335
+ `npx model-eol` always checks against current dates.
241
336
  - Not yet: feed signing, gateway route resolvers.
242
337
 
243
338
  ## Contributing
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, or schedule'
8
+ description: 'Command to run: check, inventory, schedule, alert, or plan'
9
9
  required: false
10
10
  default: 'check'
11
11
  paths:
@@ -13,35 +13,56 @@ inputs:
13
13
  required: false
14
14
  default: '.'
15
15
  days:
16
- description: 'Fail when a model retires within this many days'
16
+ description: 'Fail when a model retires within this many days (defaults to CLI or repository config)'
17
17
  required: false
18
- default: '90'
18
+ default: ''
19
19
  via:
20
20
  description: 'Judge lifecycles by a distributor clock (e.g. azure-ai-foundry, aws-bedrock) instead of the publisher''s'
21
21
  required: false
22
22
  default: ''
23
23
  scope:
24
- description: 'Check scope: all tracked model IDs, or direct to leave cloud/gateway refs as inventory hints'
24
+ description: 'Check scope: all tracked model IDs, or direct to leave cloud/gateway refs as inventory hints (defaults to CLI or repository config)'
25
25
  required: false
26
- default: 'all'
26
+ default: ''
27
27
  changed:
28
28
  description: 'Base ref for the PR gate: check only lines added relative to this ref (requires fetch-depth: 0)'
29
29
  required: false
30
30
  default: ''
31
31
  format:
32
- description: 'Alert output format when command is alert: github, markdown, or badge'
32
+ description: 'Output format: json, cyclonedx, or text for inventory; github, markdown, badge, or json for alert. Defaults to json for inventory and github for alert.'
33
33
  required: false
34
- default: 'github'
34
+ default: ''
35
35
  feeds:
36
36
  description: 'Directory of model-eol feeds (defaults to the feeds bundled with the action)'
37
37
  required: false
38
38
  default: ''
39
+ config:
40
+ description: 'Path to a model-eol JSON config (defaults to repository config discovery)'
41
+ required: false
42
+ default: ''
43
+ include-docs:
44
+ description: 'Scan documentation files in addition to the default source and config files'
45
+ required: false
46
+ default: 'false'
47
+ allow-incomplete:
48
+ description: 'Allow check and plan to continue when scan coverage is incomplete'
49
+ required: false
50
+ default: 'false'
51
+ json:
52
+ description: 'Emit JSON for check, inventory, schedule, or alert'
53
+ required: false
54
+ default: 'false'
55
+ output-file:
56
+ description: 'Optional path that receives an exact copy of stdout while stdout remains visible in the Actions log'
57
+ required: false
58
+ default: ''
39
59
  runs:
40
60
  using: 'composite'
41
61
  steps:
42
62
  - name: model-eol check
43
63
  shell: bash
44
64
  env:
65
+ MODEL_EOL_ACTION_PATH: ${{ github.action_path }}
45
66
  MODEL_EOL_COMMAND: ${{ inputs.command }}
46
67
  MODEL_EOL_PATHS: ${{ inputs.paths }}
47
68
  MODEL_EOL_DAYS: ${{ inputs.days }}
@@ -50,7 +71,14 @@ runs:
50
71
  MODEL_EOL_SCOPE: ${{ inputs.scope }}
51
72
  MODEL_EOL_FORMAT: ${{ inputs.format }}
52
73
  MODEL_EOL_FEEDS: ${{ inputs.feeds }}
74
+ MODEL_EOL_CONFIG: ${{ inputs.config }}
75
+ MODEL_EOL_INCLUDE_DOCS: ${{ inputs['include-docs'] }}
76
+ MODEL_EOL_ALLOW_INCOMPLETE: ${{ inputs['allow-incomplete'] }}
77
+ MODEL_EOL_JSON: ${{ inputs.json }}
78
+ MODEL_EOL_OUTPUT_FILE: ${{ inputs['output-file'] }}
53
79
  run: |
80
+ set -euo pipefail
81
+
54
82
  split_model_eol_paths() {
55
83
  PATH_ARGS=()
56
84
  if [[ "$MODEL_EOL_PATHS" == *$'\n'* ]]; then
@@ -62,10 +90,97 @@ runs:
62
90
  fi
63
91
  }
64
92
  split_model_eol_paths
65
- ARGS=("$MODEL_EOL_COMMAND" "${PATH_ARGS[@]}" --days "$MODEL_EOL_DAYS")
93
+
94
+ case "$MODEL_EOL_COMMAND" in
95
+ check|inventory|schedule|alert|plan) ;;
96
+ *)
97
+ echo "model-eol action: command must be check, inventory, schedule, alert, or plan" >&2
98
+ exit 2
99
+ ;;
100
+ esac
101
+ if [ -n "$MODEL_EOL_CHANGED" ] && [ "$MODEL_EOL_COMMAND" != 'check' ]; then
102
+ echo "model-eol action: changed is only supported by the check command" >&2
103
+ exit 2
104
+ fi
105
+ if [ -n "$MODEL_EOL_FORMAT" ] && [ "$MODEL_EOL_COMMAND" != 'inventory' ] && [ "$MODEL_EOL_COMMAND" != 'alert' ]; then
106
+ echo "model-eol action: format is only supported by the inventory and alert commands" >&2
107
+ exit 2
108
+ fi
109
+
110
+ ARGS=("$MODEL_EOL_COMMAND" "${PATH_ARGS[@]}")
111
+ if [ -n "$MODEL_EOL_DAYS" ]; then ARGS+=(--days "$MODEL_EOL_DAYS"); fi
66
112
  if [ -n "$MODEL_EOL_VIA" ]; then ARGS+=(--via "$MODEL_EOL_VIA"); fi
67
113
  if [ -n "$MODEL_EOL_CHANGED" ]; then ARGS+=(--changed "$MODEL_EOL_CHANGED"); fi
68
114
  if [ -n "$MODEL_EOL_SCOPE" ]; then ARGS+=(--scope "$MODEL_EOL_SCOPE"); fi
69
115
  if [ -n "$MODEL_EOL_FORMAT" ]; then ARGS+=(--format "$MODEL_EOL_FORMAT"); fi
70
116
  if [ -n "$MODEL_EOL_FEEDS" ]; then ARGS+=(--feeds "$MODEL_EOL_FEEDS"); fi
71
- node "${{ github.action_path }}/check.mjs" "${ARGS[@]}"
117
+ if [ -n "$MODEL_EOL_CONFIG" ]; then ARGS+=(--config "$MODEL_EOL_CONFIG"); fi
118
+
119
+ append_boolean_flag() {
120
+ local value="$1"
121
+ local flag="$2"
122
+ local input_name="$3"
123
+ case "$value" in
124
+ true) ARGS+=("$flag") ;;
125
+ false|'') ;;
126
+ *)
127
+ echo "model-eol action: $input_name must be true or false" >&2
128
+ exit 2
129
+ ;;
130
+ esac
131
+ }
132
+ append_boolean_flag "$MODEL_EOL_INCLUDE_DOCS" --include-docs include-docs
133
+ case "$MODEL_EOL_ALLOW_INCOMPLETE" in
134
+ true)
135
+ if [ "$MODEL_EOL_COMMAND" != 'check' ] && [ "$MODEL_EOL_COMMAND" != 'plan' ]; then
136
+ echo "model-eol action: allow-incomplete is only supported by the check and plan commands" >&2
137
+ exit 2
138
+ fi
139
+ ARGS+=(--allow-incomplete)
140
+ ;;
141
+ false|'') ;;
142
+ *)
143
+ echo "model-eol action: allow-incomplete must be true or false" >&2
144
+ exit 2
145
+ ;;
146
+ esac
147
+ case "$MODEL_EOL_JSON" in
148
+ 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)
154
+ ;;
155
+ false|'') ;;
156
+ *)
157
+ echo "model-eol action: json must be true or false" >&2
158
+ exit 2
159
+ ;;
160
+ esac
161
+
162
+ if [ -z "$MODEL_EOL_OUTPUT_FILE" ]; then
163
+ exec node "$MODEL_EOL_ACTION_PATH/check.mjs" "${ARGS[@]}"
164
+ fi
165
+ if [ -d "$MODEL_EOL_OUTPUT_FILE" ]; then
166
+ echo "model-eol action: output-file must name a file, not a directory" >&2
167
+ exit 2
168
+ fi
169
+
170
+ OUTPUT_TEMP="$(mktemp "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/model-eol-output.XXXXXX")"
171
+ trap 'rm -f "$OUTPUT_TEMP"' EXIT
172
+ set +e
173
+ node "$MODEL_EOL_ACTION_PATH/check.mjs" "${ARGS[@]}" | tee "$OUTPUT_TEMP"
174
+ PIPE_STATUSES=("${PIPESTATUS[@]}")
175
+ CLI_STATUS="${PIPE_STATUSES[0]}"
176
+ TEE_STATUS="${PIPE_STATUSES[1]}"
177
+ COPY_STATUS=0
178
+ if [ "$TEE_STATUS" -eq 0 ]; then
179
+ cp -- "$OUTPUT_TEMP" "$MODEL_EOL_OUTPUT_FILE"
180
+ COPY_STATUS=$?
181
+ fi
182
+
183
+ FINAL_STATUS="$CLI_STATUS"
184
+ if [ "$FINAL_STATUS" -eq 0 ] && [ "$TEE_STATUS" -ne 0 ]; then FINAL_STATUS="$TEE_STATUS"; fi
185
+ if [ "$FINAL_STATUS" -eq 0 ] && [ "$COPY_STATUS" -ne 0 ]; then FINAL_STATUS="$COPY_STATUS"; fi
186
+ exit "$FINAL_STATUS"