Use this file every time you cut a release. Work top to bottom.
-
CHANGELOG.md— new## [X.Y.Z] - YYYY-MM-DDsection written with today's date
That is the only edit a release needs. The git tag is the single source of every
version number in this project. At build time CI stamps the tag (minus the
leading v) into all of them:
| File | Stamped by |
|---|---|
crates/*/Cargo.toml and their inter-crate version = pins |
release and publish jobs |
npm/package.json |
publish-npm |
vscode/package.json |
publish-vscode, via npm version |
chrome/manifest.json |
chrome-extension, which then asserts it took |
crates/vastlint-mcp/server.json (version, package URL, SHA-256) |
publish-mcp-registry |
None of those commits are pushed, so the in-repo values are cosmetic and exist
only so a local cargo run reports its version honestly. Keep them equal to the
last released tag. Do not maintain separate version numbers for the VS Code
and Chrome extensions. An earlier version of this checklist asked for that,
and it produced entries in CHANGELOG.md naming extension versions that were
never published: the Marketplace has always carried the release version.
git add CHANGELOG.md
git commit -m "chore: release vX.Y.Z"
git tag vX.Y.Z
git push && git push origin vX.Y.ZSync the cosmetic in-repo versions to the tag in the same commit, so a local build reports the right number:
sed -i '' "s/^version = \".*\"/version = \"X.Y.Z\"/" crates/*/Cargo.toml
sed -i '' "s/^vastlint-core = { path = \"..\/vastlint-core\", version = \"[^\"]*\" }/vastlint-core = { path = \"..\/vastlint-core\", version = \"X.Y.Z\" }/" crates/*/Cargo.toml
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"X.Y.Z\"/" \
npm/package.json vscode/package.json chrome/package.json chrome/manifest.json \
crates/vastlint-mcp/server.json
cargo check --workspace # refreshes Cargo.lock
git diff crates/ # only version lines should appearThe second sed names vastlint-core in full on purpose. An earlier version
matched any version = "..." }, which also rewrote third-party pins written in
table form: cutting 0.11.3 it turned quick-xml = { version = "0.41" } into
quick-xml = { version = "0.11.3" }. Read the git diff before committing.
CI stamps the same values again at build time, so a sync that is one release
behind still ships correctly. It just makes cargo run -- --version honest.
Go to GitHub Actions → Release and watch the run. One human approval gate (production environment) unblocks all publish jobs simultaneously.
| Job | Destination | Gate variable |
|---|---|---|
release |
GitHub Release + binary assets | always on |
publish-vscode |
VS Code Marketplace + Open VSX | ENABLE_VSCODE_PUBLISH=true |
publish-npm |
npmjs.com (vastlint) |
ENABLE_NPM_PUBLISH=true |
publish |
crates.io (vastlint-core, vastlint-cli) |
ENABLE_CRATES_PUBLISH=true |
publish-docker |
Docker Hub (aleksuix/vastlint) |
ENABLE_DOCKER_PUBLISH=true |
publish-mcp-registry |
MCP Registry (io.github.aleksUIX/vastlint) |
ENABLE_MCP_REGISTRY_PUBLISH=true |
provenance |
GitHub attestation store (SLSA L3) | always on |
chrome-extension |
GitHub Release asset + Chrome Web Store | tag push, or publish_chrome=true on manual Release dispatch |
Chrome publish paths:
- Tagged release: the
chrome-extensionjob inReleaseruns on tag pushes and publishes the zipped extension. - Standalone publish: run the
Chrome Extensionworkflow withpublish: trueand optionalversion: 0.11.2when you need to ship the extension outside a full repo release.
These used to be manual. They are jobs in the same Release run now, each
gated on a repo variable and a deploy key, and each pushes straight to main
of its target repo.
| Job | Target repo | What it writes | Gate variable |
|---|---|---|---|
sync-homebrew-tap |
aleksUIX/homebrew-tap |
Formula/vastlint.rb: version, four URLs, four SHA-256 values taken from the run's own tarballs |
ENABLE_HOMEBREW_TAP_SYNC=true |
sync-go |
aleksUIX/vastlint-go |
prebuilt libs/ FFI artifacts |
ENABLE_GO_SYNC=true |
sync-python |
aleksUIX/vastlint-python |
release commit + tag | ENABLE_PYTHON_SYNC=true |
sync-erlang |
aleksUIX/vastlint-erlang |
prebuilt NIFs and checksums | ENABLE_ERLANG_SYNC=true |
Check each one landed rather than assuming: gh api repos/aleksUIX/<repo>/commits -q '.[0].commit.message'.
sync-infra exists in the workflow but stays skipped: it needs
ENABLE_INFRA_SYNC=true and a VASTLINT_INFRA_DEPLOY_KEY secret, and neither
is set. Until they are, bump the pin by hand after publish-npm finishes:
cd ../vastlint-infra
sed -i '' 's/"vastlint": "[^"]*"/"vastlint": "X.Y.Z"/' package.json apps/vastlint-web/package.json
pnpm install && pnpm build:web # the build is the only check that the new WASM loads
git commit -am "chore: point the web validator at vastlint X.Y.Z" && git pushPushing to main deploys the web app and the worker through deploy.yml.
- GitHub Release page has correct assets and CHANGELOG notes
-
npm info vastlint versionreturns new version - VS Code Marketplace page shows new version
- Open VSX page shows new version (
open-vsx.org/extension/aleksUIX/vastlint) -
docker pull aleksuix/vastlint:latestpulls new image - MCP Registry entry updated (if
ENABLE_MCP_REGISTRY_PUBLISH=true) -
cargo info vastlint-coreandcargo info vastlint-clireport the new version - Homebrew:
brew upgrade vastlintinstalls new version - vastlint.org/validate reports the release version and the current rule count
Read the job list, do not read the run's overall conclusion. A failed publish
job leaves the run red while everything else shipped, and a skipped one leaves
it green. gh run view <id> --json jobs -q '.jobs[] | "\(.name): \(.conclusion)"'.
If crates.io failed on its own, re-run just that half:
gh workflow run publish-crates.yml -f tag=vX.Y.Z.
Everything published carries the tag version. The only version numbers that need a human are the ones outside this repo.
| File | Updated by | Notes |
|---|---|---|
CHANGELOG.md |
manual | always |
crates/*/Cargo.toml |
CI (sed at build time) | in-repo value is cosmetic |
npm/package.json |
CI (npm version) |
in-repo value is cosmetic |
vscode/package.json |
CI (npm version) |
in-repo value is cosmetic |
chrome/manifest.json + chrome/package.json |
CI (node, then asserted) | in-repo value is cosmetic |
crates/vastlint-mcp/server.json |
CI (jq: version, URL, SHA-256) | in-repo value is cosmetic |
homebrew-tap/Formula/vastlint.rb |
CI (sync-homebrew-tap) |
version, URLs and SHA-256 values |
vastlint-erlang, vastlint-go, vastlint-python |
CI (sync-* jobs) |
prebuilt artifacts and release commits |
vastlint-infra package.json + apps/vastlint-web/package.json |
manual | pins the vastlint npm package the web validator runs |