model-eol 0.4.1 → 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 +173 -17
- package/SPEC.md +21 -6
- package/action.yml +30 -9
- package/bot/bot.mjs +361 -165
- package/bot/lib/git.mjs +49 -0
- package/bot/lib/github.mjs +4 -0
- package/bot.yml.example +20 -11
- package/check.mjs +55 -4
- package/examples/model-eol-eval.mjs +84 -0
- package/examples/workflows/model-eol.yml +52 -0
- package/feeds/amazon.json +5 -1
- package/feeds/anthropic.json +4 -1
- package/lib/apply.mjs +393 -231
- package/lib/config.mjs +16 -1
- package/lib/feeds.mjs +22 -3
- package/lib/json-schema.mjs +291 -0
- package/lib/reports.mjs +95 -22
- package/lib/scanner.mjs +159 -25
- package/lib/validate-document.mjs +320 -0
- package/lib/validate-feed.mjs +7 -1
- package/package.json +4 -3
- package/schema/model-eol.alert.schema.json +46 -10
- package/schema/model-eol.bot-config.schema.json +8 -4
- package/schema/model-eol.check.schema.json +56 -0
- package/schema/model-eol.inventory.schema.json +82 -81
- package/schema/model-eol.plan.schema.json +5 -5
- package/schema/model-eol.schedule.schema.json +10 -10
- package/schema/model-eol.schema.json +24 -8
package/bot/lib/git.mjs
CHANGED
|
@@ -105,6 +105,55 @@ export const verifyBotBranch = (cwd, branch, expectedHead, auth = null) => {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export const verifyRemoteBranchHead = (cwd, branch, expectedHead, auth = null) => {
|
|
109
|
+
try {
|
|
110
|
+
run(cwd, ['fetch', 'origin', '--prune'], false, auth)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { head: null, safe: false, error: error.message }
|
|
113
|
+
}
|
|
114
|
+
const head = run(cwd, ['rev-parse', '--verify', `refs/remotes/origin/${branch}`], true)
|
|
115
|
+
return {
|
|
116
|
+
head,
|
|
117
|
+
safe: head === expectedHead,
|
|
118
|
+
error: null,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const exactObjectId = value => typeof value === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value)
|
|
123
|
+
|
|
124
|
+
const requireExactObjectId = (value, name) => {
|
|
125
|
+
if (!exactObjectId(value)) throw new Error(`refusing remote branch mutation without an exact ${name} commit`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const isCommitAvailable = (cwd, head) => exactObjectId(head)
|
|
129
|
+
&& run(cwd, ['cat-file', '-e', `${head}^{commit}`], true) !== null
|
|
130
|
+
|
|
131
|
+
export const deleteRemoteBranch = (cwd, branch, expectedHead, auth = null) => {
|
|
132
|
+
requireExactObjectId(expectedHead, 'expected')
|
|
133
|
+
const ref = `refs/heads/${branch}`
|
|
134
|
+
run(cwd, [
|
|
135
|
+
'push',
|
|
136
|
+
`--force-with-lease=${ref}:${expectedHead}`,
|
|
137
|
+
'origin',
|
|
138
|
+
`:${ref}`,
|
|
139
|
+
], false, auth)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const restoreRemoteBranch = (cwd, branch, expectedCurrentHead, restoreHead, auth = null) => {
|
|
143
|
+
requireExactObjectId(expectedCurrentHead, 'current')
|
|
144
|
+
requireExactObjectId(restoreHead, 'restore')
|
|
145
|
+
if (!isCommitAvailable(cwd, restoreHead)) {
|
|
146
|
+
throw new Error(`refusing remote branch rollback because restore commit ${restoreHead} is unavailable`)
|
|
147
|
+
}
|
|
148
|
+
const ref = `refs/heads/${branch}`
|
|
149
|
+
run(cwd, [
|
|
150
|
+
'push',
|
|
151
|
+
`--force-with-lease=${ref}:${expectedCurrentHead}`,
|
|
152
|
+
'origin',
|
|
153
|
+
`${restoreHead}:${ref}`,
|
|
154
|
+
], false, auth)
|
|
155
|
+
}
|
|
156
|
+
|
|
108
157
|
export const pushBranch = (cwd, branch, expectedHead = null, auth = null, { allowMissing = false } = {}) => {
|
|
109
158
|
const destination = `HEAD:refs/heads/${branch}`
|
|
110
159
|
if (expectedHead !== null) {
|
package/bot/lib/github.mjs
CHANGED
|
@@ -149,6 +149,10 @@ export class GitHubClient {
|
|
|
149
149
|
return data
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
async getPull(number) {
|
|
153
|
+
return (await this.request('GET', `/repos/${this.repo}/pulls/${number}`)).data
|
|
154
|
+
}
|
|
155
|
+
|
|
152
156
|
async listIssues() {
|
|
153
157
|
const issues = await this.listAll(`/repos/${this.repo}/issues`)
|
|
154
158
|
return issues.filter(issue => !issue.pull_request)
|
package/bot.yml.example
CHANGED
|
@@ -36,9 +36,12 @@ jobs:
|
|
|
36
36
|
exit 1
|
|
37
37
|
fi
|
|
38
38
|
printf '%s\n' "$MODEL_EOL_VERSION" > "$RUNNER_TEMP/model-eol-plan/model-eol-version"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
# Resolve npx outside the consumer checkout so a repository whose own
|
|
40
|
+
# package is named model-eol cannot shadow the requested published bin.
|
|
41
|
+
cd "$RUNNER_TEMP/model-eol-plan"
|
|
42
|
+
npx --yes --package="model-eol@$MODEL_EOL_VERSION" \
|
|
43
|
+
-c 'cd "$GITHUB_WORKSPACE" && if [ -f .model-eol.json ]; then model-eol plan .; else model-eol plan . --days 90 --scope direct; fi' \
|
|
44
|
+
> "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json"
|
|
42
45
|
- name: Upload plan
|
|
43
46
|
uses: actions/upload-artifact@v7
|
|
44
47
|
with:
|
|
@@ -55,8 +58,9 @@ jobs:
|
|
|
55
58
|
permissions:
|
|
56
59
|
contents: read
|
|
57
60
|
steps:
|
|
58
|
-
#
|
|
59
|
-
#
|
|
61
|
+
# Add only the provider secrets required by trusted default-branch eval
|
|
62
|
+
# code, and list the same names in eval.pass_env. Environment filtering is
|
|
63
|
+
# not an OS sandbox; this job's hard boundary is that it has no write token.
|
|
60
64
|
- uses: actions/checkout@v7
|
|
61
65
|
with:
|
|
62
66
|
persist-credentials: false
|
|
@@ -71,10 +75,8 @@ jobs:
|
|
|
71
75
|
path: ${{ runner.temp }}/model-eol-plan
|
|
72
76
|
- name: Apply and evaluate each migration independently
|
|
73
77
|
env:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
77
|
-
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
78
|
+
# Example (opt in only when needed by eval.pass_env):
|
|
79
|
+
# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
78
80
|
# A non-empty repository variable overrides eval.command. Otherwise
|
|
79
81
|
# the strictly validated command in .model-eol.json is used.
|
|
80
82
|
MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
|
|
@@ -85,8 +87,9 @@ jobs:
|
|
|
85
87
|
echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
|
|
86
88
|
exit 1
|
|
87
89
|
fi
|
|
90
|
+
cd "$RUNNER_TEMP/model-eol-eval"
|
|
88
91
|
npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot evaluate \
|
|
89
|
-
--target-dir
|
|
92
|
+
--target-dir "$GITHUB_WORKSPACE" \
|
|
90
93
|
--plan-file "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json" \
|
|
91
94
|
--output-file "$RUNNER_TEMP/model-eol-eval/model-eol-eval-results.json"
|
|
92
95
|
- name: Upload bounded eval results
|
|
@@ -134,15 +137,21 @@ jobs:
|
|
|
134
137
|
MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
|
|
135
138
|
MODEL_EOL_TOKEN_KIND: ${{ secrets.MODEL_EOL_BOT_TOKEN && 'external-token' || 'github-token' }}
|
|
136
139
|
run: |
|
|
140
|
+
mkdir -p "$RUNNER_TEMP/model-eol-publish"
|
|
137
141
|
MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
|
|
138
142
|
if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
139
143
|
echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
|
|
140
144
|
exit 1
|
|
141
145
|
fi
|
|
142
|
-
|
|
146
|
+
cd "$RUNNER_TEMP/model-eol-publish"
|
|
147
|
+
npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot \
|
|
148
|
+
--repo "${{ github.repository }}" \
|
|
149
|
+
--target-dir "$GITHUB_WORKSPACE"
|
|
143
150
|
|
|
144
151
|
# A PR made with GITHUB_TOKEN does not trigger pull_request workflows. For
|
|
145
152
|
# required checks, store a narrowly scoped fine-grained PAT or GitHub App token
|
|
146
153
|
# as MODEL_EOL_BOT_TOKEN. Keep provider keys out of the publish job.
|
|
154
|
+
# For migration evidence, copy examples/model-eol-eval.mjs into the consumer,
|
|
155
|
+
# customize its dedicated eval command, and set eval.command in .model-eol.json.
|
|
147
156
|
# Set feeds.allow_vendored_fallback: true only for degraded report-only runs.
|
|
148
157
|
# Independent concurrent invocations need external mutual exclusion.
|
package/check.mjs
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
// node check.mjs alert [paths...] [--format github|markdown|badge] [--json]
|
|
11
11
|
// node check.mjs plan [paths...] [--days N] [--scope all|direct] [--via DISTRIBUTOR] [--feeds DIR]
|
|
12
12
|
// node check.mjs apply --plan plan.json [--dry-run]
|
|
13
|
+
// node check.mjs validate document.json [...] [--type feed|config|check|inventory|schedule|alert|plan]
|
|
13
14
|
//
|
|
14
15
|
// exit codes: check/alert 0 = clear, 1 = finding, 2 = usage error;
|
|
15
|
-
// apply 0 = all items applied, 1 = item refused, 2 = usage or plan error
|
|
16
|
+
// apply 0 = all items applied, 1 = item refused, 2 = usage or plan error;
|
|
17
|
+
// validate 0 = every document valid, 2 = usage, parse, schema, or semantic error.
|
|
16
18
|
|
|
17
19
|
import fs from 'node:fs'
|
|
18
20
|
import path from 'node:path'
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
formatSchedule,
|
|
46
48
|
} from './lib/reports.mjs'
|
|
47
49
|
import { addedLinesForTargets, filterFindingsToChanged, gitRootFor, incompleteScanNotes, scanTargets } from './lib/scanner.mjs'
|
|
50
|
+
import { DOCUMENT_TYPES, formatDocumentErrors, validateDocumentFile } from './lib/validate-document.mjs'
|
|
48
51
|
|
|
49
52
|
const rootForTarget = target => {
|
|
50
53
|
const absolute = path.resolve(target)
|
|
@@ -93,7 +96,7 @@ const repoPathForFile = (file, roots) => {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
const main = () => {
|
|
96
|
-
const COMMANDS = new Set(['check', 'inventory', 'schedule', 'alert', 'plan', 'apply', 'help'])
|
|
99
|
+
const COMMANDS = new Set(['check', 'inventory', 'schedule', 'alert', 'plan', 'apply', 'validate', 'help'])
|
|
97
100
|
const args = process.argv.slice(2)
|
|
98
101
|
|
|
99
102
|
if (args[0] === '--help' || args[0] === '-h') args[0] = 'help'
|
|
@@ -115,6 +118,7 @@ try {
|
|
|
115
118
|
'allow-incomplete': { type: 'boolean' },
|
|
116
119
|
'dry-run': { type: 'boolean' },
|
|
117
120
|
changed: { type: 'string' },
|
|
121
|
+
type: { type: 'string' },
|
|
118
122
|
help: { type: 'boolean', short: 'h' },
|
|
119
123
|
},
|
|
120
124
|
allowPositionals: true,
|
|
@@ -148,6 +152,7 @@ Usage:
|
|
|
148
152
|
node check.mjs alert [paths...] [--config FILE] [--days N] [--scope all|direct] [--format github|markdown|badge] [--json]
|
|
149
153
|
node check.mjs plan [paths...] [--config FILE] [--days N] [--scope all|direct] [--via DISTRIBUTOR] [--feeds DIR] [--allow-incomplete]
|
|
150
154
|
node check.mjs apply --plan plan.json [--dry-run]
|
|
155
|
+
node check.mjs validate document.json [...] [--type feed|config|check|inventory|schedule|alert|plan]
|
|
151
156
|
|
|
152
157
|
Commands:
|
|
153
158
|
check Fail when tracked model IDs are retired or retiring within --days.
|
|
@@ -159,6 +164,8 @@ Commands:
|
|
|
159
164
|
alert Emit GitHub Actions annotations, Markdown, or a Shields badge JSON and fail on errors.
|
|
160
165
|
plan Emit a JSON migration plan with only safely patchable direct API references.
|
|
161
166
|
apply Apply a plan with line-hash and occurrence checks; use --dry-run to preview.
|
|
167
|
+
validate Validate a feed, config, check, inventory, schedule, alert, or plan document.
|
|
168
|
+
The type is inferred from its discriminator or .model-eol.json filename unless --type is set.
|
|
162
169
|
|
|
163
170
|
Scopes:
|
|
164
171
|
all Check every tracked model ID found. This preserves the original behavior.
|
|
@@ -171,12 +178,21 @@ Configuration:
|
|
|
171
178
|
Exit codes:
|
|
172
179
|
0 Clean, or report generated successfully.
|
|
173
180
|
1 Findings, alert errors, or refused apply items.
|
|
174
|
-
2 Usage, feed, scan, or
|
|
181
|
+
2 Usage, feed, scan, plan, or validation errors.
|
|
175
182
|
3 Output stream failure other than EPIPE.
|
|
176
183
|
`)
|
|
177
184
|
return 0
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
if (positionals.some(value => value.length === 0)) {
|
|
188
|
+
console.error('positional paths must be non-empty')
|
|
189
|
+
return 2
|
|
190
|
+
}
|
|
191
|
+
if (values.via === '') {
|
|
192
|
+
console.error('--via must be a non-empty lifecycle channel')
|
|
193
|
+
return 2
|
|
194
|
+
}
|
|
195
|
+
|
|
180
196
|
if (command === 'apply') {
|
|
181
197
|
if (!PLAN_FILE || positionals.length) {
|
|
182
198
|
console.error('apply requires --plan plan.json and accepts no target paths')
|
|
@@ -191,6 +207,41 @@ if (command === 'apply') {
|
|
|
191
207
|
}
|
|
192
208
|
}
|
|
193
209
|
|
|
210
|
+
if (command === 'validate') {
|
|
211
|
+
const allowedOptions = new Set(['type'])
|
|
212
|
+
const unsupported = Object.keys(values).find(option => !allowedOptions.has(option))
|
|
213
|
+
if (positionals.length === 0 || unsupported) {
|
|
214
|
+
const detail = unsupported ? `; --${unsupported} is not supported by validate` : ''
|
|
215
|
+
console.error(`validate requires one or more JSON documents${detail}`)
|
|
216
|
+
return 2
|
|
217
|
+
}
|
|
218
|
+
if (values.type !== undefined && !DOCUMENT_TYPES.includes(values.type) && values.type !== 'bot-config' && values.type !== 'model-eol-config') {
|
|
219
|
+
console.error(`unknown document type ${JSON.stringify(values.type)}; expected ${DOCUMENT_TYPES.join(', ')}`)
|
|
220
|
+
return 2
|
|
221
|
+
}
|
|
222
|
+
let failed = false
|
|
223
|
+
for (const file of positionals) {
|
|
224
|
+
try {
|
|
225
|
+
const result = validateDocumentFile(file, { type: values.type ?? null })
|
|
226
|
+
if (result.errors.length) {
|
|
227
|
+
for (const error of formatDocumentErrors(result.errors)) console.error(`${file}: ${error}`)
|
|
228
|
+
failed = true
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
console.log(`${file}: valid ${result.type} document`)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`${file}: validation failed: ${error.message}`)
|
|
234
|
+
failed = true
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return failed ? 2 : 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (values.type !== undefined) {
|
|
241
|
+
console.error('--type is only supported by the validate command')
|
|
242
|
+
return 2
|
|
243
|
+
}
|
|
244
|
+
|
|
194
245
|
let configLocation
|
|
195
246
|
let repositoryConfig
|
|
196
247
|
try {
|
|
@@ -355,7 +406,7 @@ if (command === 'plan') {
|
|
|
355
406
|
|
|
356
407
|
if (command === 'check') {
|
|
357
408
|
if (AS_JSON) {
|
|
358
|
-
console.log(JSON.stringify({ threshold_days: DAYS, distributor: VIA, scope: SCOPE, scan_notes: scan.notes, findings: changedFindings }, null, 2))
|
|
409
|
+
console.log(JSON.stringify({ schema: 'model-eol/check@0.1', threshold_days: DAYS, distributor: VIA, scope: SCOPE, scan_notes: scan.notes, findings: changedFindings }, null, 2))
|
|
359
410
|
} else {
|
|
360
411
|
console.log(formatCheck({ findings: changedFindings, bad, scannedFiles: scan.files.length, days: DAYS, scope: SCOPE }))
|
|
361
412
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Copy this file into the consuming repository and replace VERIFY with the
|
|
4
|
+
// smallest command that proves the model-dependent behavior you rely on.
|
|
5
|
+
// model-eol runs it from an isolated checkout after applying one migration.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
|
|
11
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
12
|
+
const VERIFY = Object.freeze({ command: npm, args: ['run', 'eval:model-eol'] })
|
|
13
|
+
|
|
14
|
+
const required = name => {
|
|
15
|
+
const value = process.env[name]
|
|
16
|
+
if (!value) throw new Error(`${name} is required`)
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const singleLine = value => String(value).replace(/[\r\n]+/g, ' ').trim()
|
|
21
|
+
|
|
22
|
+
let reportPath = process.env.MODEL_EOL_REPORT || null
|
|
23
|
+
let exitCode = 1
|
|
24
|
+
const report = ['## Repository migration eval', '']
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const oldId = required('MODEL_EOL_OLD_ID')
|
|
28
|
+
const newId = required('MODEL_EOL_NEW_ID')
|
|
29
|
+
const planPath = required('MODEL_EOL_PLAN')
|
|
30
|
+
reportPath = required('MODEL_EOL_REPORT')
|
|
31
|
+
const plan = JSON.parse(fs.readFileSync(planPath, 'utf8'))
|
|
32
|
+
if (plan.plan_schema !== 'model-eol.plan/0.1' || !Array.isArray(plan.items) || plan.items.length === 0) {
|
|
33
|
+
throw new Error('MODEL_EOL_PLAN is not a non-empty model-eol plan')
|
|
34
|
+
}
|
|
35
|
+
if (plan.items.some(item => item.id !== oldId || item.replacement !== newId)) {
|
|
36
|
+
throw new Error('MODEL_EOL_PLAN contains a migration outside MODEL_EOL_OLD_ID/MODEL_EOL_NEW_ID')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const files = [...new Set(plan.items.map(item => item.file))].sort()
|
|
40
|
+
report.push(
|
|
41
|
+
`- Migration: \`${singleLine(oldId)}\` to \`${singleLine(newId)}\``,
|
|
42
|
+
`- Planned references: ${plan.items.length} across ${files.length} file(s)`,
|
|
43
|
+
`- Verification: \`${VERIFY.command} ${VERIFY.args.join(' ')}\``,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const started = Date.now()
|
|
47
|
+
const result = spawnSync(VERIFY.command, VERIFY.args, {
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
env: process.env,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
53
|
+
})
|
|
54
|
+
const elapsedMs = Date.now() - started
|
|
55
|
+
const passed = !result.error && result.status === 0
|
|
56
|
+
report.push(
|
|
57
|
+
`- Result: ${passed ? 'pass' : 'fail'}`,
|
|
58
|
+
`- Exit code: ${Number.isInteger(result.status) ? result.status : 'unavailable'}`,
|
|
59
|
+
`- Duration: ${elapsedMs} ms`,
|
|
60
|
+
)
|
|
61
|
+
if (result.signal) report.push(`- Signal: ${singleLine(result.signal)}`)
|
|
62
|
+
if (result.error) report.push(`- Runner error: ${singleLine(result.error.message)}`)
|
|
63
|
+
report.push('', passed
|
|
64
|
+
? 'The repository verification command passed in the isolated patched checkout.'
|
|
65
|
+
: 'The repository verification command failed. Reproduce it locally for full logs; command output is intentionally not copied into the publishable report.')
|
|
66
|
+
exitCode = passed ? 0 : 1
|
|
67
|
+
} catch (error) {
|
|
68
|
+
report.push(`- Result: fail`, '', `Harness error: ${singleLine(error.message)}`)
|
|
69
|
+
exitCode = 1
|
|
70
|
+
} finally {
|
|
71
|
+
if (reportPath) {
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(path.dirname(path.resolve(reportPath)), { recursive: true })
|
|
74
|
+
fs.writeFileSync(reportPath, `${report.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`could not write MODEL_EOL_REPORT: ${error.message}`)
|
|
77
|
+
exitCode = 1
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
console.error('MODEL_EOL_REPORT is required')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.exitCode = exitCode
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# model-eol - copy this file to .github/workflows/model-eol.yml in YOUR repo.
|
|
2
|
+
#
|
|
3
|
+
# Two gates:
|
|
4
|
+
# - every PR: fail only if THIS change adds a model that is retired or
|
|
5
|
+
# retiring within the threshold (existing references stay out of the way)
|
|
6
|
+
# - weekly: full-repository check, so legacy references surface on a schedule
|
|
7
|
+
# instead of in an outage
|
|
8
|
+
#
|
|
9
|
+
# Judged by a distributor's clock instead? Add `via: aws-bedrock` (or
|
|
10
|
+
# azure-ai-foundry) to the step's `with:` block.
|
|
11
|
+
|
|
12
|
+
name: model-eol
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
pull_request:
|
|
16
|
+
schedule:
|
|
17
|
+
- cron: '17 6 * * 1' # weekly, Monday 06:17 UTC
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
pr-gate:
|
|
21
|
+
if: github.event_name == 'pull_request'
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0 # --changed needs history to diff against the base ref
|
|
27
|
+
- uses: actions/setup-node@v7
|
|
28
|
+
with:
|
|
29
|
+
node-version: 22
|
|
30
|
+
package-manager-cache: false
|
|
31
|
+
- uses: thossullivan/model-eol@v0
|
|
32
|
+
with:
|
|
33
|
+
command: check
|
|
34
|
+
changed: origin/${{ github.base_ref }}
|
|
35
|
+
days: 90
|
|
36
|
+
scope: direct
|
|
37
|
+
|
|
38
|
+
weekly-full-check:
|
|
39
|
+
if: github.event_name == 'schedule'
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v7
|
|
43
|
+
- uses: actions/setup-node@v7
|
|
44
|
+
with:
|
|
45
|
+
node-version: 22
|
|
46
|
+
package-manager-cache: false
|
|
47
|
+
- uses: thossullivan/model-eol@v0
|
|
48
|
+
with:
|
|
49
|
+
command: alert
|
|
50
|
+
days: 90
|
|
51
|
+
scope: direct
|
|
52
|
+
format: github
|
package/feeds/amazon.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"spec": "model-eol/0.1",
|
|
3
3
|
"publisher": "amazon",
|
|
4
|
-
"generated": "2026-08-
|
|
4
|
+
"generated": "2026-08-18T12:06:03.442Z",
|
|
5
5
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html",
|
|
6
6
|
"models": [
|
|
7
7
|
{
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"via": "aws-bedrock",
|
|
15
15
|
"announced": "2026-03-30",
|
|
16
16
|
"shutdown": "2026-09-30",
|
|
17
|
+
"status": "legacy",
|
|
17
18
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
18
19
|
}
|
|
19
20
|
]
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"via": "aws-bedrock",
|
|
29
30
|
"announced": "2026-03-30",
|
|
30
31
|
"shutdown": "2026-09-30",
|
|
32
|
+
"status": "legacy",
|
|
31
33
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
32
34
|
}
|
|
33
35
|
]
|
|
@@ -42,6 +44,7 @@
|
|
|
42
44
|
"via": "aws-bedrock",
|
|
43
45
|
"announced": "2026-03-13",
|
|
44
46
|
"shutdown": "2026-09-14",
|
|
47
|
+
"status": "legacy",
|
|
45
48
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
46
49
|
}
|
|
47
50
|
]
|
|
@@ -56,6 +59,7 @@
|
|
|
56
59
|
"via": "aws-bedrock",
|
|
57
60
|
"announced": "2026-03-13",
|
|
58
61
|
"shutdown": "2026-09-14",
|
|
62
|
+
"status": "legacy",
|
|
59
63
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
60
64
|
}
|
|
61
65
|
]
|
package/feeds/anthropic.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"spec": "model-eol/0.1",
|
|
3
3
|
"publisher": "anthropic",
|
|
4
|
-
"generated": "2026-08-
|
|
4
|
+
"generated": "2026-08-18T12:06:03.442Z",
|
|
5
5
|
"source": "https://platform.claude.com/docs/en/about-claude/model-deprecations",
|
|
6
6
|
"policy": {
|
|
7
7
|
"min_notice_days": 60,
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"via": "aws-bedrock",
|
|
46
46
|
"announced": "2026-03-10",
|
|
47
47
|
"shutdown": "2026-09-10",
|
|
48
|
+
"status": "extended-access",
|
|
48
49
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
49
50
|
}
|
|
50
51
|
],
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"via": "aws-bedrock",
|
|
75
76
|
"announced": "2026-04-14",
|
|
76
77
|
"shutdown": "2026-10-14",
|
|
78
|
+
"status": "extended-access",
|
|
77
79
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
78
80
|
}
|
|
79
81
|
],
|
|
@@ -103,6 +105,7 @@
|
|
|
103
105
|
"via": "aws-bedrock",
|
|
104
106
|
"announced": "2026-07-08",
|
|
105
107
|
"shutdown": "2027-01-08",
|
|
108
|
+
"status": "extended-access",
|
|
106
109
|
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
107
110
|
}
|
|
108
111
|
],
|