Enable Forgejo Actions (Phase 1) (#48)
All checks were successful
Test CI / test (push) Successful in 0s

## Summary
- Refactor Forgejo app.ini to be managed by ansible with secrets from 1Password
- Enable Forgejo Actions in config (`[actions] ENABLED = true`)
- Add `repo.actions` to DEFAULT_REPO_UNITS
- Clean up unused MySQL database fields (we use SQLite)

## Phase 1 Progress
This PR covers the first part of Phase 1 (ci-cd-bootstrap plan):
- [x] Refactor app.ini to ansible template
- [x] Store secrets in 1Password
- [x] Enable Actions in config
- [ ] Deploy config changes (pending review)
- [ ] Create runner registration token
- [ ] Deploy runner to k8s
- [ ] Test with simple workflow

## Deployment and Testing
- [ ] Run `mise run provision-indri -- --tags forgejo` to deploy
- [ ] Verify Forgejo restarts correctly
- [ ] Verify Actions tab appears in repo settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/48
This commit is contained in:
Erich Blume 2026-01-23 17:00:12 -08:00
commit 7893c41020
15 changed files with 426 additions and 15 deletions

View file

@ -0,0 +1,29 @@
name: Test CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout (git clone)
run: |
# For PRs use head_ref (branch name), for pushes use ref_name
BRANCH="${{ gitea.head_ref || gitea.ref_name }}"
git clone --depth 1 --branch "$BRANCH" \
"${{ gitea.server_url }}/${{ gitea.repository }}.git" .
env:
GIT_SSL_NO_VERIFY: "true"
- name: Hello World
run: |
echo "Hello from Forgejo Actions!"
echo "Runner: $(hostname)"
echo "Repository: ${{ gitea.repository }}"
echo "Event: ${{ gitea.event_name }}"
echo "Ref: ${{ gitea.ref }}"
ls -la

View file

@ -102,6 +102,11 @@ kubectl --context=minikube-indri logs -n <namespace> <pod> # View logs
Note: The user has fish abbreviations `ki` for `kubectl --context=minikube-indri` and `k9i` for `k9s --context=minikube-indri`, but these only work in interactive shells.
**ArgoCD login (when token expires):**
```fish
argocd login argocd.tail8d86e.ts.net --username admin --password "$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get srogeebssulhtb6tnqd7ls6qey --fields password --reveal)"
```
### Indri Services (via Ansible)
Some services remain on indri outside of Kubernetes:

View file

@ -74,6 +74,10 @@ Hooks include:
- **TOML**: taplo
- **JSON**: prettier
## CI/CD
This repo uses [Forgejo Actions](https://forgejo.org/docs/latest/user/actions/) for CI/CD. Workflows live in `.forgejo/workflows/` (not `.github/workflows/`). The runner executes jobs in host mode within the Kubernetes cluster.
## Documentation
Detailed documentation lives in my personal zettelkasten, which is not included in this repository. You can view the docs with:

View file

@ -22,6 +22,45 @@
no_log: true
tags: [borgmatic]
# Forgejo secrets
- name: Fetch forgejo LFS JWT secret
ansible.builtin.command:
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields lfs-jwt-secret --reveal
delegate_to: localhost
register: _forgejo_lfs_jwt
changed_when: false
no_log: true
check_mode: false
tags: [forgejo]
- name: Fetch forgejo internal token
ansible.builtin.command:
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields internal-token --reveal
delegate_to: localhost
register: _forgejo_internal_token
changed_when: false
no_log: true
check_mode: false
tags: [forgejo]
- name: Fetch forgejo OAuth2 JWT secret
ansible.builtin.command:
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields oauth2-jwt-secret --reveal
delegate_to: localhost
register: _forgejo_oauth2_jwt
changed_when: false
no_log: true
check_mode: false
tags: [forgejo]
- name: Set forgejo secrets facts
ansible.builtin.set_fact:
forgejo_lfs_jwt_secret: "{{ _forgejo_lfs_jwt.stdout }}"
forgejo_internal_token: "{{ _forgejo_internal_token.stdout }}"
forgejo_oauth2_jwt_secret: "{{ _forgejo_oauth2_jwt.stdout }}"
no_log: true
tags: [forgejo]
roles:
- role: alloy
tags: alloy

View file

@ -0,0 +1,51 @@
---
# Forgejo configuration
# Secrets are fetched from 1Password in the playbook pre_tasks
forgejo_app_name: Forgejo
forgejo_app_slogan: "Beyond coding. We Forge."
forgejo_run_user: forgejo
forgejo_run_mode: prod
# Paths (brew-managed for now, will change to mcquack in Phase 3)
forgejo_work_path: /opt/homebrew/var/forgejo
forgejo_config_path: "{{ forgejo_work_path }}/custom/conf/app.ini"
forgejo_data_path: "{{ forgejo_work_path }}/data"
forgejo_repo_root: "{{ forgejo_data_path }}/forgejo-repositories"
forgejo_lfs_path: "{{ forgejo_data_path }}/lfs"
forgejo_log_path: "{{ forgejo_work_path }}/log"
# Server settings
forgejo_http_addr: 0.0.0.0
forgejo_http_port: 3001
forgejo_domain: forge.tail8d86e.ts.net
forgejo_ssh_domain: "{{ forgejo_domain }}"
forgejo_root_url: "https://{{ forgejo_domain }}/"
forgejo_offline_mode: true
# SSH settings (built-in SSH server)
forgejo_disable_ssh: false
forgejo_start_ssh_server: true
forgejo_builtin_ssh_user: forgejo
forgejo_ssh_port: 22
forgejo_ssh_listen_port: 2200
forgejo_lfs_start_server: true
# Database (SQLite)
forgejo_db_type: sqlite3
forgejo_db_path: "{{ forgejo_data_path }}/forgejo.db"
# Service settings
forgejo_disable_registration: true
forgejo_require_signin_view: false
# Session
forgejo_session_provider: file
# Logging
forgejo_log_mode: console
forgejo_log_level: info
# Actions (Forgejo CI)
forgejo_actions_enabled: true
forgejo_actions_default_url: https://code.forgejo.org

View file

@ -1,26 +1,29 @@
---
# Note: forgejo config at /opt/homebrew/var/forgejo/custom/conf/app.ini
# is not managed here (contains secrets). It is backed up by borgmatic.
# Forgejo role
#
# Currently uses brew-managed forgejo. Phase 3 of ci-cd-bootstrap will
# transition to mcquack LaunchAgent with CI-built binary.
#
# Secrets (lfs_jwt_secret, internal_token, oauth2_jwt_secret) are fetched
# from 1Password in the playbook pre_tasks.
- name: Install forgejo via homebrew
community.general.homebrew:
name: forgejo
state: present
- name: Check forgejo config exists
ansible.builtin.stat:
path: /opt/homebrew/var/forgejo/custom/conf/app.ini
register: forgejo_config
- name: Ensure forgejo config directory exists
ansible.builtin.file:
path: "{{ forgejo_work_path }}/custom/conf"
state: directory
mode: '0755'
- name: Fail if forgejo config is missing
ansible.builtin.fail:
msg: |
Forgejo config not found at /opt/homebrew/var/forgejo/custom/conf/app.ini
This file contains secrets and is not managed by ansible.
To restore from backup, run:
borgmatic --config ~/.config/borgmatic/config.yaml extract --archive latest \
--path /opt/homebrew/var/forgejo/custom/conf/app.ini
when: not forgejo_config.stat.exists
- name: Deploy forgejo config
ansible.builtin.template:
src: app.ini.j2
dest: "{{ forgejo_config_path }}"
mode: '0600'
notify: Restart forgejo
- name: Ensure forgejo service is started
ansible.builtin.command: brew services start forgejo

View file

@ -0,0 +1,82 @@
# {{ ansible_managed }}
APP_NAME = {{ forgejo_app_name }}
APP_SLOGAN = {{ forgejo_app_slogan }}
RUN_USER = {{ forgejo_run_user }}
WORK_PATH = {{ forgejo_work_path }}
RUN_MODE = {{ forgejo_run_mode }}
[server]
HTTP_ADDR = {{ forgejo_http_addr }}
HTTP_PORT = {{ forgejo_http_port }}
SSH_DOMAIN = {{ forgejo_ssh_domain }}
DOMAIN = {{ forgejo_domain }}
ROOT_URL = {{ forgejo_root_url }}
APP_DATA_PATH = {{ forgejo_data_path }}
DISABLE_SSH = {{ forgejo_disable_ssh | lower }}
START_SSH_SERVER = {{ forgejo_start_ssh_server | lower }}
BUILTIN_SSH_SERVER_USER = {{ forgejo_builtin_ssh_user }}
SSH_PORT = {{ forgejo_ssh_port }}
SSH_LISTEN_PORT = {{ forgejo_ssh_listen_port }}
LFS_START_SERVER = {{ forgejo_lfs_start_server | lower }}
LFS_JWT_SECRET = {{ forgejo_lfs_jwt_secret }}
OFFLINE_MODE = {{ forgejo_offline_mode | lower }}
[database]
DB_TYPE = {{ forgejo_db_type }}
PATH = {{ forgejo_db_path }}
LOG_SQL = false
[repository]
ROOT = {{ forgejo_repo_root }}
DEFAULT_REPO_UNITS = repo.code,repo.issues,repo.pulls,repo.releases,repo.wiki,repo.projects,repo.packages,repo.actions
[lfs]
PATH = {{ forgejo_lfs_path }}
[mailer]
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = {{ forgejo_disable_registration | lower }}
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = {{ forgejo_require_signin_view | lower }}
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.indri
[openid]
ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false
[cron.update_checker]
ENABLED = false
[session]
PROVIDER = {{ forgejo_session_provider }}
[log]
MODE = {{ forgejo_log_mode }}
LEVEL = {{ forgejo_log_level }}
ROOT_PATH = {{ forgejo_log_path }}
[repository.pull-request]
DEFAULT_MERGE_STYLE = merge
[repository.signing]
DEFAULT_TRUST_MODEL = committer
[security]
INSTALL_LOCK = true
INTERNAL_TOKEN = {{ forgejo_internal_token }}
PASSWORD_HASH_ALGO = pbkdf2_hi
[oauth2]
JWT_SECRET = {{ forgejo_oauth2_jwt_secret }}
[actions]
ENABLED = {{ forgejo_actions_enabled | lower }}
DEFAULT_ACTIONS_URL = {{ forgejo_actions_default_url }}

View file

@ -0,0 +1,23 @@
# Forgejo Actions Runner
# Runs in k8s, polls Forgejo for workflow jobs
#
# Before syncing, create the runner token secret:
# kubectl create namespace forgejo-runner
# op inject -i argocd/manifests/forgejo-runner/secret-token.yaml.tpl | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: forgejo-runner
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/forgejo-runner
destination:
server: https://kubernetes.default.svc
namespace: forgejo-runner
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: forgejo-runner-config
namespace: forgejo-runner
data:
config.yaml: |
log:
level: info
runner:
file: /data/.runner
capacity: 1
timeout: 3h

View file

@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: forgejo-runner
namespace: forgejo-runner
spec:
replicas: 1
selector:
matchLabels:
app: forgejo-runner
template:
metadata:
labels:
app: forgejo-runner
spec:
serviceAccountName: forgejo-runner
containers:
- name: runner
image: code.forgejo.org/forgejo/runner:3.5.1
env:
# Use internal k8s service via Tailscale operator egress
- name: FORGEJO_INSTANCE_URL
value: "http://forge.tailscale.svc.cluster.local:3001"
- name: RUNNER_NAME
value: "k8s-runner-1"
- name: RUNNER_TOKEN
valueFrom:
secretKeyRef:
name: forgejo-runner-token
key: token
command:
- /bin/sh
- -c
- |
# Register runner if not already registered
if [ ! -f /data/.runner ]; then
forgejo-runner register \
--instance "$FORGEJO_INSTANCE_URL" \
--token "$RUNNER_TOKEN" \
--name "$RUNNER_NAME" \
--labels "ubuntu-latest:host,ubuntu-22.04:host" \
--no-interactive
fi
# Start the runner daemon with config
forgejo-runner daemon --config /config/config.yaml
volumeMounts:
- name: runner-data
mountPath: /data
- name: runner-config
mountPath: /config
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
volumes:
- name: runner-data
emptyDir: {}
- name: runner-config
configMap:
name: forgejo-runner-config

View file

@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: forgejo-runner
resources:
- namespace.yaml
- serviceaccount.yaml
- configmap.yaml
- deployment.yaml

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: forgejo-runner

View file

@ -0,0 +1,10 @@
# Template for op inject
# Usage: op inject -i secret-token.yaml.tpl | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: forgejo-runner-token
namespace: forgejo-runner
type: Opaque
stringData:
token: "op://blumeops/w3663ffnvkewbftncqxtcpeavy/runner_reg"

View file

@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: forgejo-runner
namespace: forgejo-runner

View file

@ -17,6 +17,78 @@ With Forgejo Actions operational, we can now build container images for:
---
## Use Case 0: Custom Runner Image
### Problem
The stock `forgejo/runner` image lacks tools needed for standard GitHub Actions:
- **Node.js** - Required by most actions (checkout, setup-*, etc.)
- **Docker CLI** - For building container images
- **Git** - For repository operations
- **Common build tools** - make, gcc, etc.
In host mode, jobs run directly in the runner container, so these tools must be pre-installed.
### Solution
Build a custom runner image with all necessary tools:
```dockerfile
# argocd/manifests/forgejo-runner/Dockerfile
FROM code.forgejo.org/forgejo/runner:3.5.1
# Install Node.js (required for most GitHub Actions)
RUN apt-get update && apt-get install -y \
nodejs \
npm \
git \
curl \
docker.io \
make \
gcc \
&& rm -rf /var/lib/apt/lists/*
```
### Workflow
Create `.forgejo/workflows/build-runner.yml`:
```yaml
name: Build Runner Image
on:
push:
paths:
- 'argocd/manifests/forgejo-runner/Dockerfile'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone --depth 1 "${{ gitea.server_url }}/${{ gitea.repository }}.git" .
- name: Build and push
run: |
cd argocd/manifests/forgejo-runner
docker build -t registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest .
docker push registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest
```
### Update Deployment
Once the custom image is built, update `argocd/manifests/forgejo-runner/deployment.yaml`:
```yaml
image: registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest
```
This enables standard GitHub Actions like `actions/checkout@v4` to work in host mode.
---
## Use Case 1: devpi Custom Image
### Current State