Files
3031023-xyz-public/docs/decap-cms-pilot.md
T

16 KiB
Raw Blame History

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.

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.

The pilot covers both the primary Happy Tails site and its journal subdomain. That matches the SloanOps repository boundary: one private repository per root 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.

Current evidence

  • 3031023.xyz and blog.3031023.xyz build independently from this repository and deploy through separate Workers Static Assets workflows.
  • A push outside main already uploads no-index primary and/or blog preview versions according to the paths changed.
  • Blog posts, FAQs, services, and team members are Markdown collections with Astro/Zod validation and are suitable for a bounded Decap pilot.
  • The live Gitea instance reported version 1.27.3 during the Phase 9 review. Decap's Gitea backend requires Gitea 1.20 or newer.
  • Credential-free preflight verification grants the exact stable CMS origin and does not grant an unrelated origin.
  • Decap authenticates to Gitea with a non-confidential PKCE OAuth application. Its Gitea backend currently has no editorial pull-request workflow, so it writes directly to the configured branch.
  • The Gitea backend does not support Git LFS. Pilot media must remain small, ordinary repository assets.

References:

The exact Gitea CORS, OAuth, and two-rule branch-protection settings are recorded in Decap CMS pilot: Gitea settings.

Repository implementation

The reviewed pilot implementation is the baseline for the permanent cms staging branch:

  • public/admin/ provides a permanently no-index admin shell and one shared configuration for both site builds.
  • The shell pins Decap CMS 3.15.1 by exact URL and verified SHA-384 integrity value rather than a floating release range.
  • The backend is fixed to SloanOps/3031023-xyz and cms and contains only the public client ID for the non-confidential Gitea PKCE application.
  • 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.
  • 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, replace, or remove an image reference while retaining accessible image descriptions and a layout fallback. URL insertion and SVG paths are rejected. Builds additionally verify the extension, raster signature, flat upload path, and 500 KiB limit.
  • .gitea/workflows/cms-content-validation.yml runs on every pull request into main, uses no secrets, and reports CMS content validation / cms-content-validation (pull_request) after checking the Decap contract and building and verifying both sites.
  • The existing branch preview workflows remain in place. Local output checks now prove preview robots rules and page metadata stay no-index and that the admin response has a no-index header rule.
  • Validation, preview, and production workflows cache npm's download store by package-lock.json while retaining clean npm ci installs with --prefer-offline --no-audit --no-fund. The runner advertises its cache on a fixed host and port reachable from job containers, avoiding the timeout caused by its former auto-detected private address. Warm jobs reuse dependency tarballs without caching node_modules.
  • All external workflow actions are pinned to reviewed commit SHAs. The runner may therefore set cache.offline_mode: true and reuse its local action 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.

Proposed pilot architecture

Decap at /admin/
        |
        | Gitea PKCE OAuth and content/media commits only
        v
cms branch
        |
        +--> primary no-index Worker preview
        |
        +--> blog no-index Worker preview
        |
        v
manual Gitea pull request to protected main
        |
        v
required validation + SloanOps approval
        |
        v
production merge and deploy only with separate approval

The shared public/ directory may serve /admin/ in both preview builds. That 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:

  • 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.

Initial Decap scope

Allow editing and creation only within these collections:

