Building CI/CD workflows with Forgejo
IDEAS / CODE / PROCESS *
Workflow guide
To add CI/CD to a repository, create .forgejo/workflows. Files such as unit-test.yml describe the steps a runner performs after a configured event such as a push or pull request.
Workflows can read private Forgejo variables called secrets. They let a job access private packages, clone repositories over SSH, deploy code or send email without committing credentials to the repository.
Reference workflow
The domain forgejo.example.com, the python-tests runner label and the paths below are examples. Adapt them to your server and repository.
name: Project tests
on:
push:
pull_request:
workflow_dispatch:
jobs:
project-tests:
name: Prepare data and run tests
runs-on: python-tests
timeout-minutes: 360
steps:
- name: Check out repository over SSH
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
ssh-strict: true
ssh-user: git
persist-credentials: false
- name: Install Python and system tools
shell: bash
run: |
set -euo pipefail
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y \
--no-install-recommends \
python3 git curl openssh-client ca-certificates
rm -rf /var/lib/apt/lists/*
python3 --version
- name: Install the internal CA certificate
shell: bash
env:
INTERNAL_CA_CERT: ${{ secrets.INTERNAL_CA_CERT }}
run: |
set -euo pipefail
test -n "$INTERNAL_CA_CERT"
printf '%s\n' "$INTERNAL_CA_CERT" \
> /usr/local/share/ca-certificates/forgejo-ca.crt
chmod 644 /usr/local/share/ca-certificates/forgejo-ca.crt
update-ca-certificates
test -s /etc/ssl/certs/ca-certificates.crt
HTTP_CODE="$(
curl --fail --silent --show-error \
--output /dev/null \
--write-out '%{http_code}' \
https://forgejo.example.com/
)"
echo "Forgejo returned HTTP $HTTP_CODE"
- name: Install uv
shell: bash
run: |
set -euo pipefail
curl -LsSf https://astral.sh/uv/install.sh \
| env UV_UNMANAGED_INSTALL="/usr/local/bin" sh
uv --version
- name: Resolve and install dependencies
shell: bash
working-directory: ${{ github.workspace }}
env:
UV_LINK_MODE: copy
UV_INDEX_FORGEJO_USERNAME: ${{ secrets.PACKAGE_READ_USERNAME }}
UV_INDEX_FORGEJO_PASSWORD: ${{ secrets.PACKAGE_READ_TOKEN }}
UV_SYSTEM_CERTS: "true"
run: |
set -euo pipefail
uv sync
test -x "$GITHUB_WORKSPACE/.venv/bin/python"
"$GITHUB_WORKSPACE/.venv/bin/python" --version
- name: Prepare test data
shell: bash
working-directory: ${{ github.workspace }}
env:
PYTHONUNBUFFERED: "1"
run: |
set -euo pipefail
PYTHON="$GITHUB_WORKSPACE/.venv/bin/python"
"$PYTHON" tests/prepare_data.py
test -d tests/data
- name: Run functional tests
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -uo pipefail
PYTHON="$GITHUB_WORKSPACE/.venv/bin/python"
TESTS_FOUND=0
FAILED_TESTS=0
while IFS= read -r -d '' TEST_FILE; do
TESTS_FOUND=$((TESTS_FOUND + 1))
echo "Running: $TEST_FILE"
if "$PYTHON" "$TEST_FILE"; then
echo "PASS: $TEST_FILE"
else
echo "FAIL: $TEST_FILE"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
done < <(
find "$GITHUB_WORKSPACE/tests" \
-type f \
\( -name "main.py" -o -name "main_*.py" \) \
-print0 | sort -z
)
if [ "$TESTS_FOUND" -eq 0 ]; then
echo "ERROR: no main.py or main_*.py files were found"
exit 1
fi
echo "Tests found: $TESTS_FOUND"
echo "Tests failed: $FAILED_TESTS"
if [ "$FAILED_TESTS" -ne 0 ]; then
exit 1
fi
Events
Apart from its name, the workflow begins with its event configuration:
on:
push:
pull_request:
workflow_dispatch:
Common events include push, pull_request, pull_request_target, issues, issue_comment, release, schedule, workflow_dispatch and workflow_call.
If both push and pull_request are unrestricted, the same tests may run twice. Limit an event to specific branches:
on:
push:
branches:
- develop
- master
or ignore selected branches:
on:
push:
branches-ignore:
- master
- documentation
Branch patterns are also supported:
on:
push:
branches:
- develop
- "feature/**"
- "fix/**"
You can filter by changed paths too:
on:
push:
paths-ignore:
- "docs/**"
- "*.md"
workflow_dispatch adds a manual run option in Forgejo, which is useful while testing a workflow:
on:
workflow_dispatch:
Jobs
A job is a complete unit of work sent to one runner. A workflow may contain several jobs assigned to different runner labels.
jobs:
project-tests:
name: Prepare data and run tests
runs-on: python-tests
timeout-minutes: 360
project-testsis the internal identifier.nameis displayed in Forgejo.runs-onselects the label configured inrunner-config.yml.timeout-minuteslimits the total execution time.
A future pipeline might separate concerns:
jobs:
lint:
# Check code formatting
tests:
# Run tests
publish:
# Publish a package
Steps
Jobs contain sequential steps. A reusable action is invoked with uses:
- name: Check out repository over SSH
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
ssh-strict: true
ssh-user: git
persist-credentials: false
Here, actions is the owner, checkout is the action and @v4 is its version. with supplies its inputs. Checkout places the requested repository commit on the runner.
Other useful step keys are:
env: environment variables for this step;working-directory: the directory in which commands run;shell: the interpreter, usually Bash;run: shell, Python or other script commands;timeout-minutes: a per-step time limit;continue-on-error: whether later steps may continue after a failure.
A reusable test-step structure
A typical testing job follows this order:
- Check out the repository.
- Install Python and required system tools.
- Install the TLS certificate needed for private packages.
- Install uv.
- Resolve dependencies from
pyproject.toml. - Prepare data and any other test prerequisites.
- Discover and run the tests.
The example test loop searches tests and its subdirectories for main.py, main_1.py, main_2.py, and similar files. New tests can therefore be added without editing the workflow. If a project must exclude specific tests, add ! -path filters to the find command.