diff --git a/.forgejo/workflows/ci-gates.yml b/.forgejo/workflows/ci-gates.yml index d513985..ff6bff8 100644 --- a/.forgejo/workflows/ci-gates.yml +++ b/.forgejo/workflows/ci-gates.yml @@ -8,6 +8,7 @@ on: jobs: checks: + if: ${{ github.server_url != 'https://codeberg.org' }} runs-on: codeberg-medium-lazy steps: - name: Fetch source (no git required) @@ -32,11 +33,19 @@ jobs: echo "error: missing GITHUB_SHA" exit 2 fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi rm -rf .repo mkdir -p .repo curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ -o .repo/src.tgz tar -xzf .repo/src.tgz -C .repo --strip-components=1 rm -f .repo/src.tgz diff --git a/.forgejo/workflows/deploy-cloudflare.yml b/.forgejo/workflows/deploy-cloudflare.yml index 3aecdd7..2a5dc2e 100644 --- a/.forgejo/workflows/deploy-cloudflare.yml +++ b/.forgejo/workflows/deploy-cloudflare.yml @@ -11,6 +11,7 @@ concurrency: jobs: checks: + if: ${{ github.server_url != 'https://codeberg.org' }} runs-on: codeberg-medium-lazy steps: - name: Fetch Source (no git required) @@ -35,12 +36,20 @@ jobs: echo "error: missing GITHUB_SHA" exit 2 fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi rm -rf .repo mkdir -p .repo curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ -o .repo/src.tgz tar -xzf .repo/src.tgz -C .repo --strip-components=1 rm -f .repo/src.tgz @@ -111,6 +120,7 @@ jobs: env -u NO_COLOR trunk build --release --public-url / deploy: + if: ${{ github.server_url != 'https://codeberg.org' }} needs: checks runs-on: codeberg-medium-lazy steps: @@ -136,13 +146,21 @@ jobs: echo "error: missing GITHUB_SHA" exit 2 fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi rm -rf .repo mkdir -p .repo # Use the authenticated API archive endpoint (works for private repos). curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ -o .repo/src.tgz tar -xzf .repo/src.tgz -C .repo --strip-components=1 rm -f .repo/src.tgz @@ -211,7 +229,7 @@ jobs: cd .repo curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"bootstrap ok"}' >/dev/null - name: Configure CI Age identity @@ -236,7 +254,7 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"age key ok"}' >/dev/null - name: Decrypt CI secrets from repo @@ -262,7 +280,7 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"decrypt ok"}' >/dev/null - name: Build site (web) @@ -301,7 +319,7 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"build ok"}' >/dev/null - name: Deploy worker @@ -317,5 +335,5 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"success","description":"deploy ok"}' >/dev/null diff --git a/.forgejo/workflows/runner-smoke.yml b/.forgejo/workflows/runner-smoke.yml index f3c470e..2d1f2e3 100644 --- a/.forgejo/workflows/runner-smoke.yml +++ b/.forgejo/workflows/runner-smoke.yml @@ -5,6 +5,7 @@ on: jobs: smoke: + if: ${{ github.server_url != 'https://codeberg.org' }} runs-on: codeberg-medium-lazy steps: - name: Basic runner + secret smoke test diff --git a/README.md b/README.md index f4861f8..e4e5cad 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ Runbook: cat docs/USAGE.md ``` +Git hosting topology: + +```sh +cat docs/GIT_HOSTING.md +``` + ## WebTransport Watch (MoQ) Publish (node -> Cloudflare relay): diff --git a/docs/BRANCH_PROTECTION.md b/docs/BRANCH_PROTECTION.md index 6c32c38..d0b3e02 100644 --- a/docs/BRANCH_PROTECTION.md +++ b/docs/BRANCH_PROTECTION.md @@ -1,4 +1,4 @@ -# Branch Protection (Codeberg) +# Branch Protection (Forgejo Primary) `main` should be protected to satisfy constitutional governance (`all changes merge through pull requests`) and to require CI before merge. @@ -20,6 +20,7 @@ Optional overrides: ```sh +EVERY_CHANNEL_FORGE_HOST=https://forge.every.channel \ EVERY_CHANNEL_FORGE_REPO=every-channel/every.channel \ EVERY_CHANNEL_PROTECTED_BRANCH=main \ EVERY_CHANNEL_REQUIRED_CHECKS="ci-gates / checks" \ @@ -29,7 +30,8 @@ EVERY_CHANNEL_REQUIRED_APPROVALS=1 \ Token source order: -1. `CODEBERG_TOKEN` env var -2. `secrets/codeberg-token.age` via `agenix` or `age` +1. `EVERY_CHANNEL_FORGE_TOKEN` / `FORGE_TOKEN` / `CODEBERG_TOKEN` env var +2. `secrets/forge-token.age` (preferred) via `agenix` or `age` +3. `secrets/codeberg-token.age` (compat) via `agenix` or `age` The token must have repository admin scope to edit branch protection. diff --git a/docs/DEPLOY_CLOUDFLARE.md b/docs/DEPLOY_CLOUDFLARE.md index 18df044..45dbef7 100644 --- a/docs/DEPLOY_CLOUDFLARE.md +++ b/docs/DEPLOY_CLOUDFLARE.md @@ -1,6 +1,7 @@ # Cloudflare Deploy (Forgejo Actions) This repo deploys `https://every.channel` via Wrangler. +The deploy workflow is intended to run on the primary Forgejo host (not Codeberg/GitHub mirrors). ## Prereqs @@ -13,6 +14,10 @@ CI and deploy workflows: - PR/main checks: `.forgejo/workflows/ci-gates.yml` - Deploy (main only, depends on checks): `.forgejo/workflows/deploy-cloudflare.yml` +Mirror behavior: + +- Workflow jobs are guarded to skip execution on `https://codeberg.org`. + ## Manual deploy (local) ```sh diff --git a/docs/GIT_HOSTING.md b/docs/GIT_HOSTING.md new file mode 100644 index 0000000..602abd6 --- /dev/null +++ b/docs/GIT_HOSTING.md @@ -0,0 +1,45 @@ +# Git Hosting Topology + +Primary host: + +- Forgejo (`origin`) + +Mirrors (push-only): + +- Codeberg (`mirror-codeberg`) +- GitHub (`mirror-github`) + +Codeberg and GitHub are distribution mirrors only. CI/actions should run on Forgejo primary. + +## Configure local remotes + +```sh +./scripts/git-configure-hosting.sh +``` + +Defaults: + +- `origin`: `git@forge.every.channel:every-channel/every.channel.git` +- `mirror-codeberg`: `git@codeberg.org:every-channel/every.channel.git` +- `mirror-github`: `git@github.com:every-channel/every.channel.git` + +You can override via env vars: + +- `EVERY_CHANNEL_PRIMARY_GIT_URL` +- `EVERY_CHANNEL_CODEBERG_GIT_URL` +- `EVERY_CHANNEL_GITHUB_GIT_URL` + +## Push mirrors + +```sh +./scripts/git-push-mirrors.sh +``` + +## Disable actions on Codeberg mirror + +```sh +EVERY_CHANNEL_FORGE_HOST=https://codeberg.org \ +EVERY_CHANNEL_FORGE_REPO=every-channel/every.channel \ +EVERY_CHANNEL_FORGE_ACTIONS_ENABLED=false \ +./scripts/forge-set-repo-actions.sh +``` diff --git a/justfile b/justfile index c5d871a..9cf8b9e 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,12 @@ default: ecp-lint: ./scripts/ecp-lint.sh +git-hosting: + ./scripts/git-configure-hosting.sh + +git-mirrors: + ./scripts/git-push-mirrors.sh + test-core: cargo test -p ec-core -p ec-crypto -p ec-moq -p ec-iroh -p ec-linux-iptv diff --git a/scripts/agenix-import-token-file.sh b/scripts/agenix-import-token-file.sh index 9c24ccb..770b9d6 100755 --- a/scripts/agenix-import-token-file.sh +++ b/scripts/agenix-import-token-file.sh @@ -5,7 +5,7 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${root}" in_file="${1:-secrets/token.txt}" -out_file="${2:-secrets/codeberg-token.age}" +out_file="${2:-secrets/forge-token.age}" rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-${root}/secrets.nix}" identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}" diff --git a/scripts/fj-auth-codeberg.sh b/scripts/fj-auth-codeberg.sh index 60f95e5..71cd2ae 100755 --- a/scripts/fj-auth-codeberg.sh +++ b/scripts/fj-auth-codeberg.sh @@ -1,35 +1,6 @@ #!/usr/bin/env bash set -euo pipefail +# Back-compat shim. Prefer `scripts/fj-auth-forge.sh`. root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "${root}" - -# Forgejo CLI: `fj` -# -# Auth token source order: -# 1) CODEBERG_TOKEN env var -# 2) `agenix -d secrets/codeberg-token.age` (optional) -# 3) `age -d -i secrets/codeberg-token.age` (optional) - -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 ]]; then - if command -v agenix >/dev/null 2>&1; then - export CODEBERG_TOKEN - CODEBERG_TOKEN="$(RULES="${rules_file}" agenix -d secrets/codeberg-token.age -i "${identity_file}")" - elif command -v age >/dev/null 2>&1; then - export CODEBERG_TOKEN - CODEBERG_TOKEN="$(age -d -i "${identity_file}" secrets/codeberg-token.age)" - fi -fi - -if [[ -z "${CODEBERG_TOKEN:-}" ]]; then - echo "error: CODEBERG_TOKEN is not set" >&2 - echo "hint: set CODEBERG_TOKEN or create secrets/codeberg-token.age via agenix" >&2 - exit 2 -fi - -# Avoid passing the token on the command line (shows up in process listings); use stdin. -printf "%s" "${CODEBERG_TOKEN}" | fj -H https://codeberg.org auth add-key every-channel -echo "fj configured. Try: fj -H https://codeberg.org whoami" +exec "${root}/scripts/fj-auth-forge.sh" diff --git a/scripts/fj-auth-forge.sh b/scripts/fj-auth-forge.sh new file mode 100755 index 0000000..6347d27 --- /dev/null +++ b/scripts/fj-auth-forge.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${root}" + +# Forgejo CLI: `fj` +# +# Auth token source order: +# 1) EVERY_CHANNEL_FORGE_TOKEN / FORGE_TOKEN / CODEBERG_TOKEN env var +# 2) `agenix -d secrets/forge-token.age` or `secrets/codeberg-token.age` (optional) +# 3) `age -d -i secrets/forge-token.age` or `secrets/codeberg-token.age` (optional) + +host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}" +account="${EVERY_CHANNEL_FORGE_ACCOUNT:-every-channel}" +token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forge-token.age}" +token_file_compat="${EVERY_CHANNEL_CODEBERG_TOKEN_FILE:-secrets/codeberg-token.age}" + +rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}" +identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}" + +token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}" + +load_token_from_file() { + local candidate="$1" + [[ -f "${candidate}" ]] || return 1 + if command -v agenix >/dev/null 2>&1; then + RULES="${rules_file}" agenix -d "${candidate}" -i "${identity_file}" 2>/dev/null || return 1 + return 0 + fi + if command -v age >/dev/null 2>&1; then + age -d -i "${identity_file}" "${candidate}" 2>/dev/null || return 1 + return 0 + fi + return 1 +} + +if [[ -z "${token}" ]]; then + token="$(load_token_from_file "${token_file_primary}" || true)" +fi +if [[ -z "${token}" ]]; then + token="$(load_token_from_file "${token_file_compat}" || true)" +fi + +if [[ -z "${token}" ]]; then + echo "error: forge token is not set" >&2 + echo "hint: set EVERY_CHANNEL_FORGE_TOKEN/FORGE_TOKEN or create ${token_file_primary}" >&2 + exit 2 +fi + +# Avoid passing the token on the command line (shows up in process listings); use stdin. +printf "%s" "${token}" | fj -H "${host}" auth add-key "${account}" +echo "fj configured. Try: fj -H ${host} whoami" diff --git a/scripts/fj-enforce-branch-protection.sh b/scripts/fj-enforce-branch-protection.sh index adf5fe7..6c7ab13 100755 --- a/scripts/fj-enforce-branch-protection.sh +++ b/scripts/fj-enforce-branch-protection.sh @@ -4,7 +4,7 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${root}" -host="${EVERY_CHANNEL_FORGE_HOST:-https://codeberg.org}" +host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}" 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}" @@ -13,26 +13,35 @@ 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}" +token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forge-token.age}" +token_file_compat="${EVERY_CHANNEL_CODEBERG_TOKEN_FILE:-secrets/codeberg-token.age}" -if [[ -z "${CODEBERG_TOKEN:-}" && -f secrets/codeberg-token.age && -f "${identity_file}" ]]; then +token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}" + +load_token_from_file() { + local candidate="$1" + [[ -f "${candidate}" && -f "${identity_file}" ]] || return 1 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 + RULES="${rules_file}" agenix -d "${candidate}" -i "${identity_file}" 2>/dev/null || return 1 + return 0 fi + if command -v age >/dev/null 2>&1; then + age -d -i "${identity_file}" "${candidate}" 2>/dev/null || return 1 + return 0 + fi + return 1 +} + +if [[ -z "${token}" ]]; then + token="$(load_token_from_file "${token_file_primary}" || true)" +fi +if [[ -z "${token}" ]]; then + token="$(load_token_from_file "${token_file_compat}" || true)" 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 +if [[ -z "${token}" ]]; then + echo "error: forge token is not set" >&2 + echo "hint: set EVERY_CHANNEL_FORGE_TOKEN/FORGE_TOKEN or configure ${token_file_primary}" >&2 exit 2 fi @@ -90,18 +99,18 @@ JSON )" status="$(curl -sS -o /dev/null -w '%{http_code}' \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ + -H "Authorization: token ${token}" \ "${api}/${branch}" || true)" if [[ "${status}" == "404" ]]; then curl -fsSL -X POST \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ + -H "Authorization: token ${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 "Authorization: token ${token}" \ -H "content-type: application/json" \ "${api}/${branch}" \ -d "${payload}" >/dev/null @@ -111,7 +120,7 @@ else fi current="$(curl -fsSL \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ + -H "Authorization: token ${token}" \ "${api}/${branch}")" if ! printf '%s' "${current}" | rg -q '"enable_status_check":\s*true'; then diff --git a/scripts/fj-set-age-key-secret.sh b/scripts/fj-set-age-key-secret.sh index 29e2a66..eb6388a 100755 --- a/scripts/fj-set-age-key-secret.sh +++ b/scripts/fj-set-age-key-secret.sh @@ -4,7 +4,7 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${root}" -host="${EVERY_CHANNEL_FORGE_HOST:-https://codeberg.org}" +host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}" repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}" secret_name="${EVERY_CHANNEL_FORGE_AGE_SECRET_NAME:-AGE_FORGE_SSH_KEY}" key_path="${1:-$HOME/.config/every.channel/keys/founder_ed25519}" @@ -18,7 +18,7 @@ if ! command -v fj >/dev/null 2>&1; then exit 2 fi -"${root}/scripts/fj-auth-codeberg.sh" >/dev/null +"${root}/scripts/fj-auth-forge.sh" >/dev/null key_data="$(base64 < "${key_path}" | tr -d '\n')" if [[ -z "${key_data}" ]]; then diff --git a/scripts/forge-set-repo-actions.sh b/scripts/forge-set-repo-actions.sh new file mode 100755 index 0000000..b2032da --- /dev/null +++ b/scripts/forge-set-repo-actions.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${root}" + +host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}" +repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}" +enabled_raw="${EVERY_CHANNEL_FORGE_ACTIONS_ENABLED:-false}" + +rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}" +identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}" +token_file_primary="${EVERY_CHANNEL_FORGE_TOKEN_FILE:-secrets/forge-token.age}" +token_file_compat="${EVERY_CHANNEL_CODEBERG_TOKEN_FILE:-secrets/codeberg-token.age}" + +token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}" + +load_token_from_file() { + local candidate="$1" + [[ -f "${candidate}" && -f "${identity_file}" ]] || return 1 + if command -v agenix >/dev/null 2>&1; then + RULES="${rules_file}" agenix -d "${candidate}" -i "${identity_file}" 2>/dev/null || return 1 + return 0 + fi + if command -v age >/dev/null 2>&1; then + age -d -i "${identity_file}" "${candidate}" 2>/dev/null || return 1 + return 0 + fi + return 1 +} + +if [[ -z "${token}" ]]; then + token="$(load_token_from_file "${token_file_primary}" || true)" +fi +if [[ -z "${token}" ]]; then + token="$(load_token_from_file "${token_file_compat}" || true)" +fi + +if [[ -z "${token}" ]]; then + echo "error: forge token is not set" >&2 + echo "hint: set EVERY_CHANNEL_FORGE_TOKEN/FORGE_TOKEN or configure ${token_file_primary}" >&2 + exit 2 +fi + +if [[ ! "${repo}" =~ ^[^/]+/[^/]+$ ]]; then + echo "error: EVERY_CHANNEL_FORGE_REPO must be '/' (got '${repo}')" >&2 + exit 2 +fi + +enabled_lc="$(printf '%s' "${enabled_raw}" | tr '[:upper:]' '[:lower:]')" +enabled="false" +if [[ "${enabled_lc}" == "true" || "${enabled_raw}" == "1" ]]; then + enabled="true" +fi + +owner="${repo%%/*}" +repo_name="${repo#*/}" +api="${host%/}/api/v1/repos/${owner}/${repo_name}" + +payload="$(cat </dev/null + +current="$(curl -fsSL \ + -H "Authorization: token ${token}" \ + "${api}")" + +if ! printf '%s' "${current}" | rg -q "\"has_actions\":\\s*${enabled}"; then + echo "error: repository actions state did not update to ${enabled}" >&2 + exit 2 +fi + +echo "ok: set has_actions=${enabled} for ${repo} on ${host}" diff --git a/scripts/git-configure-hosting.sh b/scripts/git-configure-hosting.sh new file mode 100755 index 0000000..c02d366 --- /dev/null +++ b/scripts/git-configure-hosting.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${root}" + +primary_remote="${EVERY_CHANNEL_PRIMARY_REMOTE:-origin}" +primary_url="${EVERY_CHANNEL_PRIMARY_GIT_URL:-git@forge.every.channel:every-channel/every.channel.git}" + +codeberg_remote="${EVERY_CHANNEL_CODEBERG_REMOTE:-mirror-codeberg}" +codeberg_url="${EVERY_CHANNEL_CODEBERG_GIT_URL:-git@codeberg.org:every-channel/every.channel.git}" + +github_remote="${EVERY_CHANNEL_GITHUB_REMOTE:-mirror-github}" +github_url="${EVERY_CHANNEL_GITHUB_GIT_URL:-git@github.com:every-channel/every.channel.git}" + +legacy_codeberg_remote="${EVERY_CHANNEL_LEGACY_CODEBERG_REMOTE:-codeberg}" + +set_remote_url() { + local name="$1" + local url="$2" + if git remote get-url "${name}" >/dev/null 2>&1; then + git remote set-url "${name}" "${url}" + else + git remote add "${name}" "${url}" + fi +} + +# If a legacy `codeberg` remote exists and mirror-codeberg does not, preserve it as a mirror remote. +if git remote get-url "${legacy_codeberg_remote}" >/dev/null 2>&1 && ! git remote get-url "${codeberg_remote}" >/dev/null 2>&1; then + git remote rename "${legacy_codeberg_remote}" "${codeberg_remote}" +fi + +set_remote_url "${primary_remote}" "${primary_url}" +set_remote_url "${codeberg_remote}" "${codeberg_url}" +set_remote_url "${github_remote}" "${github_url}" + +echo "ok: configured primary + mirror remotes" +git remote -v diff --git a/scripts/git-push-mirrors.sh b/scripts/git-push-mirrors.sh new file mode 100755 index 0000000..a8ad8e1 --- /dev/null +++ b/scripts/git-push-mirrors.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${root}" + +branch="${EVERY_CHANNEL_MIRROR_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" +push_tags="${EVERY_CHANNEL_MIRROR_PUSH_TAGS:-true}" +remotes="${EVERY_CHANNEL_MIRROR_REMOTES:-mirror-codeberg mirror-github}" + +for remote in ${remotes}; do + if ! git remote get-url "${remote}" >/dev/null 2>&1; then + echo "warn: remote not configured, skipping: ${remote}" >&2 + continue + fi + echo "sync: ${remote} (${branch})" + git push "${remote}" "${branch}:${branch}" + if [[ "${push_tags}" == "true" || "${push_tags}" == "1" ]]; then + git push "${remote}" --tags + fi +done + +echo "ok: mirror push complete" diff --git a/secrets.nix b/secrets.nix index 9a38352..1f3f59c 100644 --- a/secrets.nix +++ b/secrets.nix @@ -6,5 +6,6 @@ let in { "secrets/cloudflare-api-token.age".publicKeys = [ founder forge ]; + "secrets/forge-token.age".publicKeys = [ founder forge ]; "secrets/codeberg-token.age".publicKeys = [ founder forge ]; } diff --git a/secrets/README.md b/secrets/README.md index c9b9fd9..2b6c736 100644 --- a/secrets/README.md +++ b/secrets/README.md @@ -16,7 +16,8 @@ nix develop -c ./scripts/fj-set-age-key-secret.sh ~/.config/every.channel/keys/f - `secrets/secrets.nix`: recipients + secret file mapping - `secrets/cloudflare-api-token.age`: encrypted Cloudflare API token (used by deploy workflow) -- `secrets/codeberg-token.age`: encrypted Codeberg/Forgejo token for `fj` (optional) +- `secrets/forge-token.age`: encrypted Forgejo API token for admin scripts (optional, preferred) +- `secrets/codeberg-token.age`: encrypted Codeberg token for compatibility/mirror admin scripts (optional) ## Create / edit secrets (local)