From e35f749ea32a361a89bc762ac7d84ababc6f08e3 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 3 Sep 2026 22:08:46 -0700 Subject: [PATCH 1/4] fix: peer ranges accept dsh 0.1.2-rc.1 npm semver never lets a prerelease satisfy a caret with a different version tuple, so a range naming only older rc lines matches no published dsh at all. dsh 0.1.2-rc.1 became npm `latest` on 2026-09-03 and installing this plugin beside it failed with ERESOLVE. Ranges now OR in each supported line, the way upstream bumps its own `^0.1.2-rc.1` pins. Proven: dsh 0.1.2-rc.1 plus all four dshworks plugins resolve to one hoisted 0.1.2-rc.1 tree, 528 packages, no ERESOLVE; the plugin imports and `dsh --profile web --patch cordis.patch.yml --dump-config` composes it. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7412495..b7b2272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", + "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.0", "license": "MIT", "homepage": "https://dsh.works/dsh-meter/", @@ -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" From f802fee48f9e2afadeae3dab62eeff7e198beac1 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 3 Sep 2026 22:15:44 -0700 Subject: [PATCH 2/4] ci: a dsh release now turns this repo red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dsh ships prereleases only, and npm semver never lets a prerelease satisfy a caret with a different version tuple. Every release moves the whole `@deepseek-ai/dsh-*` line outside any range already written, and no test here can see it: the break is in the range, not the code. `npm i` succeeding is the weaker question and it passes while broken. Installed alone, npm satisfies a stale `^0.1.0-rc.6` by HOISTING dsh-llm 0.1.0-rc.8 to the root and pushing dsh 0.1.2-rc.1's own copy into a nested node_modules: 691 packages instead of 528, zero warnings, and the plugin imports a four-release-old harness while the host imports the current one. Measured on the published @dshworks/dsh-watch 0.2.0: THIRTEEN harness packages split. So the daily check installs for real and asserts one version of every harness package — twice, because they fail separately: this tree (did we fix it?) and the published package (did the fix ship? a fix users cannot install is not a fix). Drift opens an issue; a red scheduled run notifies nobody. It reads red right now against every published plugin, and clears on publish. --- .github/workflows/dsh-release-watch.yml | 75 ++++++++++++++++ scripts/check-dsh-release.mjs | 111 ++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 .github/workflows/dsh-release-watch.yml create mode 100644 scripts/check-dsh-release.mjs diff --git a/.github/workflows/dsh-release-watch.yml b/.github/workflows/dsh-release-watch.yml new file mode 100644 index 0000000..b7a9790 --- /dev/null +++ b/.github/workflows/dsh-release-watch.yml @@ -0,0 +1,75 @@ +# 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 ` 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 + + - id: check + run: | + set +e + node scripts/check-dsh-release.mjs > 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 diff --git a/scripts/check-dsh-release.mjs b/scripts/check-dsh-release.mjs new file mode 100644 index 0000000..6da8d3a --- /dev/null +++ b/scripts/check-dsh-release.mjs @@ -0,0 +1,111 @@ +// 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' + +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}`) +check([`@deepseek-ai/dsh@${latest}`, `${pkg.name}@latest`], `published ${pkg.name} beside dsh ${latest}`) + +console.log(report.join('\n')) +process.exit(failed ? 1 : 0) From 4fabb5de618dc210f22063bbcb32e76992f80849 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 3 Sep 2026 22:15:55 -0700 Subject: [PATCH 3/4] release: 0.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7b2272..1360470 100644 --- a/package.json +++ b/package.json @@ -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 — one line under the composer.", - "version": "0.4.0", + "version": "0.4.1", "license": "MIT", "homepage": "https://dsh.works/dsh-meter/", "publishConfig": { From f77d6a3ede21cbb388454089d420df28b719e2ec Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 3 Sep 2026 22:53:27 -0700 Subject: [PATCH 4/4] ci: --tree-only on a pull request, and regenerate the feed for 0.4.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release check was red on all four PRs that fix the thing it checks. By construction: the published package is still the broken one until the PR merges and ships, so gating a PR on the published half makes the tripwire red on its own fix — which is how a tripwire gets switched off. A pull request now checks only what this branch would publish. The scheduled run keeps both halves, and it is the only run that can ever see the published one clear. docs/pricing.json carries the package version, so the 0.4.1 bump left it stale and the generated-artifact check caught it. Regenerated. The lesson is ordering: bump, then regenerate, then test — I tested before the bump. --- .github/workflows/dsh-release-watch.yml | 6 +++++- docs/pricing.json | 2 +- scripts/check-dsh-release.mjs | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dsh-release-watch.yml b/.github/workflows/dsh-release-watch.yml index b7a9790..f226b75 100644 --- a/.github/workflows/dsh-release-watch.yml +++ b/.github/workflows/dsh-release-watch.yml @@ -33,10 +33,14 @@ jobs: 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 > report.md + node scripts/check-dsh-release.mjs ${{ github.event_name == 'pull_request' && '--tree-only' || '' }} > report.md echo "status=$?" >> "$GITHUB_OUTPUT" cat report.md set -e diff --git a/docs/pricing.json b/docs/pricing.json index c317299..d5324d3 100644 --- a/docs/pricing.json +++ b/docs/pricing.json @@ -1,6 +1,6 @@ { "schema": "dsh-meter/pricing@2", - "generator": "@dshworks/dsh-meter@0.4.0", + "generator": "@dshworks/dsh-meter@0.4.1", "homepage": "https://dsh.works/dsh-meter/", "source": { "usd": "https://api-docs.deepseek.com/quick_start/pricing", diff --git a/scripts/check-dsh-release.mjs b/scripts/check-dsh-release.mjs index 6da8d3a..c798bbb 100644 --- a/scripts/check-dsh-release.mjs +++ b/scripts/check-dsh-release.mjs @@ -26,6 +26,8 @@ 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 = [] @@ -105,7 +107,15 @@ try { } check([`@deepseek-ai/dsh@${latest}`, tarball], `this tree beside dsh ${latest}`) -check([`@deepseek-ai/dsh@${latest}`, `${pkg.name}@latest`], `published ${pkg.name} 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(failed ? 1 : 0) +process.exit((PR_ONLY ? treeFailed : failed) ? 1 : 0)