diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index c6d51f2..29499e3 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -1,23 +1,23 @@ --- name: release -description: Prepare and publish a submarine release. Finds the latest release, picks the next semver from the changes since, runs a release check, creates and pushes the git tag, hands over to the developer for `make release`, then creates the Gitea release with MCP. Use only when the user asks to release. +description: Prepare and publish a submarine release. Finds the latest release, picks the next semver from the changes since, runs a release check, creates and pushes the git tag, writes the release notes, hands over to the developer for `make release`, then verifies the GitHub release. Use only when the user asks to release. disable-model-invocation: true --- # Release submarine A release is built on the developer's machine (`make release`, see -`docs/architecture.md#distribution`). `make release` publishes the binaries -of the **clean `vX.Y.Z` tag at HEAD** to the Gitea package registry. This -skill does everything around it. The developer runs `make release` -themselves, in the middle: you never run it and never read or print `.env` -or `GITEA_TOKEN`. +`docs/architecture.md#distribution`). `make release` creates the GitHub +release of the **clean `vX.Y.Z` tag at HEAD**, with the binaries as assets. +This skill does everything around it. The developer runs `make release` +themselves, in the middle: you never run it and never read or print `.env`, +`GH_TOKEN` or `GITEA_TOKEN`. Pushing a tag and creating a release are outward-facing and hard to undo. Do them only after the confirmation in step 5, and never force-push, move or delete a tag without asking. -Gitea: owner `simonfranken`, repo `submarine`, `https://git.simon-franken.de`. +GitHub: owner `simonfranken`, repo `submarine`, remote `origin`. ## 1. Preconditions @@ -29,7 +29,7 @@ git branch --show-current # main git status --porcelain # empty: make release refuses a dirty tree git rev-parse HEAD origin/main # equal: HEAD is pushed, nothing to release from a local-only commit git tag --points-at HEAD # empty: HEAD is not released yet -grep -q '^GITEA_TOKEN=.\+' .env # exists: only test it, never print it +gh auth status >/dev/null 2>&1 || grep -q '^GH_TOKEN=.\+' .env # one holds: only test, never print ``` ## 2. Latest release @@ -38,11 +38,12 @@ grep -q '^GITEA_TOKEN=.\+' .env # exists: only test it, never print i git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1 ``` -Cross-check with the `mcp__gitea__get_latest_release` MCP tool (load it with -ToolSearch first). If the tag and the Gitea release differ (a tag without a +Cross-check with the `mcp__github__get_latest_release` MCP tool (load it with +ToolSearch first). If the tag and the GitHub release differ (a tag without a release is normal for the one in progress, a release without a tag is not), tell the user and ask before going on. With no tag at all, the first release -is `v0.1.0`. +is `v0.1.0`. While the Makefile has `release-gitea` (step 10), GitHub has +no releases yet: cross-check with `mcp__gitea__get_latest_release` instead. ## 3. Changes since @@ -55,7 +56,7 @@ No commits since the last tag: stop, there is nothing to release. Read the commits, and where a subject is not clear, the diff. Sort each change as: - **breaking**: removes or changes a key, a command or flag, the on-disk or - registry contract (`install.sh`, asset names, `submarine-latest/latest/VERSION`, + release contract (`install.sh`, asset names, `latest/download/VERSION`, `submarine update`), or behavior users rely on; - **feature**: a new key, action, view or option, or a visible behavior; - **fix / other**: bug fixes, docs, refactoring, tests, performance. @@ -108,51 +109,64 @@ git ls-remote --tags origin vX.Y.Z # the tag is on the remote If the push fails, report git's message. Do not retry with force. If the tag is wrong after it was pushed, ask before deleting it. -## 7. Hand over: the developer runs `make release` +## 7. Write the release notes + +Write them to `/tmp/submarine-vX.Y.Z-notes.md` (outside the repository: +`make release` refuses a dirty tree). They are for users of the tool, not a +commit dump: short, grouped under **Breaking changes** (only if any), +**New**, **Fixes** (leave out empty groups and pure refactoring/tests), each +item one line naming the key or behavior (`b` switches the branch of a +repository...). End with: + +``` +Install or update: `curl -fsSL https://raw.githubusercontent.com/simonfranken/submarine/main/install.sh | sh`, or `submarine update`. + +Full changelog: https://github.com/simonfranken/submarine/compare/...vX.Y.Z +``` + +Show the notes to the user; they can change them before the next step. + +## 8. Hand over: the developer runs `make release` HEAD is now the clean tag, which is what `make release` needs. Tell the developer to run, in the repository: ```sh -make release +make release NOTES=/tmp/submarine-vX.Y.Z-notes.md ``` -(it reads `GITEA_TOKEN` from `.env`, tests, builds, uploads, and points -`submarine-latest` at the version last). **Stop here and wait** until they -say it finished. If it failed, help with the output; do not run it yourself. +(it uses `gh`, tests, builds, creates the release as a draft with all +assets and publishes it last). **Stop here and wait** until they say it +finished. If it failed, help with the output; do not run it yourself. A +leftover draft from a failed run must be deleted (ask first) before a retry. -## 8. Verify the upload, create the Gitea release +## 9. Verify the release Once the developer reports success: ```sh +curl -fsSL https://github.com/simonfranken/submarine/releases/latest/download/VERSION +``` + +must print `vX.Y.Z` (this is what `install.sh` and `submarine update` +read). Check with `mcp__github__get_release_by_tag` that it is not a draft +and has the assets `submarine_{linux,darwin}_{amd64,arm64}`, +`checksums.txt` and `VERSION`. If anything is missing, report it. Finish +with the link to the release and the version. + +## 10. Bridge release (only while the Makefile has `release-gitea`) + +The first GitHub release is also published to the old Gitea registry, so +binaries that still update from Gitea pick it up (D33). After step 9, and +after asking the user, push the tag to the `gitea` remote and hand over +again: + +```sh +git push gitea main vX.Y.Z +make release-gitea # run by the developer, needs GITEA_TOKEN curl -fsS https://git.simon-franken.de/api/packages/simonfranken/generic/submarine-latest/latest/VERSION ``` -must print `vX.Y.Z` (the registry is public; `install.sh` reads it the same -way). If it doesn't, the upload did not complete: do not create the release, -report it. - -Then create the release with `mcp__gitea__create_release` (load it with -ToolSearch first): - -- `owner`: `simonfranken`, `repo`: `submarine` -- `tag_name`: `vX.Y.Z` -- `target`: the tag's commit, `git rev-parse vX.Y.Z^{commit}` -- `title`: `vX.Y.Z` -- `is_draft`: false, `is_pre_release`: false -- `body`: release notes, see below - -Release notes are for users of the tool, not a commit dump: short, grouped -under **Breaking changes** (only if any), **New**, **Fixes** (leave out empty -groups and pure refactoring/tests), each item one line naming the key or -behavior (`b` switches the branch of a repository...). End with: - -``` -Install or update: `curl -fsSL https://git.simon-franken.de/simonfranken/submarine/raw/branch/main/install.sh | sh`, or `submarine update`. - -Full changelog: https://git.simon-franken.de/simonfranken/submarine/compare/...vX.Y.Z -``` - -The binaries live in the package registry, not on the release: attach -nothing. Finish with the link to the release and the version. +The last command must print `vX.Y.Z`. Then create a Gitea release for the +tag with `mcp__gitea__create_release`, its body saying that submarine moved +to https://github.com/simonfranken/submarine. diff --git a/.env.template b/.env.template index d3cc2a6..79465ee 100644 --- a/.env.template +++ b/.env.template @@ -1,8 +1,13 @@ # Copy to .env (gitignored); the Makefile loads it automatically. -# Access token with write:package scope. Required for `make release`. -GITEA_TOKEN= +# GitHub token with contents:write on the repo, for `make release`. +# Not needed if `gh auth login` was run. +# GH_TOKEN= -# Optional overrides (defaults shown). +# Optional override (default shown). +# GITHUB_REPO=simonfranken/submarine + +# Bridge release only (`make release-gitea`): token with write:package scope. +GITEA_TOKEN= # GITEA_URL=https://git.simon-franken.de # GITEA_OWNER=simonfranken diff --git a/AGENTS.md b/AGENTS.md index a196caf..3d62d6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ go run ./cmd/submarine [dir] # run on a repository (default: cwd) go run ./cmd/submarine --mock # run on mock data scripts/make-fixture.sh DIR # build a test superproject in DIR make dist # cross-compile into dist/ -GITEA_TOKEN=… make release # publish a tagged release (outward-facing; run by the developer) +make release NOTES=FILE # publish a tagged release to GitHub (outward-facing; run by the developer) go vet ./... go test ./... gofmt -l . # must print nothing @@ -32,7 +32,7 @@ gofmt -l . # must print nothing Use the `release` skill (`/release`, `.claude/skills/release/SKILL.md`): it picks the next semver, runs a check, tags and pushes, asks the developer to -run `make release`, then creates the Gitea release. Never run `make release` +run `make release`, then verifies the GitHub release. Never run `make release` or push tags outside of it. ## Rules diff --git a/Makefile b/Makefile index a234592..4ae33d7 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ -# Build and publish submarine. Releases are built locally and uploaded to the -# Gitea generic package registry (see docs/architecture.md#distribution). +# Build and publish submarine. Releases are built locally and uploaded as +# GitHub release assets (see docs/architecture.md#distribution). # -# make dist cross-compile into dist/ -# GITEA_TOKEN=... make release test, build and publish the tagged HEAD +# make dist cross-compile into dist/ +# make release NOTES=FILE test, build and publish the tagged HEAD (gh) +# GITEA_TOKEN=... make release-gitea bridge only: the same version to the old Gitea registry # Settings from .env (see .env.template); `make VAR=...` on the command line wins. -include .env -export GITEA_TOKEN +export GITEA_TOKEN GH_TOKEN + +GITHUB_REPO ?= simonfranken/submarine +RELEASES := https://github.com/$(GITHUB_REPO)/releases GITEA_URL ?= https://git.simon-franken.de GITEA_OWNER ?= simonfranken @@ -17,7 +21,7 @@ LDFLAGS := -s -w -X main.version=$(VERSION) PLATFORMS := linux_amd64 linux_arm64 darwin_amd64 darwin_arm64 BINARIES := $(addprefix dist/submarine_,$(PLATFORMS)) -.PHONY: build test vet dist release clean FORCE +.PHONY: build test vet dist check-tag release release-gitea clean FORCE build: go build -ldflags '$(LDFLAGS)' -o submarine ./cmd/submarine @@ -41,11 +45,31 @@ dist/submarine_%: FORCE dist/checksums.txt: $(BINARIES) cd dist && sha256sum $(notdir $(BINARIES)) > checksums.txt -release: - @test -n "$$GITEA_TOKEN" || { echo "GITEA_TOKEN is not set (needs write:package)"; exit 1; } +check-tag: @echo '$(VERSION)' | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$$' || \ { echo "VERSION '$(VERSION)' is not a clean tag: tag HEAD as vX.Y.Z with a clean tree"; exit 1; } @test -z "$$(git status --porcelain)" || { echo "working tree is not clean"; exit 1; } + +# A draft gets all assets first and is published last, so releases/latest +# never points at a release with missing files. +release: check-tag + @test -n "$(NOTES)" && test -f "$(NOTES)" || { echo "NOTES=FILE with the release notes is required"; exit 1; } + @command -v gh >/dev/null || { echo "gh (GitHub CLI) is required"; exit 1; } + @gh auth status >/dev/null 2>&1 || { echo "gh is not logged in: run gh auth login or set GH_TOKEN"; exit 1; } + $(MAKE) clean test vet dist VERSION=$(VERSION) + printf '%s\n' '$(VERSION)' > dist/VERSION + gh release create '$(VERSION)' --repo '$(GITHUB_REPO)' --verify-tag --draft \ + --title '$(VERSION)' --notes-file '$(NOTES)' $(BINARIES) dist/checksums.txt dist/VERSION + gh release edit '$(VERSION)' --repo '$(GITHUB_REPO)' --draft=false --latest + @echo "released $(VERSION): $(RELEASES)/tag/$(VERSION)" + +# Bridge release for binaries that still update from Gitea: same version, +# old registry layout. Refuses until GitHub serves this version, so a binary +# updated from Gitea finds its release on GitHub. Remove after the bridge. +release-gitea: check-tag + @test -n "$$GITEA_TOKEN" || { echo "GITEA_TOKEN is not set (needs write:package)"; exit 1; } + @test "$$(curl -fsSL '$(RELEASES)/latest/download/VERSION' | tr -d '[:space:]')" = '$(VERSION)' || \ + { echo "GitHub does not serve $(VERSION) as latest yet: run make release first"; exit 1; } $(MAKE) clean test vet dist VERSION=$(VERSION) @set -e; \ for f in $(BINARIES) dist/checksums.txt; do \ @@ -59,7 +83,7 @@ release: "$(GITEA_URL)/api/v1/packages/$(GITEA_OWNER)/generic/submarine-latest/latest" >/dev/null || true; \ curl -fsS -X PUT -H "Authorization: token $$GITEA_TOKEN" \ --upload-file dist/VERSION "$(REGISTRY)/submarine-latest/latest/VERSION" >/dev/null; \ - echo "released $(VERSION)" + echo "released $(VERSION) to Gitea" clean: rm -rf dist submarine diff --git a/README.md b/README.md index f790cc1..6e6e049 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ the questions you actually have: ## Install ```sh -curl -fsSL https://git.simon-franken.de/simonfranken/submarine/raw/branch/main/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/simonfranken/submarine/main/install.sh | sh ``` Installs `sub` and `submarine` (linux/macOS, amd64/arm64) to `~/.local/bin`. diff --git a/cmd/submarine/main.go b/cmd/submarine/main.go index c842f6c..43b8746 100644 --- a/cmd/submarine/main.go +++ b/cmd/submarine/main.go @@ -93,7 +93,7 @@ func main() { } // latestRelease returns the version of a newer release, or "" if there is -// none or the registry can't be reached within a few seconds. +// none or GitHub can't be reached within a few seconds. func latestRelease(ctx context.Context) string { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() diff --git a/docs/README.md b/docs/README.md index bedeedc..6196e80 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,12 +20,12 @@ bare commit hash. ## Install ```sh -curl -fsSL https://git.simon-franken.de/simonfranken/submarine/raw/branch/main/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/simonfranken/submarine/main/install.sh | sh ``` Installs `submarine` and the alias `sub` (linux/macOS, amd64/arm64) to `~/.local/bin`. Update with `submarine update`. Maintainers publish with -`GITEA_TOKEN=… make release` (see [architecture](architecture.md#distribution)). +`make release NOTES=…` (see [architecture](architecture.md#distribution)). ## Status diff --git a/docs/architecture.md b/docs/architecture.md index 0a78be1..48ca050 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,7 +23,7 @@ internal/submodule/git/ provider using the git CLI (reads, fetch, watch) switch.go Branches, SwitchLoss, Switch (stage the tip, restore, attach or fast-forward the branch) gitdir.go resolves .git dirs and gitdir: files internal/submodule/mock/ fixture provider with artificial latency -internal/update/ self-update from the package registry; NO UI imports +internal/update/ self-update from GitHub releases; NO UI imports internal/tui/ Bubble Tea application app.go root model: layout, message routing, loading keys.go key bindings (also feed the help view) @@ -203,17 +203,24 @@ No CI runner: releases are built and published from a developer machine. - `make dist` cross-compiles `CGO_ENABLED=0` binaries for linux/darwin × amd64/arm64 into `dist/` plus `checksums.txt`; the version comes from `git describe` and is baked in with `-X main.version`. -- `make release` with `GITEA_TOKEN` set, e.g. in `.env` (copy `.env.template`; token needs `write:package`; HEAD must be a - clean `vX.Y.Z` tag) runs tests, builds and uploads to the Gitea generic - registry `https://git.simon-franken.de/api/packages/simonfranken/generic`: - - `submarine//submarine__` and `checksums.txt` - - `submarine-latest/latest/VERSION`, replaced **last**, so a failed upload - never advertises a broken version. +- `make release NOTES=FILE` (needs `gh` logged in, or `GH_TOKEN` in `.env`, + see `.env.template`; HEAD must be a clean `vX.Y.Z` tag pushed to GitHub) + runs tests, builds and creates the GitHub release `vX.Y.Z` with the notes + and the assets `submarine__`, `checksums.txt` and `VERSION`. It + is created as a **draft** and published last, so `releases/latest` never + points at a release with missing assets. Clients read, without the API + (plain redirects, no rate limit), below + `https://github.com/simonfranken/submarine/releases`: + - `latest/download/VERSION` + - `download//submarine__` and `checksums.txt` +- Before the move to GitHub, releases lived in the Gitea generic registry + (`git.simon-franken.de/api/packages/simonfranken/generic`, see D33). It + stays online for old binaries. `make release-gitea` publishes the bridge + version there in the old layout and refuses until GitHub serves it. - The `release` skill (`.claude/skills/release/SKILL.md`) drives a release: latest tag → next semver from the commits since → check (`vet`, `test`, `gofmt`, `make dist`) → tag and push → the developer runs `make release` → - the skill verifies the registry's `VERSION` and creates the Gitea release - with MCP (notes only; the binaries stay in the registry). + the skill verifies that `latest/download/VERSION` serves the new version. - `install.sh` (`curl -fsSL …/raw/branch/main/install.sh | sh`) detects OS/arch, verifies the sha256, installs `submarine` and a `sub` symlink to `~/.local/bin` (`SUBMARINE_INSTALL_DIR`, `SUBMARINE_VERSION`, @@ -221,7 +228,7 @@ No CI runner: releases are built and published from a developer machine. - `submarine update [--check] [--force]` (package `internal/update`) reads `VERSION`, downloads the asset for the running platform, verifies the checksum and atomically replaces the real executable (symlinks resolved, - so `sub` works too). `SUBMARINE_UPDATE_URL` overrides the registry. + so `sub` works too). `SUBMARINE_UPDATE_URL` overrides the releases root. - On every start the TUI looks for a newer release in the background (3 s timeout, no cache, no blocking). `main` passes `tui.WithUpdateCheck` and the TUI gets the result as a message; the footer then offers `q quit and diff --git a/docs/decisions.md b/docs/decisions.md index 7e9ccac..83c8ba4 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -201,7 +201,8 @@ package so versions stay clean. corrupt or partial downloads, not a compromised registry (no signatures). The update check is a deliberate, outside-the-provider network use: only on `update` and a background check at startup that can be disabled (it -was once a day; see D27). +was once a day; see D27). The registry was replaced by GitHub releases in +D33; local builds, `install.sh` and the pointer file stay. ## Copy path with `c` via OSC 52 @@ -634,3 +635,30 @@ skipped. Contextual palettes that grey out impossible keys: larger, kept for later (item 10 of the usability plan). **Consequences.** `footer.Keys` drops `help.KeyMap` for `Tools`, `HelpGroups` and `LegendKeys`; the footer renders the full help itself. + +## D33 — Releases move to GitHub, with one bridge release on Gitea + +**Context.** The repository moves from `git.simon-franken.de` (Gitea) to +`github.com/simonfranken/submarine`. Installed binaries only know the Gitea +registry (D21), and check it on every start (D27). +**Decision.** +- Releases are GitHub releases with the binaries, `checksums.txt` and + `VERSION` as assets. GitHub Packages has no generic registry. Clients read + `releases/latest/download/VERSION` and `releases/download//`: + plain redirects, not the API, so the check on every start is not rate + limited and needs no token. +- `make release` creates a draft, uploads all assets, then publishes: the + draft takes the role of D21's "pointer last". +- One bridge version, the first that reads from GitHub, is also published to + Gitea in the old layout (`make release-gitea`). Old binaries update to it + from Gitea, and from then on update from GitHub. `release-gitea` refuses + until GitHub serves the version, so no bridge binary finds GitHub empty. +- The Gitea registry stays online with the bridge as its latest version, and + the Gitea repository is archived, not deleted: a binary started for the + first time in months still finds its way over. +**Alternatives.** Keep Gitea and mirror to GitHub: two places to publish +to. Have the updater try both hosts: permanent code for a one-time move. +**Consequences.** `SUBMARINE_UPDATE_URL` and `SUBMARINE_BASE_URL` now take +a releases root, not a registry root. Versions before the bridge exist only +on Gitea; `SUBMARINE_VERSION=` with the new `install.sh` doesn't find +them. `release-gitea` and the Gitea settings go once the bridge is out. diff --git a/install.sh b/install.sh index 20702f7..8601ee6 100755 --- a/install.sh +++ b/install.sh @@ -1,14 +1,14 @@ #!/bin/sh # Install submarine (and the `sub` alias). -# curl -fsSL https://git.simon-franken.de/simonfranken/submarine/raw/branch/main/install.sh | sh +# curl -fsSL https://raw.githubusercontent.com/simonfranken/submarine/main/install.sh | sh # # Environment: # SUBMARINE_VERSION install this version (default: latest) # SUBMARINE_INSTALL_DIR target directory (default: ~/.local/bin) -# SUBMARINE_BASE_URL registry root (default: the Gitea generic registry) +# SUBMARINE_BASE_URL releases root (default: the GitHub releases) set -eu -base="${SUBMARINE_BASE_URL:-https://git.simon-franken.de/api/packages/simonfranken/generic}" +base="${SUBMARINE_BASE_URL:-https://github.com/simonfranken/submarine/releases}" dir="${SUBMARINE_INSTALL_DIR:-$HOME/.local/bin}" die() { echo "install: $*" >&2; exit 1; } @@ -35,7 +35,7 @@ fi version="${SUBMARINE_VERSION:-}" if [ -z "$version" ]; then - version="$(curl -fsSL "$base/submarine-latest/latest/VERSION")" || die "cannot determine the latest version" + version="$(curl -fsSL "$base/latest/download/VERSION")" || die "cannot determine the latest version" version="$(printf '%s' "$version" | tr -d '[:space:]')" fi case "$version" in @@ -48,8 +48,8 @@ tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT INT TERM echo "Installing submarine $version ($os/$arch) to $dir" -curl -fsSL -o "$tmp/$asset" "$base/submarine/$version/$asset" || die "download failed: $asset" -curl -fsSL -o "$tmp/checksums.txt" "$base/submarine/$version/checksums.txt" || die "download failed: checksums.txt" +curl -fsSL -o "$tmp/$asset" "$base/download/$version/$asset" || die "download failed: $asset" +curl -fsSL -o "$tmp/checksums.txt" "$base/download/$version/checksums.txt" || die "download failed: checksums.txt" want="$(awk -v a="$asset" '$2 == a || $2 == "*" a { print $1 }' "$tmp/checksums.txt")" [ -n "$want" ] || die "no checksum for $asset" diff --git a/internal/update/update.go b/internal/update/update.go index 838e543..dfee0ef 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -1,5 +1,5 @@ -// Package update implements self-update from the Gitea generic package -// registry. It has no UI imports; the network is only used on explicit calls. +// Package update implements self-update from GitHub release assets. It has no +// UI imports; the network is only used on explicit calls. package update import ( @@ -19,16 +19,18 @@ import ( "time" ) -// DefaultBaseURL is the registry root; see docs/architecture.md#distribution. -const DefaultBaseURL = "https://git.simon-franken.de/api/packages/simonfranken/generic" +// DefaultBaseURL is the releases root; see docs/architecture.md#distribution. +// Both paths below it are plain redirects to the assets, not API calls, so +// they are not rate limited. +const DefaultBaseURL = "https://github.com/simonfranken/submarine/releases" -// EnvBaseURL overrides the registry root (tests, manual verification). +// EnvBaseURL overrides the releases root (tests, manual verification). const EnvBaseURL = "SUBMARINE_UPDATE_URL" // maxBinary bounds a download so a bad server cannot fill the disk. const maxBinary = 256 << 20 -// Client talks to the registry. +// Client talks to the releases. type Client struct { BaseURL string HTTP *http.Client @@ -36,7 +38,7 @@ type Client struct { GOARCH string } -// New returns a client for the default (or $SUBMARINE_UPDATE_URL) registry. +// New returns a client for the default (or $SUBMARINE_UPDATE_URL) releases. func New() *Client { base := os.Getenv(EnvBaseURL) if base == "" { @@ -52,13 +54,13 @@ func New() *Client { // Latest returns the newest released version, e.g. "v0.2.0". func (c *Client) Latest(ctx context.Context) (string, error) { - b, err := c.get(ctx, c.BaseURL+"/submarine-latest/latest/VERSION", 1<<10) + b, err := c.get(ctx, c.BaseURL+"/latest/download/VERSION", 1<<10) if err != nil { return "", err } v := strings.TrimSpace(string(b)) if _, ok := parse(v); !ok { - return "", fmt.Errorf("registry returned an invalid version %q", v) + return "", fmt.Errorf("releases returned an invalid version %q", v) } return v, nil } @@ -120,7 +122,7 @@ func (c *Client) Install(ctx context.Context, version string) (string, error) { // InstallTo is Install with an explicit target path. func (c *Client) InstallTo(ctx context.Context, version, target string) error { asset := fmt.Sprintf("submarine_%s_%s", c.GOOS, c.GOARCH) - base := c.BaseURL + "/submarine/" + version + "/" + base := c.BaseURL + "/download/" + version + "/" sums, err := c.get(ctx, base+"checksums.txt", 1<<20) if err != nil { diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 1e54134..fd45d61 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -32,16 +32,25 @@ func TestNewer(t *testing.T) { } } +// registry mimics GitHub releases: every asset URL redirects to storage. func registry(t *testing.T, bin, sum string) *Client { t.Helper() mux := http.NewServeMux() - mux.HandleFunc("/submarine-latest/latest/VERSION", func(w http.ResponseWriter, _ *http.Request) { + redirect := func(from, to string) { + mux.HandleFunc(from, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, to, http.StatusFound) + }) + } + redirect("/latest/download/VERSION", "/assets/VERSION") + redirect("/download/v0.2.0/checksums.txt", "/assets/checksums.txt") + redirect("/download/v0.2.0/submarine_linux_arm64", "/assets/submarine_linux_arm64") + mux.HandleFunc("/assets/VERSION", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("v0.2.0\n")) }) - mux.HandleFunc("/submarine/v0.2.0/checksums.txt", func(w http.ResponseWriter, _ *http.Request) { + mux.HandleFunc("/assets/checksums.txt", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte(sum + " submarine_linux_arm64\n")) }) - mux.HandleFunc("/submarine/v0.2.0/submarine_linux_arm64", func(w http.ResponseWriter, _ *http.Request) { + mux.HandleFunc("/assets/submarine_linux_arm64", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte(bin)) }) srv := httptest.NewServer(mux)