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

214 lines
8.8 KiB
Markdown

# 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`.
The repository implementation targets these fixed public values:
| Setting | Value |
| --- | --- |
| Gitea repository | `SloanOps/3031023-xyz` |
| Decap write branch | `cms` |
| 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/` |
| Required status context | `CMS content validation / cms-content-validation (pull_request)` |
The pilot and permanent branch names are intentionally slash-free. Decap
CMS 3.15.1 inserts the configured Gitea branch directly into its tree-API URL;
a slash-named branch such as `cms/pilot` is split as an API path and its 404 is
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.
## Runner cache settings
The Dockerized runner uses a persistent `/data` volume and the same dedicated
Docker network as its job containers. Its non-confidential cache settings are:
```yaml
cache:
enabled: true
dir: /data/cache
host: sloanops-gitea-runner
port: 8088
offline_mode: true
container:
network: sloanops-actions
```
`offline_mode` is safe here because every external `uses:` reference is pinned
to a reviewed full commit SHA. It prevents the runner from refreshing mutable
action branches on every job; npm packages remain keyed separately by
`package-lock.json` through setup-node's cache integration.
## 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:
```yaml
- GITEA__cors__ENABLED=true
- GITEA__cors__ALLOW_DOMAIN=https://cms-3031023-xyz-www.ksloan90.workers.dev
- 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.
After an approved Gitea deployment, verify a preflight without including a
token or cookie:
```bash
curl --request OPTIONS 'https://git.sloanops.com/api/v1/version' \
--header 'Origin: https://cms-3031023-xyz-www.ksloan90.workers.dev' \
--header 'Access-Control-Request-Method: GET' \
--header 'Access-Control-Request-Headers: authorization,content-type'
```
The response must allow only the configured origin, requested method, and
headers. Repeat with an unrelated origin and confirm that Gitea does not grant
it cross-origin access. Also verify normal Git, web UI, API, and Actions
behavior before continuing.
## OAuth application
The Gitea OAuth2 application was created after the stable preview origin and
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.
- 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,
or Decap configuration.
The CMS account still needs repository write access; the branch rules below
are the security boundary that reduces that write access to pilot content and
media paths.
## Applied branch protection rules
Create both rules before granting a CMS/client account write access or testing
login. Gitea uses only the first matching rule, so the order is material.
### Priority 1: production `main`
| Gitea control | Required value |
| --- | --- |
| Protected branch pattern | `main` |
| Priority | `1` (first/highest) |
| Push | Disabled for everyone; no push allowlist |
| Force push | Disabled |
| Required approvals | `1` |
| Approval allowlist | Enabled; only the named SloanOps operator user/team |
| Dismiss stale approvals | Enabled |
| Ignore stale approvals | Disabled |
| Block rejected reviews | Enabled |
| Block outstanding official review requests | Enabled |
| Block outdated pull requests | Enabled |
| Status checks | Enabled |
| Status check pattern | `CMS content validation / cms-content-validation (pull_request)` |
| Merge allowlist | Enabled; only the named SloanOps operator user/team |
| Administrators must follow protection | Enabled |
| Protection bypass allowlist | Disabled |
Leave both file-pattern fields empty on this rule. Direct pushes are already
disabled, and executable or configuration changes must remain possible only in
a separate operator-authored, reviewed pull request. The fallback rule prevents
CMS users from placing those files on any source branch.
This rule means every `main` change arrives through a current pull request,
passes the one secret-free job that validates the CMS contract and builds and
verifies both sites, receives a current SloanOps approval, and is merged by a
SloanOps operator. A CMS/client account must not be added to the approval,
merge, administrator, or bypass allowlists.
### Priority 2: all-branch fallback
| Gitea control | Required value |
| --- | --- |
| Protected branch pattern | `*` |
| Priority | `2` (after `main`) |
| Push | Disabled, except the unprotected file patterns below |
| Force push | Disabled |
| Unprotected file patterns | Exact semicolon-separated value below |
| Protected file patterns | Empty; every path omitted from the unprotected list remains protected |
| Protection bypass allowlist | Enabled only for the named SloanOps operator user/team |
| CMS/client account | Write access, but absent from every allowlist |
Enter this exact single value in **Unprotected file 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
```
Do not add a nested upload glob. The current Decap configuration writes media
directly into the flat uploads directory, and the local verifier rejects nested
media paths. Gitea file patterns are case-insensitive; CI verifies actual file
signatures and the 500 KiB limit before a preview can upload.
Everything not listed is protected from CMS/client pushes on `cms` and
on every differently named branch, including:
```text
.gitea/**
astro.config.mjs
package.json
package-lock.json
schemas/**
scripts/**
src/**
wrangler/**
sites.json
sites/*/site.json
public/admin/**
public/_headers
public/images/*
```
The final `public/images/*` line describes the effect of omission, not an
additional Gitea field value: existing approved images stay protected while
the explicitly listed `public/images/uploads/*.<raster>` paths remain writable.
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
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.
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
content/media integrity boundary.