Plan 130: ship npm + PyPI distribution and tag-driven version stamp - #239
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #239 +/- ##
==========================================
+ Coverage 95.14% 95.21% +0.06%
==========================================
Files 151 156 +5
Lines 18020 18390 +370
==========================================
+ Hits 17146 17510 +364
- Misses 533 538 +5
- Partials 341 342 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
Implements plan 130 by adding release automation and new distribution channels so mdsmith can be shipped via npm, PyPI, and the VS Code marketplaces with consistent tag-stamped versions across channels.
Changes:
- Added version stamping + CI guarding scripts to keep manifests pinned to
0.0.0-devonmainand rewrite them from release tags during publishing. - Added npm and PyPI distribution scaffolding (Node shim + platform packages build, Python wheel wrapper + wheel build script) plus Go tests for parts of the release scripting.
- Extended GitHub release workflow to publish npm/PyPI, publish VS Code extensions (Marketplace/Open VSX), and run post-release smoke tests; updated install documentation accordingly.
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/set-version.sh | Rewrites versions in tracked manifests from a supplied tag version. |
| scripts/check-versions.sh | CI guard to assert manifests remain at 0.0.0-dev between releases. |
| scripts/build-wheels.sh | Builds platform-specific Python wheel artifacts from downloaded binaries. |
| scripts/build-npm-platforms.sh | Generates per-platform npm packages from downloaded binaries. |
| README.md | Adds install commands and links to the new install guide. |
| python/README.md | Documents the PyPI distribution wrapper behavior. |
| python/pyproject.toml | Defines the Python package metadata/build configuration. |
| python/mdsmith/main.py | Python entrypoint that execs the bundled mdsmith binary. |
| python/mdsmith/init.py | Exposes binary_path and main for the Python wrapper. |
| plan/130_binary-distribution-and-versioning.md | Checks off completed plan tasks and marks follow-ups. |
| npm/mdsmith/tsconfig.json | TypeScript config for shim tests and JS checking. |
| npm/mdsmith/test/shim.test.ts | Bun tests for platform resolution and error behavior. |
| npm/mdsmith/README.md | Documents the npm distribution and platform packages. |
| npm/mdsmith/package.json | Defines the root npm package and its optionalDependencies pins. |
| npm/mdsmith/bun.lock | Locks Bun dev dependencies for the npm package tests. |
| npm/mdsmith/bin/mdsmith.js | Node shim that resolves and runs the platform binary. |
| npm/mdsmith/.npmignore | Ensures publish tarball stays minimal for the root npm package. |
| npm/mdsmith/.gitignore | Ignores node lockfiles and node_modules in the npm package dir. |
| internal/release/setversion_test.go | Go tests covering set-version/check-versions script behavior. |
| internal/release/doc.go | Package doc for the release helper-script tests. |
| internal/release/buildnpm_test.go | Go tests covering build-npm-platforms output structure. |
| editors/vscode/package.json | Replaces hard-coded extension version with 0.0.0-dev sentinel. |
| docs/guides/install.md | Adds an installation guide covering all supported channels. |
| docs/guides/index.md | Adds the installation guide to the guides catalog table. |
| CLAUDE.md | Adds the installation guide link to the documentation catalog. |
| AGENTS.md | Adds the installation guide link to the documentation catalog. |
| .mdsmith.yml | Updates directory-structure allowlist to include new dirs. |
| .github/workflows/release.yml | Adds stamping, npm/pypi publishing, and smoke-test validation. |
| .github/workflows/ci.yml | Adds a version-guard job to enforce manifest sentinel versions. |
| .github/copilot-instructions.md | Adds the installation guide to the linked docs list. |
jeduden
pushed a commit
that referenced
this pull request
May 5, 2026
- set-version.sh: accept SemVer with both pre-release and build metadata (1.2.3-rc.1+build.5) and fail fast when a required manifest is missing instead of silently skipping. - check-versions.sh: detect a deleted optionalDependencies key in npm/mdsmith/package.json so the guard catches drift, not just pin mismatches. - build-wheels.sh: retag wheels with `python -m wheel tags` so the dist-info/WHEEL metadata matches the filename instead of leaving it stamped py3-none-any. Install `wheel` alongside `build` in the pypi job. - pyproject.toml: drop the comment that mentioned a non-existent marker file. - release.yml smoke-test: switch the npm channel from node:lts-alpine to node:lts so the default bash shell exists before the install step runs. - internal/release: add TestBuildWheelsLayout that exercises build-wheels.sh and asserts the WHEEL metadata reflects each platform tag. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
jeduden
pushed a commit
that referenced
this pull request
May 5, 2026
- check-versions.sh: fail fast when a tracked manifest is missing, matching set-version.sh's require_file behavior. A renamed or deleted manifest now blows up the version-guard CI job rather than silently passing. - release.yml smoke-test: wrap the npm and mise install commands in the same retry-with-backoff loop the pip channel uses, since npm registry propagation can lag the publish by ~60s. - internal/release: add TestCheckVersionsFailsOnMissingManifest. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
jeduden
pushed a commit
that referenced
this pull request
May 5, 2026
- README.md: drop the misleading mise/asdf one-liner. Until the registry/plugin follow-ups land, the short forms don't resolve. - docs/guides/install.md: replace the mise row with the working `mise use -g ubi:jeduden/mdsmith@latest` form (no registry PR required), drop the asdf row from the quick-start table, and flag both the asdf section and the short mise form as pending-follow-up. - release.yml smoke-test: switch the mise channel from `mdsmith@VER` to `ubi:jeduden/mdsmith@VER` so the smoke test passes on the very first tag, before the mise registry PR. - build-wheels.sh: wrap each per-wheel build in a subshell with an EXIT trap so a `python -m build` failure under `set -e` still cleans up the staging directory. The previous RETURN trap only fired on a normal return. - ci.yml test job: install build/wheel/hatchling so the new TestBuildWheelsLayout actually runs in CI instead of skipping. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
jeduden
pushed a commit
that referenced
this pull request
May 5, 2026
- set-version.sh: pre-flight grep before each rewrite. If a manifest is missing the expected version field (or the npm root has no @mdsmith/* optionalDependencies block), abort with an actionable error instead of letting perl no-op silently and shipping 0.0.0-dev. Same guard is added to rewrite_pyproject_version. - release.yml smoke-test: track an `ok` flag inside each retry loop and `exit 1` after the loop if no attempt succeeded. The previous `if cmd; then break; fi` pattern fell through under bash -e, so a registry that stayed unresolvable would surface later as an opaque "mdsmith: command not found" instead of "install never succeeded". - internal/release: add two regression tests covering the new rewrite-fails-fast paths. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
jeduden
pushed a commit
that referenced
this pull request
May 5, 2026
- set-version.sh: tighten the SemVer regex to reject leading zeros in MAJOR/MINOR/PATCH (per semver.org grammar). Both npm and PyPI reject 01.2.3-style tags downstream; refuse them here so the failure has an actionable message. - check-versions.sh: name the drifted pin in error output, e.g. "@mdsmith/linux-x64 pin '1.2.3'" instead of just "pin '1.2.3'", so a multi-pin drift produces actionable lines. - internal/release: add TestSetVersionRejectsLeadingZero and update TestCheckVersionsRejectsOptionalDepDrift to assert on the new error format. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
jeduden
pushed a commit
that referenced
this pull request
May 5, 2026
- set-version.sh: tighten the SemVer regex to the full semver.org grammar. Pre-release identifiers that are purely numeric must not have leading zeros (so "-01" is rejected, but "-rc01" stays valid because it is alphanumeric). Build metadata identifiers keep the spec-mandated leading-zero allowance. - docs/guides/install.md: clarify the PyPI console script runs the binary via os.execv on POSIX and subprocess.run on Windows, matching the actual entrypoint instead of implying execv on every platform. - internal/release: add TestSetVersionAcceptsValidSemverShapes and extend TestSetVersionRejectsLeadingZero to cover prerelease numeric identifiers. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
zizmor flagged the three new actions/setup-go invocations I added to the vscode, npm, and pypi jobs as cache-poisoning surfaces. The existing build job already uses cache: false; mirror that. Codecov also dropped on the patch — exercise the easy uncovered paths in cmd/mdsmith-release and internal/release: - cmd/mdsmith-release: cover bad-arity dispatcher branches for every subcommand, the invalid-version stamp path, and the reportError nil/non-nil exit-code mapping. - internal/release: cover BuildNpmPlatforms when the npm root manifest is missing, BuildWheels when the python source is missing, the LICENSE-copy branch, and ValidateSemver on empty input. Coverage on the new code: internal/release 79.4% → 81.8%, cmd/mdsmith-release 50.0% → 81.6%. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
The CLI now uses spf13/pflag with one FlagSet per subcommand, matching the conventions in cmd/mdsmith/main.go (subcommand Usage, ContinueOnError, the reportFlagParseErr helper that pretty-prints non-help parse errors). None of the subcommands take flags today; the FlagSets exist so adding one later is a local edit and so help/error messages match the user-facing binary. Also raise patch coverage on the release toolkit by exercising the small helpers directly: - listWheels (empty, mixed-extension, missing dir) - moveWheels (empty staging, multi-file relocate) - copyDir (nested tree) - stagePythonTree (missing asset) - BuildWheels (missing artifact, no python invocation needed) - subcommand --help and --bogus dispatch (covers reportFlagParseErr) Coverage on the new code: internal/release 81.8% → 84.6%, cmd/mdsmith-release 81.6% → 88.9%. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
- Convert internal/release and cmd/mdsmith-release tests to use
testify per CLAUDE.md ("require for preconditions that abort,
assert for checks that continue"). Behaviour unchanged; just
shorter, more idiomatic test code.
- Spin the asdf-plugin repo (jeduden/asdf-mdsmith) and the
mise-plugins/registry submission out of plan/130 into
plan/145 — they ship in separate repos and don't gate the
multi-channel release this branch enables. Plan/130 now points
at plan/145 for the asdf+mise narrative; plan/145 owns the
task list and acceptance criteria for both registry
submissions plus the doc/smoke-test cleanup that follows.
- mdsmith fix . to keep PLAN.md and the catalog in
CLAUDE.md/docs/guides/index.md in sync.
https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
- cmd/mdsmith-release tests now exercise the success branch of runCheck (println "all manifests pinned at"), the happy-path build-npm dispatch end-to-end, and reportError-translated failures for both build-npm and build-wheels. Coverage on the CLI: 88.9% → 94.4%. - python/pyproject.toml: remove the made-up `authors = [...]` entry I added when scaffolding the wheel. The PyPI metadata field is optional; until the real author info is provided, no entry is better than a wrong one. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
CI's codecov gate failed because patch coverage (79.82%) was well below the project's auto-target (95.14%). Push the new code's coverage by exercising the small but numerous IO error returns directly: - internal/release helpers: stage / list / move / copy / retag primitives all get direct error-path tests (missing source, rename target as a file, missing dir, etc.). - BuildNpmPlatforms: cover the per-platform mkdir-fails branch by colliding the platform path with a regular file. - TrackedManifests: pin the readdir-of-platform-subpackages branch so the slice growth path is observable. - cmd/mdsmith-release: direct unit test for the reportFlagParseErr nil branch and end-to-end dispatch paths that surface reportError-translated failures for build-npm and build-wheels. Coverage: cmd/mdsmith-release 88.9% → 95.8%, internal/release 84.6% → 88.3%. Remaining uncovered statements in the library are IO-error returns (mkdir failure mid-stage, write failure) that need OS-level fault injection to trigger reliably; those will catch up when codecov re-measures with the new tests. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
The codecov gate kept failing because the release-toolkit's IO
error returns (mkdir-failure mid-pipeline, ReadFile/WriteFile
faults, Rename onto a non-directory) cannot be triggered against
a healthy filesystem, leaving entire `if err != nil { return err }`
arms uncovered.
Introduce a small writable FS interface alongside the existing
read-only fs.FS pattern in the rest of the codebase. The Toolkit
struct holds an FS field, defaults to osFS in production, and
accepts a fault-injecting fakeFS in tests so every error-return
branch is exercised.
- internal/release/fs.go — FS interface + osFS impl + Toolkit
- internal/release/{version,buildnpm,buildwheels}.go — methods on
Toolkit; package-level Stamp / Check / BuildNpmPlatforms /
BuildWheels keep the original signatures by delegating to a
default Toolkit.
- internal/release/fault_test.go — fakeFS with per-method
failOn{N}Call counters plus a battery of fault-injection tests
covering each IO error return.
Coverage on the package: 88.3% → 94.2%. The remaining holes are
the python-orchestration branches (require python on PATH) and
two unreachable defaults.
https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
The remaining patch-coverage gap was the python-orchestration
failure branches in buildwheels.go (runPythonBuild's "python -m
build" failed, retagWheels' "python -m wheel tags" failed). Both
paths require python on PATH AND a way to make python fail —
hard to drive against a real interpreter.
Add a tiny Runner interface alongside FS:
type Runner interface {
RunCommand(dir, name string, args ...string) error
}
Toolkit grows a runner field; New() defaults to osRunner, tests
construct via NewWithDeps(fakeFS, fakeRunner). runPythonBuild
becomes a method on Toolkit; retagWheels switches its inner
exec.Command to t.runner.RunCommand.
Three new fault tests (runPythonBuild fail, retagWheels fail,
buildOneWheel propagates python-build fail) close the
orchestration-failure coverage holes.
Coverage on internal/release: 94.2% → 95.6%.
https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
Two small gaps left after the FS / Runner refactor: - buildOneNpmPlatform 93.8% — the package.json WriteFile failure (the second per-platform write, between the binary copy and an optional LICENSE) had no fault test. Add one. - listWheels 90% — the IsDir-skip branch (a directory whose name ends in `.whl`) was never exercised. Drop a `subdir.whl/` into the filter test so the skip path runs. Coverage on internal/release: 95.6% → 96.4%. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
…OneWheel After the FS+Runner refactor, the only buildOneWheel statements left uncovered were the two error returns following runPythonBuild: retagWheels-fails and moveWheels-fails. Both need a wheel actually present in staging so the helpers have work to do (otherwise their inner loops short-circuit and never trigger the failure paths). Add a preStageWheel helper that drops a real fake.whl into the staging dir before the test calls buildOneWheel, then arm either the Runner (for retagWheels) or FS.Rename (for moveWheels) to fail. Coverage on internal/release: 96.4% → 96.9%. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
Two unreached statements left after the buildOneWheel coverage push: - checkManifest's "no version field found" note() — a manifest that exists but has no top-level version key. Add a Check test against an edited fixture. - copyDir's MkdirAll(dst) failure — an FS where ReadDir(src) succeeds but the destination mkdir fails. Use the existing failOnMkdirAllCall harness. Coverage on internal/release: 96.9% → 98.2%. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
…errors Three remaining unreachable lines after the FS+Runner refactor: - buildOneWheel's outDir/.staging-<plat> mkdir failure — fixed the existing TestBuildWheelsFailsOnStagingMkdir which counted off by one (staging is MkdirAll call #4, not #3). - copyDir's recursive call error — stage src with a subdir so the inner copyDir is invoked, then fail the inner copyFile to bubble the error back through the outer call. - moveWheels' listWheels error — direct fault test that asserts a ReadDir failure surfaces as a moveWheels error rather than a silent no-op. Coverage on internal/release: 98.2% → 99.1%. Remaining lines are physically unreachable: main()'s 1-line os.Exit wrapper, run()'s os.Getwd error path (cwd was deleted under us), and copyDir's e.Info() race (entry vanished between ReadDir and Info). https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
The asdf-plugin repo and mise-registry submission moved out of plan/130 into plan/145 in an earlier commit, but the install guide's two "Pending follow-up" callouts still linked back to plan/130. Update both references so readers land on the right tracking plan. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
Remaining reachable lines in internal/release got direct fault
tests (buildOneWheel propagates stagePythonTree failure, copyDir
propagates DirEntry.Info() error via a bespoke errInfoEntry).
internal/release coverage: 99.1% → 100.0%.
Then five Copilot review comments on top:
- internal/release/version.go: clarify the TrackedManifests
comment about npm/platforms/. BuildNpmPlatforms writes to its
outDir argument (npm/dist in CI), not to npm/platforms/; the
helper just stamps platform sub-package manifests if some
prior step happened to materialise them.
- internal/release/buildwheels_test.go: fix the assert.Failf
mis-call. failureMessage is the title; %q verbs belong in the
msg+args parameters, not the title.
- internal/release/buildwheels.go: pick python executable at
runtime via pythonExecutable() (python first, fall back to
python3) so the binary works on hosts that only ship python3.
Test gating uses the same resolver.
- docs/guides/install.md: the direct-download snippet now
curl's checksums.txt before sha256sum -c so a copy-paste
install actually works.
- PR description (via gh pr edit): drop the
scripts/{set,check}-version.sh references, mention the Go
cmd/mdsmith-release CLI, and link plan/145.
https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
Two Copilot findings: - internal/release/buildwheels.go: the previous over-eager `replace_all` of "python" → pythonExecutable() also rewrote `filepath.Join(rootDir, "python")`, which is the source-tree path. On hosts where `python` isn't on PATH (Debian/Ubuntu with only python3) BuildWheels would have looked for <rootDir>/python3 instead of <rootDir>/python and failed every wheel build. Revert that one site to the literal "python". - TestBuildWheelsFailsWhenPythonSourceMissing now also asserts the error names <root>/python so the same regression cannot return. - plan/130: the vscode-publish task still said run `set-version.sh` before vsce package; flip the wording to `mdsmith-release stamp` to match the implemented workflow. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements plan/130_binary-distribution-and-versioning.md. The asdf-plugin repo and mise-registry submission moved out into plan/145_asdf-mise-registry-submissions.md since they ship in separate repos.
@mdsmith/cli) plus 5 platform sub-packages (@mdsmith/<node-platform>-<arch>), the PyPI wheel skeleton, and the VS Code Marketplace + Open VSX publish steps. Adds a smoke-test matrix that assertsmdsmith versionmatches the tag on every channel.0.1.2ineditors/vscode/package.jsonwith a0.0.0-devsentinel. The internalcmd/mdsmith-release stamp <ver>Go CLI rewrites every tracked manifest from that sentinel to the cleaned tag;cmd/mdsmith-release checkruns in a newversion-guardCI job so a hand edit onmainfails fast.FSandRunnerinterfaces so every IO and python-orchestration error branch is unit-tested.Test plan
go test ./...passes (release-toolkit tests ininternal/release/...andcmd/mdsmith-release/...cover the four subcommands plus IO and runner fault injection).go tool golangci-lint runreports no issues.mdsmith check .is clean.bun testinnpm/mdsmith/passes (13 tests cover the platform-resolver shim).bunx tsc --noEmitinnpm/mdsmith/is clean.vX.Y.Ztag; smoke-test job will assertmdsmith vX.Y.Zon npm, pip, and mise.Pre-tag setup needed
Before the next
vX.Y.Ztag, store these repo secrets:VSCE_PAT— Azure DevOps PAT scoped toMarketplace > Managefor thejedudenpublisher.OVSX_PAT— Open VSX publisher token after claiming thejedudennamespace.NPM_TOKEN— npm automation token for the@mdsmithorg.mdsmithproject (no token needed once configured).https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6