Files
my-cd-webhook/ai-context/infrastructure/gitea-to-github-release.md
T
fisher 3a6fafb5c7 Generic host/repo/branch-aware CD on adnanh/webhook
Replaces the per-project deploy scripts and self-contained webhook
receivers with one manifest-driven implementation that lives outside the
application repositories and can be updated independently of them.

First targets: domaindingo test and prod on s5.
2026-08-23 07:49:52 +00:00

4.9 KiB

Gitea → GitHub Release Distribution

Pattern for projects where development lives privately in Gitea and public releases ship to a separate GitHub repo. GitHub only ever sees curated, redacted release snapshots — a clean tagged history, no Gitea history carried over. Repo/owner/package names below are placeholders; substitute per project.

Placeholders

Placeholder Meaning
$DEV_REMOTE Gitea dev repo (https://gitea.example/o/p.git)
$REL_REPO Local release git folder (publish staging repo)
$REL_REMOTE Optional GitHub remote on $REL_REPO, added later
$RELEASE_REF Gitea commit/branch/tag to cut from (e.g. main)
$VERSION Version string from package metadata

The release destination is a local git folder ($REL_REPO). Cutting a release needs no remote, username, or password. Wiring $REL_REMOTE (GitHub) and pushing is an optional later step, done once the public repo exists.

Principles

  1. Two repos, two histories. Never wired as upstream/downstream. Gitea is the source of truth; GitHub sees only release snapshots.
  2. Allow-list, never deny-list. Build the release tree from an explicit list of paths to include. A blanket worktree copy (minus excludes) is how secrets leak — forbidden. Anything not listed does not ship.
  3. Runtime-only minimal payload. Ship what's needed to install and run: source package (incl. bundled locale/, templates, assets), packaging manifest (pyproject.toml/equiv), README.md, LICENSE, and sanitized *.example.* config/service files. No tests, no CI by default.
  4. Redact real deployment data. Real configs (hosts, channels, accounts, credentials, customer data) must never reach a public repo. Treat any non-example config as private until proven otherwise.
  5. Clean history. One tagged commit (v$VERSION) per release; release diffs visible on GitHub, Gitea dev history not.
  6. Deliberate and manual. Cut from a known-good $RELEASE_REF by running a checked-in script by hand. No push-on-commit.

Do not ship: internal docs, planning/spec material, agent/tooling instructions (AGENTS.md, CLAUDE.md, copilot/agent dirs), ai-context notes, Gitea/runner ops docs. (A Gitea self-hosted-runner workflow is meaningless on GitHub Actions — if you want CI public, rewrite it GitHub-native, don't copy.)

SSH auth (when the push is wired up)

Use a per-project SSH host alias backed by a dedicated deploy key in ~/.ssh/config:

Host github-<project>-public
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_<project>-public
  IdentitiesOnly yes

Remote becomes github-<project>-public:OWNER/REPO.git — no URL/user/password at release time, key scoped to one repo, IdentitiesOnly yes stops SSH offering unrelated keys. IdentityFile may point at the .pub when the private key is in ssh-agent (agent signs); otherwise point at the private key. The release script should check the alias exists before pushing, failing early with guidance.

Once the alias and deploy key are in place for the project, remove this note's entry from docs/active-context.md — the setup is one-time and shouldn't keep occupying agent context.

Release procedure

Keep $REL_REPO as a separate local folder from the dev clone. Each release:

  1. Pick + verify $RELEASE_REF in Gitea (tests/CI green).
  2. Read $VERSION from package metadata.
  3. Rebuild $REL_REPO's tracked content from the allow-list at $RELEASE_REF via git archive (committed files only).
  4. Commit as a single Release $VERSION, tag v$VERSION — all local.
  5. Optionally push to GitHub once $REL_REMOTE exists, and optionally create a GitHub Release. PyPI publishing, if any, is a separate explicit step.

Allow-list export (sketch)

Keep the allow-list in the script, reviewed like code:

#!/usr/bin/env bash
set -euo pipefail
DEV_REF="${1:-main}"
STAGE="$(mktemp -d)"

PATHS=(                       # Anything not listed does NOT ship.
  src pyproject.toml README.md LICENSE
  examples/app.service examples/config.example.yaml examples/content.example.yaml
)

git archive "$DEV_REF" -- "${PATHS[@]}" | tar -x -C "$STAGE"   # committed only

# Guard: refuse if a non-example file slipped into examples/
if find "$STAGE/examples" -type f ! -name '*.example.*' ! -name '*.service' | grep -q .; then
  echo "Refusing: non-example file in examples/ — possible private data" >&2
  exit 1
fi

# Sync into $REL_REPO, preserving its .git, then commit/tag (push later):
#   rsync -a --delete --exclude='.git' "$STAGE"/ "$REL_REPO"/
#   git -C "$REL_REPO" add -A
#   git -C "$REL_REPO" commit -m "Release $VERSION"
#   git -C "$REL_REPO" tag "v$VERSION"
#   git -C "$REL_REPO" push -u --follow-tags origin main   # once a remote exists