fix(deploy): archive script defaults to old host and checks for the 2021 build
DEPLOY: set LETSENCRYPT_HOST with VIRTUAL_HOST in step 4, blog redirects point to /work/writing/<slug>/, drop the archive refresh row, and note that sources stay stale while a broken commit is at head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd68876149
commit
ec416bdade
3 files changed
+84
-12
No files matched your search
+12
-8
@@ -90,21 +90,25 @@ both stacks to `site.yml` and the payload table in `server/README.md` like the o
|
||||
`lucaswinther.info` currently serves the 2021 site, and the blog lives at `blog.lucaswinther.info`
|
||||
(Ghost). Do these in order and check each step before the next.
|
||||
|
||||
1. [ ] Run `scripts/archive-old-site.sh` if `archive/2021/` is stale; check `git status`, commit.
|
||||
1. [ ] If `archive/2021/` is stale, refresh it while the 2021 site is still on the apex:
|
||||
`OLD_SITE_URL=https://lucaswinther.info scripts/archive-old-site.sh`; check `git status`, commit.
|
||||
(The script's default is `https://old.lucaswinther.info`, and it refuses to write unless the page is
|
||||
the 2021 Create React App build, so it can never archive the new site.)
|
||||
2. [ ] DNS: add `old.lucaswinther.info` pointing at the homelab (same target as the current site).
|
||||
3. [ ] Deploy the `old-site` stack. Open `https://old.lucaswinther.info/`: projects, images and the
|
||||
three report PDFs load.
|
||||
4. [ ] Deploy `portfolio-next` **without** moving the apex yet: temporarily set `VIRTUAL_HOST` to
|
||||
`new.lucaswinther.info` (and add that DNS record), or test with a hosts-file entry. Verify
|
||||
4. [ ] Deploy `portfolio-next` **without** moving the apex yet: temporarily set both `VIRTUAL_HOST` and
|
||||
`LETSENCRYPT_HOST` to `new.lucaswinther.info` (and add that DNS record), or test with a hosts-file entry. Verify
|
||||
`/status` as above, plus `/`, `/work/`, `/off-the-clock/`.
|
||||
5. [ ] Move `lucaswinther.info` to the new container: stop whatever serves the 2021 site on the apex,
|
||||
set `VIRTUAL_HOST`/`LETSENCRYPT_HOST: lucaswinther.info`, redeploy, wait for the certificate.
|
||||
`curl -I https://lucaswinther.info/` must show 200 from the new site.
|
||||
6. [ ] Confirm the new site's links to the 2021 archive point at `https://old.lucaswinther.info/`.
|
||||
7. [ ] Later, once stable: blog redirects. Old post URLs (e.g. `lucaswinther.info/blog/...` or the
|
||||
former blog paths) get a 301 to the same post on `https://blog.lucaswinther.info/` (nginx-proxy
|
||||
`vhost.d` location files, or Ghost redirects). List the old URLs from the 2021 site and the
|
||||
blog's own history before writing them; this repo does not know them.
|
||||
7. [ ] Later, once stable: blog redirects (spec §6.2). Old post URLs on `blog.lucaswinther.info/<slug>/`
|
||||
get a 301 **to** the same post on the new site, `https://lucaswinther.info/work/writing/<slug>/`
|
||||
(nginx-proxy `vhost.d` location files for the blog host, or Ghost redirects). Off-the-clock posts
|
||||
live at `/off-the-clock/<slug>/` instead. List the old URLs from the blog's own history before
|
||||
writing them; this repo does not know them.
|
||||
8. [ ] Rollback at any point: set `VIRTUAL_HOST` back to the old container and redeploy; the archive
|
||||
stays up at `old.lucaswinther.info`.
|
||||
|
||||
@@ -116,7 +120,6 @@ both stacks to `site.yml` and the payload table in `server/README.md` like the o
|
||||
| Update the app | Merge to `main`; once CI is green, deploy with `-e pull=always`. A new app version rebuilds the current content. |
|
||||
| Roll back the app | Pin `image:` to `...portfolio-next:<sha>` of a good commit and deploy. |
|
||||
| Rotate a secret | Update it in OpenBao (same `bao kv put`, all four fields), deploy (re-renders `.env`, recreates the container). |
|
||||
| Refresh the 2021 archive | `scripts/archive-old-site.sh [--force]`, commit, copy to the config repo. |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -129,6 +132,7 @@ Start with `/status` (`lastError`, `sources`), then `docker logs portfolio-next`
|
||||
| A source shows `ok: false` | Its token expired or the service is down. | Rotate the token; meanwhile cached data is served. |
|
||||
| Pages answer 503 "No build yet" | No successful build so far. | Read `lastError`. |
|
||||
| `lastError` lists `file → field: message` | The content commit is invalid; the previous build keeps serving. | Fix the content; push. |
|
||||
| Ghost/Trakt/Hardcover data stops updating | While a broken commit sits at the branch head, the live build is not rebuilt, so its sources are not refreshed either. | Fix the content; the next build refetches. |
|
||||
| `EACCES` under `/data` | `data/` is not owned by uid 1000. | Re-run the playbook or `chown -R 1000:1000 data`. |
|
||||
| Compose refuses to start: `... is missing a value` | `.env` lacks a secret. | Step 1, then deploy again. |
|
||||
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mirrors the live 2021 site (a Create React App build) into archive/2021/ so it can be served
|
||||
# Mirrors the 2021 site (a Create React App build) into archive/2021/ so it can be served
|
||||
# frozen at old.lucaswinther.info (deploy/compose.old-site.yml). Idempotent: files already
|
||||
# present are skipped; delete archive/2021 (or pass --force) to refetch. Files that 404 are
|
||||
# skipped with a warning; any other failure aborts.
|
||||
# Usage: scripts/archive-old-site.sh [--force]
|
||||
# It refuses to write anything unless the page at the base URL is that CRA build, so it can never
|
||||
# archive the new site over the old one. Before the DNS cutover the 2021 site is still on the apex:
|
||||
# OLD_SITE_URL=https://lucaswinther.info scripts/archive-old-site.sh
|
||||
# Usage: [OLD_SITE_URL=…] [ARCHIVE_DEST=…] scripts/archive-old-site.sh [--force]
|
||||
set -euo pipefail
|
||||
base=${OLD_SITE_URL:-https://lucaswinther.info}
|
||||
base=${OLD_SITE_URL:-https://old.lucaswinther.info}
|
||||
base=${base%/}
|
||||
dest="$(cd "$(dirname "$0")/.." && pwd)/archive/2021"
|
||||
dest=${ARCHIVE_DEST:-"$(cd "$(dirname "$0")/.." && pwd)/archive/2021"}
|
||||
force=0; [ "${1:-}" = "--force" ] && force=1
|
||||
|
||||
probe=$(mktemp)
|
||||
trap 'rm -f "$probe"' EXIT
|
||||
if ! curl -sS --fail --location --output "$probe" "$base/"; then
|
||||
echo "error: could not fetch $base/" >&2; exit 1
|
||||
fi
|
||||
if ! grep -qE 'webpackJsonpprojects-page|Lucas Winther Portfolio' "$probe"; then
|
||||
echo "error: $base/ is not the 2021 site (no CRA build markers); refusing to write $dest" >&2
|
||||
echo " before the cutover use OLD_SITE_URL=https://lucaswinther.info" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$dest"
|
||||
fetched=0 skipped=0 missing=0
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// Runs scripts/archive-old-site.sh against a local server, writing into a temp ARCHIVE_DEST.
|
||||
let page = '';
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const path = new URL(req.url).pathname;
|
||||
if (path === '/') return new Response(page, { headers: { 'content-type': 'text/html' } });
|
||||
if (path === '/info.json') return Response.json({});
|
||||
return new Response('not found', { status: 404 });
|
||||
},
|
||||
});
|
||||
const base = `http://localhost:${server.port}`;
|
||||
|
||||
// Async spawn: the server runs in this process, so a blocking spawn would starve it.
|
||||
async function run(dest: string) {
|
||||
const p = Bun.spawn(['bash', 'scripts/archive-old-site.sh'], {
|
||||
env: { ...process.env, ARCHIVE_DEST: dest, OLD_SITE_URL: base },
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
const [exitCode, stderr] = await Promise.all([p.exited, new Response(p.stderr).text()]);
|
||||
return { exitCode, stderr };
|
||||
}
|
||||
|
||||
afterAll(() => server.stop(true));
|
||||
|
||||
describe('archive-old-site.sh', () => {
|
||||
test('defaults to old.lucaswinther.info', () => {
|
||||
expect(readFileSync('scripts/archive-old-site.sh', 'utf8')).toContain('base=${OLD_SITE_URL:-https://old.lucaswinther.info}');
|
||||
});
|
||||
|
||||
test('refuses to write when the page is not the 2021 CRA build', async () => {
|
||||
page = '<!doctype html><html><head><title>Example Person</title></head><body>new site</body></html>';
|
||||
const dest = mkdtempSync(join(tmpdir(), 'archive-'));
|
||||
const r = await run(dest);
|
||||
expect(r.exitCode).not.toBe(0);
|
||||
expect(r.stderr).toContain('not the 2021 site');
|
||||
expect(readdirSync(dest)).toEqual([]);
|
||||
});
|
||||
|
||||
test('archives the 2021 build', async () => {
|
||||
page = '<!doctype html><html><head><title>Lucas Winther Portfolio</title></head><body><script>window.webpackJsonpprojects-page=1</script></body></html>';
|
||||
const dest = join(mkdtempSync(join(tmpdir(), 'archive-')), '2021');
|
||||
const r = await run(dest);
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(existsSync(join(dest, 'index.html'))).toBe(true);
|
||||
expect(readFileSync(join(dest, 'index.html'), 'utf8')).toContain('Lucas Winther Portfolio');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user