Migrate deployed docs on project rename (fixes #122) #123

Merged
qwc merged 2 commits from fix/project-rename-unreachable-docs into main 2026-07-07 20:01:53 +02:00
Owner

Problem

Fixes #122. Deployed documentation and the search index are keyed on the project's mutable slug. Renaming a project (changing its slug) only updated the DB row — the on-disk files stayed at the old-slug directory, so every doc URL 404'd, and search results kept pointing at the old slug.

Layer On rename oldnew Before After
DB (projects.slug) updated
Filesystem files stay at {base}/old/… stranded → 404 moved to {base}/new/…
versions.storage_path stale old paths breaks reindex rewritten
Bleve index project_slug/URLs = old 404 refreshed to new

Changes

  • docs.Storage.MoveProject(old, new) — relocates a project's directory on rename. No-op if nothing is deployed yet; refuses to overwrite an existing destination so it can never clobber another project's docs.
  • projects.Service.Update(project, oldSlug) — persists the row, moves storage, and rewrites each version's StoragePath. If the storage move fails it rolls the slug back in the DB so the project stays reachable at its old URL. Slug collisions map to ErrSlugConflictHTTP 409 (was a generic 500).
  • handleAdminUpdateProject — routes through the service and, on a slug change, asynchronously refreshes the search index for the renamed project's versions (delete + reindex; doc IDs are keyed on project/version ID, so stale entries are fully replaced).
  • Race guard — a per-project keyed lock (by the immutable project ID) makes archive extraction (upload) and rename mutually exclusive. Without it, a rename's os.Rename could move the directory out from under an in-flight extract, corrupting or stranding the deployment.
  • Built-in docs — the first-project tutorial now documents rename behavior (URLs change, deployed versions migrate automatically, collisions are rejected).

Tests

  • docs: MoveProject happy path, no-source no-op, refuses existing destination.
  • projects: rename migrates storage + rewrites version paths; rollback on storage-move failure keeps the project reachable; rename into a taken slug returns ErrSlugConflict.
  • Full suite green (go test ./...).

AI-assisted.

🤖 Generated with Claude Code

## Problem Fixes #122. Deployed documentation **and** the search index are keyed on the project's mutable `slug`. Renaming a project (changing its slug) only updated the DB row — the on-disk files stayed at the old-slug directory, so every doc URL 404'd, and search results kept pointing at the old slug. | Layer | On rename `old` → `new` | Before | After | |---|---|---|---| | DB (`projects.slug`) | updated | ✓ | ✓ | | Filesystem | files stay at `{base}/old/…` | stranded → **404** | moved to `{base}/new/…` | | `versions.storage_path` | stale old paths | breaks reindex | rewritten | | Bleve index | `project_slug`/URLs = `old` | **404** | refreshed to `new` | ## Changes - **`docs.Storage.MoveProject(old, new)`** — relocates a project's directory on rename. No-op if nothing is deployed yet; refuses to overwrite an existing destination so it can never clobber another project's docs. - **`projects.Service.Update(project, oldSlug)`** — persists the row, moves storage, and rewrites each version's `StoragePath`. If the storage move fails it **rolls the slug back** in the DB so the project stays reachable at its old URL. Slug collisions map to `ErrSlugConflict` → **HTTP 409** (was a generic 500). - **`handleAdminUpdateProject`** — routes through the service and, on a slug change, asynchronously refreshes the search index for the renamed project's versions (delete + reindex; doc IDs are keyed on project/version ID, so stale entries are fully replaced). - **Race guard** — a per-project keyed lock (by the *immutable* project ID) makes archive extraction (upload) and rename mutually exclusive. Without it, a rename's `os.Rename` could move the directory out from under an in-flight extract, corrupting or stranding the deployment. - **Built-in docs** — the first-project tutorial now documents rename behavior (URLs change, deployed versions migrate automatically, collisions are rejected). ## Tests - `docs`: `MoveProject` happy path, no-source no-op, refuses existing destination. - `projects`: rename migrates storage + rewrites version paths; rollback on storage-move failure keeps the project reachable; rename into a taken slug returns `ErrSlugConflict`. - Full suite green (`go test ./...`). AI-assisted. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Migrate deployed docs on project rename (fixes #122)
All checks were successful
CI / test (pull_request) Successful in 1m16s
CI / build (pull_request) Successful in 46s
CI / docker (pull_request) Has been skipped
77b514df70
Doc storage and the search index are keyed on the project slug, so
renaming a project (changing its slug) left the deployed files stranded
at the old-slug path and every doc URL 404'd. Search results pointed at
the old slug too.

- docs.Storage.MoveProject relocates a project's directory on rename
  (no-op if nothing deployed yet; refuses to overwrite an existing dest).
- projects.Service.Update persists the row, moves storage, and rewrites
  each version's StoragePath; on a storage-move failure it rolls the slug
  back so the project stays reachable. Slug collisions map to
  ErrSlugConflict (409 instead of a generic 500).
- handleAdminUpdateProject routes through the service and asynchronously
  refreshes the search index for the renamed project's versions.
- A per-project keyed lock (by immutable project ID) makes archive
  extraction and rename mutually exclusive, so a rename can't os.Rename
  the directory out from under an in-flight upload.
- Built-in docs: document rename behavior in the first-project tutorial.

AI-assisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for rename race guard and handler rename path
All checks were successful
CI / test (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 43s
CI / docker (pull_request) Has been skipped
7bf2135926
Covers the pieces added for the project-rename fix that lacked direct
tests:

- keyedMutex (locks_test.go): same-key sections are serialized (max
  concurrency 1), different keys are independent, and unlock releases.
- handleAdminUpdateProject: renaming a project keeps its deployed docs
  reachable at the new slug and 404s the old one (regression for #122),
  and a rename onto a taken slug returns 409.

Also pin the in-memory test DB pool to a single connection
(SetMaxOpenConns(1)). A ":memory:" SQLite DB is per-connection, so the
new async reindex-after-rename job could race a request onto a second,
unmigrated database. Pinning the pool makes concurrent access share one
in-memory DB — a test-harness fix; production uses a shared file/SQL DB.

AI-assisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
qwc merged commit 208b380050 into main 2026-07-07 20:01:53 +02:00
qwc deleted branch fix/project-rename-unreachable-docs 2026-07-07 20:01:53 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
qwc-open/asiakirjat!123
No description provided.