docs(plan): implementation plan for under-construction placeholder
Four-task plan: tests → page+assets → workflow → README. TDD via
src/placeholder.test.ts (vitest only discovers src/**/*.test.ts).
Atomic commits per task; the plan is wired off the spec at commit
9b0c36b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
# Under-construction placeholder Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Ship a branded "back shortly" page for cameleer.io plus a manual-trigger Gitea workflow that swaps it onto the live origin on demand, recoverable by re-running the existing `deploy.yml`.
|
||||
|
||||
**Architecture:** Standalone HTML in a top-level `placeholder/` directory, plus two PNG asset copies. A new `.gitea/workflows/deploy-placeholder.yml` rsyncs that directory to the same Hetzner docroot used by `deploy.yml` (`--delete` enabled, `deploy-production` concurrency group shared so the two workflows queue rather than race). No Astro build dependency, so the placeholder still ships when the main build is broken — which is the worst case where one is needed.
|
||||
|
||||
**Tech Stack:** Plain HTML5 + inlined CSS, Google Fonts (DM Sans, JetBrains Mono), bash + rsync over SSH:222, Vitest 1 for static-content assertions.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-25-under-construction-placeholder-design.md` (commit `9b0c36b`).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Status | Responsibility |
|
||||
|---|---|---|
|
||||
| `placeholder/index.html` | Create | Static under-construction page. Single self-contained file, references the two sibling PNGs by relative path. Contains `__SALES_EMAIL__` substitution token (used twice — `mailto:` href and link text). |
|
||||
| `placeholder/cameleer-logo.png` | Create (copy) | Hero logo. Copy of `public/icons/cameleer-192.png` (~36 KB). |
|
||||
| `placeholder/favicon.png` | Create (copy) | Browser tab icon. Copy of `public/icons/cameleer-32.png` (~2.4 KB). |
|
||||
| `src/placeholder.test.ts` | Create | Static assertions that the placeholder HTML has the contract the deploy workflow depends on (sentinel string, token, references, no JS, etc.). Lives in `src/` because `vitest.config.ts` only discovers `src/**/*.test.ts`. |
|
||||
| `.gitea/workflows/deploy-placeholder.yml` | Create | Manual-dispatch workflow: substitute sales email → rsync `placeholder/` to docroot → smoke-test the live origin. |
|
||||
| `README.md` | Modify | Append a "Placeholder mode" subsection under "Deployment". |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add tests for the placeholder HTML
|
||||
|
||||
**Why first:** Establishes the contract the workflow depends on — sentinel string, substitution token, asset references — before any markup is written, so we can't accidentally drop one of them later.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/placeholder.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test file**
|
||||
|
||||
Create `src/placeholder.test.ts` with this exact content:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const placeholderDir = join(process.cwd(), 'placeholder');
|
||||
const indexPath = join(placeholderDir, 'index.html');
|
||||
|
||||
describe('placeholder/index.html', () => {
|
||||
const html = readFileSync(indexPath, 'utf8');
|
||||
|
||||
it('starts with the HTML5 doctype', () => {
|
||||
expect(html.toLowerCase().trimStart()).toMatch(/^<!doctype html>/);
|
||||
});
|
||||
|
||||
it('has the back-shortly title', () => {
|
||||
expect(html).toContain('<title>Cameleer — Back shortly</title>');
|
||||
});
|
||||
|
||||
it('is not indexable by search engines', () => {
|
||||
expect(html).toContain('<meta name="robots" content="noindex">');
|
||||
});
|
||||
|
||||
it('declares the dark color-scheme matching the live site', () => {
|
||||
expect(html).toContain('<meta name="color-scheme" content="dark">');
|
||||
expect(html).toContain('<meta name="theme-color" content="#060a13">');
|
||||
});
|
||||
|
||||
it('contains the sentinel string the deploy workflow greps for', () => {
|
||||
// The workflow's post-deploy smoke test fails if this string is missing.
|
||||
expect(html).toContain('Routes are remapping');
|
||||
});
|
||||
|
||||
it('uses the live hero subhead verbatim', () => {
|
||||
expect(html).toContain(
|
||||
'Cameleer is the hosted runtime and observability platform for Apache Camel — auto-traced, replay-ready, cross-service correlated. The 3 AM page becomes a 30-second answer.'
|
||||
);
|
||||
});
|
||||
|
||||
it('contains __SALES_EMAIL__ tokens at both the mailto href and the link text', () => {
|
||||
const matches = html.match(/__SALES_EMAIL__/g) ?? [];
|
||||
expect(matches.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('contains no other __TOKEN__ style placeholders', () => {
|
||||
// Guard against a forgotten token that would survive the sed substitution.
|
||||
const allTokens = html.match(/__[A-Z][A-Z0-9_]+__/g) ?? [];
|
||||
const nonSales = allTokens.filter((t) => t !== '__SALES_EMAIL__');
|
||||
expect(nonSales).toEqual([]);
|
||||
});
|
||||
|
||||
it('references the sibling cameleer-logo.png by relative path', () => {
|
||||
expect(html).toContain('src="./cameleer-logo.png"');
|
||||
});
|
||||
|
||||
it('references the sibling favicon.png by relative path', () => {
|
||||
expect(html).toContain('href="./favicon.png"');
|
||||
});
|
||||
|
||||
it('has no <script> tags (placeholder must work without JS)', () => {
|
||||
expect(html).not.toMatch(/<script[\s>]/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder/ asset siblings', () => {
|
||||
it('cameleer-logo.png exists on disk', () => {
|
||||
expect(existsSync(join(placeholderDir, 'cameleer-logo.png'))).toBe(true);
|
||||
});
|
||||
|
||||
it('favicon.png exists on disk', () => {
|
||||
expect(existsSync(join(placeholderDir, 'favicon.png'))).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test suite to verify the new tests fail**
|
||||
|
||||
Run: `npm test`
|
||||
|
||||
Expected: vitest fails when loading `src/placeholder.test.ts` because `readFileSync` throws `ENOENT` on the missing `placeholder/index.html`. The pre-existing `src/middleware.test.ts` suite must still pass.
|
||||
|
||||
- [ ] **Step 3: Commit the failing tests**
|
||||
|
||||
```bash
|
||||
git add src/placeholder.test.ts
|
||||
git commit -m "test(placeholder): add static-content tests for under-construction page"
|
||||
```
|
||||
|
||||
Note: this commit is intentionally a red bar. Task 2 turns it green in a single follow-up commit. If you prefer a green-only history, fold this commit into Task 2's commit at the end.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create the placeholder page and copy assets
|
||||
|
||||
**Files:**
|
||||
- Create: `placeholder/index.html`
|
||||
- Create: `placeholder/cameleer-logo.png` (copy of `public/icons/cameleer-192.png`)
|
||||
- Create: `placeholder/favicon.png` (copy of `public/icons/cameleer-32.png`)
|
||||
|
||||
- [ ] **Step 1: Copy the PNG assets into `placeholder/`**
|
||||
|
||||
The repo's full-resolution `public/cameleer-logo.svg` is 1.5 MB (embedded raster data) and is not used here. The 192 px PNG is the correct size and weight for the placeholder hero.
|
||||
|
||||
```bash
|
||||
mkdir -p placeholder
|
||||
cp public/icons/cameleer-192.png placeholder/cameleer-logo.png
|
||||
cp public/icons/cameleer-32.png placeholder/favicon.png
|
||||
ls -la placeholder/
|
||||
```
|
||||
|
||||
Expected: both files present. `cameleer-logo.png` ~36 KB; `favicon.png` ~2.4 KB.
|
||||
|
||||
- [ ] **Step 2: Write `placeholder/index.html`**
|
||||
|
||||
Create `placeholder/index.html` with this exact content. Note both occurrences of `__SALES_EMAIL__` — the deploy workflow substitutes them via `sed ... -g`.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#060a13">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="description" content="Cameleer is briefly offline. We'll be back on the trail in a moment.">
|
||||
|
||||
<title>Cameleer — Back shortly</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./favicon.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono&display=swap">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #060a13;
|
||||
--accent: #f0b429;
|
||||
--text: #e8eaed;
|
||||
--text-muted: #9aa3b2;
|
||||
--text-faint: #828b9b;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
background-image: radial-gradient(60% 60% at 50% 50%, rgba(240, 180, 41, 0.10), transparent 70%);
|
||||
color: var(--text);
|
||||
font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
}
|
||||
main {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2.5rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin: 0 0 1.75rem;
|
||||
}
|
||||
.eyebrow {
|
||||
display: inline-block;
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
color: var(--accent);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 1.5rem;
|
||||
font-weight: 700;
|
||||
font-size: clamp(2.25rem, 4.5vw, 4rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
max-width: 18ch;
|
||||
}
|
||||
.subhead {
|
||||
margin: 0 0 2rem;
|
||||
max-width: 42rem;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.micro {
|
||||
margin: 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.micro a { color: inherit; text-decoration: none; }
|
||||
.micro a:hover, .micro a:focus { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img class="logo" src="./cameleer-logo.png" alt="Cameleer" width="96" height="96">
|
||||
<p class="eyebrow">✦ Routes are remapping.</p>
|
||||
<h1>We're back on the trail<br>in a moment.</h1>
|
||||
<p class="subhead">Cameleer is the hosted runtime and observability platform for Apache Camel — auto-traced, replay-ready, cross-service correlated. The 3 AM page becomes a 30-second answer.</p>
|
||||
<p class="micro">cameleer.io · <a href="mailto:__SALES_EMAIL__">__SALES_EMAIL__</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the placeholder tests to verify they pass**
|
||||
|
||||
Run: `npm test -- placeholder`
|
||||
|
||||
Expected: all tests in `src/placeholder.test.ts` pass. (`-- placeholder` is a vitest test-name filter that runs only the new file's `describe` blocks for fast feedback.)
|
||||
|
||||
- [ ] **Step 4: Run the full test suite to verify no regressions**
|
||||
|
||||
Run: `npm test`
|
||||
|
||||
Expected: every existing test still passes (the middleware/CSP suite is the only other one).
|
||||
|
||||
- [ ] **Step 5: Visual verification in a browser**
|
||||
|
||||
The `__SALES_EMAIL__` token will be visible in the rendered page — that is expected; it's substituted at deploy time. Confirm visual treatment.
|
||||
|
||||
```bash
|
||||
npx serve placeholder -l 4322
|
||||
# then open http://localhost:4322 in a browser
|
||||
```
|
||||
|
||||
Confirm by eye:
|
||||
1. Centered single-column layout, logo on top.
|
||||
2. Dark background (#060a13) with a faint amber radial glow centered.
|
||||
3. Italic amber eyebrow `✦ Routes are remapping.`.
|
||||
4. Bold display heading wraps onto two lines on desktop ("We're back on the trail" / "in a moment.").
|
||||
5. Subhead reads as muted body text below.
|
||||
6. Mono microcopy at the bottom shows `cameleer.io · __SALES_EMAIL__` in faint grey, with the token rendered as a `mailto:` link.
|
||||
7. Resize the window to ~360 px wide — layout stays centered, heading scales down via `clamp()`, no horizontal scroll.
|
||||
|
||||
Stop the server with Ctrl-C when done.
|
||||
|
||||
- [ ] **Step 6: Commit the placeholder page and assets**
|
||||
|
||||
```bash
|
||||
git add placeholder/index.html placeholder/cameleer-logo.png placeholder/favicon.png
|
||||
git commit -m "feat(placeholder): add under-construction page with branded teaser
|
||||
|
||||
Standalone HTML + two sibling PNGs, no Astro build dependency.
|
||||
Carries __SALES_EMAIL__ substitution tokens that the new deploy
|
||||
workflow replaces at deploy time."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add the deploy-placeholder workflow
|
||||
|
||||
**Files:**
|
||||
- Create: `.gitea/workflows/deploy-placeholder.yml`
|
||||
|
||||
- [ ] **Step 1: Write `.gitea/workflows/deploy-placeholder.yml`**
|
||||
|
||||
Create with this exact content. Mirrors `deploy.yml`'s SSH/rsync pattern but skips `npm ci`/`astro build`/Lighthouse — the placeholder is hand-authored static and must be deployable when the main build is broken.
|
||||
|
||||
```yaml
|
||||
# -----------------------------------------------------------------------------
|
||||
# cameleer-website — Deploy under-construction placeholder
|
||||
#
|
||||
# MANUAL TRIGGER ONLY. Replaces the live cameleer.io docroot with a static
|
||||
# "back shortly" page. Recovery: trigger Actions → deploy → Run workflow on
|
||||
# the desired main commit.
|
||||
#
|
||||
# Shares the deploy-production concurrency group with deploy.yml so the two
|
||||
# workflows queue rather than race on the same docroot.
|
||||
#
|
||||
# This workflow does NOT run npm/astro. The placeholder is hand-authored
|
||||
# static HTML in placeholder/, deliberately decoupled from the main build so
|
||||
# it can ship even when the main build is broken (which is the worst case in
|
||||
# which a placeholder is needed).
|
||||
#
|
||||
# Required secrets (repo settings → Actions → Secrets):
|
||||
# SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS
|
||||
# PUBLIC_SALES_EMAIL
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
name: deploy-placeholder
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Substitute sales email into placeholder
|
||||
env:
|
||||
PUBLIC_SALES_EMAIL: ${{ secrets.PUBLIC_SALES_EMAIL }}
|
||||
run: |
|
||||
set -e
|
||||
: "${PUBLIC_SALES_EMAIL:?PUBLIC_SALES_EMAIL secret must be set}"
|
||||
sed -i "s|__SALES_EMAIL__|${PUBLIC_SALES_EMAIL}|g" placeholder/index.html
|
||||
if grep -q '__SALES_EMAIL__' placeholder/index.html; then
|
||||
echo "Token __SALES_EMAIL__ still present after substitution — refusing to ship."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
SFTP_KEY: ${{ secrets.SFTP_KEY }}
|
||||
SFTP_KNOWN_HOSTS: ${{ secrets.SFTP_KNOWN_HOSTS }}
|
||||
run: |
|
||||
set -e
|
||||
: "${SFTP_KEY:?SFTP_KEY secret must be set}"
|
||||
: "${SFTP_KNOWN_HOSTS:?SFTP_KNOWN_HOSTS secret must be set}"
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "$SFTP_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
if ! command -v rsync >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y --no-install-recommends rsync openssh-client
|
||||
fi
|
||||
|
||||
- name: Deploy via rsync
|
||||
env:
|
||||
SFTP_USER: ${{ secrets.SFTP_USER }}
|
||||
SFTP_HOST: ${{ secrets.SFTP_HOST }}
|
||||
SFTP_PATH: ${{ secrets.SFTP_PATH }}
|
||||
run: |
|
||||
: "${SFTP_USER:?SFTP_USER secret must be set}"
|
||||
: "${SFTP_HOST:?SFTP_HOST secret must be set}"
|
||||
: "${SFTP_PATH:?SFTP_PATH secret must be set}"
|
||||
# Hetzner Webhosting splits SSH into two ports:
|
||||
# port 22 — SFTP only, no remote command exec
|
||||
# port 222 — full SSH with shell exec (rsync needs this)
|
||||
# `--rsync-path=/usr/bin/rsync` tells the local rsync where to find
|
||||
# the remote binary on Hetzner's locked-down PATH.
|
||||
# `BatchMode=yes` disables interactive prompts.
|
||||
rsync -avz --delete --rsync-path=/usr/bin/rsync \
|
||||
-e "ssh -p 222 -i $HOME/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" \
|
||||
placeholder/ "$SFTP_USER@$SFTP_HOST:$SFTP_PATH/"
|
||||
|
||||
- name: Post-deploy smoke test
|
||||
run: |
|
||||
set -e
|
||||
echo "Confirming the placeholder is live on www.cameleer.io..."
|
||||
BODY=$(curl -sf https://www.cameleer.io/)
|
||||
echo "$BODY" | grep -qF 'Routes are remapping' \
|
||||
|| { echo "Sentinel string missing — placeholder did not land."; exit 1; }
|
||||
echo "$BODY" | grep -qF 'mailto:' \
|
||||
|| { echo "mailto: link missing — sales email substitution may have failed."; exit 1; }
|
||||
curl -sfI https://www.cameleer.io/cameleer-logo.png > /dev/null \
|
||||
|| { echo "cameleer-logo.png not reachable on the live origin."; exit 1; }
|
||||
echo "Placeholder is live."
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the YAML parses**
|
||||
|
||||
Run a quick Node-based parse check (no extra dep needed; Node ships with no YAML parser, so use a one-off `npx`):
|
||||
|
||||
```bash
|
||||
npx --yes js-yaml .gitea/workflows/deploy-placeholder.yml > /dev/null && echo "YAML OK"
|
||||
```
|
||||
|
||||
Expected: `YAML OK`. If `js-yaml` errors, re-read the file for stray tabs or unbalanced quoting.
|
||||
|
||||
- [ ] **Step 3: Verify the existing deploy.yml is unchanged**
|
||||
|
||||
Run: `git diff .gitea/workflows/deploy.yml`
|
||||
|
||||
Expected: empty output (the new workflow is additive only).
|
||||
|
||||
- [ ] **Step 4: Commit the workflow**
|
||||
|
||||
```bash
|
||||
git add .gitea/workflows/deploy-placeholder.yml
|
||||
git commit -m "ci(deploy): add deploy-placeholder workflow
|
||||
|
||||
Manual-trigger workflow that substitutes PUBLIC_SALES_EMAIL into
|
||||
placeholder/index.html, rsyncs placeholder/ to the Hetzner docroot
|
||||
over SSH:222, then smoke-tests the live origin for the sentinel
|
||||
string, mailto link, and logo URL.
|
||||
|
||||
Shares the deploy-production concurrency group with deploy.yml so
|
||||
the two workflows can never race on the same docroot. Recovery is
|
||||
the regular deploy.yml — no separate un-placeholder workflow."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Document the placeholder mode in the README
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md` (append a subsection under "## Deployment", after the existing "Rollback" paragraph and before "**Security headers**")
|
||||
|
||||
- [ ] **Step 1: Read the current README to locate the insertion point**
|
||||
|
||||
Run: `cat README.md`
|
||||
|
||||
Locate the line `Rollback: trigger the deploy workflow on the previous \`main\` commit (Actions UI lets you pick a ref).`. The new subsection goes immediately after it, separated by a blank line, before the `**Security headers**` paragraph.
|
||||
|
||||
- [ ] **Step 2: Insert the placeholder-mode subsection**
|
||||
|
||||
Use the `Edit` tool with these exact arguments. `old_string` is the existing two-paragraph boundary; `new_string` reproduces it with the new `### Placeholder mode` subsection wedged in between.
|
||||
|
||||
`file_path`: `README.md`
|
||||
|
||||
`old_string`:
|
||||
````
|
||||
Rollback: trigger the deploy workflow on the previous `main` commit (Actions UI lets you pick a ref).
|
||||
|
||||
**Security headers** (HSTS, CSP, X-Frame-Options, etc.) are owned by **Cloudflare Transform Rules**, not by anything in this repo.
|
||||
````
|
||||
|
||||
`new_string`:
|
||||
````
|
||||
Rollback: trigger the deploy workflow on the previous `main` commit (Actions UI lets you pick a ref).
|
||||
|
||||
### Placeholder mode
|
||||
|
||||
To put the site into "back shortly" mode, trigger Gitea → **Actions → deploy-placeholder → Run workflow**. To bring the real site back, trigger **Actions → deploy → Run workflow** on the desired `main` commit. Both workflows share the `deploy-production` concurrency group, so they can never run simultaneously.
|
||||
|
||||
The placeholder is hand-authored static HTML in `placeholder/` and does NOT depend on `npm`/`astro build` — it is deliberately decoupled from the main build so it can ship even when that build is broken.
|
||||
|
||||
**Security headers** (HSTS, CSP, X-Frame-Options, etc.) are owned by **Cloudflare Transform Rules**, not by anything in this repo.
|
||||
````
|
||||
|
||||
- [ ] **Step 3: Verify the README still renders cleanly**
|
||||
|
||||
Run: `cat README.md | head -60`
|
||||
|
||||
Confirm by eye that the new subsection appears under "Deployment", the surrounding paragraphs are intact, and there is exactly one blank line between adjacent blocks.
|
||||
|
||||
- [ ] **Step 4: Commit the README update**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs(readme): add placeholder mode section
|
||||
|
||||
Documents the deploy-placeholder workflow trigger and the recovery
|
||||
path back to the real site via deploy.yml."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
- [ ] **Run the full test suite one more time**
|
||||
|
||||
Run: `npm test`
|
||||
|
||||
Expected: all tests pass — both `src/middleware.test.ts` and `src/placeholder.test.ts`.
|
||||
|
||||
- [ ] **Confirm the four commits are in place**
|
||||
|
||||
Run: `git log --oneline -5`
|
||||
|
||||
Expected (top-down): README docs commit, deploy-placeholder.yml commit, placeholder feat commit, placeholder test commit, then the spec commit (`9b0c36b docs(spec): ...`).
|
||||
|
||||
- [ ] **Sanity-check the placeholder directory ships only what it should**
|
||||
|
||||
Run: `ls -la placeholder/`
|
||||
|
||||
Expected: exactly three files — `index.html`, `cameleer-logo.png`, `favicon.png`. No stray `.test.ts`, `.DS_Store`, etc. (If anything else appears, remove it before merging — `rsync --delete` would otherwise push it to the live origin.)
|
||||
|
||||
- [ ] **Push and trigger the first real run** (operator step, not part of the implementation)
|
||||
|
||||
Push the branch, merge to `main` once reviewed, then in Gitea: **Actions → deploy-placeholder → Run workflow** on `main`. Verify by visiting `https://www.cameleer.io/` that the placeholder renders, then trigger **Actions → deploy → Run workflow** to restore the real site.
|
||||
Reference in New Issue
Block a user