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

225 lines
9.5 KiB
Markdown

# Decap CMS: Gitea settings
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:
| 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` |
| 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
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 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
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` 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,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
```
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:
```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 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,
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 |
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
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 a 25 MiB source safety ceiling before deployment. The build
re-encodes the deployed copy to at most 1920 pixels and 500 KiB and strips
embedded metadata.
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.
## Routine verification and promotion
1. Run `npm ci`, `npm run verify:cms`, and both site build/verifier pairs.
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
content/media integrity boundary.