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.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# Gitea workflow
|
||||
|
||||
## Branch flow
|
||||
|
||||
`main` → `test` → `dev` → feature branches.
|
||||
|
||||
`test` is the default branch, protected, merge-only.
|
||||
|
||||
For a brand-new empty repo: create `test` from `main`, then `dev` from `test`.
|
||||
|
||||
## Posting with `tea`
|
||||
|
||||
When posting to Gitea with `tea` (issues, comments, PRs):
|
||||
|
||||
- Run it in the **foreground**.
|
||||
- **Use the right body argument** — there is no `--body` flag and no
|
||||
read-from-file option:
|
||||
- `tea issues create` / `tea pulls create`: body is `--description` / `-d`
|
||||
- `tea comment <index> [<body>]`: body is a **positional** argument (no flag)
|
||||
- **Never put `\n` escape sequences in the body.** `tea` stores the string
|
||||
verbatim — it does not interpret escapes, so any `\n` you pass appears
|
||||
*literally* in the posted text. The body must already contain real newlines:
|
||||
write it to a file (the Write tool, or `printf` — never `echo "...\n..."`),
|
||||
then pass it by command substitution, e.g. `-d "$(cat body.md)"` or
|
||||
`tea comment 42 "$(cat body.md)"`. Inline heredocs and `\n`-laden argument
|
||||
strings are what cause the literal `\n`.
|
||||
- **Confirm it actually posted** — check the exit code *and* re-read the created
|
||||
item; verify the body renders with real line breaks, not literal `\n`. `tea`
|
||||
can fail silently.
|
||||
- **Write subcommands hang on inherited stdin.** Even with the body passed as an
|
||||
argument, `tea` (`comment`, `issues edit`, `issues close`, `pulls create`, …)
|
||||
reads stdin; in a non-TTY shell the inherited stdin never sends EOF, so the
|
||||
command blocks until it is killed. **Fix: redirect stdin — append `</dev/null`**
|
||||
to every `tea` *write* subcommand (e.g. `tea comment 42 "$(cat body.md)"
|
||||
</dev/null`). Plain reads are unaffected.
|
||||
- `--repo` is optional inside the repo — it is not the fix for a silent failure.
|
||||
|
||||
## Reading and editing with `tea` (prefer `tea` over the API)
|
||||
|
||||
`tea` covers every issue/PR operation this workflow needs — there is **no
|
||||
capability gap** versus the raw Gitea API, so reach for `tea` first and drop to
|
||||
`curl` on `/api/v1/...` only if a genuinely new gap appears. Recent findings that
|
||||
correct earlier assumptions:
|
||||
|
||||
- **Show a full issue/PR body:** `tea issue <n>` (note: `tea issues view <n>`
|
||||
does *not* print the full body).
|
||||
- **Show comments:** `tea issue <n> --comments`. Comments are hidden by default,
|
||||
but this flag renders them — you do **not** need the API to read comments.
|
||||
- **Edit an existing issue body:** `tea issues edit <n> -d "$(cat body.md)"
|
||||
</dev/null`. `-d`/`--description` does a **full-body replace**. This subcommand
|
||||
exists — earlier notes claiming `tea` could not edit bodies (and that the API
|
||||
was required) were wrong. `tea issues edit` also sets title/labels/milestone/
|
||||
assignees.
|
||||
- The `curl` fallback has its own friction (the shell's proxy can mangle piped
|
||||
`curl` output — write to a file then parse; comment POSTs need a
|
||||
`{"body": "..."}` wrapper) and offers no capability upside, so it is a last
|
||||
resort, not the default.
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,95 @@
|
||||
# Rootless Docker Gitea Runner Notes
|
||||
|
||||
Reusable setup for running Gitea Actions jobs on a **rootless** Docker host when
|
||||
jobs need Docker access (Playwright, Buildx). The runner runs as a container;
|
||||
job containers reach the host's rootless socket. Substitute the rootless socket
|
||||
path `/run/user/<UID>/docker.sock` for your runner user (examples use `3100`).
|
||||
|
||||
## Runner compose
|
||||
|
||||
Mount the rootless socket into the runner container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gitea-runner-1:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: s4-runner-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GITEA_INSTANCE_URL: https://gitea.example.com
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||
GITEA_RUNNER_NAME: s4-runner-1
|
||||
CONFIG_FILE: /data/config.yaml
|
||||
volumes:
|
||||
- runner1_data:/data
|
||||
- ./config.yaml:/data/config.yaml:ro
|
||||
- /run/user/3100/docker.sock:/var/run/docker.sock
|
||||
volumes:
|
||||
runner1_data:
|
||||
```
|
||||
|
||||
## Runner config
|
||||
|
||||
```yaml
|
||||
container:
|
||||
docker_host: "-" # critical
|
||||
options: "-v /run/user/3100/docker.sock:/var/run/docker.sock"
|
||||
valid_volumes:
|
||||
- /run/user/3100/docker.sock # source path only
|
||||
```
|
||||
|
||||
`docker_host: "-"` stops `act_runner` from adding its own socket mount on top of
|
||||
the one in `options`, which otherwise fails job creation with
|
||||
`Duplicate mount point: /var/run/docker.sock`. Keep `valid_volumes` as the host
|
||||
source path only (not the `src:dst` bind form).
|
||||
|
||||
## Workflow patterns
|
||||
|
||||
- **Buildx**: use `docker/setup-buildx-action@v3` with `driver: docker`. The
|
||||
default driver starts a separate BuildKit container that misbehaves on this
|
||||
runner shape; the `docker` driver uses the mounted daemon directly.
|
||||
- **No setup-node cache**: drop `cache: pnpm` from `actions/setup-node@v4`; the
|
||||
job network often can't reach the Gitea Actions cache service (minutes of
|
||||
restore/save timeouts). Use `pnpm install --frozen-lockfile` instead. Revisit
|
||||
only once the cache-service network path is confirmed.
|
||||
- **Playwright**: split browser E2E from validation — build + upload artifact in
|
||||
a normal job, then run Playwright in the official image
|
||||
(`mcr.microsoft.com/playwright:v1.58.2-noble`) against the downloaded artifact.
|
||||
|
||||
## Host checks
|
||||
|
||||
```sh
|
||||
docker context show; echo "$DOCKER_HOST"; ls -l /run/user/3100/docker.sock; docker info
|
||||
# DOCKER_HOST should be unix:///run/user/3100/docker.sock
|
||||
docker run --rm -v /run/user/3100/docker.sock:/var/run/docker.sock docker:27-cli docker version
|
||||
```
|
||||
|
||||
If that container can't use the socket, fix rootless Docker before touching the
|
||||
runner.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`Duplicate mount point: /var/run/docker.sock`** — set `docker_host: "-"`,
|
||||
keep the bind only in `options`, `valid_volumes` = source path only, recreate.
|
||||
- **`WARNING: IPv4 forwarding is disabled`** may persist even after host
|
||||
`net.ipv4.ip_forward=1`. `systemctl --user restart docker` and re-check
|
||||
`docker info`. If jobs create containers and reach their networks, treat it as
|
||||
stale output, not a failure.
|
||||
- **Buildx/build fails** — use `driver: docker`; confirm `docker version` works
|
||||
through the mounted socket.
|
||||
|
||||
## Useful commands
|
||||
|
||||
```sh
|
||||
docker ps --format '{{.ID}} {{.Image}} {{.Status}} {{.Names}}'
|
||||
docker logs --tail 120 s4-runner-1
|
||||
docker compose up -d --force-recreate gitea-runner-1
|
||||
tea actions runs --repo owner/repo
|
||||
tea actions runs logs <run-id> --job <job-id> --repo owner/repo
|
||||
```
|
||||
|
||||
## Known-good (Grindex / current runner)
|
||||
|
||||
Host `s4.fisher.hu`; container `s4-runner-1`; image `gitea/act_runner:latest`;
|
||||
socket `/run/user/3100/docker.sock`; workflows in `.gitea/workflows`; Buildx
|
||||
`driver: docker`; setup-node cache disabled.
|
||||
Reference in New Issue
Block a user