From 7685619dee6bf0ddcdbe88547a18ea1c55d28685 Mon Sep 17 00:00:00 2001 From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:44:39 +1000 Subject: [PATCH 1/2] Stop the unlisting from listing Basilisk on Zed Basilisk is not in the Zed extension registry and never was. There is no `[basilisk]` block in zed-industries/extensions/extensions.toml, no extensions/basilisk submodule, and no commit in that repo's history mentions Basilisk; the only related PR is #4871, for a differently-named extension, closed unmerged. The `publish-zed` job was removed from release.yml after its registry step failed the v0.41.0 release, and the listing had not landed before that. The roadmap plan said as much all along. The runbook did not know this. `delist/00-publish-zed-final.sh` would have opened a PR ADDING Basilisk to that registry in the middle of unlisting it, and `delist/06-unlist-zed.sh` would have asked a maintainer of someone else's repo to delete an entry that does not exist. What is actually public is the mirror, Nimblesite/basilisk-zed. Zed installs a dev extension straight from a clone of that layout, and its `main` still serves the OLD extension: a [language_servers.basilisk] block launching `basilisk lsp`, a command the inert CLI no longer has, under a description advertising diagnostics, autocomplete, refactoring and profiling. That is a live product claim for a checker that was producing incorrect results. So 00 now replaces the mirror contents with the notice-only extension and opens no registry PR, and 06 archives the mirror read-only the way 05 archives the Neovim one. Both re-check the registry first and refuse to run if a `[basilisk]` entry ever appears, because that would make every assumption here false. 01 no longer waits on a bump PR that is never coming. scripts/publish_zed_registry.py, its test, and delist/remove_registry_entry.py are deleted rather than kept as dead code: the only thing they can do is list Basilisk somewhere, and the CI step proving the listing edit goes with them. ROADMAP-NEXT-STEPS-PLAN.md gets a superseded banner naming the two claims in it that would cause harm if followed. --- .github/workflows/ci.yml | 12 +- delist/00-publish-zed-final.sh | 67 ++++---- delist/01-verify-final-release.sh | 17 +-- delist/06-unlist-zed.sh | 78 +++++----- delist/README.md | 9 +- delist/remove_registry_entry.py | 56 ------- docs/plans/ROADMAP-NEXT-STEPS-PLAN.md | 9 ++ docs/specs/ZED-SPEC.md | 6 +- scripts/publish_zed_registry.py | 211 -------------------------- scripts/test_publish_zed_registry.py | 133 ---------------- 10 files changed, 107 insertions(+), 491 deletions(-) delete mode 100755 delist/remove_registry_entry.py delete mode 100755 scripts/publish_zed_registry.py delete mode 100644 scripts/test_publish_zed_registry.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 161549f8..3b53d189 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -361,15 +361,9 @@ jobs: scripts/render-zed-mirror.sh "${RUNNER_TEMP}/zed-mirror" 0.0.0-ci ( cd "${RUNNER_TEMP}/zed-mirror" && cargo build --release --target wasm32-wasip2 ) - # The registry listing edits a ~1400-entry file in a repo we do not own, - # and it only ever runs during a tagged release — so it gets its proof - # here, on the PR, rather than the first time it touches upstream. Pure - # text-editing functions only; no network ([ZED-MIRROR]). - - name: Test the zed-industries listing edit - run: | - set -euo pipefail - pip install pytest==9.1.1 - python3 -m pytest scripts/test_publish_zed_registry.py -q + # The zed-industries listing edit used to be proved here. It is gone: + # Basilisk is not in that registry and never was, so the only thing the + # script could do is ADD a listing during an unlisting ([ZED-MIRROR]). # ── Rust coverage + thresholds (runs in parallel) ────────────────────────── test-rust: diff --git a/delist/00-publish-zed-final.sh b/delist/00-publish-zed-final.sh index 689bb8ae..b6dff360 100755 --- a/delist/00-publish-zed-final.sh +++ b/delist/00-publish-zed-final.sh @@ -1,53 +1,66 @@ #!/usr/bin/env bash -# Publish the FINAL Zed extension — run this BEFORE 01-verify-final-release.sh. +# Replace the public Zed mirror with the notice-only extension. # -# Implements [WITHDRAWAL-UNLIST] and [ZED-MIRROR]. Zed is the one channel the -# Release workflow does not publish: the `publish-zed` job was removed from -# release.yml after its registry-listing step failed the v0.41.0 release, so -# every other channel ships from the tag and Zed ships from here, by hand. +# Implements [WITHDRAWAL-UNLIST] and [ZED-MIRROR]. # -# Why it still has to ship. Zed users are not reached by the CLI release: their -# extension downloads the binary itself, so once the final binary is inert their -# editor shows "language server failed to start" and never shows the statement. -# The final extension is what replaces that with the statement — it registers no -# language server at all and prints the notice under `/basilisk`. +# Basilisk is NOT in the Zed extension registry and never was. There is no +# `[basilisk]` block in zed-industries/extensions/extensions.toml, no +# extensions/basilisk submodule, and no commit in that repo has ever mentioned +# it — the `publish-zed` job was removed from release.yml after its registry +# step failed the v0.41.0 release, and it never landed before that. So there is +# nothing to bump and nothing to remove there, and opening a listing PR NOW +# would add Basilisk to a registry it was never in, in the middle of unlisting +# it. Do not do that. 06-unlist-zed.sh re-checks this and fails if it changes. # -# Two things happen here, in order: -# 1. push + tag the rendered tree to Nimblesite/basilisk-zed (the mirror) -# 2. open the PR bumping `basilisk` in zed-industries/extensions to that tag +# What IS public is the mirror, Nimblesite/basilisk-zed. Anyone can read it, and +# Zed installs a dev extension straight from a local clone of exactly this +# layout. Its `main` still serves the OLD extension: a [language_servers.basilisk] +# block that launches `basilisk lsp` — a command the inert CLI no longer has — +# and a description advertising diagnostics, autocomplete, refactoring, and +# profiling. That is a live product claim for a checker that was wrong. # -# Step 2 lands in someone else's review queue. Until it merges, Zed serves the -# previous version — so `06-unlist-zed.sh` (the removal PR) waits for it. +# This script replaces that tree with the notice-only extension, so the mirror +# says what every other surface says. 06-unlist-zed.sh then archives it. # # Needs: gh authenticated; push rights to Nimblesite/basilisk-zed; cargo with # the wasm32-wasip2 target (the push is gated on a real standalone build). # -# delist/00-publish-zed-final.sh v0.42.0 [--yes] +# delist/00-publish-zed-final.sh v0.41.2 [--yes] source "$(dirname "${BASH_SOURCE[0]}")/common.sh" VERSION="${1:-}" -[ -n "$VERSION" ] || fail "usage: 00-publish-zed-final.sh [--yes]" +[ -n "$VERSION" ] || fail "usage: 00-publish-zed-final.sh [--yes]" shift parse_args "$@" BARE="${VERSION#v}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REGISTRY_TOML="https://raw.githubusercontent.com/zed-industries/extensions/main/extensions.toml" -banner "Zed extension — Nimblesite/basilisk-zed + zed-industries/extensions" +banner "Zed mirror — Nimblesite/basilisk-zed" -require_cmd gh "the registry PR is opened through the GitHub API" +require_cmd gh "the mirror is pushed over an authenticated remote" require_cmd git "the mirror is pushed as a clone" require_cmd cargo "the push is gated on a standalone wasm build" +require_cmd curl "the registry is checked before anything is published" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT +# If a listing ever appears, every assumption above is void: publishing would +# then be updating a real listing, and the removal PR in 06 becomes necessary. +step "Confirm Basilisk is still absent from the Zed registry" +if curl -fsSL "$REGISTRY_TOML" | grep -q '^\[basilisk\]'; then + fail "zed-industries/extensions now lists basilisk — re-read 06-unlist-zed.sh before publishing" +fi +ok "no basilisk entry in the registry; this publishes to the mirror only" + step "Render the standalone tree at $BARE" "$REPO_ROOT/scripts/render-zed-mirror.sh" "$work/render" "$BARE" -# Gate the push on the same build the registry will run. A tree that does not -# compile standalone is a listing that fails on their CI, not ours. -step "Build it exactly as the registry will" +# Gate the push on a real build. A tree that does not compile standalone is a +# broken dev extension for anyone who clones the mirror. +step "Build it standalone" ( cd "$work/render" && cargo build --release --target wasm32-wasip2 ) ok "standalone wasm build passed" @@ -63,7 +76,7 @@ grep -q "Basilisk is unlisted" "$work/render/src/withdrawal_notice.txt" || fail "the rendered tree carries no withdrawal notice" ok "no language server, no debug adapter, no grammar; the notice is present" -if confirm "publish the final Zed extension and open the registry bump PR"; then +if confirm "replace the public Zed mirror with the notice-only extension"; then step "Push the mirror" # render-zed-mirror.sh replaces the clone's tracked content and preserves # its .git, so the mirror keeps its history rather than being force-reset. @@ -75,10 +88,6 @@ if confirm "publish the final Zed extension and open the registry bump PR"; then act git -C "$work/mirror" tag "$VERSION" act git -C "$work/mirror" push origin "$VERSION" - step "Open the registry bump PR" - act python3 "$REPO_ROOT/scripts/publish_zed_registry.py" "$BARE" "$VERSION" - - ok "mirror pushed and tagged $VERSION; bump PR opened" - warn "Zed still serves the PREVIOUS version until a maintainer merges that PR." - warn "Do not run 06-unlist-zed.sh until it is merged and live." + ok "mirror replaced and tagged $VERSION" + warn "No registry PR was opened, and none should be: Basilisk is not listed on Zed." fi diff --git a/delist/01-verify-final-release.sh b/delist/01-verify-final-release.sh index 2e3da392..e303d49d 100755 --- a/delist/01-verify-final-release.sh +++ b/delist/01-verify-final-release.sh @@ -71,24 +71,23 @@ nvim_tag="$(curl -fsSL "https://api.github.com/repos/Nimblesite/basilisk.nvim/ta check "Nimblesite/basilisk.nvim" "$nvim_tag" # Zed does not ship from the release workflow — delist/00-publish-zed-final.sh -# pushes the mirror by hand. Check the mirror tag here; the registry entry it -# points at only goes live once a Zed maintainer merges the bump PR, which is a -# separate wait and not a blocker for the other channels ([ZED-MIRROR]). +# pushes the mirror by hand, and the mirror is the whole Zed surface: Basilisk +# is not in the zed-industries registry and never was ([ZED-MIRROR]). step "Zed mirror tag" zed_tag="$(curl -fsSL "https://api.github.com/repos/Nimblesite/basilisk-zed/tags" | python3 -c 'import json,sys; print(json.load(sys.stdin)[0]["name"].lstrip("v"))' 2>/dev/null || echo "")" check "Nimblesite/basilisk-zed" "$zed_tag" +# Not a version check: an entry appearing here at all would mean Basilisk got +# listed on Zed during its unlisting, and both Zed scripts refuse to run. step "Zed registry entry" zed_listed="$(curl -fsSL "https://raw.githubusercontent.com/zed-industries/extensions/main/extensions.toml" | python3 -c 'import sys,tomllib; print(tomllib.loads(sys.stdin.read()).get("basilisk", {}).get("version", ""))' 2>/dev/null || echo "")" -if [ "$zed_listed" = "$BARE" ]; then - ok "Zed registry is at $BARE" -elif [ -z "$zed_listed" ]; then - warn "Zed registry lists no basilisk entry — nothing to unlist there" +if [ -z "$zed_listed" ]; then + ok "Zed registry lists no basilisk entry — as expected; nothing to unlist there" else - warn "Zed registry is still at '$zed_listed' — the bump PR has not merged yet." - warn "Do not run 06-unlist-zed.sh until it lands." + warn "Zed registry now lists basilisk at '$zed_listed' — it was never listed before." + warn "Re-read delist/06-unlist-zed.sh: a removal PR is needed after all." fi echo diff --git a/delist/06-unlist-zed.sh b/delist/06-unlist-zed.sh index e2b959cc..cd08d906 100755 --- a/delist/06-unlist-zed.sh +++ b/delist/06-unlist-zed.sh @@ -1,49 +1,57 @@ #!/usr/bin/env bash -# Open the PR that removes Basilisk from the Zed extension registry. +# Archive the Zed extension mirror. # -# Implements [WITHDRAWAL-UNLIST]. The Zed registry is zed-industries/extensions, -# a repo we do not own: the entry is a `[basilisk]` block in extensions.toml -# plus a git submodule. Removing it is a pull request, so this script prepares -# and opens that PR — a human on their side merges it. +# Implements [WITHDRAWAL-UNLIST] and [ZED-MIRROR]. # -# Needs: gh, authenticated; a fork of zed-industries/extensions is created if -# one does not exist. +# This script used to open a PR removing `basilisk` from zed-industries/extensions. +# There is nothing there to remove. Basilisk is not in the Zed registry and never +# was: no `[basilisk]` block in extensions.toml, no extensions/basilisk submodule, +# and no commit in that repo has ever mentioned it. The `publish-zed` job was +# removed from release.yml after its registry step failed the v0.41.0 release, +# and it had not landed before that. A removal PR would ask a maintainer of +# someone else's repo to delete an entry that does not exist. +# +# The mirror, Nimblesite/basilisk-zed, IS the listing: it is public, and Zed +# installs a dev extension straight from a clone of that layout. 00 replaces its +# contents with the notice-only extension; this archives it. Archived rather +# than deleted, for the same reason as the Neovim mirror — deleting breaks every +# pinned clone and erases the record, while archiving is read-only and visibly +# dead. +# +# Needs: gh, authenticated with admin access to Nimblesite/basilisk-zed. # # delist/06-unlist-zed.sh [--yes] source "$(dirname "${BASH_SOURCE[0]}")/common.sh" parse_args "$@" -banner "Zed extension registry — zed-industries/extensions" +banner "Zed extension mirror — Nimblesite/basilisk-zed" -require_cmd gh "the PR is opened through the GitHub API" -require_cmd git "the registry is edited as a clone" +require_cmd gh "the repo is edited through the GitHub API" +require_cmd curl "the registry is re-checked before archiving" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT -branch="remove-basilisk" +REGISTRY_TOML="https://raw.githubusercontent.com/zed-industries/extensions/main/extensions.toml" -body="Please remove the \`basilisk\` extension from the registry. - -Basilisk's type checker was producing incorrect results. We asked for it to be -removed from the python/typing conformance results, and it has been removed -(https://github.com/python/typing/pull/2330). The code responsible is not -isolated to a known set of rules, so we cannot say how many rules are affected. -A code-quality tool that does not produce correct results is worse than useless, -so Basilisk is being unlisted from every distribution channel and its CLI is -inert — the extension can no longer start a language server. +# Re-checked rather than assumed. If a listing ever appears, archiving the +# mirror strands it — the registry entry points at a submodule of this repo — +# and a removal PR becomes the right move after all. +step "Confirm Basilisk is absent from the Zed registry" +if curl -fsSL "$REGISTRY_TOML" | grep -q '^\[basilisk\]'; then + fail "zed-industries/extensions now lists basilisk — open a removal PR there BEFORE archiving the mirror" +fi +ok "no basilisk entry in the registry — nothing to remove there" -Full statement: https://www.basilisk-python.dev/" +step "Confirm the mirror carries the statement" +manifest="$(curl -fsSL https://raw.githubusercontent.com/Nimblesite/basilisk-zed/main/extension.toml 2>/dev/null || echo "")" +case "$manifest" in + *"[language_servers"*) warn "the mirror still declares a language server — run 00-publish-zed-final.sh first" ;; + *"unlisted"*) ok "the mirror manifest already carries the statement" ;; + *) warn "could not read the mirror manifest — check it by hand before archiving" ;; +esac -if confirm "open a PR removing basilisk from zed-industries/extensions"; then - act gh repo fork zed-industries/extensions --clone=false --remote=false - act gh repo clone zed-industries/extensions "$work/extensions" -- --depth 1 - act git -C "$work/extensions" checkout -b "$branch" - act git -C "$work/extensions" submodule deinit -f extensions/basilisk - act git -C "$work/extensions" rm -f extensions/basilisk - act python3 "$(dirname "${BASH_SOURCE[0]}")/remove_registry_entry.py" "$work/extensions/extensions.toml" basilisk - act git -C "$work/extensions" commit -am "Remove basilisk" - act git -C "$work/extensions" push --set-upstream "$(gh api user --jq .login)" "$branch" - act gh pr create --repo zed-industries/extensions \ - --title "Remove basilisk" --body "$body" --head "$branch" - ok "PR opened — track it until merged, then confirm the extension is gone from Zed's registry" +if confirm "archive Nimblesite/basilisk-zed (read-only, permanent-ish)"; then + act gh repo edit Nimblesite/basilisk-zed \ + --description "Basilisk's type checker produced incorrect results. Basilisk is unlisted and is being rebuilt from the ground up as a new product." \ + --homepage "https://www.basilisk-python.dev" + act gh repo archive Nimblesite/basilisk-zed --yes + ok "archived — confirm at https://github.com/Nimblesite/basilisk-zed" fi diff --git a/delist/README.md b/delist/README.md index 3870fea6..0f0a1d43 100644 --- a/delist/README.md +++ b/delist/README.md @@ -16,13 +16,13 @@ Every script is **dry run by default** and prints what it would do. Pass `--yes` | # | Script | Does | |---|---|---| -| 0 | `00-publish-zed-final.sh v0.42.0` | Publishes the final Zed extension: pushes and tags the mirror, then opens the registry bump PR. **Zed is the one channel the Release workflow does not publish** — its `publish-zed` job was removed after it failed the v0.41.0 release, so Zed ships from here, by hand, right after the tag. | -| 1 | `01-verify-final-release.sh v0.42.0` | Read-only. Asserts every channel is serving the final version. **Nothing below runs until this passes.** | +| 0 | `00-publish-zed-final.sh v0.41.2` | Replaces the public Zed mirror with the notice-only extension. **Zed is the one channel the Release workflow does not publish** — its `publish-zed` job was removed after it failed the v0.41.0 release. It opens no registry PR: see the Zed note below. | +| 1 | `01-verify-final-release.sh v0.41.2` | Read-only. Asserts every channel is serving the final version. **Nothing below runs until this passes.** | | 2 | `02-unlist-marketplace.sh` | `vsce unpublish` removes the extension from the VS Code Marketplace. Needs `VSCE_PAT`. | | 3 | `03-unlist-homebrew.sh` | Deletes `Formula/basilisk.rb` from `Nimblesite/homebrew-tap`. Needs `gh`. | | 4 | `04-unlist-scoop.sh` | Deletes `bucket/basilisk.json` from `Nimblesite/scoop-bucket`. Needs `gh`. | | 5 | `05-unlist-nvim-mirror.sh` | Archives `Nimblesite/basilisk.nvim` (read-only, not deleted). Needs `gh`. | -| 6 | `06-unlist-zed.sh` | Opens the PR removing `basilisk` from `zed-industries/extensions`. Needs `gh`. | +| 6 | `06-unlist-zed.sh` | Archives `Nimblesite/basilisk-zed` (read-only, not deleted). Needs `gh`. | | 7 | `07-unlist-github-repo.sh` | Rewrites the repo description/topics and disables the Release workflow. Needs `gh`. | | 8 | `08-verify-unlisted.sh` | Read-only. Asks each channel's public API what it still serves. Run after, and again a day later. | @@ -35,8 +35,7 @@ These have no API that a token can drive, or they end in someone else's review q | **PyPI — `basilisk-python`** | **Yank every release** (Manage project → Releases → each version → Options → Yank). Yank, do not delete: deleting breaks existing pinned lockfiles and destroys the record, while yanking removes the release from resolution so no new install picks it up. | https://pypi.org/manage/project/basilisk-python/releases/ | `08-verify-unlisted.sh` reports every release yanked | | **PyPI — project description** | The project page stays, so its description must be the statement. It is set by the wheel metadata, so this is already correct if the final release published — check the rendered page. | https://pypi.org/project/basilisk-python/ | The page opens with "Basilisk is unlisted" | | **Open VSX** | There is no unpublish in the `ovsx` CLI and no public API for it. Open an issue asking the Eclipse Foundation to remove `Nimblesite.basilisk`, stating that the extension is withdrawn; link the statement. | https://github.com/EclipseFdn/open-vsx.org/issues | The extension 404s at https://open-vsx.org/extension/Nimblesite/basilisk | -| **Zed registry — final version** | Script 0 opens the *bump* PR. Until a maintainer merges it, Zed serves the previous version, whose extension launches a language server that no longer exists — so a Zed user sees "server failed to start", not the statement. Chase it. | https://github.com/zed-industries/extensions/pulls | `extensions.toml` lists `basilisk` at the final version | -| **Zed registry — removal** | Script 6 opens the *removal* PR; a Zed maintainer merges it. Open it only after the bump above is merged and live, or you are asking one reviewer to merge two contradictory PRs. | https://github.com/zed-industries/extensions/pulls | The `basilisk` entry is gone from `extensions.toml` | +| **Zed registry — nothing to do** | **Do not open a PR against `zed-industries/extensions`.** Basilisk is not listed there and never was: no `[basilisk]` block in `extensions.toml`, no `extensions/basilisk` submodule, no commit in its history mentioning it. Listing it now would add Basilisk to a registry it was never in, while unlisting it everywhere else. Scripts 0 and 6 both re-check and refuse to run if an entry ever appears. | — | Confirmed absent — re-checked by scripts 0 and 6 | | **VS Code Marketplace publisher** | If `Nimblesite` publishes nothing else, remove the publisher's marketing profile text too — the publisher page survives the extension's removal. | https://marketplace.visualstudio.com/manage/publishers/Nimblesite | The publisher page lists no Basilisk | | **GitHub Release workflow secrets** | Revoke `VSCODE_MARKETPLACE_PAT`, `OPEN_VSX_PAT` and `BREW_SCOOP_PAT` once unlisting is done. A disabled workflow plus live publish tokens is one re-enable away from republishing. | Org Settings → Secrets and variables → Actions | The three secrets are deleted | | **PyPI Trusted Publisher** | Remove the `pypi` trusted publisher for `Nimblesite/Basilisk` / `release.yml`, for the same reason. | https://pypi.org/manage/project/basilisk-python/settings/publishing/ | No publisher listed | diff --git a/delist/remove_registry_entry.py b/delist/remove_registry_entry.py deleted file mode 100755 index e0ce2966..00000000 --- a/delist/remove_registry_entry.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -"""Delete one `[name]` block from a Zed registry `extensions.toml`. - -Implements [WITHDRAWAL-UNLIST]. The registry file is a flat list of -`[extension-id]` blocks in a repository we do not own, so the edit must be -surgical: remove exactly the named block and leave every other byte — ordering, -spacing, comments — untouched, or the removal PR arrives full of unrelated diff. - - delist/remove_registry_entry.py path/to/extensions.toml basilisk -""" - -from __future__ import annotations - -import sys -from pathlib import Path - - -def without_block(text: str, name: str) -> str: - """`text` with the `[name]` block and its trailing blank line removed.""" - header = f"[{name}]" - lines = text.splitlines(keepends=True) - kept: list[str] = [] - dropping = False - for line in lines: - if line.strip() == header: - dropping = True - continue - if dropping: - # The block ends at the next header, or at the blank line before it. - if line.startswith("["): - dropping = False - elif not line.strip(): - dropping = False - continue - else: - continue - kept.append(line) - return "".join(kept) - - -def main(argv: list[str]) -> int: - if len(argv) != 3: - print(__doc__, file=sys.stderr) - return 2 - path, name = Path(argv[1]), argv[2] - text = path.read_text(encoding="utf-8") - if f"[{name}]" not in text: - print(f"{path}: no [{name}] entry — already removed", file=sys.stderr) - return 0 - path.write_text(without_block(text, name), encoding="utf-8") - print(f"removed [{name}] from {path}") - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md b/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md index 8f2bfb52..55465f36 100644 --- a/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md +++ b/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md @@ -1,5 +1,14 @@ # Basilisk Roadmap {#NEXTSTEPS-ROADMAP} +> **Superseded. Kept as the record, not as a plan.** Basilisk is unlisted and its +> CLI is inert ([WITHDRAWAL](../specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md#WITHDRAWAL)). +> Nothing below is scheduled and nothing below authorises work. Two items are +> flatly wrong now and are called out because they would cause harm if followed: +> the checker is not "under audit" — it is finished and being deleted; and the +> Zed mirror push is **not** "safe to list" — Basilisk is not in the Zed registry +> and must not be added to it ([ZED-MIRROR](../specs/ZED-SPEC.md#ZED-MIRROR)). +> `scripts/publish_zed_registry.py`, referenced below, is deleted. + This file contains only cross-cutting work that does not belong to a focused implementation plan. Specs remain authoritative for behavior; focused plans own the engineering detail. diff --git a/docs/specs/ZED-SPEC.md b/docs/specs/ZED-SPEC.md index f32b7edc..423eedab 100644 --- a/docs/specs/ZED-SPEC.md +++ b/docs/specs/ZED-SPEC.md @@ -157,11 +157,9 @@ Zed has no upload API. Extensions are listed in [`zed-industries/extensions`](ht The mirror version equals the monorepo tag (`v1.2.3` → `1.2.3`). -**This publishes one final version, and then the listing is removed.** The order is fixed by [WITHDRAWAL-UNLIST](DOCS-WITHDRAWAL-MESSAGING-SPEC.md#WITHDRAWAL-UNLIST): publish the final version → verify it is live → unlist. Unlisting alone would leave every existing install on the last feature release, never showing the statement. The removal PR against `zed-industries/extensions` is `delist/06-unlist-zed.sh`. +**Basilisk was never listed on Zed, so there is no listing to update or remove.** `zed-industries/extensions` has no `[basilisk]` block in `extensions.toml`, no `extensions/basilisk` submodule, and no commit in its history mentions Basilisk; the only related PR is [#4871](https://github.com/zed-industries/extensions/pull/4871), for a differently-named extension, closed unmerged. The `publish-zed` job was removed from `release.yml` after its registry step failed the v0.41.0 release, and the listing had not landed before that. Opening a listing PR now would **add** Basilisk to a registry it was never in, in the middle of unlisting it — so `scripts/publish_zed_registry.py` and its test are deleted rather than kept as dead code, and `delist/06-unlist-zed.sh` opens no removal PR. -**Pushing the mirror publishes nothing.** Zed installs only what `zed-industries/extensions` lists, so the mirror push is a prerequisite, not the release. `scripts/publish_zed_registry.py`, run by `publish-zed` immediately after the mirror is tagged, performs the listing itself: it forks the registry, resets a `listing-basilisk` branch to upstream's head, adds or re-pins the `extensions/basilisk` submodule to the release tag, sets `[basilisk] version` in `extensions.toml`, re-sorts `.gitmodules`, and opens the PR — or, once that PR is open, moves the pointer on the same branch. Every release therefore proposes its own bump; upstream maintainers still merge it. - -The registry is a repository Basilisk does not own, and its `extensions.toml` holds ~1400 entries, so the edit is **surgical, not a rewrite**: the entry is spliced into alphabetical position and every other entry stays byte-identical, since a reformatting diff across someone else's registry is a rejected PR. `scripts/test_publish_zed_registry.py` proves both properties — placement, non-disturbance, bump-not-duplicate, and idempotence — in the `zed` CI job, because the real thing runs only during a tagged release. +**The mirror is therefore the whole Zed surface.** [`Nimblesite/basilisk-zed`](https://github.com/Nimblesite/basilisk-zed) is public, and Zed installs a dev extension straight from a clone of exactly that layout — so what it serves is what a Zed user gets. `delist/00-publish-zed-final.sh` replaces its contents with the notice-only extension (gated on a real standalone wasm build and on the rendered manifest declaring no server, adapter or grammar), and `delist/06-unlist-zed.sh` archives it afterwards, read-only rather than deleted. Both scripts re-check the registry first and refuse to run if a `[basilisk]` entry has appeared, because that would make every assumption in this section false. ## Record of what was built {#ZED-FEATURES} diff --git a/scripts/publish_zed_registry.py b/scripts/publish_zed_registry.py deleted file mode 100755 index acd4186a..00000000 --- a/scripts/publish_zed_registry.py +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python3 -"""Submit or bump Basilisk's listing in `zed-industries/extensions`. - -Implements [ZED-MIRROR]; see docs/specs/ZED-SPEC.md#ZED-MIRROR. - -Zed has no upload API. Pushing the rendered tree to `Nimblesite/basilisk-zed` -publishes nothing on its own — the extension only becomes installable once -`zed-industries/extensions` lists it, as a git submodule pinned to a commit plus -an `extensions.toml` entry naming the version. That listing step used to be a -manual to-do that was never done, which is why Basilisk has never appeared in -Zed's extensions view. This script performs it, and is safe to re-run: the first -release opens the listing PR, every later release moves the submodule pointer -and the version on the same branch. - -Usage: - scripts/publish_zed_registry.py - scripts/publish_zed_registry.py 0.41.0 v0.41.0 - -Requires `gh` authenticated as a token that can fork into UPSTREAM_FORK's owner -and push to that fork. -""" - -from __future__ import annotations - -import os -import subprocess -import sys -import tempfile -import tomllib -from pathlib import Path - -UPSTREAM = "zed-industries/extensions" -FORK = "Nimblesite/extensions" -MIRROR_URL = "https://github.com/Nimblesite/basilisk-zed.git" -EXTENSION_ID = "basilisk" -SUBMODULE_PATH = f"extensions/{EXTENSION_ID}" -REGISTRY_TOML = "extensions.toml" -GITMODULES = ".gitmodules" - - -def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> str: - """Run `cmd`, echoing it, and return stdout.""" - print(f" $ {' '.join(cmd)}", flush=True) - done = subprocess.run(cmd, cwd=cwd, check=check, capture_output=True, text=True) - if done.stdout: - print(done.stdout.rstrip(), flush=True) - if done.returncode != 0 and done.stderr: - print(done.stderr.rstrip(), file=sys.stderr, flush=True) - return done.stdout - - -def blocks(text: str, opener: str) -> list[tuple[str, list[str]]]: - """Split a flat TOML-ish file into `(header, lines)` blocks. - - `extensions.toml` and `.gitmodules` are both a preamble followed by a flat - run of sections, each introduced by a line starting with `opener`. Splitting - on those headers lets us edit one section without reformatting the other - ~2000, which a whole-document rewrite would do. The result is parse-verified - by `tomllib` in `write_registry` before anything is committed. - """ - out: list[tuple[str, list[str]]] = [] - header, current = "", [] - for line in text.splitlines(keepends=True): - if line.startswith(opener): - if header or current: - out.append((header, current)) - header, current = line.strip(), [line] - else: - current.append(line) - if header or current: - out.append((header, current)) - return out - - -def sort_key(header: str) -> str: - """The section's sort key, matching upstream's `pnpm sort-extensions`.""" - return header.strip("[]").strip('"').casefold() - - -def splice(text: str, opener: str, header: str, body: list[str]) -> str: - """Replace the `header` section in `text`, or insert it in sorted order.""" - sections = blocks(text, opener) - kept = [s for s in sections if s[0] != header] - entry = (header, body) - at = len(kept) - for index, (existing, _) in enumerate(kept): - if existing.startswith(opener) and sort_key(existing) > sort_key(header): - at = index - break - kept.insert(at, entry) - return "".join("".join(lines) for _, lines in kept) - - -def write_registry(repo: Path, version: str) -> None: - """Point `extensions.toml`'s `[basilisk]` entry at `version`.""" - path = repo / REGISTRY_TOML - body = [ - f"[{EXTENSION_ID}]\n", - f'submodule = "{SUBMODULE_PATH}"\n', - f'version = "{version}"\n', - "\n", - ] - updated = splice(path.read_text(encoding="utf-8"), "[", f"[{EXTENSION_ID}]", body) - path.write_text(updated, encoding="utf-8") - - listed = tomllib.loads(updated).get(EXTENSION_ID) - if listed != {"submodule": SUBMODULE_PATH, "version": version}: - raise SystemExit(f"✗ {REGISTRY_TOML} did not round-trip: {listed!r}") - print(f" {REGISTRY_TOML}: [{EXTENSION_ID}] version = {version}") - - -def write_gitmodules(repo: Path) -> None: - """Re-sort `.gitmodules` so `git submodule add`'s append stays ordered.""" - path = repo / GITMODULES - text = path.read_text(encoding="utf-8") - sections = blocks(text, "[submodule ") - preamble = [s for s in sections if not s[0].startswith("[submodule ")] - entries = sorted( - (s for s in sections if s[0].startswith("[submodule ")), - key=lambda s: sort_key(s[0].removeprefix("[submodule ")), - ) - ordered = "".join("".join(lines) for _, lines in [*preamble, *entries]) - if ordered != text: - path.write_text(ordered, encoding="utf-8") - print(f" {GITMODULES}: re-sorted") - - -def clone_fork(work: Path) -> Path: - """Fork upstream if needed, then clone the fork reset to upstream's head.""" - run(["gh", "repo", "fork", UPSTREAM, "--clone=false", "--remote=false"]) - repo = work / "extensions" - run(["gh", "repo", "clone", FORK, str(repo), "--", "--depth=50"]) - run( - ["git", "remote", "add", "upstream", f"https://github.com/{UPSTREAM}.git"], repo - ) - run(["git", "fetch", "--depth=50", "upstream", "HEAD"], repo) - run(["git", "checkout", "-B", f"listing-{EXTENSION_ID}", "FETCH_HEAD"], repo) - return repo - - -def pin_submodule(repo: Path, tag: str) -> None: - """Add the mirror submodule if absent, then pin it to `tag`.""" - module = repo / SUBMODULE_PATH - if not (module / ".git").exists(): - run(["git", "submodule", "add", "--force", MIRROR_URL, SUBMODULE_PATH], repo) - run(["git", "fetch", "--tags", "origin"], module) - run(["git", "checkout", f"tags/{tag}"], module) - - -def commit_and_push(repo: Path, version: str) -> bool: - """Commit the listing change and push the branch. False if nothing changed.""" - run(["git", "config", "user.name", "github-actions[bot]"], repo) - email = "41898282+github-actions[bot]@users.noreply.github.com" - run(["git", "config", "user.email", email], repo) - run(["git", "add", "-A"], repo) - if not run(["git", "status", "--porcelain"], repo).strip(): - print(f" {UPSTREAM} already lists {EXTENSION_ID} {version}") - return False - run(["git", "commit", "-m", f"{EXTENSION_ID}: {version}"], repo) - run( - ["git", "push", "--force-with-lease", "origin", f"listing-{EXTENSION_ID}"], repo - ) - return True - - -def open_pr(repo: Path, version: str) -> None: - """Open the listing PR, unless one is already open for this branch.""" - head = f"{FORK.split('/')[0]}:listing-{EXTENSION_ID}" - existing = run( - ["gh", "pr", "list", "--repo", UPSTREAM, "--head", head, "--json", "url"], - check=False, - ) - if '"url"' in existing: - print(f" PR already open for {head} — pointer updated in place") - return - body = ( - f"Adds the Basilisk language-server extension at {version}.\n\n" - f"Submodule: {MIRROR_URL} (pinned to the release tag).\n" - "The extension attaches to Zed's built-in Python language and ships no " - "`languages/` tree or grammar, so it does not shadow the built-in " - "definition.\n\nRefs https://github.com/Nimblesite/Basilisk\n" - ) - create = ["gh", "pr", "create", "--repo", UPSTREAM, "--head", head] - create += ["--title", f"Add {EXTENSION_ID} {version}", "--body", body] - run(create, repo, check=False) - - -def main(argv: list[str]) -> int: - if len(argv) != 2: - print(__doc__, file=sys.stderr) - return 2 - version, tag = argv - if not os.environ.get("GH_TOKEN") and not os.environ.get("GITHUB_TOKEN"): - print( - "✗ GH_TOKEN not set — cannot fork or open the listing PR", file=sys.stderr - ) - return 1 - - with tempfile.TemporaryDirectory() as tmp: - repo = clone_fork(Path(tmp)) - pin_submodule(repo, tag) - write_registry(repo, version) - write_gitmodules(repo) - if commit_and_push(repo, version): - open_pr(repo, version) - print(f" {EXTENSION_ID} {version} submitted to {UPSTREAM}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/test_publish_zed_registry.py b/scripts/test_publish_zed_registry.py deleted file mode 100644 index 4f93be3a..00000000 --- a/scripts/test_publish_zed_registry.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Tests for `scripts/publish_zed_registry.py` — the Zed registry listing edit. - -Covers [ZED-MIRROR] (docs/specs/ZED-SPEC.md#ZED-MIRROR). The listing edit runs -against a ~1400-entry third-party file that Basilisk does not own, so the two -properties worth proving are that the Basilisk entry lands correctly *and* that -nothing else in the file moves — a reformatting diff across someone else's -registry is a rejected PR. Network-free by construction: only the pure -text-editing functions are exercised. -""" - -from __future__ import annotations - -import sys -import tomllib -from pathlib import Path - -import pytest - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from publish_zed_registry import ( # noqa: E402 - EXTENSION_ID, - SUBMODULE_PATH, - blocks, - sort_key, - splice, - write_gitmodules, - write_registry, -) - -REGISTRY = """\ -[aardvark] -submodule = "extensions/aardvark" -version = "1.0.0" - -[basher] -submodule = "extensions/basher" -version = "0.2.1" - -[batman] -submodule = "extensions/batman" -version = "3.0.0" - -[zig] -submodule = "extensions/zig" -version = "0.9.9" -""" - - -def entry(version: str) -> list[str]: - return [ - f"[{EXTENSION_ID}]\n", - f'submodule = "{SUBMODULE_PATH}"\n', - f'version = "{version}"\n', - "\n", - ] - - -def keys(text: str) -> list[str]: - return [h.strip("[]") for h, _ in blocks(text, "[") if h.startswith("[")] - - -def test_inserts_in_alphabetical_position() -> None: - out = splice(REGISTRY, "[", f"[{EXTENSION_ID}]", entry("0.41.0")) - assert keys(out) == ["aardvark", "basher", "basilisk", "batman", "zig"] - - -def test_leaves_every_other_entry_byte_identical() -> None: - out = splice(REGISTRY, "[", f"[{EXTENSION_ID}]", entry("0.41.0")) - before = tomllib.loads(REGISTRY) - after = {k: v for k, v in tomllib.loads(out).items() if k != EXTENSION_ID} - assert after == before - for name in before: - assert f'[{name}]\nsubmodule = "extensions/{name}"\n' in out - - -def test_bump_replaces_rather_than_duplicates() -> None: - first = splice(REGISTRY, "[", f"[{EXTENSION_ID}]", entry("0.41.0")) - second = splice(first, "[", f"[{EXTENSION_ID}]", entry("0.42.0")) - assert keys(second).count(EXTENSION_ID) == 1 - assert tomllib.loads(second)[EXTENSION_ID]["version"] == "0.42.0" - assert keys(second) == keys(first) - - -def test_splice_is_idempotent() -> None: - once = splice(REGISTRY, "[", f"[{EXTENSION_ID}]", entry("0.41.0")) - assert splice(once, "[", f"[{EXTENSION_ID}]", entry("0.41.0")) == once - - -def test_sort_key_is_case_insensitive_and_unquoted() -> None: - assert sort_key("[Basilisk]") == "basilisk" - # `.gitmodules` headers are keyed on the path, after the caller strips the - # `[submodule ` prefix — the quotes must not survive into the sort order. - header = '[submodule "extensions/Zig"]' - assert sort_key(header.removeprefix("[submodule ")) == "extensions/zig" - - -def test_write_registry_pins_the_version(tmp_path: Path) -> None: - (tmp_path / "extensions.toml").write_text(REGISTRY, encoding="utf-8") - write_registry(tmp_path, "0.41.0") - listed = tomllib.loads((tmp_path / "extensions.toml").read_text(encoding="utf-8")) - assert listed[EXTENSION_ID] == {"submodule": SUBMODULE_PATH, "version": "0.41.0"} - - -def test_write_registry_rejects_a_corrupt_result(tmp_path: Path) -> None: - (tmp_path / "extensions.toml").write_text("[aardvark\nbroken", encoding="utf-8") - with pytest.raises((SystemExit, tomllib.TOMLDecodeError)): - write_registry(tmp_path, "0.41.0") - - -def test_write_gitmodules_sorts_appended_submodules(tmp_path: Path) -> None: - unsorted = ( - '[submodule "extensions/aardvark"]\n' - "\tpath = extensions/aardvark\n" - '[submodule "extensions/zig"]\n' - "\tpath = extensions/zig\n" - '[submodule "extensions/basilisk"]\n' - "\tpath = extensions/basilisk\n" - ) - (tmp_path / ".gitmodules").write_text(unsorted, encoding="utf-8") - write_gitmodules(tmp_path) - ordered = (tmp_path / ".gitmodules").read_text(encoding="utf-8") - assert ordered.index("aardvark") < ordered.index("basilisk") < ordered.index("zig") - assert ordered.count("[submodule ") == 3 - - -def test_write_gitmodules_is_idempotent(tmp_path: Path) -> None: - path = tmp_path / ".gitmodules" - path.write_text('[submodule "extensions/a"]\n\tpath = extensions/a\n', "utf-8") - write_gitmodules(tmp_path) - once = path.read_text(encoding="utf-8") - write_gitmodules(tmp_path) - assert path.read_text(encoding="utf-8") == once From 6c7539c62ef6a66d5b579201dbab47e5a133f0b8 Mon Sep 17 00:00:00 2001 From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:56:45 +1000 Subject: [PATCH 2/2] Make the unlisting verification able to fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 08-verify-unlisted.sh checked the Zed registry by fetching extensions/basilisk/extension.toml from the registry's raw host. Registry entries are git SUBMODULES, so the parent repo serves no files under extensions// and that path 404s for every extension in the registry. Checked against controls: `ty` and `pyrefly` are both listed in extensions.toml and both return 404 on that path. The check reported "gone" for listed extensions and could not have failed. It now reads extensions.toml, the file that actually holds the listing — the same question 00 and 06 ask before they run. Two channels had no check at all. The Zed and Neovim mirrors ARE listings: both repos are public, Zed installs a dev extension from a clone of the mirror layout, and plugin managers install straight from basilisk.nvim. Unlisting them means archiving them, so 08 now asks the GitHub API whether each is archived and fails while either is still writable. The Marketplace check is left alone and its reasoning recorded: it is a real signal, verified against ms-python.python and rust-lang.rust-analyzer, which both return 200 while an unpublished extension returns 404. The gallery extensionquery API is the wrong thing to ask — it keeps answering for an unpublished extension, listing `unpublished` among its flags. --- delist/08-verify-unlisted.sh | 47 +++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/delist/08-verify-unlisted.sh b/delist/08-verify-unlisted.sh index 47dc378e..2093547c 100755 --- a/delist/08-verify-unlisted.sh +++ b/delist/08-verify-unlisted.sh @@ -32,11 +32,56 @@ gone() { } step "Channels that must 404" +# The Marketplace item page is a real signal, checked against controls: it +# returns 200 for live extensions (ms-python.python, rust-lang.rust-analyzer) +# and 404 once an extension is unpublished. The gallery `extensionquery` API +# keeps answering for an unpublished extension — with `unpublished` among its +# flags — so the API is the wrong thing to ask here. gone "VS Code Marketplace" "https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk" gone "Open VSX" "https://open-vsx.org/api/Nimblesite/basilisk" gone "Homebrew formula" "https://raw.githubusercontent.com/Nimblesite/homebrew-tap/main/Formula/basilisk.rb" gone "Scoop manifest" "https://raw.githubusercontent.com/Nimblesite/scoop-bucket/main/bucket/basilisk.json" -gone "Zed registry entry" "https://raw.githubusercontent.com/zed-industries/extensions/main/extensions/basilisk/extension.toml" + +# Zed is NOT a `gone` URL check. Registry entries are git SUBMODULES, so the +# parent repo serves no files under extensions// and that path 404s for +# every extension in the registry — checking it reported "gone" for `ty` and +# `pyrefly`, which are both listed. A check that cannot fail is worse than no +# check. Ask the file that actually holds the listing ([ZED-MIRROR]). +step "Zed registry entry" +if curl -fsSL "https://raw.githubusercontent.com/zed-industries/extensions/main/extensions.toml" | + grep -q '^\[basilisk\]'; then + printf "%b✗ Zed registry: STILL LISTED — a [basilisk] entry exists in extensions.toml%b\n" "$RED" "$RESET" + still_listed=$((still_listed + 1)) +else + ok "Zed registry: no [basilisk] entry (it was never listed there)" +fi + +# The mirror IS the Zed listing — public, and Zed installs a dev extension from +# a clone of it. Archived, not deleted, so it must still resolve. +step "Zed mirror archived" +zed_archived="$(curl -fsSL "https://api.github.com/repos/Nimblesite/basilisk-zed" | + python3 -c 'import json,sys; print(json.load(sys.stdin).get("archived"))' 2>/dev/null || echo "unreachable")" +case "$zed_archived" in + True) ok "Nimblesite/basilisk-zed is archived (read-only)" ;; + False) + printf "%b✗ Nimblesite/basilisk-zed is NOT archived — run 06-unlist-zed.sh%b\n" "$RED" "$RESET" + still_listed=$((still_listed + 1)) + ;; + *) warn "could not read Nimblesite/basilisk-zed — check by hand" ;; +esac + +# Same for the Neovim mirror: plugin managers install straight from the repo. +step "Neovim mirror archived" +nvim_archived="$(curl -fsSL "https://api.github.com/repos/Nimblesite/basilisk.nvim" | + python3 -c 'import json,sys; print(json.load(sys.stdin).get("archived"))' 2>/dev/null || echo "unreachable")" +case "$nvim_archived" in + True) ok "Nimblesite/basilisk.nvim is archived (read-only)" ;; + False) + printf "%b✗ Nimblesite/basilisk.nvim is NOT archived — run 05-unlist-nvim-mirror.sh%b\n" "$RED" "$RESET" + still_listed=$((still_listed + 1)) + ;; + *) warn "could not read Nimblesite/basilisk.nvim — check by hand" ;; +esac step "PyPI — yanked, not deleted" # Yanking keeps the files installable by exact pin (so existing lockfiles do not