Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/dsh-release-watch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# A new dsh release should turn this repo red, not quietly uninstall it.
#
# dsh ships prereleases only, and npm semver never lets a prerelease satisfy a
# caret with a different version tuple. Every release therefore moves the whole
# `@deepseek-ai/dsh-*` line outside any range we have already written, and
# `npm i <this plugin>` beside current dsh fails with ERESOLVE. It happened on
# 2026-08-21 and nobody noticed until 2026-09-04, because no test installs the
# published plugin next to the published harness.
#
# The check reads npm, so it fails loudly rather than silently: a red scheduled
# run notifies nobody, and an issue does.
name: dsh-release-watch

on:
schedule:
- cron: '41 4 * * *'
workflow_dispatch:
pull_request:
paths: ['package.json', 'scripts/check-dsh-release.mjs', '.github/workflows/dsh-release-watch.yml']

permissions:
contents: read
issues: write

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm install --no-audit --no-fund

# `--tree-only` on a pull request: the published package is still the
# broken one on the very PR that fixes it, so gating a PR on it would
# make this check red on its own fix. The scheduled run owns that half —
# it is also the only run that can ever see it clear.
- id: check
run: |
set +e
node scripts/check-dsh-release.mjs ${{ github.event_name == 'pull_request' && '--tree-only' || '' }} > report.md
echo "status=$?" >> "$GITHUB_OUTPUT"
cat report.md
set -e

# A pull request wants the answer in its own checks, not in an issue.
- if: github.event_name == 'pull_request' && steps.check.outputs.status != '0'
run: exit 1

- if: github.event_name != 'pull_request' && steps.check.outputs.status != '0'
env:
GH_TOKEN: ${{ github.token }}
run: |
{
if [ "${{ steps.check.outputs.status }}" = "2" ]; then
echo "The daily dsh release check could not finish."
else
echo "A published dsh version no longer satisfies this plugin's peer ranges."
echo "Installing this plugin beside current dsh fails with \`ERESOLVE\`."
echo
echo "Fix: OR the new line into every harness peer range in \`package.json\`,"
echo "re-run the install proof, then publish — a fix on main that never"
echo "reaches npm leaves every user broken."
fi
echo
cat report.md
} > issue.md
# `gh issue list --label` errors on a label that does not exist yet,
# which would swallow the very first alert.
gh label create dsh-release --color B60205 --force \
--description "A dsh release moved out from under this plugin"
open=$(gh issue list --label dsh-release --state open --limit 1 --json number --jq '.[0].number')
if [ -n "$open" ]; then
gh issue comment "$open" --body-file issue.md
else
gh issue create --title "A dsh release moved out from under this plugin" --label dsh-release --body-file issue.md
fi
2 changes: 1 addition & 1 deletion docs/pricing.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@dshworks/dsh-meter",
"description": "The DeepSeek time-of-use meter for dsh: what this session cost, which tariff is running, when it flips, and the account balance behind it \u2014 one line under the composer.",
"version": "0.4.0",
"description": "The DeepSeek time-of-use meter for dsh: what this session cost, which tariff is running, when it flips, and the account balance behind it one line under the composer.",
"version": "0.4.1",
"license": "MIT",
"homepage": "https://dsh.works/dsh-meter/",
"publishConfig": {
Expand Down Expand Up @@ -67,7 +67,7 @@
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-session-projection": "^0.1.0-rc.6 || ^0.1.1-rc.1"
"@deepseek-ai/dsh-session-projection": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-rc.1"
},
"devDependencies": {
"vitest": "^3.2.4"
Expand Down
121 changes: 121 additions & 0 deletions scripts/check-dsh-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Does this plugin still get the SAME harness the host is running?
//
// Not "does `npm i` succeed" — that is the weaker question, and it passes while
// broken. dsh ships prereleases only, and npm semver never lets a prerelease
// satisfy a caret with a different version tuple: `^0.1.0-rc.6` matches
// 0.1.0-rc.8 and nothing after it. So a stale peer range fails two ways.
//
// Loudly, when two plugins disagree: `npm i` stops with ERESOLVE.
//
// Quietly, and this is the one that matters, when a plugin is installed alone:
// npm is happy to satisfy `^0.1.0-rc.6` by HOISTING `@deepseek-ai/dsh-llm`
// 0.1.0-rc.8 to the root and pushing dsh's own 0.1.2-rc.1 copy down into a
// nested `node_modules`. 691 packages instead of 528, zero warnings, and the
// plugin now imports a four-release-old harness while the host imports the
// current one. Instances do not match, types do not match, and nothing throws.
//
// So the assertion is single-version resolution, checked on a real install.
// Twice, because they fail separately: this tree (did we fix it?) and the
// PUBLISHED package (did the fix ship? — this org has published off an
// unmerged branch before, and a fix users cannot install is not a fix).
//
// Exit 0 clean, 1 drift, 2 could not check.

import { execFileSync } from 'node:child_process'
import { mkdtempSync, readFileSync, readdirSync, existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

// `--tree-only`: check what this branch would publish, and nothing else.
const PR_ONLY = process.argv.includes('--tree-only')
const ROOT = new URL('..', import.meta.url).pathname
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
const report = []
let failed = false

const run = (cmd, args, cwd) =>
execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })

