Releasing
Releasing
How OSS releases are cut, built, published, and tagged.
This document is for maintainers cutting a release. End users should look at README.md; contributors at CONTRIBUTING.md. The Homebrew tap layout is documented separately in specs/homebrew-tap.md.
All packages in this monorepo (the @nowline/* npm packages and the VS Code extension) share a single version and ship together. The release pipeline lives in .github/workflows/release.yml.
Versioning scheme
Section titled “Versioning scheme”We use Semantic Versioning — MAJOR.MINOR.PATCH, tagged vMAJOR.MINOR.PATCH. Every package in packages/* is kept lock-step at the same version.
0.x.y— public API and AST JSON schema may change between minor versions; patch releases are bug-fix only. Call out breaking changes in the CHANGELOG.1.0.0and after —MAJORis reserved for breaking changes to the DSL, AST schema, or CLI surface. Minors add features without breaking. Patches are bug-fixes.
The DSL itself uses an independent integer-only version (nowline v1, v2, …) declared inside .nowline files; that contract lives in specs/dsl.md and is not tied to package SemVer.
Dev-build version string
Section titled “Dev-build version string”packages/*/package.json#version always reflects the last released version on main. To keep dev builds distinguishable from real releases without rewriting package.json between every commit, the CLI appends git build metadata to its --version output (per SemVer §10):
| Build | nowline --version |
|---|---|
Tagged release (HEAD == vX.Y.Z) | 0.1.0 |
| Dev build, clean tree | 0.1.0+abc1234 |
| Dev build, uncommitted changes | 0.1.0+abc1234.dirty |
The +... suffix is informational metadata only; npm and the VS Code Marketplace strip / reject it on their own version fields, so it never reaches a published artifact. The metadata is captured at compile time by packages/cli/scripts/bundle-templates.mjs (which shells out to git rev-parse, git describe --exact-match, and git status --porcelain).
Pre-flight
Section titled “Pre-flight”Before cutting a release, on main:
- CI is green on the latest
maincommit (Linux, macOS, Windows). CHANGELOG.mdis up to date. See Changelog workflow below — contributors should already have appended entries to## [Unreleased]as part of their PRs; thecut-releasejob automatically moves them into a new## [vX.Y.Z] - YYYY-MM-DDsection. No manual edit is required before triggering the dispatch.- Examples render cleanly.
pnpm build(which runspnpm samplesandpnpm fixtures) should produce the expected SVGs without warnings. - Smoke-test the standalone binary locally with
pnpm --filter @nowline/cli compile:local, then runexamples/minimal.nowlinethrough every export format using the platform-suffixed binary the script produces —./packages/cli/dist-bin/nowline-<platform>-<arch>(e.g.nowline-macos-arm64on Apple Silicon,nowline-linux-x64on Linux/amd64). This catchesbun compileregressions that the CI smoke test cannot reach for cross-platform binaries. - Required secrets are in place. All five secrets in the Required secrets table below must exist on
lolay/nowline. The first-ever release also needed thelolay/homebrew-taprepo seeded and the Marketplace / Open VSX namespaces created — that one-time setup is now done; subsequent releases skip it. (Forking maintainers can recreate the namespaces from each registry’s “create publisher” flow; the formula seed lives atscripts/homebrew-tap/.)
Cutting the release
Section titled “Cutting the release”There are two ways to trigger a release; the dispatch UI is the default.
1. Dispatch UI (primary)
Section titled “1. Dispatch UI (primary)”- Go to Actions → Release → Run workflow on
lolay/nowline. - Pick the
level(patch/minor/major). - Click Run workflow.
This kicks off the cut-release job, which:
- Checks out
mainusingRELEASE_TAG_PAT(a user-scoped PAT —GITHUB_TOKEN-pushed tags do not trigger downstream workflows, which would defeat the whole point). - Runs
node .github/scripts/bump-version.mjs <level>to rewrite everypackages/*/package.jsonto the next SemVer. - Runs
node .github/scripts/release-changelog.mjs <version>to promote every entry under## [Unreleased]inCHANGELOG.mdandpackages/vscode-extension/CHANGELOG.mdinto a new## [X.Y.Z] - YYYY-MM-DDsection, leaving an empty## [Unreleased]skeleton in place. - Commits the bump and changelog promotion as
release vX.Y.Z. - Tags
vX.Y.Z. - Pushes both the commit and the tag to
main.
The tag push then re-triggers release.yml under event_name == 'push', which runs the actual build/publish jobs (the cut-release job is gated to dispatch-only; the build/publish jobs are gated to tag-pushes-only).
2. Manual fallback
Section titled “2. Manual fallback”If the dispatch flow is unusable (e.g. PAT expired), you can do the same thing locally:
node .github/scripts/bump-version.mjs patch # or minor / major; prints new versionnode .github/scripts/release-changelog.mjs X.Y.Z # or: make release-changelog VERSION=X.Y.Zgit commit -am "release vX.Y.Z"git tag vX.Y.Zgit push origin maingit push origin vX.Y.ZThe tag push triggers the same downstream jobs.
Pipeline
Section titled “Pipeline”Motivating incident. The v0.3.0 release run (#26337633859) failed because CI did not exercise the same build surface as the release. The root-cause fix is the dual-event design below: every PR now runs the same 10-cell matrix the release does.
flowchart LR prOpen["Open PR / push to PR head"] --> ci["ci.yml \n lint-workflows + build-test + bundle-size \n + release-build-smoke -> build.yml (upload: false) \n PR pushes cancel prior in-flight runs"] mergePush["Squash-merge to main"] --> ci bumppush["cut-release bump commit on main \n (author = nowline-release-bot)"] --> ciLight["ci.yml \n lint-workflows + build-test + bundle-size \n (release-build-smoke skipped by author filter) \n main pushes never cancel"] tagpush["Tag v* push"] --> reuse2["release.yml -> build.yml (upload: true)"] --> publish["publish matrix"] dispatch["workflow_dispatch"] --> cut[cut-release] --> bumppush cut --> tagpush branchPush["Push to branch \n (no PR)"] -.->|no trigger| void["(nothing runs)"]build.yml is the single source of truth for the 10-cell build matrix. ci.yml calls it with upload: false on every PR commit and every squash-merge to main; release.yml’s build job calls it with upload: true, gated on tag push. The upload flag gates only the actions/upload-artifact steps — every build, pack, and verify step always runs, so PRs exercise the exact surface a tag push would.
publish does needs: build, so GitHub Actions waits for every cell of the build matrix to succeed before any cell of publish starts — that is the gate, no separate job required. Inside the github-release cell of publish, the homebrew tap commit runs as the last sequential step, so the tap fires right after the GH release publish without waiting on the npm or vscode cells.
Bump-commit skip filter
Section titled “Bump-commit skip filter”ci.yml’s release-build-smoke job carries this if: condition:
if: ${{ github.event_name != 'push' || github.event.head_commit.author.email != 'nowline-release-bot@lolay.com' }}This skips the heavy 10-cell matrix on the version-bump commit that cut-release produces, because release.yml is already about to run the same matrix via build.yml with upload: true. Running both in parallel would waste compute without adding any safety signal — both are reading the same SHA.
The filter uses head_commit.author.email rather than commit-message matching because a human PR could coincidentally start with “release v…”. The email nowline-release-bot@lolay.com is set via the GIT_AUTHOR_EMAIL env variable at the top of release.yml. Cross-reference contract: if that email ever changes, the if: condition in ci.yml’s release-build-smoke job must be updated to match — otherwise bump commits either silently re-run the full matrix in parallel (wasted compute) or the filter stops working (infinite-loop risk during release).
cut-release
Section titled “cut-release”Dispatch-only. Bumps versions, commits, tags, and pushes. See Cutting the release.
Defined in build.yml. One job, one matrix, ten cells. Heterogeneous on purpose so the publish phase can needs: build and inherit “wait for every cell” gating from GitHub Actions for free.
| Cell id | Runner | Produces |
|---|---|---|
bin-macos-arm64 | macos-latest | nowline-macos-arm64 artifact |
bin-macos-x64 | macos-latest | nowline-macos-x64 artifact |
bin-linux-x64 | ubuntu-latest | nowline-linux-x64 + nowline_amd64.deb artifacts |
bin-linux-arm64 | ubuntu-latest | nowline-linux-arm64 + nowline_arm64.deb artifacts |
bin-windows-x64 | windows-latest | nowline-windows-x64.exe artifact |
bin-windows-arm64 | windows-latest | nowline-windows-arm64.exe artifact |
pack-npm | ubuntu-latest | npm-tarballs artifact (eighteen .tgz files) |
pack-vsix | ubuntu-latest | nowline-vscode.vsix artifact |
pack-action | ubuntu-latest | action-mirror bundle artifact |
pack-mcp-mcpb | ubuntu-latest | nowline.mcpb artifact (Claude Desktop Extensions bundle built from packages/mcp/) |
Binary cells use bun compile and run the same per-format smoke test (SVG, PNG, PDF, HTML, Mermaid, XLSX, MS Project XML) against examples/minimal.nowline, except cross-target combinations that cannot execute on the runner. The two linux cells additionally invoke scripts/build-deb.sh on the binary they just produced — keeping the binary→deb chain intra-cell skips an artifact upload/download round-trip.
pack-npm runs pnpm pack for the eighteen publishable packages in dependency order (@nowline/core, @nowline/layout, @nowline/renderer, @nowline/browser, @nowline/embed, @nowline/preview-shell, @nowline/lsp, @nowline/lsp-worker, @nowline/export-core, the six per-format @nowline/export-* packages, @nowline/config, @nowline/cli, @nowline/mcp). pnpm 10 rewrites workspace:* to the resolved version inside each tarball, so the publish phase uses plain npm publish <tarball> with no workspace-protocol shenanigans.
Note on
@nowline/lsp. As of v0.4.0,@nowline/lspis published to npm.@nowline/lsp-workerdeclares"@nowline/lsp": "workspace:*"in its runtimedependencies; pnpm rewrites that to the resolved version inside the published tarball, sonpm install @nowline/lsp-workerneeds@nowline/lspresolvable on the registry. Publishing@nowline/lspalso fulfills the contract documented in its own README (npx nowline-lspfor Neovim/JetBrains/Helix/Emacs). The alternative — bundling@nowline/lspinto@nowline/lsp-workervia esbuild — was evaluated and deferred:lsp-workerhas no bundler config today (tsc -bonly), and introducing one would require designing externals for all four entry points pluslangium/vscode-languageserver; that’s a separate project, not a pre-release patch.
Note on
@nowline/config. As of v0.4.0,@nowline/configis also published to npm.@nowline/clideclares"@nowline/config": "workspace:*"in its runtimedependencies; pnpm rewrites that to the resolved version inside the published tarball, sonpm install -g @nowline/clineeds@nowline/configresolvable on the registry. The primary distribution channels (Homebrew,.deb, GitHub Releases, VS Code Marketplace) are unaffected — they usebun compilebinaries where@nowline/configis bundled at compile time.
pack-vsix runs pnpm package in packages/vscode-extension, which produces dist/nowline-vscode.vsix via esbuild + vsce package --no-dependencies. The .vsix bundles the workspace dependencies, so the vscode publish cell never needs to read from npm.
pack-action stages the Marketplace action mirror bundle for lolay/nowline-action.
pack-mcp-mcpb runs make pack-mcpb, producing dist-mcpb/nowline.mcpb for Claude Desktop Extensions. Marketplace distribution (registry publish, .mcpb attach, manual submission tracking) is handled by the separate publish-mcp job — see § MCP publishing artifacts and ops/mcp-marketplace.md.
Option B — version-from-tag (future direction). Today the
cut-releasecommit bumpspackages/*/package.json#version, which is what stamps the binary’s--version, the embed banner, the .deb control file, the .vsix manifest, and eachpnpm packtarball. A future refactor could keeppackage.jsonat0.0.0-developmentpermanently and inject version from the tag at build time, enabling true cache-by-SHA across CI and release — PRs would build the exact same artifacts as the release without any version-bump commit in the way. Out of scope for v0.4.0; revisit after estate cleanups.
publish
Section titled “publish”A single job with one matrix of four cells. needs: build means every cell of the build matrix must succeed before any cell here starts — built-in gating, no no-op job. Each cell downloads only the artifacts it needs, then pushes; there is no pnpm install or pnpm -r build happening alongside any external upload.
| Cell id | Action |
|---|---|
npm | Downloads npm-tarballs, runs npm publish <tarball> --access public for each tarball in dependency order. Uses NPM_TOKEN. |
vscode | Downloads nowline-vscode.vsix, runs vsce publish --packagePath … then ovsx publish …. Uses VSCE_PAT and OVSX_PAT. |
github-release | Downloads binary + deb artifacts, stages them with the man page (nowline.1) and any nowline.<locale>.1 overlays, publishes the GitHub Release via softprops/action-gh-release@v3, then commits a refreshed Formula/nowline.rb to lolay/homebrew-tap using HOMEBREW_TAP_TOKEN. The formula references the release-asset URLs that the same cell just published and embeds SHA256s computed on the fly. The cell fails loudly if any expected artifact is missing rather than emitting an all-zero SHA. See specs/homebrew-tap.md for the formula structure and seed-repo bootstrap. |
| action-mirror | Downloads action-mirror bundle, syncs to lolay/nowline-action, tags, and publishes a GitHub Release on the mirror. Uses MARKETPLACE_MIRROR_PAT. |
The matrix uses fail-fast: false so a flaky npm publish does not cancel an in-flight Marketplace publish or the github-release/tap cell.
publish-mcp
Section titled “publish-mcp”Defined in publish-mcp.yml. Reusable workflow invoked from release.yml with needs: [build, publish] so the MCP registry pointer is never pushed for a half-failed release or before npm is live. A thin standalone caller (publish-mcp-standalone.yml) reuses the same implementation for maintainer re-runs.
Ordered steps inside publish-mcp.yml:
- Verify
npm view @nowline/mcp@$VERSIONresolves (waits out npm propagation lag). - Attach
nowline.mcpbto the GitHub Release (gh release upload --clobber). make publish-mcp-registry— DNS domain auth +mcp-publisher publish(last automated step).- Open a
maintainer-only+release-opsGitHub issue with the Claude Desktop + Gemini manual submission checklist.
Uses MCP_PRIVATE_KEY. Operator runbook: ops/mcp-marketplace.md.
We deliberately ship every tag as a stable release — Marketplace pre-release channels require SemVer pre-release suffixes (e.g. 0.1.0-rc.1) that we do not currently produce. Revisit at 1.0 if we want a “next” channel.
Why the homebrew tap commit lives inside the github-release cell
Section titled “Why the homebrew tap commit lives inside the github-release cell”GitHub Actions matrix cells cannot depend on each other (no intra-matrix needs:). The tap commit must run after the GH release publish because the formula references releases/download/v…/… URLs that have to resolve. Folding the tap steps into the github-release cell as sequential steps on the same runner gives “tap fires right after release, doesn’t wait for vscode or npm” with no extra job, no inter-job artifact re-download, and no separate gate. The trade is that re-running the cell after a tap-only failure also re-attempts the GH release publish; softprops/action-gh-release@v3 is upsert-style on the same tag and overwrites asset uploads, so re-runs are safe.
Files attached to the GitHub Release
Section titled “Files attached to the GitHub Release”nowline-macos-arm64,nowline-macos-x64nowline-linux-x64,nowline-linux-arm64nowline-windows-x64.exe,nowline-windows-arm64.exenowline_amd64.deb,nowline_arm64.debnowline.1(man page; referenced as a Homebrew resource), plus anynowline.<locale>.1overlays.nowline.mcpb(Claude Desktop Extensions bundle; attached bypublish-mcp.ymlafter the main publish matrix is green).
Hotfix flow
Section titled “Hotfix flow”When a released line needs a fix without dragging in newer work from main:
- Cut a
release/vX.Ybranch from the tag you need to patch (git switch -c release/v0.1 v0.1.0) and push it. - Open a PR against that branch with the fix.
- Apply the
backport mainlabel. - After CI passes, merge.
.github/workflows/backport.yml(usingkorthout/backport-action) auto-opens a follow-up PR cherry-picking the squash-commit ontomain. Reviewer validates CI on the backport PR and merges it. Auto-merge is intentionally off because cherry-picks can conflict with newer work onmain. - Cut a new tag from
release/vX.Y(v0.1.1) via the manualReleaseworkflow dispatch (run it from therelease/v0.1branch via the Use workflow from dropdown). The tag itself does not need to live onmain; the published binaries / packages just need the right code at the right SHA.
Required secrets
Section titled “Required secrets”All secrets live under Settings → Secrets and variables → Actions on lolay/nowline.
| Secret | Used by | Purpose |
|---|---|---|
RELEASE_TAG_PAT | cut-release | User-scoped PAT (fine-grained, contents: write on lolay/nowline) used to push the release commit + tag. GITHUB_TOKEN-pushed tags do not trigger downstream workflow runs, which would prevent the build/publish jobs from firing. |
NPM_TOKEN | publish (npm cell) | npm publish for @nowline/* packages. Use an automation token. |
VSCE_PAT | publish (vscode cell) | Azure DevOps personal access token with Marketplace → Manage scope, scoped to the nowline publisher. |
OVSX_PAT | publish (vscode cell) | Open VSX personal access token. |
HOMEBREW_TAP_TOKEN | publish (github-release cell) | Fine-grained PAT with contents: write on lolay/homebrew-tap for committing the refreshed formula. |
MCP_PRIVATE_KEY | publish-mcp | Ed25519 private key (64-character hex) for DNS domain auth on nowline.io. Grants io.nowline/* namespace for automated MCP registry publish. One-time DNS TXT record setup — see ops/mcp-marketplace.md. |
MARKETPLACE_MIRROR_PAT | publish (action-mirror cell) | Fine-grained PAT with contents: write on lolay/nowline-action for mirroring the Marketplace action bundle. |
MCP publishing artifacts
Section titled “MCP publishing artifacts”@nowline/mcp ships to more channels than any other Nowline artifact because each agent harness has its own discovery mechanism. All OSS distribution builds and releases from lolay/nowline via release.yml, independently of the .vsix.
Per-channel actions at release time
Section titled “Per-channel actions at release time”| Channel | Artifact | Action at release |
|---|---|---|
| npm | @nowline/mcp tarball | Published automatically by the npm publish cell (same as all other @nowline/* packages). |
Public MCP registry (io.nowline/nowline) | Registry entry (JSON metadata + npm package ref) | Automated by publish-mcp.yml via mcp-publisher login dns + mcp-publisher publish (runs after npm is live; registry publish is the last automated step). Feeds the VS Code MCP gallery and Cursor Marketplace. |
| Cursor Marketplace | Registry-sourced listing | No separate action — reads the public MCP registry. |
| VS Code MCP gallery | Registry-sourced listing | No separate action — reads the public MCP registry. |
| GitHub Release | nowline.mcpb | Automated — publish-mcp.yml attaches the artifact built by pack-mcp-mcpb. |
| Claude Desktop Extensions directory | nowline.mcpb | Manual submission (no public API). CI opens a maintainer-only tracking issue with checklist + link to ops/mcp-marketplace.md. |
| Gemini CLI Extension channel | Extension bundle + GEMINI.md | Manual submission per the Gemini CLI extension publishing process; tracked on the same release issue. |
Curated marketplace submissions (Claude Desktop Extensions, public MCP registry, Gemini CLI extension channel) may require a review period of hours to days for initial acceptance. Subsequent updates to the same publisher id generally receive faster approval. Lead time: plan for at least one sprint of buffer between tagging a release and these marketplace listings going live.
Manual config harnesses (Claude Code’s claude mcp add, Codex CLI’s ~/.codex/config.toml) do not require a submission step — users reference npx @nowline/mcp directly, which resolves from npm.
Naming and publishing-id convention
Section titled “Naming and publishing-id convention”This section is the canonical record for how Nowline MCP artifacts are named and identified across channels. It is a going-forward convention: already-published VS Code and Cursor .vsix artifacts keep their existing ids (nowline.vscode-nowline) and are not re-published or renamed to apply this convention.
Two independent levers:
- Publishing id (stable, default
nowline): every OSS MCP artifact uses the barenowlineid. The-cloudsuffix is added only where the cloud artifact would collide with the OSS one in the same namespace or store (currently only the public MCP registry). The OSS id is never suffixed (no-oss). - Display name (phased): “Nowline” today (OSS only); when Nowline Cloud launches, OSS becomes “Nowline OSS” and the cloud “Nowline Cloud”. “Pro”/“Enterprise” are account tiers that gate Nowline Cloud, never connector or display names.
Concrete ids per channel:
| Channel | OSS id | Cloud id | Notes |
|---|---|---|---|
| npm | @nowline/mcp | n/a (Go server) | OSS package; cloud is remote-only |
| Public MCP registry | io.nowline/nowline | io.nowline/nowline-cloud | Only channel where both tiers share a namespace, so cloud is suffixed |
| Claude Desktop Extensions | name: nowline (.mcpb manifest) | n/a (Connectors directory, separate store) | Connectors directory = separate store; no collision |
| Claude Connectors Directory | n/a (Extensions directory, separate store) | nowline | Each store holds only one tier |
| Cursor Marketplace | nowline (local) | nowline-cloud (remote OAuth) | One store, two listings |
| VS Code MCP gallery | nowline (local) | nowline-cloud (remote OAuth) | Sourced from public MCP registry |
| Gemini CLI extension | name: nowline | remote http entry (same channel) |
Hard rules:
- OSS stays
@nowline/mcp(Apache-2.0) on npm + public MCP registry (io.nowline/nowline) +.mcpb(name: nowline). - The cloud artifact and any bundle that wires it are proprietary “Nowline Cloud” artifacts shipped only from the proprietary side — never from
lolay/nowline. - Tool names stay identical across OSS and Cloud (
validate,render,read, etc.); only the server display name + id differ. - Do NOT re-publish or rename the already-published
.vsix(nowline.vscode-nowline) or Cursor extension to apply this convention. It is a going-forward-only change.
Operand-first descriptions on every listing (regardless of phase):
- OSS: “Create and edit your roadmaps in a plain-text format.”
- Cloud: “Reads/writes your Nowline roadmaps in the cloud. Requires a Nowline account; cloud features depend on your plan (Pro/Enterprise).”
See specs/mcp.md and specs/cli-distribution.md § MCP distribution for the full harness coverage matrix.
Changelog workflow
Section titled “Changelog workflow”CHANGELOG.md follows Keep a Changelog. Two roles, one file:
- Contributors append an entry to the
## [Unreleased]section as part of their PR. Use the existing subsections (Added,Changed,Deprecated,Removed,Fixed,Security) and link to the PR number where useful. cut-releaseautomatically moves every entry under## [Unreleased]into a new## [X.Y.Z] - YYYY-MM-DDsection directly above the previous release, leaving an empty## [Unreleased]skeleton for the next cycle. This is handled by.github/scripts/release-changelog.mjs(therelease-changelogMake target) and runs as part of thecut-releasejob — no manual edit is required.
Both CHANGELOG.md (root) and packages/vscode-extension/CHANGELOG.md are promoted in the same commit.
Empty
[Unreleased]section. If## [Unreleased]has no entries when a release is cut, the script still succeeds and emits an empty## [X.Y.Z] - YYYY-MM-DDsection. A fail-on-empty pre-flight guard remains a possible future enhancement.
After release
Section titled “After release”- Verify the GitHub Release page lists all eight binaries / debs.
- Verify each
@nowline/*package shows the new version on npm (all 17 should returnX.Y.Z):npm view @nowline/core versionnpm view @nowline/layout versionnpm view @nowline/renderer versionnpm view @nowline/browser versionnpm view @nowline/embed versionnpm view @nowline/preview-shell versionnpm view @nowline/lsp versionnpm view @nowline/lsp-worker versionnpm view @nowline/export-core versionnpm view @nowline/export-png versionnpm view @nowline/export-pdf versionnpm view @nowline/export-html versionnpm view @nowline/export-mermaid versionnpm view @nowline/export-xlsx versionnpm view @nowline/export-msproj versionnpm view @nowline/config versionnpm view @nowline/cli versionnpm view @nowline/mcp version
- Verify Homebrew works:
brew update && brew install lolay/tap/nowline && nowline --version— should printX.Y.Z(no+shasuffix on a release build). - Verify the VS Code extension shows the new version on the Marketplace and Open VSX.
Rollback
Section titled “Rollback”There’s no automated rollback. If a release is broken:
- Open a GitHub issue describing what’s wrong.
- Cut a hotfix release with the next patch version (e.g.
v0.1.0 → v0.1.1) via the Hotfix flow; do not delete or overwrite the broken tag. - Mark the broken release as a pre-release on GitHub so package managers stop offering it.
- For npm-specific breakage,
npm deprecate '@nowline/<pkg>@<version>' "broken release; use vX.Y.Z+1"rather than unpublishing — unpublish has a 72-hour window and breaks existing lockfiles. - For Marketplace breakage, you can unpublish a version with
vsce unpublish nowline.vscode-nowline@X.Y.Z. Open VSX has a similarovsx unpublishcommand.