Add Gitea transfer skill

This commit is contained in:
2026-04-28 14:03:31 +01:00
commit 52d6526077

651
gitea-transfer/SKILL.md Normal file
View File

@@ -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
<!-- opencode-review -->
```
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