tftsr-devops_investigation/docs/wiki/CICD-Pipeline.md
Shaun Arman f1461a0d7e
Some checks failed
Test / rust-fmt-check (pull_request) Failing after 1s
Test / frontend-typecheck (pull_request) Successful in 2m47s
Test / rust-clippy (pull_request) Failing after 1s
Test / rust-tests (pull_request) Failing after 0s
PR Review Automation / review (pull_request) Successful in 3m25s
Test / frontend-tests (pull_request) Failing after 1m33s
perf(ci): use pre-baked images and add cargo/npm caching
Switch all test and release build jobs from raw base images to the
pre-baked images already defined in .docker/ and pushed to the local
Gitea registry. Add actions/cache@v3 for Cargo registry and npm to
eliminate redundant downloads on subsequent runs.

Changes:
- Dockerfile.linux-amd64/arm64: bake in rustfmt and clippy components
- test.yml: rust jobs → trcaa-linux-amd64:rust1.88-node22; drop inline
  apt-get and rustup component-add steps; add cargo cache
- test.yml: frontend jobs → add npm cache
- auto-tag.yml: build-linux-amd64 → trcaa-linux-amd64; drop Install
  dependencies step and rustup target add
- auto-tag.yml: build-windows-amd64 → trcaa-windows-cross; drop Install
  dependencies step and rustup target add
- auto-tag.yml: build-linux-arm64 → trcaa-linux-arm64 (ubuntu:22.04-based);
  drop ~40-line Install dependencies step, . "$HOME/.cargo/env", and
  rustup target add (all pre-baked in image ENV PATH)
- All build jobs: add cargo and npm cache steps
- docs/wiki/CICD-Pipeline.md: document pre-baked images, cache keys,
  and insecure-registries daemon prerequisite

Expected savings: ~70% faster PR test suite (~1.5 min vs ~5 min),
~72% faster release builds (~7 min vs ~25 min) after cache warms up.

NOTE: Trigger build-images.yml via workflow_dispatch before merging
to ensure images contain rustfmt/clippy before workflow changes land.
2026-04-12 18:17:35 -05:00

12 KiB

CI/CD Pipeline

Infrastructure

Component URL Notes
Gitea https://gogs.tftsr.com / http://172.0.0.29:3000 Git server (migrated from Gogs 0.14)
Woodpecker CI (direct) http://172.0.0.29:8084 v2.x
Woodpecker CI (proxy) http://172.0.0.29:8085 nginx reverse proxy
PostgreSQL (Gitea DB) Container: gogs_postgres_db DB: gogsdb, User: gogs

CI Agents

Agent Platform Host Purpose
gitea_act_runner_amd64 (Docker) linux-amd64 172.0.0.29 Native x86_64 — test builds + amd64/windows release
act_runner (systemd) linux-arm64 172.0.0.29 Native aarch64 — arm64 release builds
act_runner (launchd) macos-arm64 sarman's local Mac Native Apple Silicon — macOS .dmg release builds

Agent labels configured in ~/.config/act_runner/config.yaml:

runner:
  labels:
    - "macos-arm64:host"

macOS runner runs jobs directly on the host (no Docker container) — macOS SDK cannot run in Docker.


Pre-baked Builder Images

CI build and test jobs use pre-baked Docker images pushed to the local Gitea registry at 172.0.0.29:3000. These images bake in all system dependencies (Tauri libs, Node.js, Rust toolchain, cross-compilers) so that CI jobs skip package installation entirely.

Image Used by jobs Contents
172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22 rust-fmt-check, rust-clippy, rust-tests, build-linux-amd64 Rust 1.88 + rustfmt + clippy + Tauri amd64 libs + Node.js 22
172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22 build-windows-amd64 Rust 1.88 + mingw-w64 + NSIS + Node.js 22
172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22 build-linux-arm64 Rust 1.88 + aarch64 cross-toolchain + arm64 multiarch libs + Node.js 22

Rebuild triggers: Rust toolchain version bump, webkit2gtk/gtk major version change, Node.js major version change.

How to rebuild images:

  1. Trigger build-images.yml via workflow_dispatch in the Gitea Actions UI
  2. Confirm all 3 images appear in the Gitea package/container registry at 172.0.0.29:3000
  3. Only then merge workflow changes that depend on the new image contents

Server prerequisite — insecure registry (one-time, on 172.0.0.29):

echo '{"insecure-registries":["172.0.0.29:3000"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker

This must be configured on every machine running an act_runner for the runner's Docker daemon to pull from the local HTTP registry.


Cargo and npm Caching

