# 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 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/*.` 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.