CMS group Repository path Initial capability
Primary site FAQs sites/www/content/faq/** Create, edit, draft, reorder, delete after confirmation
Primary services sites/www/content/services/** Create, edit, draft, reorder, feature
Primary team sites/www/content/team/** Create, edit, draft, reorder, add, replace, or remove a portrait
Primary page images sites/www/content/page-images/primary.md Add, replace, or remove home, About, Services, and Contact images and edit their descriptions
Journal posts sites/blog/content/posts/** Create, edit, draft, date, tag, update
Shared pilot media public/images/uploads/** Select or upload bounded raster images

Do not expose sites.json, either site.json, schemas, workflows, Wrangler configuration, package files, source components, scripts, contact routing, or analytics through the CMS during this pilot.

Configure the default media library with a conservative file-size limit. Do not accept executables, archives, HTML, scripts, or SVG uploads. Asset rights, alt text, crop, dimensions, and optimization remain operator review items even when the CMS accepts an upload.

The media gallery lists only public/images/uploads, so the pilot seeds that directory with the images referenced by CMS-controlled fields. Removing an image from content clears the field and uses the page's text-only or initials fallback; it does not delete a potentially shared asset. Delete a gallery asset only after confirming that no content entry references it.

Repository protection model

main

  • Disable direct pushes for CMS/client users.
  • Require a pull request.
  • Require at least one SloanOps approval.
  • Require the dedicated CMS content validation status.
  • Block merging an outdated or rejected pull request.
  • Permit merges only by the SloanOps operator allowlist.
  • Keep force-push disabled.

Fallback rule for non-production branches

Create a branch-protection fallback covering every branch, with the specific main rule at higher priority. CMS/client users may push only the following unprotected patterns:

sites/www/content/**
sites/blog/content/**
public/images/uploads/*.jpg
public/images/uploads/*.jpeg
public/images/uploads/*.png
public/images/uploads/*.webp
public/images/uploads/*.avif

Add the equivalent nested-path globs if the CMS is allowed to create media subdirectories. Listing raster extensions explicitly prevents an editor from using the media directory as a general unprotected file path.

All other paths remain protected, including:

.gitea/**
astro.config.mjs
package.json
package-lock.json
schemas/**
scripts/**
src/**
wrangler/**
sites.json
sites/*/site.json
public/admin/**

SloanOps operator accounts may use the branch-rule bypass allowlist for reviewed development. Verify the restrictions through the same Gitea account used by Decap; a UI-only field restriction is not a repository security boundary.

The protected-path fallback matters even though main requires a pull request. Preview workflows inherit Cloudflare credentials. An editor must not be able to create another branch, alter a workflow or executable build file, and run unreviewed code with those credentials.

Pull-request validation

Add a secret-free validation workflow for pull requests into main. It should always:

  1. install locked dependencies;
  2. build and verify the primary site;
  3. build and verify the blog;
  4. reject content that fails the Astro/Zod schema; and
  5. report one stable required status context.

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.

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

  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.

Test matrix

Record the Gitea execution, commit, and preview identifiers for each test. Do not record OAuth tokens or Cloudflare credentials.

ID Test Expected result
DC-01 Build both sites before CMS changes Current build and verifier suites pass.
DC-02 Open each preview /admin/ route Admin shell loads and remains no-index.
DC-03 Authenticate with the approved operator account PKCE login succeeds and the configured collections load.
DC-04 Authenticate with an account without repository write access Repository editing is unavailable.
DC-05 Edit an existing draft journal post One content-only commit lands on cms; the blog preview rebuilds.
DC-06 Create, edit, and delete a disposable journal post Filename, dates, tags, body, draft state, and cleanup remain schema-valid.
DC-07 Edit a primary FAQ Only the primary preview rebuilds and displays the change.
DC-08 Edit service ordering and featured state YAML types remain correct and the primary verifier passes.
DC-09 Add, replace, and remove a team portrait The selected upload renders in the team card; removal uses the initials fallback.
DC-10 Upload a bounded raster image and select it in the fixed page-image record File lands only in the approved media path and renders on the relevant primary preview page.
DC-11 Attempt an oversized upload and a disallowed file type The CMS size limit rejects the oversized file before upload; Gitea's unprotected-extension patterns reject the disallowed path.
DC-12 Attempt to modify .gitea/workflows/**, source, package, script, schema, Wrangler, and site-manifest paths with the CMS account Gitea rejects every push.
DC-13 Attempt to create a differently named branch and change a protected file The fallback rule rejects the push; branch naming cannot bypass protection.
DC-14 Attempt a direct push to main Gitea rejects it.
DC-15 Open a pull request from cms to main Both sites validate; required review and merge controls are present.
DC-16 Attempt to merge without status or approval Gitea blocks the merge.
DC-17 Inspect both preview responses Expected routes work; preview metadata/headers prevent indexing; production remains unchanged.
DC-18 Revert disposable CMS changes on cms Preview returns to baseline without changing either public site.
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.

Acceptance criteria

Accept the pilot only when:

  • both site families are understandable in one CMS;
  • content edits preserve every modeled field and pass Astro validation;
  • previews update reliably without moving production traffic;
  • direct main pushes and protected-path changes fail for the CMS account;
  • PR validation and operator approval are mandatory;
  • media limits and repository growth remain acceptable;
  • login/session behavior is usable; and
  • rollback requires no production-domain or DNS change.

Stop and redesign if the Gitea backend cannot honor the branch/file protections, OAuth grants materially broader access than the accepted account role, content serialization loses fields, or previews require granting the CMS account control over executable deployment files.

Rollback and cleanup

If the pilot is rejected or paused:

  1. Revert or delete disposable changes on cms.
  2. Delete the two disposable preview aliases/versions if desired; do not delete production Workers or retained legacy rollback resources.
  3. Revoke/delete the Decap OAuth application.
  4. Remove the pilot CORS origins and redeploy Gitea after verifying normal Git, UI, API, and Actions behavior.
  5. Remove /admin/ only through a reviewed commit.
  6. Remove or retain branch protections deliberately; never weaken main as cleanup.

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 protection semantics require troubleshooting.

Work Estimate
Decap admin shell, pinned dependency, and Gitea configuration 1–1.5 hours
Model blog, FAQ, services, and team collections 1.5–2 hours
Add secret-free PR validation and preview links/instructions 1–1.5 hours
Configure CORS, OAuth, CMS branch, and protection rules 1–1.5 hours
Run the authentication, editing, protection, preview, and rollback matrix 1.5–2.5 hours
Record results and resolve small integration defects 0.5 hour

This estimate covers the internal demo pilot, not a polished client-editable service. Client rollout still needs observed usability, account invitation and offboarding procedures, support scope, media policy, and pricing.