All Rust and build jobs use actions/cache@v3 to cache downloaded package artifacts. Gitea 1.22 implements the GitHub Actions cache API natively.

Cargo cache (Rust jobs):

- name: Cache cargo registry
  uses: actions/cache@v3
  with:
    path: |
      ~/.cargo/registry/index
      ~/.cargo/registry/cache
      ~/.cargo/git/db      
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
    restore-keys: |
      ${{ runner.os }}-cargo-      

npm cache (frontend and build jobs):

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

Cache keys for cross-compile jobs use a suffix to avoid collisions:

  • Windows build: ${{ runner.os }}-cargo-windows-${{ hashFiles('**/Cargo.lock') }}
  • arm64 build: ${{ runner.os }}-cargo-arm64-${{ hashFiles('**/Cargo.lock') }}

Test Pipeline (.gitea/workflows/test.yml)

Triggers: Pull requests only.

Pipeline jobs (run in parallel):
  1. rust-fmt-check     → cargo fmt --check
  2. rust-clippy        → cargo clippy -- -D warnings
  3. rust-tests         → cargo test  (64 tests)
  4. frontend-typecheck → npx tsc --noEmit
  5. frontend-tests     → npm run test:run (13 Vitest tests)

Docker images used:

  • 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22 — Rust steps (replaces rust:1.88-slim)
  • node:22-alpine — Frontend steps

Release Pipeline (.gitea/workflows/auto-tag.yml)

Triggers: Pushes to master (auto-tag), then release build/upload jobs run after autotag.

Auto tags are created by .gitea/workflows/auto-tag.yml using git tag + git push. Release jobs are executed in the same workflow and depend on autotag completion.

Jobs (run in parallel after autotag):
  build-linux-amd64   → image: trcaa-linux-amd64:rust1.88-node22
                         → cargo tauri build (x86_64-unknown-linux-gnu)
                         → {.deb, .rpm, .AppImage} uploaded to Gitea release
                         → fails fast if no Linux artifacts are produced
  build-windows-amd64 → image: trcaa-windows-cross:rust1.88-node22
                         → cargo tauri build (x86_64-pc-windows-gnu) via mingw-w64
                         → {.exe, .msi} uploaded to Gitea release
                         → fails fast if no Windows artifacts are produced
  build-linux-arm64   → image: trcaa-linux-arm64:rust1.88-node22 (ubuntu:22.04-based)
                         → cargo tauri build (aarch64-unknown-linux-gnu)
                         → {.deb, .rpm, .AppImage} uploaded to Gitea release
                         → fails fast if no Linux artifacts are produced
  build-macos-arm64   → cargo tauri build (aarch64-apple-darwin) — runs on local Mac
                         → {.dmg} uploaded to Gitea release
                         → existing same-name assets are deleted before upload (rerun-safe)
                         → unsigned; after install run: xattr -cr /Applications/TFTSR.app

Per-step agent routing (Woodpecker 2.x labels):

steps:
  - name: build-linux-amd64
    labels:
      platform: linux/amd64   # → woodpecker_agent on 172.0.0.29

  - name: build-linux-arm64
    labels:
      platform: linux/arm64   # → woodpecker-agent.service on local arm64 machine

Multi-agent workspace isolation:

Steps routed to different agents do not share a workspace. The arm64 step clones the repo directly within its commands (using http://172.0.0.29:3000, accessible from the local machine) and uploads its artifacts inline. The upload-release step (amd64) handles amd64 + windows artifacts only.

Clone override (auto-tag.yml — amd64 workspace):

clone:
  git:
    image: alpine/git
    network_mode: gogs_default
    commands:
      - git init -b master
      - git remote add origin http://gitea_app:3000/sarman/tftsr-devops_investigation.git
      - git fetch --depth=1 origin +refs/tags/${CI_COMMIT_TAG}:refs/tags/${CI_COMMIT_TAG}
      - git checkout ${CI_COMMIT_TAG}

Windows cross-compile environment:

environment:
  TARGET: x86_64-pc-windows-gnu
  CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
  CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc

Artifacts per platform:

  • Linux amd64: .deb, .rpm, .AppImage
  • Windows amd64: .exe (NSIS installer), .msi
  • Linux arm64: .deb, .rpm, .AppImage

Upload step (requires gogs_default network for amd64, host IP for arm64):

# amd64 upload step
upload-release:
  image: curlimages/curl:latest
  labels:
    platform: linux/amd64
  network_mode: gogs_default
  secrets: [GOGS_TOKEN]

The GOGS_TOKEN Woodpecker secret must be created via the Woodpecker UI or API after migration. The secret name stays GOGS_TOKEN for pipeline compatibility.

Gitea Release API (replaces Gogs API — same endpoints, different container name):

# Create release
POST http://gitea_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases
Authorization: token $GOGS_TOKEN

# Upload artifact
POST http://gitea_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases/{id}/assets

From the arm64 agent (local machine), use http://172.0.0.29:3000/api/v1 instead.


Multi-File Pipeline Support (Woodpecker 2.x)

Woodpecker 2.x supports multiple pipeline files in the .woodpecker/ directory. All .yml files are evaluated on every trigger; when: conditions control which pipelines actually run.

Current files:

  • .woodpecker/test.yml — runs on every push/PR
  • .woodpecker/release.yml — runs on v* tags only

No DB config path switching needed (unlike Woodpecker 0.15.4).


Webhook Configuration

Woodpecker 2.x with Gitea OAuth2:

After migration, Woodpecker 2.x registers webhooks automatically when a repo is activated via the UI. No manual JWT-signed webhook setup required.

  1. Log in at http://172.0.0.29:8085 via Gitea OAuth2
  2. Add repo sarman/tftsr-devops_investigation
  3. Woodpecker creates webhook in Gitea automatically

Branch Protection

Master branch is protected: all changes require a PR.

-- Gitea branch protection (via psql on gogs_postgres_db container)
-- Check protection
SELECT name, protected, require_pull_request FROM protect_branch WHERE repo_id=42;

-- Temporarily disable for urgent fixes (restore immediately after!)
UPDATE protect_branch SET protected=false WHERE repo_id=42 AND name='master';
-- ... push ...
UPDATE protect_branch SET protected=true, require_pull_request=true WHERE repo_id=42 AND name='master';

Known Issues & Fixes

Debian Multiarch Breaks arm64 Cross-Compile (held broken packages)

When using rust:1.88-slim (Debian Bookworm) with dpkg --add-architecture arm64, apt resolves amd64 and arm64 simultaneously against the same mirror. The binary-all package index is duplicated and certain -dev package pairs cannot be co-installed because they don't declare Multi-Arch: same. This produces E: Unable to correct problems, you have held broken packages and cannot be fixed by tweaking sources.list entries.

Fix: Use ubuntu:22.04 as the container image. Ubuntu routes arm64 through ports.ubuntu.com/ubuntu-ports — a separate mirror from archive.ubuntu.com (amd64). There are no cross-arch index overlaps and the dependency resolver succeeds. Rust must be installed manually via rustup since it is not pre-installed in the Ubuntu base image.

Step Containers Cannot Reach gitea_app

Default Docker bridge containers cannot resolve gitea_app or reach 172.0.0.29:3000 (host firewall). Fix: use network_mode: gogs_default in any step that needs Gitea access. Requires repo_trusted=1.

CI=woodpecker Rejected by Tauri CLI

Woodpecker sets CI=woodpecker; cargo tauri build expects a boolean. Fix: prefix with CI=true cargo tauri build.

Agent Stalls After Server Restart

After restarting the Woodpecker server, the agent may enter a loop cleaning up orphaned containers and stop picking up new builds. Fix:

docker rm -f $(docker ps -aq --filter 'name=0_')
docker volume rm $(docker volume ls -q | grep '0_')
docker restart woodpecker_agent

Windows DLL Export Ordinal Too Large

/usr/bin/x86_64-w64-mingw32-ld: error: export ordinal too large: 106290

Fix: src-tauri/.cargo/config.toml:

[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]

GOGS_TOKEN Secret Must Be Recreated After Migration

After migrating from Woodpecker 0.15.4 to 2.x, recreate the GOGS_TOKEN secret:

  1. Log in to Gitea, create a new API token under Settings → Applications
  2. In Woodpecker UI → Repository → Secrets, add secret GOGS_TOKEN with the token value

Gitea PostgreSQL Access

docker exec gogs_postgres_db psql -U gogs -d gogsdb -c "SELECT id, lower_name FROM repository;"

Database name is gogsdb (unchanged from Gogs migration).


Migration Notes (Gogs 0.14 → Gitea)

Gitea auto-migrates the Gogs PostgreSQL schema on first start. Users, repos, teams, and issues are preserved. API tokens stored in the DB are also migrated but should be regenerated for security.

Key changes after migration:

  • Container name: gogs_appgitea_app
  • Config dir: /data/gitea (was /data/gogs inside container, same host volume)
  • Repo dir: gogs-repositoriesgitea-repositories (renamed on host during migration)
  • OAuth2 provider: Gitea now supports OAuth2 (Woodpecker 2.x uses this for login)
  • Woodpecker 2.x multi-file pipeline support enabled (no more single config file limitation)