/** Every `@deepseek-ai/dsh*` copy under a tree, as name -> set of versions. */
function harnessVersions(dir) {
const seen = new Map()
const walk = (nm) => {
if (!existsSync(nm)) return
for (const entry of readdirSync(nm, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
if (entry.name === '@deepseek-ai') {
const scope = join(nm, entry.name)
for (const p of readdirSync(scope)) {
if (!p.startsWith('dsh')) continue
const manifest = join(scope, p, 'package.json')
if (!existsSync(manifest)) continue
const { version } = JSON.parse(readFileSync(manifest, 'utf8'))
if (!seen.has(p)) seen.set(p, new Set())
seen.get(p).add(version)
walk(join(scope, p, 'node_modules'))
}
continue
}
walk(join(nm, entry.name, 'node_modules'))
}
}
walk(join(dir, 'node_modules'))
return seen
}

/** Install `specs` together and assert one version of every harness package. */
function check(specs, label) {
const dir = mkdtempSync(join(tmpdir(), 'dsh-release-'))
try {
run('npm', ['init', '-y'], dir)
run('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts', ...specs], dir)
} catch (error) {
failed = true
const out = `${error.stdout ?? ''}${error.stderr ?? ''}`
const why = out.split('\n').filter((l) => /npm error/.test(l)).slice(0, 8).join('\n')
report.push(`- FAIL ${label} — install refused\n\n\`\`\`\n${why}\n\`\`\`\n`)
return
}
const split = [...harnessVersions(dir)].filter(([, versions]) => versions.size > 1)
if (split.length === 0) {
report.push(`- ok ${label} — one version of every harness package`)
return
}
failed = true
const lines = split.map(([name, versions]) => ` @deepseek-ai/${name}: ${[...versions].sort().join(', ')}`)
report.push(
`- FAIL ${label} — the plugin and the host resolve different copies:\n\n\`\`\`\n${lines.join('\n')}\n\`\`\`\n`,
)
}

let latest
try {
latest = run('npm', ['view', '@deepseek-ai/dsh', 'dist-tags.latest'], ROOT).trim()
report.push(`dsh \`latest\` on npm: **${latest}**\n`)
} catch (error) {
console.error(`could not read dsh dist-tags: ${error.message}`)
process.exit(2)
}

let tarball
try {
tarball = join(ROOT, run('npm', ['pack', '--silent', '--ignore-scripts'], ROOT).trim().split('\n').pop())
} catch (error) {
console.error(`could not pack this tree: ${error.message}`)
console.log(report.join('\n'))
process.exit(2)
}

check([`@deepseek-ai/dsh@${latest}`, tarball], `this tree beside dsh ${latest}`)
const treeFailed = failed

// On a pull request only THIS TREE can be green: the published package is by
// definition still the broken one on the very PR that fixes it, and a check
// that is red on its own fix is a check people switch off. The published half
// belongs to the scheduled run, which is also the only place it can clear.
if (!PR_ONLY) {
check([`@deepseek-ai/dsh@${latest}`, `${pkg.name}@latest`], `published ${pkg.name} beside dsh ${latest}`)
}

console.log(report.join('\n'))
process.exit((PR_ONLY ? treeFailed : failed) ? 1 : 0)