model-eol 0.2.3 → 0.3.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,6 +1,7 @@
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)
5
6
  ![spec](https://img.shields.io/badge/spec-model--eol%2F0.1-blue)
6
7
  ![deps](https://img.shields.io/badge/runtime%20deps-zero-brightgreen)
@@ -8,17 +9,18 @@
8
9
  ![license](https://img.shields.io/badge/license-MIT-blue)
9
10
 
10
11
  **A machine-readable deprecation feed format for AI models, plus the reference
11
- tooling that turns it into a Dependabot for models.** v0.2.2.
12
+ tooling that turns it into a Dependabot for models.** v0.2.4.
12
13
 
13
14
  ![model-eol demo: retired models, distributor clocks, worst-case date](docs/img/demo.gif)
14
15
 
15
16
  ## The problem
16
17
 
17
- On July 23, 2026, OpenAI shut down 18 models on schedule. Teams that pinned dated
18
- snapshots - the provider's own recommended practice - got production 404s, because a
19
- model ID is the one dependency your toolchain can't see. When an npm package is
20
- deprecated, the warning prints at install, Dependabot opens the PR, the advisory
21
- fails CI. A model's retirement date lives in an HTML docs page and an email.
18
+ On July 23, 2026, OpenAI shut down 15 model snapshots on schedule. One of my
19
+ tuned research workflows returned a model-not-found error that morning even though
20
+ the retirement had been documented for months. A model ID is the one dependency
21
+ my toolchain could not see. When an npm package is deprecated, the warning prints
22
+ at install, Dependabot opens the PR, and an advisory can fail CI. A model's
23
+ retirement date usually lives in an HTML docs page and an email.
22
24
 
23
25
  Ordinary software solved this layer: endoflife.date for EOL data, OSV for
24
26
  vulnerability feeds. This is the model version - a shared schema first, tooling
@@ -30,14 +32,15 @@ second, so the existing trackers can converge instead of each scraping alone.
30
32
  replacement, `distributions` for per-channel lifecycles, and publisher `policy`
31
33
  floors. Small enough that a provider could serve it at
32
34
  `/.well-known/model-eol.json` in an afternoon.
33
- - **`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
35
+ - **`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 and Google Vertex AI lifecycle pages, feed data generated 2026-08-03<!-- /feeds-status -->. Every
34
36
  dated entry carries a source URL.
35
37
  - **`check.mjs`** - zero-dependency CLI: CI gate, PR diff gate, inventory, CycloneDX
36
38
  ML-BOM export, retirement schedule, alerts and badges, migration plan/apply.
37
39
  - **`refresh/`** - regenerates the feeds from provider pages, models endpoints, and
38
40
  distributor lifecycle pages, with a semantic diff for human review.
39
41
  - **`bot/`** - the Dependabot part: a cron GitHub workflow that maintains one
40
- migration PR or issue per retiring model.
42
+ migration PR or issue per retiring model, published as the `model-eol-bot`
43
+ binary in the same npm package.
41
44
  - **`scripts/feed-changelog.mjs`** - the feeds' git history as Atom/markdown, so
42
45
  "model retirements as they are announced" is a feed you can subscribe to.
43
46
 
@@ -100,6 +103,65 @@ PRs to trigger checks), and provider API keys enter the picture in exactly two
100
103
  optional places: your own eval hook command, and the feed-refresh models-endpoint
101
104
  coverage.
102
105
 
106
+ Both command-line binaries ship in one zero-dependency npm artifact. The test
107
+ suite packs that artifact, installs it into an empty consumer directory with the
108
+ network disabled, and runs both bins; it also fails if runtime, development,
109
+ peer, or optional npm dependencies are introduced.
110
+
111
+ ## Use it as a GitHub Action
112
+
113
+ The same gate as a composite action - no npm install, feeds bundled, pinned to
114
+ the `v0` line:
115
+
116
+ ```yaml
117
+ - uses: actions/checkout@v4
118
+ with:
119
+ fetch-depth: 0 # --changed needs history to diff against the base ref
120
+ - uses: thossullivan/model-eol@v0
121
+ with:
122
+ command: check
123
+ changed: origin/${{ github.base_ref }}
124
+ days: 90
125
+ scope: direct
126
+ ```
127
+
128
+ That is the PR gate: fail only when *this* change adds a model that is retired
129
+ or retiring within the threshold. Add `via: aws-bedrock` (or `azure-ai-foundry`)
130
+ to judge by a distributor's clock. The copy-paste version with both gates - PR
131
+ diff plus a weekly full-repository check - is
132
+ [`examples/workflows/model-eol.yml`](examples/workflows/model-eol.yml). The
133
+ Action also exposes `inventory`, `schedule`, `alert`, and `plan`. Use
134
+ `format: cyclonedx` for an ML-BOM, or `output-file` to retain any report as a
135
+ workflow artifact while keeping the same output in the job log.
136
+
137
+ ## One repository policy
138
+
139
+ The CLI, Action, and bot all discover a strict `.model-eol.json` at the target
140
+ Git repository root. CLI flags or Action inputs override configured values, and
141
+ `--config FILE` selects an explicit policy when auto-discovery is ambiguous:
142
+
143
+ ```json
144
+ {
145
+ "days": 90,
146
+ "scope": "direct",
147
+ "via": null,
148
+ "ignore": {
149
+ "models": ["o3-deep-research"],
150
+ "paths": ["test/fixtures/**", "vendor"]
151
+ },
152
+ "issues": { "enabled": true },
153
+ "eval": { "command": "npm test" }
154
+ }
155
+ ```
156
+
157
+ Unknown keys and invalid values fail closed. A canonical model ID or any alias
158
+ in `ignore.models` suppresses the whole alias family; `ignore.paths` uses
159
+ repo-relative `*`, `**`, and `?` globs and excludes matching files before scan
160
+ coverage limits are counted. With no config the CLI keeps its historical
161
+ `scope: all`; once a config exists, omitted values use the shared bot defaults
162
+ (`days: 90`, `scope: direct`, and the publisher clock). The complete contract is
163
+ [`schema/model-eol.bot-config.schema.json`](schema/model-eol.bot-config.schema.json).
164
+
103
165
  ## Same weights, different clocks
104
166
 
105
167
  The same model retires on different dates per channel, and a checker that ignores
@@ -107,17 +169,17 @@ this is wrong in both directions: o3-deep-research died at OpenAI on July 23 but
107
169
  lives on Azure AI Foundry until December 26; claude-opus-4-1 retires at Anthropic on
108
170
  August 5 but lives on Amazon Bedrock until January 8, 2027. `distributions` in the
109
171
  spec carries these per-channel clocks, `--via <distributor>` judges your repo by the
110
- channel you actually call, and the bedrock distributor fetcher keeps the clocks
111
- current from AWS's own lifecycle page. Treat a distributor's later date as runway
112
- for the same migration, not as a destination.
172
+ channel you actually call, and the distributor refresh keeps Bedrock and Vertex
173
+ clocks current from their lifecycle pages. Treat a distributor's later date as
174
+ runway for the same migration, not as a destination.
113
175
 
114
- ## Policy floors - a forward guarantee from absence
176
+ ## Policy floors - a planning floor from absence
115
177
 
116
178
  A feed entry with no `announced` and no `shutdown` is an affirmative "nothing
117
179
  scheduled as of `generated`" - something scraping HTML can never say. Publishers
118
- with a stated minimum notice period (Anthropic commits to >=60 days) carry a
119
- `policy` floor, so the schedule can say `guaranteed until <date> per anthropic
120
- stated policy`. Stated policy, not a contract - the wording says so. OpenAI
180
+ with a stated minimum notice period (Anthropic states >=60 days) carry a
181
+ `policy` floor, so the schedule can calculate a no-earlier-than planning date
182
+ from the feed refresh. Stated policy, not a contract. OpenAI
121
183
  publishes no formal floor, so its feed makes no forward claim.
122
184
 
123
185
  ## Direct-first inventory
@@ -148,25 +210,41 @@ directories stay excluded.
148
210
  Copy `bot.yml.example` to `.github/workflows/model-eol-bot.yml` for a weekly run
149
211
  that maintains exactly one labelled migration PR per retiring model (full feed
150
212
  context in the body, never auto-merged) and one issue per finding that needs a
151
- human. Dismissals are respected; a changed shutdown date reopens. Configure via
152
- `.model-eol.json`: thresholds, scope, ignores, and an optional eval hook that runs
153
- your own regression command against the replacement before the PR opens. Preview
154
- everything with no GitHub calls:
213
+ human. The workflow runs `model-eol@0` from npm in both jobs; consumer repositories
214
+ do not need to copy `check.mjs` or the `bot/` source directory. Dismissals are
215
+ respected; a changed shutdown date reopens. Configure via `.model-eol.json`:
216
+ thresholds, scope, ignores, and an optional plan-level eval hook that can inspect
217
+ `MODEL_EOL_PLAN` before publication. Preview everything with no GitHub calls and
218
+ without adding a project dependency:
155
219
 
156
220
  ```sh
157
- node bot/bot.mjs --dry-run --target-dir . --repo OWNER/REPO
221
+ npx --yes --package=model-eol@0 model-eol-bot --dry-run --target-dir . --repo OWNER/REPO
158
222
  ```
159
223
 
224
+ The workflow honors configured `days`, `scope`, and `via` values; without a config
225
+ it uses the bot defaults of 90 days and direct references. `eval.command` also
226
+ comes from the config unless the `MODEL_EOL_EVAL_COMMAND` repository variable is
227
+ set as an explicit workflow override. In the split workflow this command runs
228
+ once in the unmodified, read-only checkout; it is a plan preflight, not a test of
229
+ each patched migration branch.
230
+
231
+ On its first actionable run, the bot creates the `model-eol` label. If repository
232
+ policy prevents label creation or assignment, it warns and still publishes work;
233
+ metadata in each PR or issue preserves idempotence. For HTTPS remotes—including
234
+ private repositories—the bot passes `GITHUB_TOKEN` to its temporary Git processes
235
+ as a host-scoped authorization header and never places it in a remote URL.
236
+
160
237
  Two operational notes the hard way teaches: PRs created with `GITHUB_TOKEN` do not
161
238
  trigger `pull_request` workflows (use a fine-grained PAT or GitHub App token when
162
- checks must run), and the workflow splits privileges - provider keys live only in
163
- the read-only plan/eval job, write tokens only in the publish job.
239
+ checks must run, stored as the optional `MODEL_EOL_BOT_TOKEN` secret), and the
240
+ workflow splits privileges—provider keys live only in the read-only plan/eval job,
241
+ write tokens only in the publish job.
164
242
 
165
243
  ## Keeping the feeds honest
166
244
 
167
245
  ```sh
168
246
  node refresh/refresh.mjs --check # semantic diff vs live pages; exit 3 = PR-worthy
169
- node refresh/refresh.mjs --distributor aws-bedrock # distributor clocks from AWS's lifecycle page
247
+ node refresh/refresh.mjs --distributor aws-bedrock,vertex-ai # distributor lifecycle clocks
170
248
  node scripts/feed-changelog.mjs # feeds' git history as an Atom feed
171
249
  ```
172
250
 
@@ -178,6 +256,14 @@ their keep: they caught the hand-compiled feeds drifting from Anthropic's
178
256
  recommendations within one week, and corrected a hand-compiled Bedrock date that
179
257
  was three months wrong.
180
258
 
259
+ The refresh also travels in the other direction. On August 3, Google removed the
260
+ previously listed October 16 earliest shutdown dates for the Gemini 2.5 Pro,
261
+ Flash, and Flash-Lite GA models. The pipeline opened
262
+ [a PR with the semantic retraction](https://github.com/thossullivan/model-eol/pull/60),
263
+ and merging it published v0.2.3 so downstream checks stopped warning from dates
264
+ Google no longer listed. A lifecycle tracker has to retract stale alarms as well
265
+ as add new ones.
266
+
181
267
  ## The actual ask
182
268
 
183
269
  The endgame is not "another checker"; it's the format. Providers already publish
@@ -192,8 +278,9 @@ move any of us can make here.
192
278
 
193
279
  - Feeds refresh automatically: the weekly feed-refresh workflow is live in this
194
280
  repo (parse failures fail the run; material changes become a reviewed PR), and
195
- CI runs the full suite on every push and PR. The bot workflow still ships as
196
- `bot.yml.example`.
281
+ CI runs the full suite on every push and PR. The copy-ready bot workflow ships
282
+ as `bot.yml.example` and consumes the published package rather than local copies
283
+ of the tooling.
197
284
  - Current-model entries (and therefore policy-floor horizons) populate only when
198
285
  refresh runs with provider API keys for the models endpoints.
199
286
  - The checker matches known IDs only - it will not discover models absent from the
@@ -203,7 +290,8 @@ move any of us can make here.
203
290
  lifecycle fetcher is not built yet.
204
291
  - On npm as [`model-eol`](https://www.npmjs.com/package/model-eol). Material feed
205
292
  changes republish automatically as patch versions (trusted publishing with
206
- provenance), so `npx model-eol` always checks against current dates.
293
+ provenance), while manual code releases require an explicit stable version, so
294
+ `npx model-eol` always checks against current dates.
207
295
  - Not yet: feed signing, gateway route resolvers.
208
296
 
209
297
  ## 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"