2 Commits
Author SHA1 Message Date
simonfrankenandClaude Opus 5.5 a8133b4884 Make GH_TOKEN the main setting in .env.template
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:42:48 +01:00
simonfrankenandClaude Opus 5.5 1ee394cfdf Move releases to GitHub, with one bridge release on Gitea
GitHub has no generic package registry, so releases become GitHub
release assets. The updater and install.sh read releases/latest/download
and releases/download/<v>/, plain redirects with no API rate limit.
make release publishes a draft with all assets, then makes it latest.
make release-gitea publishes the same version to the old Gitea registry
for binaries that still update from there, and refuses until GitHub
serves it. See D33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:38:50 +01:00
12 changed files with 183 additions and 93 deletions
+59 -45
View File
@@ -1,23 +1,23 @@
--- ---
name: release 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 disable-model-invocation: true
--- ---
# Release submarine # Release submarine
A release is built on the developer's machine (`make release`, see A release is built on the developer's machine (`make release`, see
`docs/architecture.md#distribution`). `make release` publishes the binaries `docs/architecture.md#distribution`). `make release` creates the GitHub
of the **clean `vX.Y.Z` tag at HEAD** to the Gitea package registry. This release of the **clean `vX.Y.Z` tag at HEAD**, with the binaries as assets.
skill does everything around it. The developer runs `make release` 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` themselves, in the middle: you never run it and never read or print `.env`,
or `GITEA_TOKEN`. `GH_TOKEN` or `GITEA_TOKEN`.
Pushing a tag and creating a release are outward-facing and hard to undo. 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 Do them only after the confirmation in step 5, and never force-push, move or
delete a tag without asking. delete a tag without asking.
Gitea: owner `simonfranken`, repo `submarine`, `https://git.simon-franken.de`. GitHub: owner `simonfranken`, repo `submarine`, remote `origin`.
## 1. Preconditions ## 1. Preconditions
@@ -29,7 +29,7 @@ git branch --show-current # main
git status --porcelain # empty: make release refuses a dirty tree 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 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 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 ## 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 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 Cross-check with the `mcp__github__get_latest_release` MCP tool (load it with
ToolSearch first). If the tag and the Gitea release differ (a tag without a 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), 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 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 ## 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: 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 - **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; `submarine update`), or behavior users rely on;
- **feature**: a new key, action, view or option, or a visible behavior; - **feature**: a new key, action, view or option, or a visible behavior;
- **fix / other**: bug fixes, docs, refactoring, tests, performance. - **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 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. 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/<last>...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 HEAD is now the clean tag, which is what `make release` needs. Tell the
developer to run, in the repository: developer to run, in the repository:
```sh ```sh
make release make release NOTES=/tmp/submarine-vX.Y.Z-notes.md
``` ```
(it reads `GITEA_TOKEN` from `.env`, tests, builds, uploads, and points (it uses `gh`, tests, builds, creates the release as a draft with all
`submarine-latest` at the version last). **Stop here and wait** until they assets and publishes it last). **Stop here and wait** until they say it
say it finished. If it failed, help with the output; do not run it yourself. 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: Once the developer reports success:
```sh ```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 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 The last command must print `vX.Y.Z`. Then create a Gitea release for the
way). If it doesn't, the upload did not complete: do not create the release, tag with `mcp__gitea__create_release`, its body saying that submarine moved
report it. to https://github.com/simonfranken/submarine.
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/<last>...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.
+9 -3
View File
@@ -1,8 +1,14 @@
# Copy to .env (gitignored); the Makefile loads it automatically. # Copy to .env (gitignored); the Makefile loads it automatically.
# Access token with write:package scope. Required for `make release`. # GitHub token for `make release` (gh): classic with the `repo` scope, or
GITEA_TOKEN= # fine-grained with Contents read/write on simonfranken/submarine.
GH_TOKEN=
# Optional overrides (defaults shown). # Optional override (default shown).
# GITHUB_REPO=simonfranken/submarine
# Bridge release only (`make release-gitea`, see docs/decisions.md D33):
# Gitea token with the write:package scope. Remove once the bridge is out.
GITEA_TOKEN=
# GITEA_URL=https://git.simon-franken.de # GITEA_URL=https://git.simon-franken.de
# GITEA_OWNER=simonfranken # GITEA_OWNER=simonfranken
+2 -2
View File
@@ -22,7 +22,7 @@ go run ./cmd/submarine [dir] # run on a repository (default: cwd)
go run ./cmd/submarine --mock # run on mock data go run ./cmd/submarine --mock # run on mock data
scripts/make-fixture.sh DIR # build a test superproject in DIR scripts/make-fixture.sh DIR # build a test superproject in DIR
make dist # cross-compile into dist/ 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 vet ./...
go test ./... go test ./...
gofmt -l . # must print nothing 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 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 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. or push tags outside of it.
## Rules ## Rules
+32 -8
View File
@@ -1,12 +1,16 @@
# Build and publish submarine. Releases are built locally and uploaded to the # Build and publish submarine. Releases are built locally and uploaded as
# Gitea generic package registry (see docs/architecture.md#distribution). # GitHub release assets (see docs/architecture.md#distribution).
# #
# make dist cross-compile into dist/ # make dist cross-compile into dist/
# GITEA_TOKEN=... make release test, build and publish the tagged HEAD # 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. # Settings from .env (see .env.template); `make VAR=...` on the command line wins.
-include .env -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_URL ?= https://git.simon-franken.de
GITEA_OWNER ?= simonfranken GITEA_OWNER ?= simonfranken
@@ -17,7 +21,7 @@ LDFLAGS := -s -w -X main.version=$(VERSION)
PLATFORMS := linux_amd64 linux_arm64 darwin_amd64 darwin_arm64 PLATFORMS := linux_amd64 linux_arm64 darwin_amd64 darwin_arm64
BINARIES := $(addprefix dist/submarine_,$(PLATFORMS)) 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: build:
go build -ldflags '$(LDFLAGS)' -o submarine ./cmd/submarine go build -ldflags '$(LDFLAGS)' -o submarine ./cmd/submarine
@@ -41,11 +45,31 @@ dist/submarine_%: FORCE
dist/checksums.txt: $(BINARIES) dist/checksums.txt: $(BINARIES)
cd dist && sha256sum $(notdir $(BINARIES)) > checksums.txt cd dist && sha256sum $(notdir $(BINARIES)) > checksums.txt
release: check-tag:
@test -n "$$GITEA_TOKEN" || { echo "GITEA_TOKEN is not set (needs write:package)"; exit 1; }
@echo '$(VERSION)' | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$$' || \ @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; } { 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; } @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) $(MAKE) clean test vet dist VERSION=$(VERSION)
@set -e; \ @set -e; \
for f in $(BINARIES) dist/checksums.txt; do \ 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; \ "$(GITEA_URL)/api/v1/packages/$(GITEA_OWNER)/generic/submarine-latest/latest" >/dev/null || true; \
curl -fsS -X PUT -H "Authorization: token $$GITEA_TOKEN" \ curl -fsS -X PUT -H "Authorization: token $$GITEA_TOKEN" \
--upload-file dist/VERSION "$(REGISTRY)/submarine-latest/latest/VERSION" >/dev/null; \ --upload-file dist/VERSION "$(REGISTRY)/submarine-latest/latest/VERSION" >/dev/null; \
echo "released $(VERSION)" echo "released $(VERSION) to Gitea"
clean: clean:
rm -rf dist submarine rm -rf dist submarine
+1 -1
View File
@@ -38,7 +38,7 @@ the questions you actually have:
## Install ## Install
```sh ```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`. Installs `sub` and `submarine` (linux/macOS, amd64/arm64) to `~/.local/bin`.
+1 -1
View File
@@ -93,7 +93,7 @@ func main() {
} }
// latestRelease returns the version of a newer release, or "" if there is // 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 { func latestRelease(ctx context.Context) string {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second) ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() defer cancel()
+2 -2
View File
@@ -20,12 +20,12 @@ bare commit hash.
## Install ## Install
```sh ```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 Installs `submarine` and the alias `sub` (linux/macOS, amd64/arm64) to
`~/.local/bin`. Update with `submarine update`. Maintainers publish with `~/.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 ## Status
+17 -10
View File
@@ -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) switch.go Branches, SwitchLoss, Switch (stage the tip, restore, attach or fast-forward the branch)
gitdir.go resolves .git dirs and gitdir: files gitdir.go resolves .git dirs and gitdir: files
internal/submodule/mock/ fixture provider with artificial latency 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 internal/tui/ Bubble Tea application
app.go root model: layout, message routing, loading app.go root model: layout, message routing, loading
keys.go key bindings (also feed the help view) 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 × - `make dist` cross-compiles `CGO_ENABLED=0` binaries for linux/darwin ×
amd64/arm64 into `dist/` plus `checksums.txt`; the version comes from amd64/arm64 into `dist/` plus `checksums.txt`; the version comes from
`git describe` and is baked in with `-X main.version`. `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 - `make release NOTES=FILE` (needs `gh` logged in, or `GH_TOKEN` in `.env`,
clean `vX.Y.Z` tag) runs tests, builds and uploads to the Gitea generic see `.env.template`; HEAD must be a clean `vX.Y.Z` tag pushed to GitHub)
registry `https://git.simon-franken.de/api/packages/simonfranken/generic`: runs tests, builds and creates the GitHub release `vX.Y.Z` with the notes
- `submarine/<version>/submarine_<os>_<arch>` and `checksums.txt` and the assets `submarine_<os>_<arch>`, `checksums.txt` and `VERSION`. It
- `submarine-latest/latest/VERSION`, replaced **last**, so a failed upload is created as a **draft** and published last, so `releases/latest` never
never advertises a broken version. 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/<version>/submarine_<os>_<arch>` 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: - The `release` skill (`.claude/skills/release/SKILL.md`) drives a release:
latest tag → next semver from the commits since → check (`vet`, `test`, latest tag → next semver from the commits since → check (`vet`, `test`,
`gofmt`, `make dist`) → tag and push → the developer runs `make release` → `gofmt`, `make dist`) → tag and push → the developer runs `make release` →
the skill verifies the registry's `VERSION` and creates the Gitea release the skill verifies that `latest/download/VERSION` serves the new version.
with MCP (notes only; the binaries stay in the registry).
- `install.sh` (`curl -fsSL …/raw/branch/main/install.sh | sh`) detects - `install.sh` (`curl -fsSL …/raw/branch/main/install.sh | sh`) detects
OS/arch, verifies the sha256, installs `submarine` and a `sub` symlink to OS/arch, verifies the sha256, installs `submarine` and a `sub` symlink to
`~/.local/bin` (`SUBMARINE_INSTALL_DIR`, `SUBMARINE_VERSION`, `~/.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 - `submarine update [--check] [--force]` (package `internal/update`) reads
`VERSION`, downloads the asset for the running platform, verifies the `VERSION`, downloads the asset for the running platform, verifies the
checksum and atomically replaces the real executable (symlinks resolved, 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 - 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 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 the TUI gets the result as a message; the footer then offers `q quit and
+29 -1
View File
@@ -201,7 +201,8 @@ package so versions stay clean.
corrupt or partial downloads, not a compromised registry (no signatures). corrupt or partial downloads, not a compromised registry (no signatures).
The update check is a deliberate, outside-the-provider network use: only 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 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 ## 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). later (item 10 of the usability plan).
**Consequences.** `footer.Keys` drops `help.KeyMap` for `Tools`, **Consequences.** `footer.Keys` drops `help.KeyMap` for `Tools`,
`HelpGroups` and `LegendKeys`; the footer renders the full help itself. `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/<v>/<asset>`:
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=<old>` with the new `install.sh` doesn't find
them. `release-gitea` and the Gitea settings go once the bridge is out.
+6 -6
View File
@@ -1,14 +1,14 @@
#!/bin/sh #!/bin/sh
# Install submarine (and the `sub` alias). # 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: # Environment:
# SUBMARINE_VERSION install this version (default: latest) # SUBMARINE_VERSION install this version (default: latest)
# SUBMARINE_INSTALL_DIR target directory (default: ~/.local/bin) # 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 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}" dir="${SUBMARINE_INSTALL_DIR:-$HOME/.local/bin}"
die() { echo "install: $*" >&2; exit 1; } die() { echo "install: $*" >&2; exit 1; }
@@ -35,7 +35,7 @@ fi
version="${SUBMARINE_VERSION:-}" version="${SUBMARINE_VERSION:-}"
if [ -z "$version" ]; then 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:]')" version="$(printf '%s' "$version" | tr -d '[:space:]')"
fi fi
case "$version" in case "$version" in
@@ -48,8 +48,8 @@ tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT INT TERM trap 'rm -rf "$tmp"' EXIT INT TERM
echo "Installing submarine $version ($os/$arch) to $dir" 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/$asset" "$base/download/$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/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")" want="$(awk -v a="$asset" '$2 == a || $2 == "*" a { print $1 }' "$tmp/checksums.txt")"
[ -n "$want" ] || die "no checksum for $asset" [ -n "$want" ] || die "no checksum for $asset"
+12 -10
View File
@@ -1,5 +1,5 @@
// Package update implements self-update from the Gitea generic package // Package update implements self-update from GitHub release assets. It has no
// registry. It has no UI imports; the network is only used on explicit calls. // UI imports; the network is only used on explicit calls.
package update package update
import ( import (
@@ -19,16 +19,18 @@ import (
"time" "time"
) )
// DefaultBaseURL is the registry root; see docs/architecture.md#distribution. // DefaultBaseURL is the releases root; see docs/architecture.md#distribution.
const DefaultBaseURL = "https://git.simon-franken.de/api/packages/simonfranken/generic" // 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" const EnvBaseURL = "SUBMARINE_UPDATE_URL"
// maxBinary bounds a download so a bad server cannot fill the disk. // maxBinary bounds a download so a bad server cannot fill the disk.
const maxBinary = 256 << 20 const maxBinary = 256 << 20
// Client talks to the registry. // Client talks to the releases.
type Client struct { type Client struct {
BaseURL string BaseURL string
HTTP *http.Client HTTP *http.Client
@@ -36,7 +38,7 @@ type Client struct {
GOARCH string 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 { func New() *Client {
base := os.Getenv(EnvBaseURL) base := os.Getenv(EnvBaseURL)
if base == "" { if base == "" {
@@ -52,13 +54,13 @@ func New() *Client {
// Latest returns the newest released version, e.g. "v0.2.0". // Latest returns the newest released version, e.g. "v0.2.0".
func (c *Client) Latest(ctx context.Context) (string, error) { 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 { if err != nil {
return "", err return "", err
} }
v := strings.TrimSpace(string(b)) v := strings.TrimSpace(string(b))
if _, ok := parse(v); !ok { 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 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. // InstallTo is Install with an explicit target path.
func (c *Client) InstallTo(ctx context.Context, version, target string) error { func (c *Client) InstallTo(ctx context.Context, version, target string) error {
asset := fmt.Sprintf("submarine_%s_%s", c.GOOS, c.GOARCH) 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) sums, err := c.get(ctx, base+"checksums.txt", 1<<20)
if err != nil { if err != nil {
+12 -3
View File
@@ -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 { func registry(t *testing.T, bin, sum string) *Client {
t.Helper() t.Helper()
mux := http.NewServeMux() 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")) 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")) 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)) w.Write([]byte(bin))
}) })
srv := httptest.NewServer(mux) srv := httptest.NewServer(mux)