tools/gitea/gitea_actions.md

Gitea Actions CI/CD Configuration and Workflow Automation

Complete reference for configuring and using Gitea Actions as a built-in CI/CD solution. Covers .gitea directory structure, workflow YAML syntax, runner setup, GITEA_TOKEN permissions, secrets, variables, scoped workflows, and advanced patterns for automated builds, tests, and deployments.

Introduction

Gitea Actions is a built-in CI/CD solution integrated directly into Gitea, available since version 1.19 and enabled by default since 1.21. It provides a GitHub Actions-compatible workflow system that allows you to automate builds, tests, and deployments entirely on your own infrastructure. The system consists of two main components: the Gitea instance itself (which schedules and manages workflows) and one or more Gitea Runners (formerly called act_runner) that execute the actual jobs.

Gitea Actions is designed to be highly compatible with GitHub Actions -- most existing GitHub Actions workflows can be reused with minimal changes, and the workflow YAML syntax is nearly identical. This makes it straightforward for teams already familiar with GitHub Actions to adopt Gitea Actions for self-hosted automation. Key differences include support for absolute action URLs (pulling actions from any git repository), Go-based custom actions, and Gitea-specific context objects (gitea.*) that provide access to repository and workflow metadata.

When to use this guidance: When setting up automated CI/CD pipelines for repositories hosted on a self-managed Gitea instance, or when migrating from GitHub Actions to a self-hosted Gitea environment.

Core Concepts

The .gitea Directory

Workflow definitions are stored as YAML files in the .gitea/workflows/ directory at the root of your repository. For backwards compatibility with GitHub, .github/workflows/ is also supported. Each YAML file in this directory defines one or more workflow configurations.

your-repository/
  .gitea/
    workflows/
      build.yaml        # Build and test workflow
      deploy.yaml       # Deployment workflow
      lint.yaml         # Linting workflow
    scoped_workflows/   # Gitea 1.28+ -- scoped workflows from source repos

Architecture Overview

Gitea Actions has a distributed architecture with four key network connections:

  1. Runner to Gitea instance: The runner polls the Gitea instance for queued jobs and reports execution results. This is the only connection the runner must establish.
  2. Job containers to Gitea instance: When actions like actions/checkout@v4 clone code, the temporary job containers connect to Gitea to fetch repository data.
  3. Runner to internet: The runner downloads action scripts (from github.com by default, or from your Gitea instance if configured) and Docker images.
  4. Job containers to internet: Setup actions (e.g., actions/setup-go@v5) may download additional resources from the internet.

Internet access for connections 3 and 4 is optional -- you can configure your Gitea instance as both an actions marketplace (via mirroring GitHub repositories) and a container registry, creating a fully offline CI/CD pipeline.

