Record completed internal CMS rollout

This commit is contained in:
Kevin
2026-09-04 22:25:57 -04:00
parent dc3ec3ff30
commit 5652339676
5 changed files with 234 additions and 142 deletions
+15 -7
View File
@@ -19,13 +19,21 @@ Phase 6 adds two validated Markdown collections:
homepage.
Both collections require frontmatter and support `draft: true`. Draft entries
are excluded from the rendered pages and sitemap. This is a Git-backed pilot;
it does not add a CMS or client-editing access.
are excluded from the rendered pages and sitemap. The internal Decap CMS now
edits these collections through the protected `cms` staging branch; client and
public editing remain disabled.
Images must live under `public/images/`, be referenced with descriptive alt
text where rendered, and remain at or below the 500 KiB pilot limit. The build
will reject malformed site JSON and oversize source images.
Journal `pubDate` and optional `updatedDate` fields use Decap's supported
`datetime` control in date-only UTC mode. New posts default `pubDate` to the
current UTC date. Repository verification rejects the unsupported `date`
widget so the missing-control failure cannot silently return.
CMS images must live in the flat `public/images/uploads/` directory and be
referenced with descriptive alt text where rendered. Approved source rasters
may be at most 25 MiB; each build produces a metadata-free public copy no
larger than 1920 pixels or 500 KiB. The build rejects malformed site JSON,
disallowed image signatures, nested upload paths, and oversized sources.
Each selected build also generates `sitemap.xml` and `robots.txt`. Preview
builds disallow indexing; a future approved production build must set
`DEPLOY_ENV=production` to publish crawlable robots rules and the sitemap URL.
builds disallow indexing; production workflows set `DEPLOY_ENV=production` to
publish crawlable robots rules and the sitemap URL.
+42 -33
View File
@@ -1,9 +1,9 @@
# Decap CMS: Gitea settings
Status: the preview-only pilot was accepted for promotion on 2026-09-04. The
repository now targets the permanent `cms` staging branch. Update the exact
CORS origin and PKCE OAuth redirect only after its first preview deployment
confirms the predicted alias. Nothing has been merged to `main`.
Status: applied and verified for internal use on 2026-09-04. The repository
targets the permanent `cms` staging branch, the CMS application is deployed
from `main`, and `sloanops.admin` is the approved internal editor. Client or
public access remains out of scope.
The repository implementation targets these fixed public values:
@@ -14,7 +14,8 @@ The repository implementation targets these fixed public values:
| Gitea base URL | `https://git.sloanops.com` |
| Gitea API root | `https://git.sloanops.com/api/v1` |
| Stable CMS origin | `https://cms-3031023-xyz-www.ksloan90.workers.dev` |
| Exact OAuth redirect URI | `https://cms-3031023-xyz-www.ksloan90.workers.dev/admin/` |
| Production admin origin | `https://3031023.xyz` |
| Exact OAuth redirects | `https://cms-3031023-xyz-www.ksloan90.workers.dev/admin/` and `https://3031023.xyz/admin/` |
| Required status context | `CMS content validation / cms-content-validation (pull_request)` |
The pilot and permanent branch names are intentionally slash-free. Decap
@@ -24,10 +25,10 @@ presented as an empty collection. The permanent `cms` branch is still a
non-production staging branch: changes reach `main` only through the protected
pull-request flow.
The Workers alias was confirmed after the first approved preview deployment.
If Cloudflare later assigns a different stable origin,
update the repository configuration, CORS origin, and OAuth redirect URI
together through review before enabling login.
The stable Worker alias and production admin route were confirmed. If either
origin changes, update repository configuration, CORS, and OAuth together
through review. Branch-specific preview aliases are intentionally not
registered and will return `Unregistered Redirect URI` during login.
## Runner cache settings
@@ -53,24 +54,23 @@ action branches on every job; npm packages remain keyed separately by
## Applied CORS policy
The tracked Gitea service in
`/home/sloan/gitRepos/SloanOps/infrastructure/docker-compose.yml` contains
these entries in the existing `gitea.environment` list and was deployed
through the normal infrastructure change process:
`/home/sloan/gitRepos/SloanOps/infrastructure/docker-compose.yml` was deployed
with exact origins for the internal CMS applications. The two origins relevant
to this repository are represented by these settings:
```yaml
- GITEA__cors__ENABLED=true
- GITEA__cors__ALLOW_DOMAIN=https://cms-3031023-xyz-www.ksloan90.workers.dev
- GITEA__cors__ALLOW_DOMAIN=https://cms-3031023-xyz-www.ksloan90.workers.dev,https://3031023.xyz
- GITEA__cors__METHODS=GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
- GITEA__cors__MAX_AGE=10m
- GITEA__cors__ALLOW_CREDENTIALS=false
- GITEA__cors__HEADERS=Authorization,Content-Type
```
This is one exact origin, no wildcard, no cookie-bearing cross-origin request,
and only the API methods and non-safelisted request headers used by the Gitea
backend. Do not add the production domains, the journal preview, arbitrary
Workers preview hosts, or localhost. Gitea's same-origin login page continues
to use its own session cookie; Decap API calls use the OAuth bearer token.
These are exact origins, not wildcards, and API requests do not carry cookies.
Do not add arbitrary Worker preview hosts or localhost. Gitea's same-origin
login page continues to use its own session cookie; Decap API calls use the
OAuth bearer token.
After an approved Gitea deployment, verify a preflight without including a
token or cookie:
@@ -94,11 +94,11 @@ CORS response were verified:
- Application name: `3031023.xyz Decap CMS`.
- Confidential client: **unchecked**. The browser client must use PKCE.
- Redirect URI: exactly
`https://cms-3031023-xyz-www.ksloan90.workers.dev/admin/`, including the
trailing slash.
- Do not add wildcards, localhost, either production hostname, or the journal
preview as additional redirects.
- Redirect URIs: exactly
`https://cms-3031023-xyz-www.ksloan90.workers.dev/admin/` and
`https://3031023.xyz/admin/`, including trailing slashes.
- Do not add wildcards, localhost, the journal hostname, or branch-specific
Worker preview aliases as additional redirects.
- Copy only the public client ID into `backend.app_id` in
`public/admin/config.yml` through a reviewed commit.
- Do not store or use a client secret in the repository, Actions, Cloudflare,
@@ -158,6 +158,11 @@ merge, administrator, or bypass allowlists.
| Protection bypass allowlist | Enabled only for the named SloanOps operator user/team |
| CMS/client account | Write access, but absent from every allowlist |
Do not assume a local Git identity can use the operator bypass. A direct
protected-file push was rejected during the final Journal repair sync, and the
successful path was a short branch followed by PR #11 into `cms`. Prefer that
reviewable path for all operator-authored CMS code and configuration changes.
Enter this exact single value in **Unprotected file patterns**:
```text
@@ -197,18 +202,22 @@ Workflows, source, scripts, package files, schemas, manifests, Wrangler
configuration, `site.json` files, and the CMS application therefore cannot be
altered by choosing another branch name.
## Verification before an editor invitation
## Routine verification and promotion
1. Run `npm ci`, `npm run verify:cms`, and both site build/verifier pairs.
2. Create `cms` from the reviewed baseline only after approval.
3. Apply both protection rules together and confirm `main` is priority 1 and
`*` is priority 2.
4. Use the same non-operator account intended for Decap to prove allowed
Markdown edits succeed and every protected-path/direct-`main` attempt fails.
5. Open, but do not merge, a pull request into `main`. Confirm Gitea reports
the exact required status context above and blocks merge without both its
successful result and a current SloanOps approval.
6. Confirm both branch previews remain no-index and production is unchanged.
2. For CMS application/configuration work, branch from current `cms`, open a
pull request back into `cms`, and review the no-index version previews.
The required CMS status is scoped to pull requests into `main`, so the local
full check is mandatory before this staging merge.
3. Open `cms` to `main` and confirm Gitea reports the exact required status
context above and blocks merge without its successful result and current
SloanOps approval.
4. Merge deliberately, then confirm each affected production Worker workflow
succeeds and smoke-test the public routes.
5. Merge the resulting `main` commit back into `cms` through another
protected-branch pull request if the histories diverge.
6. Confirm previews remain no-index and never expand OAuth/CORS for a temporary
branch alias.
Do not treat a Decap UI restriction as authorization. Gitea's first-match
branch rules are the enforcement boundary, and the pull-request build is the
+84 -50
View File
@@ -1,19 +1,21 @@
# Decap CMS Gitea pilot and promotion
Status: the preview-only pilot was accepted for promotion to the permanent
`cms` staging branch on 2026-09-04. Repository configuration now targets the
predicted stable `cms` preview origin. CORS and OAuth must be changed together
only after that first preview confirms the alias. Nothing has been merged to
`main`, and production remains unchanged.
Status: completed and operational for internal SloanOps use as of 2026-09-04.
The reviewed CMS application is deployed from `main`, Decap writes only to the
protected `cms` staging branch, and the final Journal date-control repair is
present on both branches. Production deployments from the approved merge
completed successfully.
This remains an internal capability for `sloanops.admin`, not a public feature
or a client-editing offer. No client account has been invited.
Updated: 2026-09-04
## Purpose
Use the `3031023.xyz` demonstration repository to determine whether
Decap CMS can provide a useful editing interface for all sites owned under one
root domain without allowing an editor to publish directly to production or
modify deployment code.
Record and operate the Decap CMS editing interface for both sites in the
`3031023.xyz` demonstration repository without allowing an editor to publish
directly to production or modify deployment code.
The pilot covers both the primary Happy Tails site and its journal subdomain.
That matches the SloanOps repository boundary: one private repository per root
@@ -21,8 +23,9 @@ domain, with independently built sites beneath `sites/`. A future client who
owns the root-domain site also owns the hosted subdomains represented in that
repository.
This plan does not authorize a production merge, domain change, contact-form
activation, repository invitation, or removal of a rollback resource.
The completed internal rollout does not authorize public discovery, a client
repository invitation, contact-form changes, a broader OAuth grant, or removal
of a rollback resource.
## Current evidence
@@ -41,6 +44,13 @@ activation, repository invitation, or removal of a rollback resource.
writes directly to the configured branch.
- The Gitea backend does not support Git LFS. Pilot media must remain small,
ordinary repository assets.
- The approved `sloanops.admin` account can authenticate at the production
admin route, load all collections, and write through the `cms` branch.
- Gitea rejects protected-file pushes to `cms` and direct pushes to `main`;
operator code/configuration changes therefore use pull requests as well.
- Arbitrary branch-preview hostnames are deliberately absent from the OAuth
redirect and CORS allowlists. Login from one returns `Unregistered Redirect
URI`; rendered branch previews remain usable without expanding OAuth.
References:
@@ -56,8 +66,8 @@ recorded in [Decap CMS pilot: Gitea settings](decap-cms-gitea-setup.md).
## Repository implementation
The reviewed pilot implementation is the baseline for the permanent `cms`
staging branch:
The reviewed production implementation and permanent `cms` staging branch
provide the following controls:
- `public/admin/` provides a permanently no-index admin shell and one shared
configuration for both site builds.
@@ -68,6 +78,10 @@ staging branch:
- All four Markdown content collections and every persisted frontmatter field
are modeled with the current Astro/Zod bounds. Markdown controls remain
text-first; embedded images and general file widgets are absent.
- Journal publish and optional updated dates use the supported Decap
`datetime` widget in date-only UTC mode. New posts default to the current UTC
date, and the verifier rejects the unsupported `date` widget that caused the
original create/update failures.
- A fixed image-only Markdown record controls the primary home, About,
Services, and Contact page imagery without exposing either `site.json`.
- Team and page-image fields accept optional bounded uploads. Editors can add,
@@ -94,11 +108,11 @@ staging branch:
repository cache without trusting a mutable tag or refreshing GitHub on every
job.
The pilot dependencies were verified on `cms-pilot`. The same protection rules
apply to `cms`; its exact-origin CORS and OAuth values are the remaining
operator-controlled promotion step.
The pilot dependencies were verified on `cms-pilot`, promoted to the permanent
`cms` branch, and deployed to `main`. Exact-origin CORS, PKCE OAuth, branch
protection, previews, validation, and production deployment are all applied.
## Proposed pilot architecture
## Operating architecture
```text
Decap at /admin/
@@ -126,16 +140,17 @@ is acceptable because both sites belong to the same root-domain repository and
the same owner. One Decap configuration exposes clearly labeled Primary Site
and Journal collection groups.
Predicted preview URLs for branch `cms`:
Confirmed stable preview URLs for branch `cms`:
- `https://cms-3031023-xyz-www.ksloan90.workers.dev/admin/`
- `https://cms-3031023-xyz-www.ksloan90.workers.dev/`
- `https://cms-3031023-xyz-blog.ksloan90.workers.dev/`
Confirm the aliases from the successful Gitea Actions run rather than treating
these predicted URLs as authoritative.
The production admin entry is `https://3031023.xyz/admin/`. Use the stable
`cms` admin origin or the production admin entry for interactive CMS testing;
do not use arbitrary branch aliases for OAuth login.
## Initial Decap scope
## Internal Decap scope
Allow editing and creation only within these collections:
@@ -248,34 +263,30 @@ Preview deployment remains branch-specific and may use the existing Worker
workflows. Cloudflare credentials stay scoped to the deployment step; content
validation does not need them.
Because Decap writes to a fixed `cms` branch, SloanOps creates the pull
request in Gitea. After an approved merge, synchronize the CMS branch with
`main` before the next editing cycle. Do not automate production merge.
Because Decap writes to a fixed `cms` branch, SloanOps creates the pull request
from `cms` to `main` in Gitea. Internal CMS application/configuration changes
start from the current `cms` state, enter protected `cms` through an
operator-reviewed pull request, and are then promoted with the staged content.
After an approved `main` merge, synchronize its merge commit back into `cms`
through a protected-branch pull request before the next editing cycle. Do not
automate production merge.
The accepted pilot is promoted by replacing the disposable branch with the
permanent, slash-free `cms` branch. It remains a preview/staging branch and
retains the same pull-request, validation, and approval path into `main`.
## Gitea and Decap setup
## Routine operating sequence
1. Add a non-confidential Gitea OAuth application for the exact preview
`/admin/` redirect URI or URIs. Store only the public client ID in Decap.
2. Enable Gitea API CORS for the exact approved preview origin or origins.
Allow only required methods and headers, including `Authorization` and
`Content-Type`; do not use a wildcard origin.
3. Create `cms` from the reviewed repository baseline.
4. Review the pinned Decap admin assets beneath `public/admin/`. Do not replace
the exact version and integrity value with a floating range.
5. Configure the Gitea backend for `SloanOps/3031023-xyz` and branch
`cms`.
6. Model every persisted frontmatter field with limits matching
`src/content.config.ts`. Do not let a CMS save silently remove an existing
field.
7. Apply and verify the `main` and fallback branch protections before inviting
or testing a non-operator editor.
8. Add the secret-free pull-request validation workflow.
9. Push only the review branch and confirm both no-index preview builds before
any CMS login test.
1. Edit through an approved admin origin and confirm the commit lands only on
`cms` in an allowed content or media path.
2. Confirm the affected no-index Worker version preview succeeds.
3. Run the local CMS verifier and both site build/verifier pairs before any
operator-authored CMS application or configuration PR into `cms`.
4. Review the complete staged diff and open `cms` to `main`.
5. Require the stable secret-free validation status and SloanOps approval.
6. Merge deliberately and confirm the affected production Worker jobs pass.
7. Synchronize the resulting `main` merge commit back into protected `cms`
before another editing cycle.
## Test matrix
@@ -305,13 +316,36 @@ not record OAuth tokens or Cloudflare credentials.
| DC-19 | Refresh, sign out, reauthenticate, and let a session expire | Session behavior is understandable and no unsaved-content loss is observed. |
| DC-20 | Review commit attribution and audit history | Gitea identifies the editor and the exact content/media changes. |
Do not merge DC-15 during the initial infrastructure and editing test. A later
harmless demo-content merge requires explicit approval and repeats both public
site checks after deployment.
The initial no-merge restriction applied only while the pilot was being
evaluated. Promotion is now allowed through the protected workflow above; each
release still requires explicit review and repeats both public site checks.
## Acceptance criteria
## Completion record — 2026-09-04
Accept the pilot only when:
- PR #1 launched the no-index Decap shell and Gitea-backed collections.
- PRs #2 and #3 tightened image filename validation and added bounded,
metadata-stripping build optimization.
- PRs #4 and #5 synchronized the reviewed implementation into `cms` and then
promoted it to `main`.
- Disposable Journal, page-image, and FAQ edits exposed workflow and schema
behavior; the test content was reverted and the temporary FAQ was removed.
- PR #9 consolidated the rollback history and resynchronized the branches.
- Failed Journal content runs were traced to Decap's unsupported `date` widget.
PR #10 replaced it with date-only `datetime` controls, defaulted new publish
dates, and added a verifier regression guard.
- PR #11 synchronized the final repair into protected `cms` after the direct
protected-file push was correctly rejected.
- CMS validation and both site builds passed locally. The PR #10 required
validation passed, and production blog run 342 plus primary run 343 both
completed successfully.
- The deployed Journal editor was opened at `3031023.xyz/admin/`; Publish date
displayed a populated UTC date and Updated date displayed an optional date
control. No post was saved during this final smoke test.
## Operating acceptance boundary
The internal pilot was accepted against these controls. Re-evaluate them before
any client rollout:
- both site families are understandable in one CMS;
- content edits preserve every modeled field and pass Astro validation;
@@ -329,7 +363,7 @@ account control over executable deployment files.
## Rollback and cleanup
If the pilot is rejected or paused:
If the internal CMS is paused:
1. Revert or delete disposable changes on `cms`.
2. Delete the two disposable preview aliases/versions if desired; do not delete
@@ -341,7 +375,7 @@ If the pilot is rejected or paused:
6. Remove or retain branch protections deliberately; never weaken `main` as
cleanup.
## Implementation estimate
## Historical implementation estimate
Expected hands-on implementation time: **6–9 hours**, normally one focused
working day. Budget up to two elapsed days if OAuth/CORS behavior or Gitea
+26 -20
View File
@@ -1,24 +1,30 @@
# Preview deployment and later cutover controls
# Worker version previews
The preview workflow deploys static output only. It has two deliberately
separate Worker names:
Non-`main` pushes build and verify the affected site, then upload a
non-production version of its production Worker with a readable branch alias.
They do not deploy the version to production traffic.
| Site key | Preview Worker | Future hostname |
| --- | --- | --- |
| `www` | `3031023-xyz-www-preview` | `3031023.xyz` |
| `blog` | `3031023-xyz-blog-preview` | `blog.3031023.xyz` |
| Site key | Versioned Worker | Stable `cms` alias | Production hostname |
| --- | --- | --- | --- |
| `www` | `3031023-xyz-www` | `cms-3031023-xyz-www.ksloan90.workers.dev` | `3031023.xyz` |
| `blog` | `3031023-xyz-blog` | `cms-3031023-xyz-blog.ksloan90.workers.dev` | `blog.3031023.xyz` |
`workers.dev` is the only preview destination. Neither Wrangler configuration
contains a route, custom domain, zone ID, account ID, or token. Cloudflare
credentials remain Gitea repository secrets. The preview workflow uses the
existing `CF_API_TOKEN`, which has Account Workers Scripts edit/write for this
pilot plus the retained Pages rollback permissions. It uses `CF_ACCOUNT_ID` as
the account selector. Do not grant DNS edit permissions for routine preview
deployment.
The workflows derive the preview alias from the branch name, build with
preview robots rules, run output verification, and call `wrangler versions
upload`. They use the existing `CF_API_TOKEN` and `CF_ACCOUNT_ID` repository
secrets but contain no DNS, routes, custom-domain mutations, account IDs, or
tokens in source.
The primary Worker must pass its public preview test matrix before an operator
records the exact Pages deployment/DNS state and requests production-cutover
approval. A production deployment should use a separately named approved
Worker or an explicitly promoted Worker version; it must not be an ordinary
push job. Attaching `3031023.xyz`, configuring the `www.3031023.xyz` redirect,
and attaching `blog.3031023.xyz` each remain manual approval-boundary actions.
Only the stable CMS and production admin origins are registered with Gitea
OAuth/CORS. A CMS login attempt from a temporary branch alias is expected to
fail with `Unregistered Redirect URI`; do not add a wildcard. Review rendered
site output on the branch alias and use `https://3031023.xyz/admin/` or the
stable `cms` admin origin for editing tests.
Relevant pushes to `main` use separate production workflows and `wrangler
deploy` for the affected Worker after a production build and verifier pass.
The existing domain bindings do not change during routine deployments.
The older `*-preview` Wrangler configurations and Workers are retained as
rollback artifacts. Current branch workflows use Worker version previews of
the production-named Workers instead.