Files
skills/gitea-transfer/SKILL.md

21 KiB

name, description
name description
gitea-transfer 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:

git status --short --branch
git remote -v
git branch --show-current

Confirm the Gitea CLI is authenticated and targeting the expected instance:

tea login list
tea repo ls

Confirm the Gitea repository exists and inspect settings:

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:

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:

tea api repos/{owner}/{repo}

Check that mirror is false and the expected default branch is set.

Enable repository features as needed:

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:

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:

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:

git fetch github --prune
git push origin {default-branch}:{default-branch}

Verify Gitea sees the branch:

tea api repos/{owner}/{repo}/branches/{default-branch}

Branch Migration

List active GitHub branches that need to move:

git branch -r

Push only active work branches, not every stale remote branch by default:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

- 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:

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:

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:

<!-- 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:

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:

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:
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:

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:

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