Workflow Lifecycle

  1. An event occurs (push, pull request, schedule, manual dispatch, etc.)
  2. Gitea scans all .gitea/workflows/*.yaml files in the repository
  3. For each workflow whose on: triggers match the event, a workflow run is created
  4. The run is queued and assigned to an available runner whose labels match the job's runs-on
  5. The runner executes each job's steps sequentially within isolated containers
  6. Results and logs stream back to the Gitea instance in real time

Expressions and Contexts

Workflows use ${{ }} expression syntax to access contextual information. Both github. (for GitHub Actions compatibility) and gitea. (recommended for Gitea-native workflows) are supported.

Gitea-specific context properties:

  • gitea.actor -- the user who triggered the workflow
  • gitea.event_name -- the name of the event that triggered the workflow
  • gitea.ref -- the Git ref the workflow was triggered on
  • gitea.repository -- the owner/repo identifier
  • gitea.workspace -- the workspace directory path on the runner
  • gitea.workflow -- the workflow name

Additional contexts:

  • runner.os -- the operating system of the runner
  • job.status -- the current status of the job (success, failure, cancelled)
  • secrets.* -- secrets accessible to the workflow
  • vars.* -- configuration variables (Gitea 1.26+)
  • env.* -- environment variables set within the workflow

Directory Structure and File Organization

Workflow Directory

.gitea/workflows/

All workflow YAML files must be placed in this directory (or .github/workflows/). Each file can contain one or more workflow definitions. Gitea scans all files in this directory on every event and queues matching workflows.

Scoped Workflows Directory (Gitea 1.28+)

.gitea/scoped_workflows/

Scoped workflows are defined in a central source repository and automatically applied to consuming repositories at the organization or instance level. This feature is useful for enforcing consistent CI standards (linting, security scans, compliance checks) across many repositories without copying workflow files.

Configured via [actions] SCOPEDWORKFLOWDIRS = .gitea/scopedworkflows in app.ini. Can be multiple comma-separated directories. Must not overlap with WORKFLOWDIRS.

Directory Naming and Permissions

  • The .gitea/ directory is a hidden directory (dotfile) tracked in Git
  • Files must be committed to the repository to take effect
  • Workflow files must have .yaml or .yml extension
  • Each repository must have Actions enabled in its settings (Settings -> Repository -> Enable Repository Actions)

Workflow YAML Syntax

Minimal Workflow

name: CI Pipeline
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Build complete"

Complete Workflow Structure

name: Build, Test, and Deploy
run-name: ${{ gitea.actor }} triggered build on ${{ gitea.ref }}

# Trigger events
on:
  push:
    branches: [main, develop]
    paths-ignore:
      - 'docs/**'
      - '*.md'
  pull_request:
    branches: [main]
  workflow_dispatch:        # Manual trigger

# Concurrency control
concurrency:
  group: ${{ gitea.workflow }}-${{ gitea.ref }}
  cancel-in-progress: true

# Environment variables available to all jobs
env:
  NODE_VERSION: '18'
  REGISTRY: ghcr.io

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_DB: test
          POSTGRES_PASSWORD: test
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/test

  build:
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    needs: [build]
    if: gitea.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          echo "Deploying ${{ gitea.sha }}"
          # deployment commands here
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

Matrix Builds

jobs:
  test-matrix:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [16, 18, 20]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test

Trigger Event Types

EventActivity TypesDescription
pushN/ATriggered on git push to the repository
pull_requestopened, edited, closed, reopened, assigned, unassigned, synchronized, labeled, unlabeledPR lifecycle events. Note: ref is refs/pull/:prNumber/head (the PR head, not a merge preview)
pullrequestreviewsubmitted, editedPull request review submission
pullrequestreview_commentcreated, editedComments on PR diffs
issuesopened, edited, closed, reopened, assigned, unassigned, milestoned, demilestoned, labeled, unlabeledIssue lifecycle events
issue_commentcreated, edited, deletedComments on issues and PRs
createN/ABranch or tag creation
deleteN/ABranch or tag deletion
releasepublished, editedRelease publishing
workflow_dispatchN/AManual trigger via the Gitea UI or API
workflow_runrequested, completedTriggered by another workflow's completion
scheduleN/ACron-based scheduling. Supports standard cron syntax and non-standard @yearly, @monthly, @weekly, @daily, @hourly
forkN/ARepository fork
gollumN/AWiki page creation or update
registry_packagepublishedContainer registry package publication

Schedule (Cron) Examples

on:
  schedule:
    # Every day at midnight UTC
    - cron: '0 0 * * *'
    # Every Monday at 9 AM
    - cron: '0 9 * * 1'
    # Non-standard Gitea extensions (not supported by GitHub)
    - cron: '@daily'
    - cron: '@weekly'

Runner Setup and Configuration

Runner Architecture

Gitea Runners are standalone programs written in Go that poll the Gitea instance for queued jobs, execute them, and report results. Based on a fork of nektos/act, they support three execution modes:

  1. Host mode: jobs run directly on the host machine (no container isolation)
  2. Docker mode (recommended): jobs run in isolated Docker containers
  3. Docker-in-Docker (DinD): the runner itself runs in a container with its own Docker daemon (best security isolation)

Registration Token

Before a runner can execute jobs, it must be registered with the Gitea instance using a registration token. Tokens can be obtained at three levels:

  • Instance level: Admin settings page (<your_gitea.com>/-/admin/actions/runners)
  • Organization level: Organization settings page (<your_gitea.com>/<org>/settings/actions/runners)
  • Repository level: Repository settings page (<your_gitea.com>/<owner>/<repo>/settings/actions/runners)

Registration tokens are valid for registering multiple runners until explicitly revoked via the web UI or API.

Runner Installation via Binary

# Download the runner binary
chmod +x runner

# Generate the default config file
./runner generate-config > config.yaml

# Register the runner (non-interactive)
./runner register --no-interactive \
  --instance https://gitea.example.com \
  --token YOUR_REGISTRATION_TOKEN \
  --name my-runner \
  --labels "ubuntu-latest:docker://node:20-bullseye,custom-label:docker://centos:8"

# Start the runner daemon
./runner daemon --config config.yaml

Runner Installation via Docker

docker run \
  -e GITEA_INSTANCE_URL=https://gitea.example.com \
  -e GITEA_RUNNER_REGISTRATION_TOKEN=YOUR_TOKEN \
  -e GITEA_RUNNER_NAME=gitea-runner-1 \
  -e GITEA_RUNNER_LABELS="ubuntu-latest:docker://node:20-bullseye" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --name gitea-runner \
  -d docker.io/gitea/runner:latest

Runner Installation via Docker Compose

version: "3.8"
services:
  runner:
    image: docker.io/gitea/runner:latest
    environment:
      CONFIG_FILE: /config.yaml
      GITEA_INSTANCE_URL: "${INSTANCE_URL}"
      GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}"
      GITEA_RUNNER_NAME: "${RUNNER_NAME}"
      GITEA_RUNNER_LABELS: "${RUNNER_LABELS}"
    volumes:
      - ./config.yaml:/config.yaml
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock

Runner as a Systemd Service

[Unit]
Description=Gitea Actions runner
Documentation=https://gitea.com/gitea/runner
After=docker.service

[Service]
ExecStart=/usr/local/bin/runner daemon --config /etc/runner/config.yaml
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/var/lib/runner
TimeoutSec=0
RestartSec=10
Restart=always
User=runner

[Install]
WantedBy=multi-user.target

Runner Labels

Labels determine which jobs a runner can accept and how they are executed. The format is:

label[:schema[:args]]
  • ubuntu-latest:docker://node:20-bullseye -- Run jobs labeled ubuntu-latest using a Docker container with the node:20-bullseye image
  • linuxamd64:host -- Run jobs labeled linuxamd64 directly on the host machine
  • custom-label:docker://centos:8 -- Run jobs labeled custom-label using a CentOS 8 container

Default labels: ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster

Custom labels are specified during registration with --labels. Starting with Gitea 1.21, labels can be modified by editing runners.labels in the runner configuration file and restarting the daemon.

Ephemeral Runners

Ephemeral runners (Gitea Runner 0.2.12+) provide enhanced security for organization- or instance-level runners. An ephemeral runner executes exactly one job and then terminates. Its credentials are automatically revoked after assignment, preventing it from polling further jobs and limiting the blast radius of compromised runners.

./runner register --no-interactive --ephemeral \
  --instance https://gitea.example.com \
  --token TOKEN \
  --name ephemeral-runner \
  --labels ubuntu-latest:docker://node:20-bullseye

For Docker deployments, set GITEARUNNEREPHEMERAL=1 and omit the /data volume (credentials are single-use).

Cache Configuration

When using actions/cache in workflows, the runner cache must be explicitly configured because the runner container and job containers are on different networks by default.

# In the runner's config.yaml
cache:
  enabled: true
  dir: ""
  host: "192.168.8.17"  # LAN IP of the runner host
  port: 8088

When starting the Docker container, map the cache port:

docker run --name gitea-runner \
  -p 8088:8088 \
  -d docker.io/gitea/runner:nightly

GITEA_TOKEN Permissions

Every Actions job receives an automatic token (${{ secrets.GITEA_TOKEN }}) that authenticates API requests and Git operations against the Gitea instance.

Permission Resolution Order

  1. Job-level permissions: jobs.<job_id>.permissions
  2. Workflow-level permissions (top-level permissions:)
  3. Default permissions from settings (owner or repository)
  4. Clamped by configured maximum token permissions

Permission Syntax

# Read-only for all scopes
permissions: read-all

# Write for all scopes
permissions: write-all

# Granular scoped permissions
permissions:
  contents: read
  issues: write
  pull-requests: none
  actions: read
  wiki: none
  projects: none
  packages: read

Supported Scopes

ScopeControls Access To
contentsCode and releases (combined)
codeRepository source code
releasesRepository releases
issuesIssue tracking
pull-requestsPull requests
actionsActions management
wikiRepository wiki
projectsProject boards
packagesPackage registry

Valid access modes: read, write, none.

Default Permission Mode

  • Permissive (default): Read and write permissions for most units in the job's repository
  • Restricted: Read-only for code, releases, and packages; no access to other units

Configured in Settings -> Actions -> General at user, organization, or repository level.

Fork Pull Request Security

Workflows triggered by pull requests from forked repositories always receive read-only permissions for repository contents, regardless of workflow permissions: settings or configuration. This prevents malicious code in forks from modifying repository resources.

Cross-Repository Access

By default, GITEA_TOKEN only has access to the job's repository (computed permissions) and public repositories (read-only). Additional private repository access can be configured in Settings -> Actions -> General -> Cross-Repository Access.

Secrets Management

Secrets store sensitive information (API keys, passwords, tokens) that workflows need but must not be exposed in logs or repository files.

steps:
  - name: Deploy to production
    run: deploy.sh ${{ secrets.DEPLOY_KEY }}
    env:
      API_TOKEN: ${{ secrets.API_TOKEN }}

Secret Naming Rules

  • Only alphanumeric characters ([a-z], [A-Z], [0-9]) and underscores (_)
  • Must not start with GITHUB or GITEA prefix
  • Must not start with a number
  • Case-insensitive (uppercased by convention)
  • Must be unique at the creation level

Secret Scoping and Precedence

Secrets can be defined at user, organization, or repository level. When the same secret name exists at multiple levels, the lowest level takes precedence:

Repository-level secret (highest priority)
  > Organization-level secret
    > User-level secret (lowest priority)

Workflow Best Practices

1. Use Gitea-Specific Context Properties

Rationale: Using gitea. instead of github. makes your workflows Gitea-native and avoids confusion in mixed environments. Both work identically, but gitea.* ensures forward compatibility with Gitea-specific features that have no GitHub equivalent.

- name: Print build info
  run: |
    echo "Triggered by: ${{ gitea.actor }}"
    echo "Event: ${{ gitea.event_name }}"
    echo "Branch: ${{ gitea.ref }}"
    echo "Repository: ${{ gitea.repository }}"

2. Always Pin Action Versions

Rationale: Pinning action versions (e.g., @v4 instead of @main or @latest) prevents unexpected breaking changes from upstream updates.

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
  - uses: actions/cache@v4

3. Use Job Dependencies and Conditional Execution

Rationale: Structuring workflows with needs: creates clear pipelines, and if: conditions prevent unnecessary execution on non-matching branches or events.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  test:
    needs: [lint]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  deploy:
    needs: [test]
    if: gitea.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

4. Use Matrix Strategy for Multi-Environment Testing

Rationale: Matrix builds test across multiple runtime versions and operating systems in parallel, catching compatibility issues early without duplicating job definitions.

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.10', '3.11', '3.12']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -r requirements.txt
      - run: pytest

5. Secure Secrets with Minimal Scope

Rationale: Using granular permissions: reduces the blast radius if a workflow is compromised. Never use permissions: write-all when only specific scopes are needed.

jobs:
  lint:
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

6. Use Concurrency Control to Prevent Duplicate Runs

Rationale: Concurrency groups cancel or queue redundant runs, saving runner resources and preventing race conditions during deployments.

concurrency:
  group: ${{ gitea.workflow }}-${{ gitea.ref }}
  cancel-in-progress: true

7. Use Service Containers for Integration Tests

Rationale: Service containers provide ephemeral dependencies (databases, message queues, caches) that start and stop with the job, keeping tests isolated and repeatable.

jobs:
  integration-test:
    runs-on: ubuntu-latest
    services:
      redis:
        image: redis:7-alpine
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_PASSWORD: secret
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:integration

8. Cache Dependencies for Speed

Rationale: Caching npm, pip, Maven, or Go module caches dramatically reduces workflow execution time.

- name: Cache npm dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      npm-

Scoped Workflows (Gitea 1.28+)

Scoped workflows allow a central source repository to provide workflow files that automatically run on consuming repositories, without copying files into each repository.

Use Cases

  • Mandatory linting and formatting checks across an organization
  • Security scanning and dependency auditing for all repositories
  • Compliance and policy enforcement at scale
  • Standardized build and release pipelines

Levels

  • Owner level: Registered by an organization or user; applies to all repositories owned by that entity
  • Instance level: Registered by a site administrator; applies to every repository on the Gitea instance

Configuration

[actions]
SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows

Source Repository Workflow

# .gitea/scoped_workflows/lint.yaml in the source repository
name: Organization-wide Linting
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - run: pip install ruff
      - run: ruff check .

The source repository is registered via Settings -> Actions -> Scoped Workflows at the organization, user, or instance level.

How Scoped Workflows Execute

  • The run belongs to and is visible in the consuming repository (its runners, secrets, GITEA_TOKEN, and branch are used)
  • Workflow content is read from the source repository's default branch at event time
  • The consuming repository's event determines which on: triggers match
  • Runs can be rerun, viewed in logs, and post commit statuses like any normal workflow

Required Workflows and Merge Gating

A scoped workflow can be marked as required. When it is required:

  • Consuming repositories cannot disable it
  • Pull requests cannot be merged until a commit status from the required workflow passes
  • The status check context format: <source-repo>: <workflow-name> / <job> (<event>)

Common Pitfalls and Anti-Patterns

Pitfall: Registering the Runner with a Loopback Address

Problem: Using 127.0.0.1 or localhost as the instance URL during runner registration. Job containers spun up by the runner are in a different network namespace and cannot reach the Gitea instance at localhost.

Recommended approach: Always use the LAN IP address or hostname of the Gitea instance.

# Correct
./runner register --instance http://192.168.1.100:3000

# Wrong -- job containers cannot reach this
./runner register --instance http://127.0.0.1:3000

Pitfall: Hardcoding Secrets in Workflow Files

Problem: Embedding API keys, tokens, or passwords directly in the YAML file violates security best practices and exposes secrets to anyone with repository read access.

Recommended approach: Always use ${{ secrets.SECRET_NAME }} and manage secrets through the Gitea UI.

# Wrong
- run: deploy --token abc123def456

# Correct
- run: deploy --token ${{ secrets.DEPLOY_TOKEN }}

Pitfall: Using Unversioned Actions References

Problem: Using @main or @latest for action versions means workflow behavior can change unexpectedly when the action is updated.

Recommended approach: Pin to a specific major version tag or commit SHA.

# Wrong
- uses: actions/checkout@main

# Correct
- uses: actions/checkout@v4

Pitfall: Ignoring the Gitea Context for Expressions

Problem: Using github.* context exclusively when working exclusively with Gitea. While compatible, this misses out on Gitea-specific properties and makes migration documentation harder to write.

Recommended approach: Use gitea.* for Gitea-native workflows.

# Works, but not Gitea-idiomatic
- run: echo ${{ github.repository }}

# Gitea-native approach
- run: echo ${{ gitea.repository }}

Pitfall: Not Configuring Cache for Actions Cache

Problem: Using actions/cache@v4 without configuring the runner cache host/port results in timeout errors because the job container cannot reach the runner container's default cache server.

Recommended approach: Configure cache.host and cache.port in the runner config.yaml and map the port in Docker.

Pitfall: Running Jobs on the Host Without Isolation

Problem: Host-mode runners execute job scripts directly on the runner machine without container isolation, posing security and cleanliness risks (stale files, conflicting dependencies, broader attack surface).

Recommended approach: Use Docker mode (the default) whenever possible. Reserve host mode for special cases like low-resource environments or OS-level operations.

Performance, Security, and Scalability Considerations

Security

  1. Runner trust model: Never use a runner you don't trust, and never provide a runner to a repository you don't trust. These two rules are the foundation of Gitea Actions security.
  2. Docker socket exposure: Mounting /var/run/docker.sock into the runner container gives jobs full Docker access. A malicious workflow could compromise the host through this socket. Use DinD mode for better isolation in multi-tenant environments.
  3. Ephemeral runners: For public or multi-tenant instances, use ephemeral runners that self-destruct after one job. This prevents credential reuse and limits blast radius.
  4. Fork PR restrictions: Workflows from forked repositories automatically get read-only permissions for contents, preventing unauthorized modifications.
  5. Secret scrubbing: Gitea automatically masks secret values in workflow logs.

Performance

  1. Runner placement: Deploy runners on the same network as the Gitea instance for low-latency job communication. Use separate machines for runners to avoid resource contention.
  2. Caching: Configure actions/cache properly to reduce dependency installation time. Cache keys should be specific enough to be useful but broad enough for cache hits.
  3. Concurrency control: Use concurrency groups to cancel stale runs and prevent resource waste.
  4. Matrix parallelism: Matrix jobs run in parallel, so design matrix strategies to maximize parallelism while respecting runner capacity.

Scalability

  1. Multiple runners: Register multiple runners (on different machines or as Docker containers) to handle concurrent job queues. Runners at different levels (instance, organization, repository) provide flexible load distribution.
  2. Custom runner images: Pre-build Docker images with frequently used tools and dependencies to reduce per-job setup time.
  3. Offline operation: For air-gapped environments, mirror actions repositories to your Gitea instance (set DEFAULTACTIONSURL = self) and use your Gitea container registry for Docker images.

Differences from GitHub Actions

Gitea-Specific Features (Not Available in GitHub)

  • Absolute action URLs: Actions can be referenced by full URL: uses: https://gitea.example.com/owner/repo@branch
  • Go-based actions: Gitea supports writing custom actions in Go
  • Extended schedule syntax: @yearly, @monthly, @weekly, @daily, @hourly in addition to standard cron
  • Scoped workflows: Centralized workflow enforcement across multiple repositories

Unsupported GitHub Actions Features

FeatureStatusWorkaround
timeout-minutesIgnoredMonitor job duration externally
continue-on-errorIgnoredUse if: ${{ !cancelled() }} patterns
environmentIgnoredManage environments via secrets and conditions
Complex runs-on groupsPartial supportUse single labels or arrays of labels
Problem MatchersNot supportedParse output manually in steps
Error annotationsNot supportedUse structured step output
id-token scopeNot supportedUse OIDC-compatible approaches
Package repo auth with GITEA_TOKENNot implementedUse a Personal Access Token (PAT)

Behavior Differences

  1. Downloading actions: DEFAULTACTIONSURL defaults to github. Set to self for intranet environments where actions are mirrored locally.
  2. Context availability: Gitea does not enforce context availability restrictions, so the env context can be used in more places than GitHub Actions allows.
  3. PR ref: Gitea uses refs/pull/:prNumber/head (PR head) instead of GitHub's refs/pull/:prNumber/merge (merge preview). Use gitea.eventname == 'pullrequest' to handle PR-specific logic.

Edge Cases and Advanced Usage

Network Architecture Troubleshooting

Gitea Actions has four network connections:

Act Runner ──(1)──> Gitea Instance
Job Containers ──(2)──> Gitea Instance
Act Runner ──(3)──> Internet (for action scripts, optional)
Job Containers ──(4)──> Internet (for setup tools, optional)

If jobs are queued but never start, verify:

  1. The runner is running and registered (./runner daemon logs)
  2. The runner can reach the Gitea instance (connection 1)
  3. Labels match: the runner's registered labels include the job's runs-on

If actions/checkout fails, verify:

  1. Job containers can reach the Gitea instance (connection 2)
  2. The runner was registered with a non-loopback address

Using Custom Actions from Private Repositories

To use actions from private repositories in workflows:

  1. In the source action repository: Settings -> Actions -> General -> add collaborative owners
  2. Additional private repositories can also be authorized via Settings -> Actions -> General -> Cross-Repository Access

Full Offline/Intranet Setup

For air-gapped Gitea instances:

  1. Set [actions].DEFAULTACTIONSURL = self in app.ini
  2. Mirror required actions repositories from GitHub to your Gitea instance
  3. Use absolute URLs for actions from other sources: uses: https://gitea.internal/team/action@v1
  4. Pre-pull Docker images to your Gitea Container Registry
  5. Configure the runner without internet access (use internal DNS and image registries)

Glossary

  • Workflow: An automated process defined in a YAML file consisting of one or more jobs
  • Job: A set of steps that execute on the same runner
  • Step: An individual task (a shell command or an action)
  • Action: A reusable custom script/plugin referenced via uses:
  • Runner: A program that executes workflow jobs, polled by Gitea for work
  • GITEA_TOKEN: An automatically generated token for authenticating API and Git requests from within a workflow job
  • Label: A tag on a runner that determines which jobs it accepts (e.g., ubuntu-latest)
  • Event: A repository activity that triggers a workflow (push, pull_request, etc.)
  • Scoped Workflow: A workflow defined in a central repository that runs automatically on consuming repositories
  • Ephemeral Runner: A runner that executes exactly one job and terminates, for enhanced security

Maintenance note: When updating this document, also update last_updated and verify compatibility with the latest Gitea version. Gitea Actions is under active development; check docs.gitea.com for new features in each release.