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.
111 lines
4.9 KiB
Markdown
111 lines
4.9 KiB
Markdown
# 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`:
|
|
|
|
```sshconfig
|
|
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:
|
|
|
|
```bash
|
|
#!/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
|
|
```
|