feat: signed releases, release feed, notification (read-only)
This commit is contained in:
1 parent
a49eec909c
commit
834493a4f5
49 files changed
+4191
-18
No files matched your search
@@ -0,0 +1,292 @@
|
||||
Feature: Updates part 1: signed releases, release feed and read-only update notification
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want to see when a newer signed release exists, and have admins see the same status read-only
|
||||
So that I know when to update, while downloading and restarting arrive in later slices
|
||||
|
||||
Background:
|
||||
Given slices 1 to 3 of Updates ship in one release, so no released build shows the command
|
||||
"renewcms enable-browser-updates" before it exists (it is implemented in slice 3)
|
||||
And every release is published as a Gitea prerelease with the six binaries, checksums.txt and checksums.txt.sig
|
||||
And cmd/releasesign is a stdlib-only tool (crypto/ed25519, no new dependency) with the commands
|
||||
keygen <private-key-path> [--pub-out <path>], sign <file> [--key <path>] [--pub <path>],
|
||||
verify <file> [--pub <path>]
|
||||
And keygen writes the private key to the path given and refuses a path inside the repository work tree
|
||||
And sign and verify use the embedded public key unless --pub is given; tests always pass --pub with a throwaway key
|
||||
And sign refuses to sign when the private key does not match the public key, naming the mismatch
|
||||
And the public key is internal/infrastructure/update/release.pub, embedded with go:embed
|
||||
And until the user runs task release:keygen and commits the real public key, release.pub holds the placeholder:
|
||||
the base64 of 32 zero bytes (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=); an all-zero key is recognised
|
||||
as the placeholder, and sign and verify refuse it with a clear message
|
||||
And the real private key is generated by the user outside the repo; the coder never generates or commits it
|
||||
And task release writes checksums.txt (SHA-256 of every binary) next to the binaries and signs it into
|
||||
checksums.txt.sig with the private key at the path in the env var RELEASE_SIGNING_KEY
|
||||
And task release fails clearly, before building anything, when RELEASE_SIGNING_KEY is unset, the file is missing,
|
||||
the embedded key is the placeholder, or the private key does not match the embedded public key
|
||||
And task release:keygen creates the one-time key pair and prints where to commit the public key
|
||||
And docs/release-notes.md gains the publish step (attach checksums.txt and checksums.txt.sig with the six binaries),
|
||||
docs/deployment.md and docs/configuration.md are updated, and nothing is published
|
||||
And the domain package internal/domain/update (stdlib only) holds Release, ReleaseNotes, version comparison,
|
||||
UpdateStatus, the ReleaseFeed interface, the StatusStore interface and the Err sentinels
|
||||
And the Gitea adapter lives under internal/infrastructure/update and implements update.ReleaseFeed with
|
||||
var _ update.ReleaseFeed = &X{}; it requests GET {UPDATE_FEED_URL}/api/v1/repos/fsylvain/RenewCMS/releases
|
||||
(the list, not "latest")
|
||||
And UPDATE_FEED_URL is in requiredEnvVars/envDefaults, .env.example and docs/configuration.md, defaulting to the repo base URL
|
||||
And a JSON file store implements update.StatusStore and keeps the last result and checked_at in update-status.json
|
||||
beside DB_FILE, shared by all owners and surviving a restart; no new env var is added
|
||||
And GetUpdateStatusUseCase and CheckForUpdateUseCase depend only on the domain interfaces; handlers in
|
||||
api/handlers/update receive them through NewHandler; api/dependecyInjection.go builds the adapters
|
||||
And NewSystemRouter is mounted at /v1/system behind jwtauth.Verifier, jwtauth.Authenticator, RequireCurrentToken
|
||||
and RequireVerifiedUser; POST /update/check also sits behind RequireOwner and a limiter of 5 checks per minute
|
||||
per user_id (constants, not env vars, separate from the auth limiter) that answers 429 with an integer Retry-After
|
||||
And GET /v1/system/update returns 200 with JSON of exactly these snake_case keys: current, latest, available,
|
||||
release_url, notes {version, released_at, items}, checked_at, can_apply, state, progress, error, started_at
|
||||
And release_url is the release's html_url, null when never checked; in slice 3 the "How to update" link is
|
||||
repointed at the "Browser updates" section of docs/deployment.md
|
||||
And in this slice can_apply is always false and state is always "idle"; never-checked values are latest "",
|
||||
available false, release_url null, notes null, checked_at null, progress null, error null, started_at null
|
||||
And versions compare numerically per segment, a final release beats a beta of the same number, and the
|
||||
running build "dev" never reports an update
|
||||
And release notes are the release body converted to a plain-text list of items
|
||||
And the API returns raw tags; the SPA shows the newer release without its leading "v" ("Version 2.3.0-beta")
|
||||
and the running version exactly as reported
|
||||
And the download and restart endpoints are not mounted; ReleaseFeed, StatusStore and UpdateStatus are shaped so
|
||||
slices 2 and 3 only add use cases
|
||||
And errors are plain text from http.Error
|
||||
|
||||
Scenario Outline: Version comparison
|
||||
Given the running version is "<current>" and the newest feed release is "<latest>"
|
||||
When the versions are compared
|
||||
Then available is <available>
|
||||
|
||||
Examples:
|
||||
| current | latest | available |
|
||||
| v2.2.0-beta | v2.3.0-beta | true |
|
||||
| v2.5.0-beta | v2.5.0 | true |
|
||||
| v2.5.0 | v2.5.0-beta | false |
|
||||
| v2.9.0 | v2.10.0 | true |
|
||||
| v2.5.0-beta | v2.5.0-beta | false |
|
||||
| v2.5.0 | v2.4.9 | false |
|
||||
| dev | v9.9.9 | false |
|
||||
|
||||
Scenario: The newest release is chosen from the list regardless of feed order
|
||||
Given the feed list holds v2.4.0-beta, v2.5.0-beta and v2.3.0-beta, all marked prerelease
|
||||
When the feed is read
|
||||
Then the latest release is v2.5.0-beta
|
||||
|
||||
Scenario: Drafts are ignored
|
||||
Given the feed list holds a draft release v9.0.0 and a published v2.5.0-beta
|
||||
When the feed is read
|
||||
Then the latest release is v2.5.0-beta
|
||||
|
||||
Scenario: Release notes become a plain-text list
|
||||
Given a release body "## What's new\n- **Media** library\n- Fixed [caching](http://x) bug\n\n* Faster start"
|
||||
When the notes are built
|
||||
Then notes.items is ["Media library","Fixed caching bug","Faster start"]
|
||||
And notes.released_at is the release's published date
|
||||
|
||||
Scenario: GET returns the idle shape, never applicable
|
||||
Given a verified user, running version "v2.2.0-beta", and a stored result with latest "v2.3.0-beta",
|
||||
html_url "https://git.example/fsylvain/RenewCMS/releases/tag/v2.3.0-beta" and checked_at 2026-09-30T09:41:00Z
|
||||
When the client sends GET /v1/system/update
|
||||
Then the status is 200 and the body has exactly the 12 keys
|
||||
And current is "v2.2.0-beta", latest is "v2.3.0-beta", available is true, can_apply is false, state is "idle"
|
||||
And release_url is the stored html_url and checked_at is "2026-09-30T09:41:00Z"
|
||||
And the feed is not contacted
|
||||
|
||||
Scenario: GET before any check
|
||||
Given no stored result
|
||||
When a verified user sends GET /v1/system/update
|
||||
Then the status is 200, available is false, latest is "", release_url, notes and checked_at are null, state is "idle"
|
||||
|
||||
Scenario Outline: GET is open to any verified user
|
||||
Given the caller is <role> with a valid jwt cookie
|
||||
When the client sends GET /v1/system/update
|
||||
Then the status is 200
|
||||
|
||||
Examples:
|
||||
| role |
|
||||
| owner |
|
||||
| admin |
|
||||
|
||||
Scenario: GET without a cookie
|
||||
When the client sends GET /v1/system/update with no jwt cookie
|
||||
Then the status is 401
|
||||
|
||||
Scenario: GET with a stale token
|
||||
Given the jwt cookie was minted with a token version older than the account's
|
||||
When the client sends GET /v1/system/update
|
||||
Then the status is 401 and the body is apperr.UnauthorizedErrorMessage
|
||||
|
||||
Scenario: GET by an unverified account
|
||||
When an unverified user with a valid cookie sends GET /v1/system/update
|
||||
Then the status is 403 and the body is apperr.VerificationRequiredErrorMessage
|
||||
|
||||
Scenario: The owner checks and the result is stored
|
||||
Given the fake feed returns v2.3.0-beta and the running version is "v2.2.0-beta"
|
||||
When the owner sends POST /v1/system/update/check
|
||||
Then the status is 200 with the same body shape as GET and available true
|
||||
And the fake store holds latest "v2.3.0-beta" and checked_at at the check time
|
||||
And a later GET returns the same result without contacting the feed
|
||||
|
||||
Scenario: A check with nothing newer
|
||||
Given the feed returns v2.2.0-beta and the running version is "v2.2.0-beta"
|
||||
When the owner sends POST /v1/system/update/check
|
||||
Then the status is 200 and available is false
|
||||
|
||||
Scenario: A dev build never reports an update
|
||||
Given the running version is "dev" and the feed returns v9.9.9
|
||||
When the owner sends POST /v1/system/update/check
|
||||
Then the status is 200 and available is false
|
||||
|
||||
Scenario: An admin cannot check
|
||||
Given the caller is an admin
|
||||
When the client sends POST /v1/system/update/check
|
||||
Then the status is 403 and the body is apperr.OwnerRequiredErrorMessage
|
||||
And the feed is not contacted
|
||||
|
||||
Scenario: Feed failure keeps the last result
|
||||
Given the store holds latest "v2.3.0-beta" checked at 2026-09-30T09:41:00Z and the feed returns an error
|
||||
When the owner sends POST /v1/system/update/check
|
||||
Then the status is 500
|
||||
And the store still holds the previous result and checked_at
|
||||
And no feed error detail appears in the body
|
||||
|
||||
Scenario: Check is rate limited per user
|
||||
When the owner sends 6 checks within one minute
|
||||
Then the first 5 answer 200 and the 6th answers 429 with apperr.TooManyRequestsErrorMessage and an integer Retry-After
|
||||
And the feed is contacted 5 times
|
||||
And a different owner is not limited by the first owner's checks
|
||||
And the auth limiter's counters are untouched
|
||||
|
||||
Scenario: The stored result survives a restart
|
||||
Given the JSON file store saved a result
|
||||
When a new store is opened on the same file
|
||||
Then the same latest, release_url and checked_at are returned
|
||||
And a missing or corrupt file reads as "no result" and is not fatal
|
||||
|
||||
Scenario: Release signing round trip with a throwaway key
|
||||
Given a key pair made by keygen at a path outside the repo
|
||||
When sign is run on checksums.txt with that key and verify with its public key
|
||||
Then verify exits 0
|
||||
And after one byte of checksums.txt changes, verify exits non-zero with a clear message
|
||||
And verify with a different public key exits non-zero
|
||||
|
||||
Scenario: keygen refuses a path inside the repository
|
||||
When keygen is given a path inside the work tree
|
||||
Then it exits non-zero, writes nothing and says why
|
||||
|
||||
Scenario: The placeholder public key is refused
|
||||
Given release.pub holds the placeholder (32 zero bytes, base64)
|
||||
When sign or verify uses the embedded key
|
||||
Then it exits non-zero with a message that the placeholder key has not been replaced
|
||||
|
||||
Scenario: sign refuses a private key that does not match the public key
|
||||
Given a private key whose public half differs from the public key in use
|
||||
When sign is run
|
||||
Then it exits non-zero naming the mismatch and writes no .sig file
|
||||
|
||||
Scenario Outline: task release refuses to sign
|
||||
Given <condition>
|
||||
When task release is run
|
||||
Then it fails before building any binary with a message naming <what>
|
||||
And nothing is published
|
||||
|
||||
Examples:
|
||||
| condition | what |
|
||||
| RELEASE_SIGNING_KEY is unset | RELEASE_SIGNING_KEY |
|
||||
| RELEASE_SIGNING_KEY points to a missing file | the missing path |
|
||||
| the embedded public key is the placeholder | the placeholder key |
|
||||
| the private key does not match the public key | the key mismatch |
|
||||
|
||||
Scenario: Settings, owner, Up to date (route /settings)
|
||||
Given the session role is "owner" and GET /v1/system/update resolves available false with checked_at set
|
||||
When the owner opens /settings
|
||||
Then the Updates card shows the status block with icon circle-check (success), title "You're up to date",
|
||||
text "You have the latest version of RenewCMS." and a meta line "Last checked ..." built from checked_at
|
||||
And the only button is "Check for updates" (refresh-cw, secondary)
|
||||
And no Restart button, hint or progress is rendered
|
||||
|
||||
Scenario: Settings, owner, Checking
|
||||
When the owner presses "Check for updates"
|
||||
Then the block shows icon loader-circle, "Checking for updates…" / "This usually takes a few seconds."
|
||||
in a polite live region
|
||||
And the button reads "Checking…" and is disabled
|
||||
And POST /v1/system/update/check is sent once
|
||||
|
||||
Scenario: Settings, owner, Check failed
|
||||
Given POST /v1/system/update/check answers 500 or never reaches the server
|
||||
Then the block shows icon wifi-off (warning), "Couldn't check for updates" /
|
||||
"RenewCMS couldn't reach the update server. Check your connection and try again. Your site isn't affected."
|
||||
and the meta "Last checked ..." from the kept result
|
||||
And the only button is "Try again" (rotate-cw, primary)
|
||||
|
||||
Scenario: Settings, owner, newer version, not prepared
|
||||
Given the session role is "owner" and GET /v1/system/update resolves available true, can_apply false, latest "v2.3.0-beta"
|
||||
When the owner opens /settings
|
||||
Then the block shows icon info, "Version 2.3.0-beta is available" /
|
||||
"This site can't update itself from the browser yet. Whoever manages your server can set it up once, or update RenewCMS for you."
|
||||
And "For whoever manages the server" shows the one-line code "renewcms enable-browser-updates" with a Copy button
|
||||
And the note "They run this once on the server. After that, Update and Restart appear here." shows
|
||||
And the link "How to update" (arrow-up-right) opens release_url in a new tab
|
||||
And no button other than Copy is rendered: not Update, not Restart, not even Check for updates
|
||||
|
||||
Scenario: Settings, owner, newer version, notes open
|
||||
Given the same state with notes.items and notes.released_at 2026-09-24
|
||||
When the owner activates "What's new in 2.3.0-beta" (a button with aria-expanded false then true)
|
||||
Then "Released 24 Sep 2026" and the plain-text list show, and activating it again collapses them
|
||||
And the notes are collapsed by default
|
||||
|
||||
Scenario: Settings, command copied
|
||||
When the owner presses Copy
|
||||
Then the button reads "Copied" with a check for 2 s, is announced, then reads "Copy" again
|
||||
|
||||
Scenario: Settings, admin view
|
||||
Given the session role is "admin"
|
||||
When GET /v1/system/update resolves available true with latest "v2.3.0-beta"
|
||||
Then the card shows "Version 2.3.0-beta is available" /
|
||||
"You have v2.2.0-beta. Only the owner can update RenewCMS. Ask the owner of this site." and no button
|
||||
When it resolves available false
|
||||
Then the card shows "You're up to date" / "Only the owner can update or restart the site." and no button
|
||||
|
||||
Scenario: Settings, Role changed (403)
|
||||
Given an owner is on /settings and POST /v1/system/update/check answers 403
|
||||
Then an error toast "Only the owner can do this" / "Ask the owner of this site to update RenewCMS." shows
|
||||
And the card reloads GET /v1/system/update and renders the admin view
|
||||
|
||||
Scenario: Settings, Too many requests (429)
|
||||
Given POST /v1/system/update/check answers 429 with Retry-After 42
|
||||
Then the block shows icon info "Too many tries" / "Wait a moment before trying again."
|
||||
And the button reads "Try again in 0:42", counts down each second and is re-enabled at zero
|
||||
|
||||
Scenario: A check runs on the first open after 24 hours, for the owner only
|
||||
Given the owner opens /settings and checked_at is null or older than 24 hours
|
||||
Then POST /v1/system/update/check is sent once automatically and the Checking state shows
|
||||
When checked_at is under 24 hours old
|
||||
Then no automatic check is sent
|
||||
And an admin never sends one
|
||||
|
||||
Scenario: Home banner for the owner, update available
|
||||
Given the owner is on /home and available is true with latest "v2.3.0-beta"
|
||||
Then a region labelled "Update available" shows above the content in info tone with icon circle-arrow-up,
|
||||
title "Version 2.3.0-beta is available",
|
||||
text "Update it from Settings. It takes about 2 minutes and the site stays online.",
|
||||
a "Go to Settings" button and a dismiss button
|
||||
When the owner dismisses it
|
||||
Then the banner is hidden and localStorage remembers the dismissed version
|
||||
And it shows again when latest changes to a newer version
|
||||
|
||||
Scenario: Settings sidebar pill and mobile dot
|
||||
Given the owner and available is true
|
||||
Then the Settings sidebar item shows an "Update" pill, and on mobile the menu icon shows a dot read as "Update available"
|
||||
And the pill stays after the banner is dismissed
|
||||
|
||||
Scenario: Admins and an up-to-date owner see no banner, pill or dot
|
||||
Then an admin, or an owner with available false, sees no banner, pill or dot
|
||||
|
||||
Scenario: Pending states of slices 2 and 3
|
||||
Then these states are listed as pending in the visual gate, never passing: Update available with its Update button
|
||||
(and its notes-open frame), Downloading, Verifying, Ready to install, Restart confirm, Restart request failed,
|
||||
Restarting, Restarting taking longer, Update done, Rolled back, Download failed, Verification failed,
|
||||
Plain restart confirm, restarting and done, Update already running (409), and the Home "ready to install" banner
|
||||
And the Update, Restart and progress elements are not rendered in any state of this slice
|
||||
@@ -0,0 +1,105 @@
|
||||
# QA: Updates part 1 (signed releases, release feed, notification)
|
||||
|
||||
## Automated Go (`task test`, package `test`, black-box)
|
||||
|
||||
Files: `test/update_test.go` (domain, use cases), `test/update_http_test.go` (HTTP), `test/update_feed_test.go`
|
||||
(Gitea adapter, JSON store), `test/releasesign_test.go` (signing tool). Fakes with unique names: `fakeReleaseFeed`
|
||||
(returns a list or an error, counts calls), `fakeUpdateStore`; reuse `mockUserRepo` from `test/user_test.go`.
|
||||
|
||||
HTTP setup: `validator.InitValidator()`; `token := jwtauth.New("HS256", []byte("test-secret"), nil)`; mount a fresh
|
||||
`chi.NewRouter()` at `/v1/system` mirroring `NewSystemRouter` (Verifier, Authenticator, RequireCurrentToken,
|
||||
RequireVerifiedUser; the check route also RequireOwner and the per-user limiter, 5 per minute); cookies via
|
||||
`token.Encode({"user_id","token_version"})` as in `test/stale_token_invalidation_http_test.go`. `httptest` only: no live
|
||||
server, no SQLite, no SMTP.
|
||||
|
||||
1. Version comparison, table-driven over the Examples in the feature (domain; `errors.Is` for sentinels).
|
||||
2. Feed adapter against `httptest.NewServer` serving a Gitea JSON list: path is
|
||||
`/api/v1/repos/fsylvain/RenewCMS/releases`; newest by numeric order regardless of order; drafts skipped; final beats
|
||||
beta; notes markdown converted to items; `html_url` carried; a 500 and invalid JSON return an error.
|
||||
3. `GET /v1/system/update` as owner and as admin: 200, exactly the 12 keys, `can_apply` false, `state` "idle", values
|
||||
from the stored result, `release_url` from the stored `html_url`; the fake feed call count is 0.
|
||||
4. GET before any check: `latest` "", `available` false, `release_url`, `notes`, `checked_at`, `progress`, `error`,
|
||||
`started_at` null.
|
||||
5. GET: no cookie 401; stale token 401 with `apperr.UnauthorizedErrorMessage`; unverified 403 with
|
||||
`apperr.VerificationRequiredErrorMessage`.
|
||||
6. `POST /v1/system/update/check` as owner: 200, store updated with latest, `release_url` and `checked_at`, feed called
|
||||
once, a later GET equals it. Nothing newer: `available` false. Running version "dev": `available` false.
|
||||
7. POST check as admin: 403, body `apperr.OwnerRequiredErrorMessage+"\n"`, feed not called.
|
||||
8. Feed error: 500, store unchanged (latest and `checked_at` as before), body contains no feed error text.
|
||||
9. Rate limit: owner A sends 6 checks: first 5 are 200, the 6th is 429 with `apperr.TooManyRequestsErrorMessage+"\n"`
|
||||
and an integer `Retry-After` of at least 1; feed called 5 times; owner B's first check still 200; a login-limiter
|
||||
instance built in the same test is unaffected.
|
||||
10. JSON file store in `t.TempDir()`: save, reopen, same values; missing file and corrupt file read as empty without error.
|
||||
11. Signing tool, run through `go run ./cmd/releasesign` (or its exported package) with a throwaway key pair generated
|
||||
in `t.TempDir()` at test time, always with `--pub`; never read the committed `release.pub` key except in case 13:
|
||||
keygen creates the key; sign then verify passes; one flipped byte fails verify; a different public key fails verify;
|
||||
keygen into a path inside the repo tree is refused and writes nothing; sign with a private key that does not match
|
||||
`--pub` fails, naming the mismatch, and writes no `.sig`.
|
||||
12. Placeholder: a `--pub` file holding base64 of 32 zero bytes makes sign and verify fail with the placeholder message.
|
||||
13. The committed `internal/infrastructure/update/release.pub` is checked only for shape: a valid base64 string that
|
||||
decodes to 32 bytes. Do not assert that it is or is not the placeholder, since the user replaces it later.
|
||||
14. Review items, not Go tests: `internal/domain/update` imports only the stdlib; use cases import no `net/http`,
|
||||
`gorm`, adapter or `config`; the handler imports no adapter; `UPDATE_FEED_URL` is in `requiredEnvVars`/`envDefaults`,
|
||||
`.env.example` and `docs/configuration.md`; no private key or secret value is in the repo.
|
||||
|
||||
## Automated SPA (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Mock `useApi.request` for `GET /v1/system/update` and `POST /v1/system/update/check`. Fake timers for the countdown and
|
||||
the 2 s "Copied". No test freezes the system date and none asserts a relative date or "Last checked today". Reuse the
|
||||
board's sample data: running `v2.2.0-beta`, latest `v2.3.0-beta`, released 24 Sep 2026, items from the frame's script.
|
||||
|
||||
One test per state:
|
||||
1. Up to date (owner): "You're up to date", only "Check for updates" rendered.
|
||||
2. Checking: text, polite live region, "Checking…" disabled, one POST.
|
||||
3. Check failed (500, then `NetworkError`): copy, only "Try again", last result kept.
|
||||
4. Not prepared (available, `can_apply` false): copy, command line, note, "How to update" link with `href` equal to
|
||||
`release_url` and an external target; no button other than Copy.
|
||||
5. Notes open: collapsed by default, `aria-expanded` toggles, "Released 24 Sep 2026" and the plain-text list.
|
||||
6. Command copied: Copy writes the command to the clipboard mock, reads "Copied" for 2 s, then "Copy".
|
||||
7. Admin view: both texts, no button; no auto check.
|
||||
8. Role changed (403): toast "Only the owner can do this" / "Ask the owner of this site to update RenewCMS.", then a
|
||||
reload of the GET and the admin view.
|
||||
9. 429: "Too many tries", button "Try again in 0:42" counting down to enabled.
|
||||
10. Auto check: sent once when `checked_at` is null or older than 24 h, not when fresh, never for admin.
|
||||
11. Home banner: shown for the owner when available, copy and "Go to Settings" navigates to /settings, dismiss writes
|
||||
the version to localStorage and hides it, same version stays hidden after remount, a newer `latest` shows it again,
|
||||
absent for admin and when not available.
|
||||
12. Sidebar pill and mobile dot: owner and available only, pill remains after the banner is dismissed; absent for admin.
|
||||
13. No Update, Restart or progressbar element exists in any of the states above; the shown version drops the leading
|
||||
"v" ("Version 2.3.0-beta") while the Version card keeps `v2.2.0-beta`.
|
||||
|
||||
## Visual (`task visual:check -- settings` and `-- home`; desktop and mobile, light and dark; every capture within 10 %)
|
||||
|
||||
Add the setups to `web/visual/states.ts` using the board's own sample data. Exact values from
|
||||
`docs/design/source/AppFrame.dc.html` (Settings and Home blocks); layout as drawn, not from the prose.
|
||||
|
||||
- [ ] Settings: Checking (mock a never-resolving check after the click setup), Not prepared, Admin view (available),
|
||||
Too many requests (429, Retry-After 42).
|
||||
- [ ] Settings, `skip: 'clock-sensitive'` (the meta line is relative: "Last checked today at 09:41", "yesterday at 18:12"):
|
||||
Up to date, Check failed, Admin view (up to date).
|
||||
- [ ] Settings, Not prepared: command copied (the "Copied" state is time-bound; Vitest only), listed as not visual.
|
||||
- [ ] Home: Update available (owner), with the sidebar pill and the mobile dot as drawn.
|
||||
- [ ] Pending, never passing (slices 2 and 3): Update available with its Update button, Update available with notes
|
||||
open (its frame draws the Update button), Downloading, Verifying, Ready to install, Restart confirm, Restart
|
||||
request failed, Restarting, Restarting taking longer, Update done, Rolled back, Download failed, Verification
|
||||
failed, Plain restart (confirm, restarting, done), Update already running (409), Home "ready to install" banner.
|
||||
- [ ] Not covered by screenshots: keyboard and focus order, focus ring on the "Updates" heading (`id="updates-h"`),
|
||||
hover and motion, the Copy announcement, the dismiss button focus.
|
||||
|
||||
## Manual verification
|
||||
|
||||
- Replace the placeholder key: run `task release:keygen`, store the private key outside the repo, set
|
||||
`RELEASE_SIGNING_KEY` to its path, commit the printed public key as `internal/infrastructure/update/release.pub`, and
|
||||
rebuild. Never commit or paste the private key. Until this is done, `task release` must fail with the placeholder message.
|
||||
- Run `task release` with `RELEASE_SIGNING_KEY` unset, pointing to a missing file, and pointing to a key that does not
|
||||
match `release.pub`: each fails before any binary is built, with a message naming the cause. With the correct key,
|
||||
`checksums.txt` and `checksums.txt.sig` appear next to the six binaries and `cmd/releasesign verify` passes on them.
|
||||
- Against the real Gitea feed (default `UPDATE_FEED_URL`), run a dev API with a lower fake version and press "Check for
|
||||
updates" in the SPA: it reports the newest release; the "How to update" link opens that release's page.
|
||||
- Real release notes (the markdown body) render as a sensible plain-text list.
|
||||
- Restart the API and confirm the last result and `checked_at` survive, in `update-status.json` beside `DB_FILE`.
|
||||
- Embedded SPA build (no `-tags nofrontend`) serves /settings and /home with the card, banner and pill.
|
||||
- Confirm `docs/release-notes.md` (publish step: attach `checksums.txt` and `checksums.txt.sig` with the six binaries),
|
||||
`docs/deployment.md` and `docs/configuration.md` are updated, and that nothing was published.
|
||||
- Release gate: slices 1 to 3 ship together, so no released build shows `renewcms enable-browser-updates` before slice 3
|
||||
implements it.
|
||||
Reference in new issue
Block a user