ops: document deploy secrets and enforce main branch protection
This commit is contained in:
parent
d89d3100f6
commit
f3f2b046b7
3 changed files with 178 additions and 9 deletions
35
docs/BRANCH_PROTECTION.md
Normal file
35
docs/BRANCH_PROTECTION.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Branch Protection (Codeberg)
|
||||||
|
|
||||||
|
`main` should be protected to satisfy constitutional governance (`all changes merge through pull requests`) and to require CI before merge.
|
||||||
|
|
||||||
|
## Required settings
|
||||||
|
|
||||||
|
- Protected branch: `main`
|
||||||
|
- Direct pushes disabled
|
||||||
|
- Required approvals: `1` (or stricter)
|
||||||
|
- Required status checks:
|
||||||
|
- `ci-gates / checks`
|
||||||
|
- Require signed commits: enabled
|
||||||
|
|
||||||
|
## Apply via script
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/fj-enforce-branch-protection.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional overrides:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
EVERY_CHANNEL_FORGE_REPO=every-channel/every.channel \
|
||||||
|
EVERY_CHANNEL_PROTECTED_BRANCH=main \
|
||||||
|
EVERY_CHANNEL_REQUIRED_CHECKS="ci-gates / checks" \
|
||||||
|
EVERY_CHANNEL_REQUIRED_APPROVALS=1 \
|
||||||
|
./scripts/fj-enforce-branch-protection.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Token source order:
|
||||||
|
|
||||||
|
1. `CODEBERG_TOKEN` env var
|
||||||
|
2. `secrets/codeberg-token.age` via `agenix` or `age`
|
||||||
|
|
||||||
|
The token must have repository admin scope to edit branch protection.
|
||||||
|
|
@ -5,18 +5,16 @@ This repo deploys `https://every.channel` via Wrangler.
|
||||||
## Prereqs
|
## Prereqs
|
||||||
|
|
||||||
- Forgejo Actions enabled on the repo.
|
- Forgejo Actions enabled on the repo.
|
||||||
- A Cloudflare API token stored as a Forgejo Actions secret:
|
- Forgejo Actions secret `AGE_FORGE_SSH_KEY` set to the SSH private key used to decrypt repo-encrypted age secrets.
|
||||||
- name: `CLOUDFLARE_API_TOKEN`
|
- `secrets/cloudflare-api-token.age` present in-repo and decryptable by `AGE_FORGE_SSH_KEY`.
|
||||||
|
|
||||||
The workflow is defined in `.forgejo/workflows/deploy-cloudflare.yml`.
|
CI and deploy workflows:
|
||||||
|
|
||||||
|
- PR/main checks: `.forgejo/workflows/ci-gates.yml`
|
||||||
|
- Deploy (main only, depends on checks): `.forgejo/workflows/deploy-cloudflare.yml`
|
||||||
|
|
||||||
## Manual deploy (local)
|
## Manual deploy (local)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd apps/tauri/ui
|
./scripts/deploy-workers.sh
|
||||||
trunk build --release --public-url /
|
|
||||||
|
|
||||||
cd deploy/cloudflare-worker
|
|
||||||
npm ci
|
|
||||||
npm run deploy
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
136
scripts/fj-enforce-branch-protection.sh
Executable file
136
scripts/fj-enforce-branch-protection.sh
Executable file
|
|
@ -0,0 +1,136 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "${root}"
|
||||||
|
|
||||||
|
host="${EVERY_CHANNEL_FORGE_HOST:-https://codeberg.org}"
|
||||||
|
repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
|
||||||
|
branch="${EVERY_CHANNEL_PROTECTED_BRANCH:-main}"
|
||||||
|
required_checks_csv="${EVERY_CHANNEL_REQUIRED_CHECKS:-ci-gates / checks}"
|
||||||
|
required_approvals="${EVERY_CHANNEL_REQUIRED_APPROVALS:-1}"
|
||||||
|
require_signed_commits_raw="${EVERY_CHANNEL_REQUIRE_SIGNED_COMMITS:-true}"
|
||||||
|
|
||||||
|
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}"
|
||||||
|
identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||||
|
|
||||||
|
if [[ -z "${CODEBERG_TOKEN:-}" && -f secrets/codeberg-token.age && -f "${identity_file}" ]]; then
|
||||||
|
if command -v agenix >/dev/null 2>&1; then
|
||||||
|
token="$(RULES="${rules_file}" agenix -d secrets/codeberg-token.age -i "${identity_file}" 2>/dev/null || true)"
|
||||||
|
if [[ -n "${token}" ]]; then
|
||||||
|
export CODEBERG_TOKEN
|
||||||
|
CODEBERG_TOKEN="${token}"
|
||||||
|
fi
|
||||||
|
elif command -v age >/dev/null 2>&1; then
|
||||||
|
token="$(age -d -i "${identity_file}" secrets/codeberg-token.age 2>/dev/null || true)"
|
||||||
|
if [[ -n "${token}" ]]; then
|
||||||
|
export CODEBERG_TOKEN
|
||||||
|
CODEBERG_TOKEN="${token}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${CODEBERG_TOKEN:-}" ]]; then
|
||||||
|
echo "error: CODEBERG_TOKEN is not set" >&2
|
||||||
|
echo "hint: set CODEBERG_TOKEN or configure secrets/codeberg-token.age" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "${repo}" =~ ^[^/]+/[^/]+$ ]]; then
|
||||||
|
echo "error: EVERY_CHANNEL_FORGE_REPO must be '<owner>/<repo>' (got '${repo}')" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${required_approvals}" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "error: EVERY_CHANNEL_REQUIRED_APPROVALS must be a non-negative integer" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
require_signed_commits_raw_lc="$(printf '%s' "${require_signed_commits_raw}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
require_signed_commits="false"
|
||||||
|
if [[ "${require_signed_commits_raw_lc}" == "true" || "${require_signed_commits_raw}" == "1" ]]; then
|
||||||
|
require_signed_commits="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
owner="${repo%%/*}"
|
||||||
|
repo_name="${repo#*/}"
|
||||||
|
api="${host%/}/api/v1/repos/${owner}/${repo_name}/branch_protections"
|
||||||
|
|
||||||
|
contexts_json=""
|
||||||
|
IFS=',' read -r -a contexts <<< "${required_checks_csv}"
|
||||||
|
for ctx in "${contexts[@]}"; do
|
||||||
|
trimmed="$(echo "${ctx}" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
[[ -n "${trimmed}" ]] || continue
|
||||||
|
escaped="$(printf '%s' "${trimmed}" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||||
|
if [[ -n "${contexts_json}" ]]; then
|
||||||
|
contexts_json+=", "
|
||||||
|
fi
|
||||||
|
contexts_json+="\"${escaped}\""
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${contexts_json}" ]]; then
|
||||||
|
echo "error: no required status checks specified (EVERY_CHANNEL_REQUIRED_CHECKS)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload="$(cat <<JSON
|
||||||
|
{
|
||||||
|
"rule_name": "${branch}",
|
||||||
|
"enable_push": false,
|
||||||
|
"enable_push_whitelist": false,
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"enable_status_check": true,
|
||||||
|
"status_check_contexts": [${contexts_json}],
|
||||||
|
"required_approvals": ${required_approvals},
|
||||||
|
"require_signed_commits": ${require_signed_commits}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
)"
|
||||||
|
|
||||||
|
status="$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api}/${branch}" || true)"
|
||||||
|
|
||||||
|
if [[ "${status}" == "404" ]]; then
|
||||||
|
curl -fsSL -X POST \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H "content-type: application/json" \
|
||||||
|
"${api}" \
|
||||||
|
-d "${payload}" >/dev/null
|
||||||
|
elif [[ "${status}" == "200" ]]; then
|
||||||
|
curl -fsSL -X PATCH \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H "content-type: application/json" \
|
||||||
|
"${api}/${branch}" \
|
||||||
|
-d "${payload}" >/dev/null
|
||||||
|
else
|
||||||
|
echo "error: unexpected status while reading branch protection: ${status}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
current="$(curl -fsSL \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${api}/${branch}")"
|
||||||
|
|
||||||
|
if ! printf '%s' "${current}" | rg -q '"enable_status_check":\s*true'; then
|
||||||
|
echo "error: branch protection update did not enable status checks" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! printf '%s' "${current}" | rg -q "\"required_approvals\":\\s*${required_approvals}"; then
|
||||||
|
echo "error: branch protection update did not set required approvals to ${required_approvals}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
for ctx in "${contexts[@]}"; do
|
||||||
|
trimmed="$(echo "${ctx}" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
[[ -n "${trimmed}" ]] || continue
|
||||||
|
if ! printf '%s' "${current}" | rg -F -q "\"${trimmed}\""; then
|
||||||
|
echo "error: required status check context missing after update: ${trimmed}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "ok: enforced branch protection for ${repo}:${branch}"
|
||||||
|
echo "ok: required checks: ${required_checks_csv}"
|
||||||
|
echo "ok: required approvals: ${required_approvals}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue