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
teaor the Gitea API for Gitea repository, pull request, Actions, and runner operations. - Do not use
ghfor 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_orGITHUB_; those prefixes are reserved or confusing in Gitea/GitHub-compatible runners. - Do not modify ignored local config files such as
bunfig.tomlunless 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, andOPENCODE_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/checkoutversion 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
.npmrcor 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.2git.bayliss.cloud/harry/gitea-ci-runner:php8.3git.bayliss.cloud/harry/gitea-ci-runner:php8.4git.bayliss.cloud/harry/gitea-ci-runner:php8.5git.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-latestubuntu-24.04ubuntu-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, and8.5, selected withARG PHP_VERSION - Composer 2
- Bun
- Node.js,
npm, andnpx - Go
- Python 3,
pip, andvenv jq- database and cache client tools such as
postgresql-clientandredis-tools - common PHP extensions including
rdkafka,redis,pcov,xdebug,imagick,imap,pgsql,mysql,sqlite3,gmp, andgd
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:repositoryread:issuewrite: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_PATHwithjq. - 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, andHEAD_SHAto$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.
originpoints to Gitea.githubremote 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
/reviewworkflow 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