Skip to content

Commit 59b75a2

Browse files
jedudenclaude
andauthored
Plan 130: ship npm + PyPI distribution and tag-driven version stamp (#239)
* Plan 130: ship npm + PyPI distribution and a tag-driven version stamp Adds the npm root + platform-package layout, the PyPI wheel skeleton, the VS Code Marketplace + Open VSX publish steps, and a smoke-test job that asserts mdsmith version matches the tag on every channel. scripts/set-version.sh rewrites every tracked manifest from the 0.0.0-dev sentinel to the cleaned tag; scripts/check-versions.sh runs in a new ci.yml version-guard so a hand edit on main fails fast. asdf and mise registry submissions are still pending and remain follow-ups in plan/130 because they live outside this repo. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6 * Address Copilot review on PR #239 - 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 * Address Copilot review pass 2 on PR #239 - 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 * Address Copilot review pass 3 on PR #239 - 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 * Fix bogus action SHA pins so the test job can resolve setup-python The setup-python pin I added in the previous commit pointed at a SHA that does not exist on actions/setup-python, causing the `test` CI job to fail with "Unable to resolve action" before any step ran. Same problem for the setup-node and gh-action-pypi-publish pins introduced in the npm/pypi jobs. Re-pin each to a real SHA looked up via the GitHub API, with versions matching what the rest of the repo uses for adjacent actions: - actions/setup-python@a309ff8b … # v6.2.0 (matches setup-go v6.2.0) - actions/setup-node@48b55a01 … # v6.4.0 (latest) - pypa/gh-action-pypi-publish@6733eb7d … # v1.14.0 (latest) https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6 * Address Copilot review pass 4 on PR #239 - 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 * Address Copilot review pass 5 on PR #239 - 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 * Address Copilot review pass 6 on PR #239 - 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 * Rename npm root to @mdsmith/cli (unscoped 'mdsmith' is taken) The unscoped `mdsmith` package on npm was registered by another project (afroldo, 2026-01-31, "CLI para gerar READMEs e arquivos Markdown"), so we cannot publish a root named `mdsmith`. The `@mdsmith` scope we already use for the platform sub-packages is free, so the root publishes there as `@mdsmith/cli`. The installed binary is still called `mdsmith` because the package's `bin` field maps the command name independent of the package name. Users run `npm install -g @mdsmith/cli` and the `mdsmith` CLI lands on `$PATH` exactly as before. Updated: - npm/mdsmith/package.json `name` and the README's install snippet - README.md install quick-start - docs/guides/install.md table row, npm section, and prose - release.yml smoke-test (npm channel pulls @mdsmith/cli@VER) - plan/130 npm strategy section + acceptance criteria The bun shim tests didn't need changes — they only exercise the platform-package resolver, which already lives under @mdsmith/. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6 * Guard Marketplace/Open VSX publish secrets before continue-on-error The two extension-publish steps run with `continue-on-error: true` so a transient Marketplace or Open VSX outage does not block the downstream `release` job. The same flag would also hide an unset or empty `VSCE_PAT` / `OVSX_PAT`, leaving us with a "successful" release that never reached either marketplace. Add a `Verify Marketplace and Open VSX tokens are set` step (no continue-on-error) that fails the job if either secret is empty. The publish steps themselves stay tolerant of registry errors, so misconfiguration fails fast while outages still allow the GitHub release to ship. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6 * Replace bash release scripts with cmd/mdsmith-release Go CLI The four bash scripts (set-version, check-versions, build-npm-platforms, build-wheels) used perl regex to edit JSON and TOML manifests, which was both fragile and indirectly tested — Go tests shelled out to bash and grepped the output, so a regression pointed at the wrapper rather than the bug. Port the logic to internal/release/ (version.go, buildnpm.go, buildwheels.go) and expose it through a separate cmd/mdsmith-release binary with four subcommands (stamp, check, build-npm, build-wheels). The release workflow invokes the Go CLI via `go run ./cmd/mdsmith-release ...`. Tests now exercise the exported functions directly. Behaviour parity: - Stamp's SemVer regex stays at the full semver.org grammar (rejects leading zeros in MAJOR/MINOR/PATCH and in numeric prerelease identifiers). - Pre-flight checks still fail fast when a manifest is missing, has no version field, or — for the npm root — drops its @mdsmith/* optionalDependencies block. - Check accumulates every problem instead of exiting on the first, so a multi-pin drift produces multiple actionable lines. - BuildWheels still orchestrates `python -m build` and `python -m wheel tags` (no native Go wheel builder exists), but the staging directories now clean up on failure via Go `defer` instead of bash's leak-prone `trap RETURN`. - Test for BuildWheels skips when python/build/wheel/hatchling aren't on PATH; the test job in ci.yml already installs them. Net: -3 toolchain (bash + perl + grep) and +1 (Go), with the JSON/TOML editing now happening in code that compiles, lints, and tests under the same toolchain as the rest of the repo. https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6 * Disable setup-go cache in release.yml + raise release-toolkit coverage 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 * mdsmith-release: switch to pflag and add helper-level coverage 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 * testify on release tests + spin asdf/mise out into plan/145 - 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 * Push CLI dispatcher coverage to 94% + drop bogus author from pyproject - 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 * Cover release-toolkit error branches: 95.8% CLI, 88.3% library 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 * release: route all IO through an injectable FS interface 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 * release: route python invocations through an injectable Runner 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 * release: cover the manifest-write and listWheels-skip-dir cases 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 * release: cover the retagWheels and moveWheels error branches in buildOneWheel 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 * release: cover the no-version-field and copyDir-mkdir branches 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 * release: cover staging-mkdir, recursive-copyDir, and moveWheels-list 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 * docs/install: point asdf+mise follow-up callouts at plan/145 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 * Address Copilot pass: 100% release coverage + 5 doc/code nits 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 * Fix BuildWheels source-tree path regression + plan/130 stale ref 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 79b8ad8 commit 59b75a2

35 files changed

Lines changed: 3617 additions & 122 deletions

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ row: "- [{summary}](../{filename})"
4141
- [Install the mdsmith VS Code extension, configure how it spawns `mdsmith lsp`, and read diagnostics inline as you edit Markdown files.](../docs/guides/editors/vscode.md)
4242
- [How to declare file kinds, assign files to them, and read the merged rule config that results.](../docs/guides/file-kinds.md)
4343
- [User guides for mdsmith directives, structure enforcement, and migration.](../docs/guides/index.md)
44+
- [Every channel that ships the mdsmith binary or the VS Code extension — npm, PyPI, asdf, mise, the GitHub release, and the Visual Studio Marketplace plus Open VSX — and which channel to pick for which workflow.](../docs/guides/install.md)
4445
- [Trade-offs and threshold guidance for readability, structure, length, and token budgets.](../docs/guides/metrics-tradeoffs.md)
4546
- [CLI commands, flags, exit codes, and output format.](../docs/reference/cli.md)
4647
- [Lint Markdown files for style issues.](../docs/reference/cli/check.md)

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ jobs:
4343
go-version-file: go.mod
4444
- run: go tool golangci-lint run
4545

46+
# Tracked manifests must stay pinned at 0.0.0-dev between releases;
47+
# the release workflow rewrites them with `mdsmith-release stamp`
48+
# before publishing each channel. A hand edit on main would ship a
49+
# wrong version on the next tag, so fail fast here.
50+
version-guard:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
54+
with:
55+
persist-credentials: false
56+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
57+
with:
58+
go-version-file: go.mod
59+
- run: go run ./cmd/mdsmith-release check
60+
4661
mdsmith:
4762
runs-on: ubuntu-latest
4863
steps:
@@ -138,6 +153,16 @@ jobs:
138153
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
139154
with:
140155
go-version-file: go.mod
156+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
157+
with:
158+
python-version: "3.12"
159+
- name: Install Python build tooling
160+
# internal/release/buildwheels_test.go exercises BuildWheels
161+
# which orchestrates `python -m build`, `python -m wheel`,
162+
# and the hatchling build backend. Without these the test
163+
# silently skips, defeating the point of having an
164+
# integration test in CI.
165+
run: python -m pip install --upgrade build wheel hatchling
141166
- name: Install mdsmith
142167
run: go install ./cmd/mdsmith
143168
- name: Run tests with coverage

.github/workflows/release.yml

Lines changed: 250 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ jobs:
6262
with:
6363
bun-version: "1.3.11"
6464
no-cache: true
65+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
66+
with:
67+
go-version-file: go.mod
68+
# zizmor's cache-poisoning rule treats the GitHub Actions
69+
# tool cache as an unprotected mutation surface in
70+
# release-context workflows; setup-go would default to
71+
# caching the module download. Disable it to match the
72+
# build job above.
73+
cache: false
74+
- name: Stamp tracked manifests with the tag
75+
env:
76+
VERSION: ${{ github.ref_name }}
77+
run: go run ./cmd/mdsmith-release stamp "${VERSION#v}"
6578
- name: Install extension dependencies
6679
working-directory: editors/vscode
6780
run: bun install --frozen-lockfile
@@ -76,15 +89,161 @@ jobs:
7689
VERSION: ${{ github.ref_name }}
7790
working-directory: editors/vscode
7891
run: |
79-
# Strip the leading 'v' so the .vsix carries a clean SemVer.
8092
ver="${VERSION#v}"
8193
bunx --bun @vscode/vsce package --no-dependencies \
8294
--out "mdsmith-${ver}.vsix"
95+
# Verify the publisher tokens are set BEFORE the publish steps
96+
# run. The publishes themselves use `continue-on-error: true`
97+
# so a transient registry outage does not block the GitHub
98+
# release. That same flag would also hide an unset/empty
99+
# secret, so guard misconfiguration here (no continue-on-error)
100+
# while still letting outages slide on the actual publish.
101+
- name: Verify Marketplace and Open VSX tokens are set
102+
env:
103+
VSCE_PAT: ${{ secrets.VSCE_PAT }}
104+
OVSX_PAT: ${{ secrets.OVSX_PAT }}
105+
run: |
106+
missing=""
107+
[ -n "${VSCE_PAT:-}" ] || missing="$missing VSCE_PAT"
108+
[ -n "${OVSX_PAT:-}" ] || missing="$missing OVSX_PAT"
109+
if [ -n "$missing" ]; then
110+
echo "missing required repo secret(s):$missing" >&2
111+
exit 1
112+
fi
113+
- name: Publish to Visual Studio Marketplace
114+
# The GitHub release .vsix is the documented fallback, so a
115+
# transient Marketplace outage should not block the release
116+
# job downstream of this one. Misconfiguration is caught by
117+
# the preceding verify step, so this only swallows runtime
118+
# registry errors.
119+
continue-on-error: true
120+
env:
121+
VERSION: ${{ github.ref_name }}
122+
VSCE_PAT: ${{ secrets.VSCE_PAT }}
123+
working-directory: editors/vscode
124+
# Reuse the exact .vsix the artifact upload below ships, so
125+
# Marketplace, Open VSX, and the GitHub release are byte-
126+
# identical. The publisher namespace is jeduden — claim it
127+
# in https://aka.ms/vscode-create-publisher before the first
128+
# release. PAT scope: Marketplace > Manage. Azure caps PATs
129+
# at one year; rotate annually and record the date in
130+
# CLAUDE.md.
131+
run: |
132+
ver="${VERSION#v}"
133+
bunx --bun @vscode/vsce publish \
134+
--no-dependencies \
135+
--packagePath "mdsmith-${ver}.vsix" \
136+
--pat "$VSCE_PAT"
137+
- name: Publish to Open VSX
138+
continue-on-error: true
139+
env:
140+
VERSION: ${{ github.ref_name }}
141+
OVSX_PAT: ${{ secrets.OVSX_PAT }}
142+
working-directory: editors/vscode
143+
# Open VSX is the registry VSCodium, Cursor, Theia, and
144+
# Gitpod query. Claim the jeduden namespace on
145+
# https://open-vsx.org and store the publisher token as the
146+
# OVSX_PAT secret before the first release. Rotate annually.
147+
run: |
148+
ver="${VERSION#v}"
149+
bunx --bun ovsx publish \
150+
--packagePath "mdsmith-${ver}.vsix" \
151+
--pat "$OVSX_PAT"
83152
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
84153
with:
85154
name: mdsmith-vscode-extension
86155
path: editors/vscode/mdsmith-*.vsix
87156

157+
npm:
158+
needs: [build]
159+
runs-on: ubuntu-latest
160+
# `id-token: write` lets `npm publish --provenance` mint an OIDC
161+
# token so the npm registry stamps each tarball with verifiable
162+
# build metadata pointing at this exact workflow run.
163+
permissions:
164+
contents: read
165+
id-token: write
166+
steps:
167+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
168+
with:
169+
persist-credentials: false
170+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
171+
with:
172+
go-version-file: go.mod
173+
cache: false
174+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
175+
with:
176+
node-version: "20"
177+
registry-url: "https://registry.npmjs.org"
178+
- name: Stamp tracked manifests with the tag
179+
env:
180+
VERSION: ${{ github.ref_name }}
181+
run: go run ./cmd/mdsmith-release stamp "${VERSION#v}"
182+
- name: Download release artifacts
183+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
184+
with:
185+
path: artifacts
186+
merge-multiple: true
187+
- name: Build platform packages
188+
run: go run ./cmd/mdsmith-release build-npm artifacts npm/dist
189+
- name: Publish platform packages
190+
# Platform packages publish first so the root never advertises
191+
# an optionalDependency npm cannot find. The root package
192+
# publishes last, after every platform exists.
193+
env:
194+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
195+
run: |
196+
for pkg in npm/dist/*; do
197+
(cd "$pkg" && npm publish --access public --provenance)
198+
done
199+
- name: Publish root package
200+
env:
201+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
202+
working-directory: npm/mdsmith
203+
run: npm publish --access public --provenance
204+
205+
pypi:
206+
needs: [build]
207+
runs-on: ubuntu-latest
208+
# `id-token: write` lets pypa/gh-action-pypi-publish use Trusted
209+
# Publishing (OIDC) so PyPI accepts each upload without a
210+
# long-lived API token. Configure the trusted publisher on
211+
# pypi.org → Manage Project → Publishing before the first tag.
212+
permissions:
213+
contents: read
214+
id-token: write
215+
steps:
216+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
217+
with:
218+
persist-credentials: false
219+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
220+
with:
221+
go-version-file: go.mod
222+
cache: false
223+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
224+
with:
225+
python-version: "3.12"
226+
- name: Stamp tracked manifests with the tag
227+
env:
228+
VERSION: ${{ github.ref_name }}
229+
run: go run ./cmd/mdsmith-release stamp "${VERSION#v}"
230+
- name: Install build tooling
231+
# `python -m build` and `python -m wheel` orchestrate the
232+
# wheel build and the platform-tag retag respectively;
233+
# hatchling is the build backend pyproject.toml selects.
234+
run: python -m pip install --upgrade build wheel hatchling
235+
- name: Download release artifacts
236+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
237+
with:
238+
path: artifacts
239+
merge-multiple: true
240+
- name: Build platform wheels
241+
run: go run ./cmd/mdsmith-release build-wheels artifacts python/dist
242+
- name: Publish to PyPI
243+
uses: pypa/gh-action-pypi-publish@6733eb7d741f0b11ec6a39b58540dab7590f9b7d # v1.14.0
244+
with:
245+
packages-dir: python/dist
246+
88247
release:
89248
needs: [build, vscode]
90249
runs-on: ubuntu-latest
@@ -105,3 +264,93 @@ jobs:
105264
files: |
106265
mdsmith-*
107266
checksums.txt
267+
268+
smoke-test:
269+
# Wait until every channel is on the new version before checking
270+
# — the npm and PyPI registries can take ~60s to surface a fresh
271+
# publish, so the channel-specific install commands re-run if the
272+
# registry briefly returns the previous version.
273+
needs: [npm, pypi, release]
274+
strategy:
275+
fail-fast: false
276+
matrix:
277+
include:
278+
- channel: npm
279+
# node:lts (debian-slim) ships bash, so the install step
280+
# runs under the action's default `bash -e` shell. Alpine
281+
# would force `shell: sh` everywhere because busybox has
282+
# no bash before `apk add` runs.
283+
container: node:lts
284+
install: |
285+
# npm registry propagation can lag the publish by ~60s,
286+
# so retry with backoff until the just-published version
287+
# is resolvable. Mirrors the pip retry loop below.
288+
ok=0
289+
for attempt in 1 2 3 4 5; do
290+
if npm install -g --force "@mdsmith/cli@${VERSION#v}"; then
291+
ok=1; break
292+
fi
293+
sleep 15
294+
done
295+
if [ "$ok" -ne 1 ]; then
296+
echo "npm install never succeeded after 5 attempts" >&2
297+
exit 1
298+
fi
299+
run: mdsmith version
300+
- channel: pip
301+
container: python:3.12-slim
302+
install: |
303+
python -m pip install --upgrade pip
304+
# `--upgrade` forces pip to pick the just-published
305+
# wheel rather than a cached older one.
306+
ok=0
307+
for attempt in 1 2 3 4 5; do
308+
if python -m pip install --upgrade "mdsmith==${VERSION#v}"; then
309+
ok=1; break
310+
fi
311+
sleep 15
312+
done
313+
if [ "$ok" -ne 1 ]; then
314+
echo "pip install never succeeded after 5 attempts" >&2
315+
exit 1
316+
fi
317+
run: mdsmith version
318+
- channel: mise
319+
container: jdxcode/mise:latest
320+
# `ubi:jeduden/mdsmith@VER` resolves the binary directly
321+
# off the GitHub release the same `release` job above
322+
# just published. The shorter `mdsmith@VER` form depends
323+
# on the mise-plugins/registry follow-up; until that PR
324+
# lands the smoke-test would fail on every release, so
325+
# exercise the form that works today.
326+
install: |
327+
ok=0
328+
for attempt in 1 2 3 4 5; do
329+
if mise use -g "ubi:jeduden/mdsmith@${VERSION#v}"; then
330+
ok=1; break
331+
fi
332+
sleep 15
333+
done
334+
if [ "$ok" -ne 1 ]; then
335+
echo "mise install never succeeded after 5 attempts" >&2
336+
exit 1
337+
fi
338+
run: |
339+
eval "$(mise activate bash --shims)"
340+
mdsmith version
341+
runs-on: ubuntu-latest
342+
container: ${{ matrix.container }}
343+
env:
344+
VERSION: ${{ github.ref_name }}
345+
steps:
346+
- name: Install
347+
run: ${{ matrix.install }}
348+
- name: Verify version
349+
run: |
350+
got=$(${{ matrix.run }})
351+
want="mdsmith ${VERSION}"
352+
if [ "$got" != "$want" ]; then
353+
echo "channel=${{ matrix.channel }}: got '$got', want '$want'" >&2
354+
exit 1
355+
fi
356+
echo "channel=${{ matrix.channel }}: $got"

.mdsmith.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ rules:
7272
- ".claude/**"
7373
- ".github/**"
7474
- "editors/**"
75+
- "npm/**"
76+
- "python/**"
7577
catalog: true
7678
required-structure: true
7779
include: true

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ row: "- [{summary}]({filename})"
4747
- [Install the mdsmith VS Code extension, configure how it spawns `mdsmith lsp`, and read diagnostics inline as you edit Markdown files.](docs/guides/editors/vscode.md)
4848
- [How to declare file kinds, assign files to them, and read the merged rule config that results.](docs/guides/file-kinds.md)
4949
- [User guides for mdsmith directives, structure enforcement, and migration.](docs/guides/index.md)
50+
- [Every channel that ships the mdsmith binary or the VS Code extension — npm, PyPI, asdf, mise, the GitHub release, and the Visual Studio Marketplace plus Open VSX — and which channel to pick for which workflow.](docs/guides/install.md)
5051
- [Trade-offs and threshold guidance for readability, structure, length, and token budgets.](docs/guides/metrics-tradeoffs.md)
5152
- [CLI commands, flags, exit codes, and output format.](docs/reference/cli.md)
5253
- [Lint Markdown files for style issues.](docs/reference/cli/check.md)

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ row: "- [{summary}]({filename})"
3333
- [Install the mdsmith VS Code extension, configure how it spawns `mdsmith lsp`, and read diagnostics inline as you edit Markdown files.](docs/guides/editors/vscode.md)
3434
- [How to declare file kinds, assign files to them, and read the merged rule config that results.](docs/guides/file-kinds.md)
3535
- [User guides for mdsmith directives, structure enforcement, and migration.](docs/guides/index.md)
36+
- [Every channel that ships the mdsmith binary or the VS Code extension — npm, PyPI, asdf, mise, the GitHub release, and the Visual Studio Marketplace plus Open VSX — and which channel to pick for which workflow.](docs/guides/install.md)
3637
- [Trade-offs and threshold guidance for readability, structure, length, and token budgets.](docs/guides/metrics-tradeoffs.md)
3738
- [CLI commands, flags, exit codes, and output format.](docs/reference/cli.md)
3839
- [Lint Markdown files for style issues.](docs/reference/cli/check.md)

PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ footer: |
4343
| 129 || sonnet | [Flag unused or duplicate link reference definitions](plan/129_no-unused-link-definitions.md) |
4444
| 130 | 🔳 | opus | [Distribute mdsmith binaries via npm, PyPI, asdf, mise, and the VS Code marketplaces](plan/130_binary-distribution-and-versioning.md) |
4545
| 131 | 🔲 | opus | [LSP symbol navigation for agents (Claude)](plan/131_lsp-symbol-navigation.md) |
46+
| 145 | 🔲 | opus | [Publish mdsmith via asdf and mise registry submissions](plan/145_asdf-mise-registry-submissions.md) |
4647
| 52 || | [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
4748
| 61 || | [Required Structure Rule Hardening](plan/61_required-structure-hardening.md) |
4849
| 65 || | [Spike WASM-Embedded Weasel Inference](plan/65_spike-wasm-embedded-inference.md) |

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,15 @@ row: "- [{summary}]({filename})"
8282

8383
```bash
8484
go install github.com/jeduden/mdsmith/cmd/mdsmith@latest
85+
npm install -g @mdsmith/cli # or: npx @mdsmith/cli
86+
pip install mdsmith # or: uvx mdsmith / pipx install mdsmith
8587
```
8688

89+
More options live in
90+
[docs/guides/install.md](docs/guides/install.md). It covers direct
91+
downloads, the VS Code extension on the Marketplace and Open VSX,
92+
and asdf and mise once their registry entries land.
93+
8794
## 🚀 Usage
8895

8996
```text

0 commit comments

Comments
 (0)