commit 52d65260779e4755e59b83576a332a86db634141 Author: Harry Bayliss Date: Tue Apr 28 14:03:31 2026 +0100 Add Gitea transfer skill diff --git a/gitea-transfer/SKILL.md b/gitea-transfer/SKILL.md new file mode 100644 index 0000000..da38b7a --- /dev/null +++ b/gitea-transfer/SKILL.md @@ -0,0 +1,651 @@ +--- +name: gitea-transfer +description: Guide for transferring repositories from GitHub to self-hosted Gitea, including remotes, branches, PR migration, Gitea Actions porting, runner images, and cutover verification. +--- + +# Gitea Transfer + +## When To Use + +Use this skill when moving a repository from GitHub to self-hosted Gitea, converting a Gitea mirror into the canonical source repo, migrating active branches and pull requests, porting GitHub Actions to Gitea Actions, or designing runner job images for CI. + +## Operating Principles + +- Treat Gitea as canonical only after confirming the repo is no longer a mirror and the default branch is pushed. +- Keep the GitHub remote as a recovery/reference remote unless the user explicitly asks to remove it. +- Prefer `tea` or the Gitea API for Gitea repository, pull request, Actions, and runner operations. +- Do not use `gh` for Gitea work. +- Do not paste, log, commit, or store registration tokens, API tokens, deploy keys, or package credentials. +- Avoid Gitea Actions secret names beginning with `GITEA_` or `GITHUB_`; those prefixes are reserved or confusing in Gitea/GitHub-compatible runners. +- Do not modify ignored local config files such as `bunfig.toml` unless the user explicitly requests it. +- Do not merge pull requests unless the user explicitly approves the merge. +- Do not force-push or rewrite shared branches unless the user explicitly approves it. +- Verify every cutover step with API or git status output before moving on. + +## Preflight Checks + +Confirm local state and remotes: + +```bash +git status --short --branch +git remote -v +git branch --show-current +``` + +Confirm the Gitea CLI is authenticated and targeting the expected instance: + +```bash +tea login list +tea repo ls +``` + +Confirm the Gitea repository exists and inspect settings: + +```bash +tea api repos/{owner}/{repo} +``` + +If Gitea is behind Nginx or another reverse proxy, verify dot directories are reachable. A common Forge/Nginx rule blocks paths such as `.github`, `.gitea`, `.codex`, and `.well-known`: + +```nginx +location ~ /\.(?!well-known).* { + deny all; +} +``` + +For Gitea, that rule can break UI/API access to workflow files and agent config paths. Update the reverse proxy deliberately rather than working around it in git. + +## Convert Mirror To Source + +If the Gitea repository was imported as a mirror, convert it to a normal source repository before cutover. Use the Gitea UI or API, then verify: + +```bash +tea api repos/{owner}/{repo} +``` + +Check that `mirror` is `false` and the expected default branch is set. + +Enable repository features as needed: + +```bash +tea api --method PATCH repos/{owner}/{repo} \ + --field has_pull_requests=true \ + --field has_actions=true +``` + +Use the actual supported fields for the installed Gitea version. Re-read the repository after patching. + +## Remote Cutover + +Keep GitHub as a named fallback remote and make Gitea `origin`: + +```bash +git remote rename origin github +git remote add origin ssh://git@git.example.com:30009/{owner}/{repo}.git +git remote -v +``` + +If `origin` already points elsewhere, adjust non-destructively: + +```bash +git remote set-url origin ssh://git@git.example.com:30009/{owner}/{repo}.git +git remote add github git@github.com:{owner-or-org}/{repo}.git +``` + +Push the default branch first: + +```bash +git fetch github --prune +git push origin {default-branch}:{default-branch} +``` + +Verify Gitea sees the branch: + +```bash +tea api repos/{owner}/{repo}/branches/{default-branch} +``` + +## Branch Migration + +List active GitHub branches that need to move: + +```bash +git branch -r +``` + +Push only active work branches, not every stale remote branch by default: + +```bash +git push origin github/{branch-name}:refs/heads/{branch-name} +``` + +For multiple open PR heads, script carefully from reviewed data. Avoid pushing deleted/stale branches blindly. + +Verify pushed branches: + +```bash +tea api repos/{owner}/{repo}/branches +``` + +## Pull Request Migration + +Gitea does not automatically migrate all GitHub PR review history during a basic repo transfer. Recreate open PRs with enough context to continue work: + +- title +- body +- base branch +- head branch +- draft/WIP state in title or body if Gitea draft support is not available +- link back to the original GitHub PR + +Use the Gitea API or `tea`: + +```bash +tea pr create \ + --repo {owner}/{repo} \ + --base {base-branch} \ + --head {head-branch} \ + --title "{title}" \ + --description "{body}" +``` + +After creating each PR, verify it is open, conflict status is correct, and the branch is correct: + +```bash +tea pr {index} --repo {owner}/{repo} +``` + +If comments/reviews matter, add a short migration note to the PR body linking to the original GitHub PR rather than attempting to recreate every review event. + +## CI Workflow Porting + +Gitea Actions workflows live in `.gitea/workflows/`. GitHub workflows can remain temporarily in `.github/workflows/` during transition, but do not assume they run on Gitea. + +Port workflows incrementally: + +- Start with tests. +- Then add build/deploy workflows. +- Keep workflow syntax close to GitHub Actions where Gitea supports it. +- Validate YAML before pushing. +- Prefer small commits so each Actions failure identifies one change. + +Example validation: + +```bash +ruby -e "require 'yaml'; YAML.load_file('.gitea/workflows/tests.yml'); puts 'ok'" +git diff --check +``` + +Common changes when porting: + +- Replace GitHub-only secrets with Gitea Actions secrets. +- Use neutral secret names such as `REGISTRY_USERNAME`, `REGISTRY_TOKEN`, `REVIEW_BOT_TOKEN`, and `OPENCODE_GO_TOKEN`. +- Remove GitHub Packages auth if the private package dependency is removed or mirrored elsewhere. +- Replace GitHub-hosted assumptions with Gitea runner/job container assumptions. +- Ensure `actions/checkout` version works with the installed Gitea Actions runner. +- Use service hostnames for service containers instead of `127.0.0.1`. +- Add explicit service readiness checks for databases. +- Do not rely on `permissions:` for security-sensitive sandboxing in Gitea Actions; some Gitea versions ignore it. + +For Postgres services, use the service name as the host: + +```yaml +services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: app_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + +env: + DB_HOST: postgres +``` + +Add a readiness loop before running tests: + +```bash +until PGPASSWORD=password pg_isready -h postgres -U postgres; do + sleep 1 +done +``` + +## Package Registry Cutover + +Identify GitHub-specific package auth before enabling Gitea CI: + +- npm/Bun scopes using `npm.pkg.github.com` +- Composer auth for GitHub-hosted private packages +- workflow steps writing `.npmrc` or package auth +- lockfiles referencing private GitHub package tarballs + +Options: + +- Remove the dependency if it can be replaced locally. +- Mirror the package to Gitea Packages. +- Keep GitHub Packages temporarily and configure Gitea secrets deliberately. + +Do not leave workflows requiring secrets that no longer exist. If deploy secrets are absent during cutover, prefer a clear skip for deploy-only workflows so the default branch is not red while secrets are being configured. + +## Runner Image Strategy + +Prefer job containers over custom runner labels for language/runtime selection. + +Use shared CI job images when several repositories need the same runtime stack. The current shared image family is: + +- `git.bayliss.cloud/harry/gitea-ci-runner:php8.2` +- `git.bayliss.cloud/harry/gitea-ci-runner:php8.3` +- `git.bayliss.cloud/harry/gitea-ci-runner:php8.4` +- `git.bayliss.cloud/harry/gitea-ci-runner:php8.5` +- `git.bayliss.cloud/harry/gitea-ci-runner:latest` + +`latest` points to PHP `8.5`. Repositories should usually hardcode the PHP tag they require instead of using `latest`, so runtime upgrades are explicit per repository. + +Good pattern: + +```yaml +jobs: + tests: + runs-on: ubuntu-latest + container: + image: git.bayliss.cloud/harry/gitea-ci-runner:php8.2 +``` + +Avoid creating labels such as `stratbase-php82` just to pick an image. Labels select eligible runners; job containers select the runtime image. This keeps the runner reusable across repositories. + +Use an instance-level or organization-level runner with generic labels such as: + +- `ubuntu-latest` +- `ubuntu-24.04` +- `ubuntu-22.04` + +If runner labels change, the runner usually needs re-registration because `.runner` pins registration details. App UI fields called "Labels Configuration" may refer to Docker metadata, not Gitea runner labels. Verify actual labels through the API: + +```bash +tea api admin/actions/runners +``` + +## Building A CI Job Image + +Create or update a shared image when workflows need system dependencies that are expensive or brittle to install per run. Prefer a central image repository over per-application image repositories unless the runtime is truly application-specific. + +Current shared image repository: + +```text +ssh://git@git.bayliss.cloud:30009/harry/gitea-ci-runner.git +https://git.bayliss.cloud/harry/gitea-ci-runner +``` + +Current shared PHP/Laravel CI image contents: + +- base Gitea runner job image +- PHP CLI for `8.2`, `8.3`, `8.4`, and `8.5`, selected with `ARG PHP_VERSION` +- Composer 2 +- Bun +- Node.js, `npm`, and `npx` +- Go +- Python 3, `pip`, and `venv` +- `jq` +- database and cache client tools such as `postgresql-client` and `redis-tools` +- common PHP extensions including `rdkafka`, `redis`, `pcov`, `xdebug`, `imagick`, `imap`, `pgsql`, `mysql`, `sqlite3`, `gmp`, and `gd` + +Do not add MongoDB to the shared image unless a repository has a real runtime or CI need for it. + +Shared Dockerfile pattern: + +```dockerfile +FROM docker.gitea.com/runner-images:ubuntu-latest + +ARG PHP_VERSION=8.5 + +ENV DEBIAN_FRONTEND=noninteractive +ENV BUN_INSTALL=/root/.bun +ENV PATH="/root/.bun/bin:${PATH}" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + golang-go \ + gnupg \ + jq \ + lsb-release \ + postgresql-client \ + python3 \ + python3-pip \ + python3-venv \ + redis-tools \ + software-properties-common \ + unzip \ + && install -d -m 0755 /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && add-apt-repository ppa:ondrej/php -y \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + nodejs \ + php${PHP_VERSION} \ + php${PHP_VERSION}-bcmath \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-imagick \ + php${PHP_VERSION}-imap \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-pcov \ + php${PHP_VERSION}-pgsql \ + php${PHP_VERSION}-redis \ + php${PHP_VERSION}-rdkafka \ + php${PHP_VERSION}-sqlite3 \ + php${PHP_VERSION}-xdebug \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-zip \ + && update-alternatives --set php /usr/bin/php${PHP_VERSION} \ + && curl -fsSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \ + && curl -fsSL https://bun.sh/install | bash \ + && ln -sf /root/.bun/bin/bun /usr/local/bin/bun \ + && ln -sf /root/.bun/bin/bunx /usr/local/bin/bunx \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN php -v \ + && php -m \ + && composer --version \ + && go version \ + && bun --version \ + && node --version \ + && python3 --version \ + && pg_isready --version +``` + +Build and push manually only when necessary: + +```bash +docker build --build-arg PHP_VERSION=8.2 -t git.bayliss.cloud/harry/gitea-ci-runner:php8.2 -f docker/Dockerfile . +docker push git.bayliss.cloud/harry/gitea-ci-runner:php8.2 +``` + +Prefer the image repository workflow for normal updates. Use a matrix over the supported PHP versions and push `latest` only from PHP `8.5`: + +```yaml +env: + REGISTRY: git.bayliss.cloud + IMAGE_NAME: harry/gitea-ci-runner + +jobs: + build: + strategy: + matrix: + php-version: + - '8.2' + - '8.3' + - '8.4' + - '8.5' + + steps: + - uses: actions/checkout@v4 + + - name: Check registry credentials + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + if [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_TOKEN" ]; then + echo "Skipping image publish because REGISTRY_USERNAME or REGISTRY_TOKEN is missing." + echo "SKIP_IMAGE_PUBLISH=true" >> "$GITHUB_ENV" + fi + + - name: Login to Gitea registry + if: env.SKIP_IMAGE_PUBLISH != 'true' + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin + + - name: Build and push version tag + if: env.SKIP_IMAGE_PUBLISH != 'true' + run: | + docker build \ + --build-arg PHP_VERSION="${{ matrix.php-version }}" \ + --tag "$REGISTRY/$IMAGE_NAME:php${{ matrix.php-version }}" \ + --file docker/Dockerfile \ + . + docker push "$REGISTRY/$IMAGE_NAME:php${{ matrix.php-version }}" + + - name: Push latest tag + if: env.SKIP_IMAGE_PUBLISH != 'true' && matrix.php-version == '8.5' + run: | + docker tag "$REGISTRY/$IMAGE_NAME:php${{ matrix.php-version }}" "$REGISTRY/$IMAGE_NAME:latest" + docker push "$REGISTRY/$IMAGE_NAME:latest" +``` + +If the runner host exposes the Gitea registry locally, prefer the runner-local endpoint in workflow job containers for faster pulls: + +```yaml +container: + image: localhost:30008/harry/gitea-ci-runner:php8.2 +``` + +Do not include `http://` in Docker image references. Configure insecure/local registry trust at the Docker daemon level if needed. + +## OpenCode PR Review Workflow + +Use this pattern when adding a repo-scoped OpenCode reviewer to Gitea Actions. + +Core behavior: + +- Trigger only from PR comments containing `/review`. +- Do not auto-review on PR open or synchronize events for the first version. +- Run OpenCode read-only. +- Post or update one aggregate PR comment instead of line comments. +- Use the PR head checkout for repository context, but keep checkout shallow. +- Do not expose Gitea API tokens to OpenCode. + +Required secrets: + +- `REVIEW_BOT_TOKEN`: Gitea token for reading the repository/PR and writing issue comments. +- `OPENCODE_GO_TOKEN`: OpenCode Go API token. + +Recommended token permissions for `REVIEW_BOT_TOKEN`: + +- `read:repository` +- `read:issue` +- `write:issue` + +Recommended workflow shape: + +```yaml +on: + issue_comment: + types: + - created + workflow_dispatch: + +jobs: + review: + runs-on: ubuntu-latest + container: + image: git.bayliss.cloud/harry/gitea-ci-runner:php8.2 +``` + +In a preparation step: + +- Read `$GITHUB_EVENT_PATH` with `jq`. +- Skip unless the event action is `created`. +- Skip unless the issue is a pull request. +- Skip unless the comment contains `/review`. +- Fetch pull request metadata from `GET /repos/{owner}/{repo}/pulls/{number}`. +- Fetch the diff from `GET /repos/{owner}/{repo}/pulls/{number}.diff`. +- Export `PR_NUMBER`, `REPO`, `BASE_BRANCH`, `HEAD_BRANCH`, and `HEAD_SHA` to `$GITHUB_ENV`. + +Checkout the PR head after preparation: + +```yaml +- name: Checkout PR head + if: env.SKIP_OPENCODE_REVIEW != 'true' + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_SHA }} + fetch-depth: 1 + persist-credentials: false +``` + +Avoid `fetch-depth: 0` unless full history is required; full-history checkout can make review runs slow on Gitea runners. + +Generate OpenCode auth at runtime from `OPENCODE_GO_TOKEN`: + +```bash +mkdir -p "$HOME/.local/share/opencode" +jq -n --arg token "$OPENCODE_GO_TOKEN" '{"opencode-go": {"type": "api", "key": $token}}' > "$HOME/.local/share/opencode/auth.json" +``` + +Disable mutation tools and unset repository tokens before invoking OpenCode: + +```bash +export OPENCODE_CONFIG_CONTENT='{"tools":{"write":false,"edit":false,"bash":false},"permission":{"edit":"deny","bash":"deny"},"share":"disabled","autoupdate":false}' +unset OPENCODE_GO_TOKEN REVIEW_BOT_TOKEN GITEA_TOKEN GITHUB_TOKEN + +opencode run \ + --model "${REVIEW_MODEL:-opencode-go/glm-5.1}" \ + --file /tmp/opencode-pr.diff \ + --title "PR #$PR_NUMBER review" \ + "Review this pull request diff for bugs, security issues, behavior regressions, and missing tests. Use the checked-out PR head tree for repository context. Focus on actionable findings. If there are no findings, say so clearly. Do not suggest style-only changes. Output Markdown with concise sections." \ + > /tmp/opencode-review.md +``` + +Use a stable marker in the review comment body so later `/review` runs update the same comment: + +```text + +``` + +Gitea Actions logs can be awkward to fetch on versions before `1.26`; if `tea actions runs logs` is unavailable or incomplete, inspect run/task state through the Gitea API. + +## Deploy Workflow Porting + +Deploy workflows often need secrets and remote side effects. Port them after tests pass. + +Guard required secrets explicitly: + +```bash +missing=() + +for required_var in SPARK_EMAIL SPARK_KEY TIPTAP_PRO_KEY DO_SPACES_KEY DO_SPACES_SECRET DO_SPACES_ENDPOINT DO_SPACES_REGION; do + if [ -z "${!required_var}" ]; then + missing+=("$required_var") + fi +done + +if [ "${#missing[@]}" -gt 0 ]; then + echo "Skipping deploy because required secrets are missing: ${missing[*]}" + echo "SKIP_DEPLOY=true" >> "$GITHUB_ENV" +fi +``` + +Gate deploy steps: + +```yaml +if: env.SKIP_DEPLOY != 'true' +``` + +This keeps the default branch green while still making the missing configuration obvious in logs. File follow-up work for the missing secrets. + +Do not manually dispatch deploy workflows that upload assets or mutate production unless the user explicitly approves it. + +## Verification Checklist + +Before declaring cutover complete: + +- Gitea repo is not a mirror. +- `origin` points to Gitea. +- `github` remote exists for recovery/reference if desired. +- Default branch is pushed to Gitea. +- Active PR branches are pushed to Gitea. +- Open PRs are recreated in Gitea. +- Repository PRs and Actions are enabled. +- Test workflow passes on PR. +- Test workflow passes on default branch after merge. +- Deploy workflow either succeeds or skips cleanly with a clear missing-secrets message. +- Optional `/review` workflow posts or updates the aggregate OpenCode review comment. +- Runner is online with generic labels. +- Job containers pull the expected image. +- Local branch is synchronized with Gitea: + +```bash +git pull --rebase origin {default-branch} +git push origin {default-branch} +git rev-list --left-right --count origin/{default-branch}...{default-branch} +git status --short --branch +``` + +Expected commit comparison after sync: + +```text +0 0 +``` + +## Common Failure Modes + +### Dot Paths 403 In Gitea UI + +Cause: reverse proxy blocks dot directories. + +Fix: adjust the proxy rule for Gitea. Do not rename workflow directories. + +### Postgres Connection Refused + +Cause: CI app connects to `127.0.0.1` while Postgres runs as a service container. + +Fix: use service hostname such as `postgres` and add readiness checks. + +### Runner Job Never Starts + +Cause: workflow `runs-on` label does not match registered runner labels. + +Fix: use generic labels and verify with `tea api admin/actions/runners`. + +### Image Pull Fails + +Cause: wrong registry hostname, missing Docker daemon trust, or private package auth issue. + +Fix: verify image reference by pulling on the runner host, then use the same host/port in the workflow. + +### Deploy Fails For Missing Secrets + +Cause: Gitea Actions secrets not configured yet. + +Fix: add a preflight skip or configure secrets. File follow-up work instead of leaving the default branch red. + +### OpenCode Review Has No Context + +Cause: the workflow passed only a diff to OpenCode and did not checkout the PR head tree. + +Fix: fetch the PR diff through the Gitea API, then shallow-checkout the PR head SHA with `fetch-depth: 1` and `persist-credentials: false`. + +### OpenCode Review Checkout Is Slow + +Cause: `actions/checkout` fetched full history with `fetch-depth: 0`. + +Fix: checkout the PR head SHA with `fetch-depth: 1` unless full git history is required. + +### Branch Tracks Old GitHub Remote + +Cause: local branch upstream still points to `github/{branch}` after remote cutover. + +Fix only if requested or useful: + +```bash +git branch --set-upstream-to=origin/{branch} {branch} +``` + +## Follow-Up Issues + +File explicit follow-up issues for work intentionally deferred during cutover, such as: + +- configuring deploy secrets in Gitea +- moving remaining Composer VCS repositories from GitHub to Gitea +- removing obsolete GitHub workflows after Gitea Actions are trusted +- deleting or archiving the GitHub repository after a retention period +- rotating any tokens that were exposed during setup