324 lines
15 KiB
Markdown
324 lines
15 KiB
Markdown
# Decap CMS Gitea pilot
|
||
|
||
Status: repository implementation prepared for review; no OAuth application,
|
||
Gitea CORS change, branch protection, branch push, preview deployment, or
|
||
production merge has been performed.
|
||
|
||
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.
|
||
- A preflight request from the proposed CMS origin currently returns `405`
|
||
without an access-control origin header. Gitea CORS must be configured before
|
||
browser-based Decap authentication or repository access will work.
|
||
- 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:
|
||
|
||
- <https://decapcms.org/docs/gitea-backend/>
|
||
- <https://decapcms.org/docs/configure-decap-cms/>
|
||
- <https://decapcms.org/docs/widgets/image/>
|
||
- <https://docs.gitea.com/development/oauth2-provider/>
|
||
- <https://docs.gitea.com/usage/access-control/protected-branches/>
|
||
- <https://github.com/decaporg/decap-cms/issues/7115>
|
||
|
||
The exact, unapplied Gitea CORS, OAuth, and two-rule branch-protection settings
|
||
are recorded in [Decap CMS pilot: unapplied Gitea settings](decap-cms-gitea-setup.md).
|
||
|
||
## Repository implementation under review
|
||
|
||
The `review/decap-cms-pilot` branch now contains the repository-side portion of
|
||
the pilot:
|
||
|
||
- `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/pilot`. Its public
|
||
OAuth client ID remains an explicit placeholder until the separately
|
||
approved non-confidential Gitea application exists.
|
||
- All four Markdown collections and every persisted frontmatter field are
|
||
modeled with the current Astro/Zod bounds. Markdown controls are text-first;
|
||
image embedding and general file widgets are absent.
|
||
- The team image field accepts an existing approved raster path or a bounded
|
||
upload. 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.
|
||
|
||
This preparation does not make the CMS usable yet: the placeholder public
|
||
client ID, unapplied CORS setting, absent `cms/pilot` remote branch, and absent
|
||
branch rules are intentional review gates.
|
||
|
||
## Proposed pilot architecture
|
||
|
||
```text
|
||
Decap at /admin/
|
||
|
|
||
| Gitea PKCE OAuth and content/media commits only
|
||
v
|
||
cms/pilot 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.
|
||
|
||
Expected preview URLs for branch `cms/pilot`:
|
||
|
||
- `https://cms-pilot-3031023-xyz-www.ksloan90.workers.dev/admin/`
|
||
- `https://cms-pilot-3031023-xyz-www.ksloan90.workers.dev/`
|
||
- `https://cms-pilot-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, select an approved image |
|
||
| Journal posts | `sites/blog/content/posts/**` | Create, edit, draft, date, tag, update |
|
||
| Shared pilot media | `public/images/uploads/**` | Upload bounded raster images after text editing is proven |
|
||
|
||
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.
|
||
|
||
## 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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
.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/pilot` 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.
|
||
|
||
## 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/pilot` 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/pilot`.
|
||
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/pilot`; 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 | Edit a team entry without changing its image | Existing fields and image path are preserved exactly. |
|
||
| DC-10 | Upload a bounded raster image | File lands only in the approved media path and renders on the relevant preview. |
|
||
| 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/pilot` 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 the disposable CMS changes on `cms/pilot` | 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/pilot`.
|
||
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.
|