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/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 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