when: platform: is evaluated at compile time (server=amd64) and silently drops the arm64 step. Per-step platform routing requires Woodpecker 2.x. Document the make release-arm64 workaround for linux/arm64 builds. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
11 KiB
CI/CD Pipeline
Infrastructure
| Component | URL | Notes |
|---|---|---|
| Gogs | https://gogs.tftsr.com / http://172.0.0.29:3000 |
Git server, version 0.14 |
| Woodpecker CI (direct) | http://172.0.0.29:8084 |
v0.15.4 |
| Woodpecker CI (proxy) | http://172.0.0.29:8085 |
nginx with custom login page |
| PostgreSQL (Gogs DB) | Container: gogs_postgres_db |
DB: gogsdb, User: gogs |
CI Agents
| Agent | Platform | Host | Purpose |
|---|---|---|---|
woodpecker_agent (Docker) |
linux/amd64 |
172.0.0.29 | Native x86_64 — test builds + amd64/windows release |
woodpecker-agent (systemd) |
linux/arm64 |
sarman's local machine | Native aarch64 — arm64 release builds |
woodpecker_agent_arm64 (Docker) |
linux/arm64 |
172.0.0.29 | QEMU fallback — kept as backup |
Test Pipeline (.woodpecker/test.yml)
Triggers: Every push and pull request to any branch.
Pipeline steps:
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:
rust:1.88-slim— Rust steps (minimum for cookie_store + time + darling)node:22-alpine— Frontend steps
Pipeline YAML format (Woodpecker 0.15.4 — legacy MAP format):
clone:
git:
image: woodpeckerci/plugin-git
network_mode: gogs_default # requires repo_trusted=1
environment:
- CI_REPO_CLONE_URL=http://gogs_app:3000/sarman/tftsr-devops_investigation.git
pipeline:
step-name: # KEY = step name (MAP, not list!)
image: rust:1.88-slim
commands:
- cargo test
⚠️ Do NOT use the newer
steps:list format — Woodpecker 0.15.4 uses the Drone-legacy map format. Usingsteps:causes "Invalid or missing pipeline section" error.
Release Pipeline (.woodpecker/release.yml)
Triggers: Git tags matching v*
Active config path: Woodpecker DB must have repo_config_path = .woodpecker/release.yml when the tag is pushed. Switch back to test.yml after tagging to restore PR/push CI.
Pipeline steps:
1. clone → alpine/git with explicit tag fetch + checkout
2. build-linux-amd64 → cargo tauri build (x86_64-unknown-linux-gnu)
→ artifacts/linux-amd64/{.deb, .rpm, .AppImage}
3. build-windows-amd64 → cargo tauri build (x86_64-pc-windows-gnu)
→ artifacts/windows-amd64/{.exe, .msi}
4. upload-release → Create Gogs release + upload all artifacts
linux/arm64 (manual): make release-arm64 GOGS_TOKEN=<token> (see below)
Clone override (release.yml):
Release builds use alpine/git with explicit commands because woodpeckerci/plugin-git:latest uses git switch which fails on tag refs:
clone:
git:
image: alpine/git
network_mode: gogs_default
commands:
- git init -b master
- git remote add origin http://gogs_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— built viamake release-arm64(see below)
Linux arm64 build (Woodpecker 0.15.4 workaround):
Woodpecker 0.15.4 evaluates when: platform: at compile time against the server's
platform (amd64), dropping arm64 steps before any agent can claim them. Per-step
agent routing is a Woodpecker 2.x feature.
To build and upload arm64 artifacts from the local aarch64 machine:
# On the local arm64 machine (Fedora Asahi 42)
cd ~/Documents/tftsr-devops_investigation
make release-arm64 TAG=v0.1.0-alpha GOGS_TOKEN=<bearer_token>
make build-arm64 runs the full Tauri build inside a rust:1.88-slim ARM64 Docker
container. make upload-arm64 uploads the resulting artifacts to the Gogs release.
Important: Artifacts must be written to the workspace (relative paths like artifacts/linux-amd64/), not to absolute paths like /artifacts/. Only the workspace is shared between pipeline steps via Docker volume.
Upload step (requires gogs_default network):
upload-release:
image: curlimages/curl:latest
network_mode: gogs_default # host firewall blocks default bridge from reaching Gogs API
secrets: [GOGS_TOKEN]
The GOGS_TOKEN Woodpecker secret is inserted into the DB:
conn.execute("""
INSERT INTO secrets (secret_repo_id, secret_name, secret_value, secret_images, secret_events, secret_skip_verify, secret_conceal)
VALUES (1, 'GOGS_TOKEN', '<bearer_token>', '', 'tag', 0, 1)
""")
Gogs Release API:
# Create release
POST http://gogs_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases
Authorization: token $GOGS_TOKEN
# Upload artifact
POST http://gogs_app:3000/api/v1/repos/sarman/tftsr-devops_investigation/releases/{id}/assets
Switching Between Test and Release Config
Woodpecker 0.15.4 supports only one config file per repo. The workflow:
# For regular pushes/PRs — use test pipeline
python3 -c "conn.execute(\"UPDATE repos SET repo_config_path='.woodpecker/test.yml'\")"
# Before pushing a release tag — switch to release pipeline
python3 -c "conn.execute(\"UPDATE repos SET repo_config_path='.woodpecker/release.yml'\")"
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
# → Switch back to test.yml after build starts
Webhook Configuration
Hook ID: 9 (in Gogs, http://gogs.tftsr.com)
Events: create, push, pull_request
URL: http://172.0.0.29:8084/hook?access_token=<JWT>
JWT signing:
- Algorithm: HS256
- Secret:
repo_hashfrom Woodpecker DB (dK8zFWtAu67qfKd3Et6N8LptqTmedumJ) - Payload:
{"text":"sarman/tftsr-devops_investigation","type":"hook","iat":<timestamp>}
Regenerate JWT when stale:
import base64, hmac, hashlib, json, time
def b64url(data):
if isinstance(data, str): data = data.encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = b64url(json.dumps({'alg':'HS256','typ':'JWT'}, separators=(',',':')))
payload = b64url(json.dumps({'text':'sarman/tftsr-devops_investigation','type':'hook','iat':int(time.time())}, separators=(',',':')))
msg = f'{header}.{payload}'
sig = hmac.new(b'dK8zFWtAu67qfKd3Et6N8LptqTmedumJ', msg.encode(), hashlib.sha256).digest()
print(f'{msg}.{b64url(sig)}')
Then update the webhook in Gogs via API:
curl -X DELETE http://172.0.0.29:3000/api/v1/repos/sarman/tftsr-devops_investigation/hooks/<old_id>
curl -X POST http://172.0.0.29:3000/api/v1/repos/sarman/tftsr-devops_investigation/hooks \
-H "Authorization: token <bearer_token>" \
-H "Content-Type: application/json" \
-d '{"type":"gogs","config":{"url":"http://172.0.0.29:8084/hook?access_token=<NEW_JWT>","content_type":"json","secret":"af5dc60e0984f2680d0969f4a087e7100a4ece7e"},"events":["push","pull_request","create"],"active":true}'
Woodpecker DB State
SQLite at /docker_mounts/woodpecker/data/woodpecker.sqlite (on host 172.0.0.29).
-- Verify config
SELECT user_token IS NOT NULL AND user_token != '' AS token_set FROM users WHERE user_login='sarman';
SELECT repo_active, repo_trusted, repo_config_path, repo_hash
FROM repos WHERE repo_full_name='sarman/tftsr-devops_investigation';
-- repo_active=1, repo_trusted=1
-- repo_config_path='.woodpecker/test.yml' (or release.yml during release)
-- repo_hash='dK8zFWtAu67qfKd3Et6N8LptqTmedumJ'
Branch Protection
Master branch is protected: all changes require a PR. Direct pushes are blocked.
-- 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';
Gogs 0.14 does not enforce required CI status checks before merging. Only
require_pull_request=trueis supported.
Known Issues & Fixes
Webhook JWT Must Use ?access_token=
token.ParseRequest() in Woodpecker 0.15.4 does not read ?token= URL params. Use ?access_token=<JWT> instead.
Directory-Based Config Not Supported
Woodpecker 0.15.4 only supports a single config file. Multi-file pipelines require v2.x+.
Empty Clone URL in Push Events
Woodpecker 0.15.4's go-gogs-client PayloadRepo struct lacks CloneURL, so build_remote is always empty. Fix: set CI_REPO_CLONE_URL in the clone step environment.
Step Containers Cannot Reach gogs_app
Default Docker bridge containers cannot resolve gogs_app or reach 172.0.0.29:3000 (host firewall). Fix: use network_mode: gogs_default in any step that needs Gogs 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:
# Kill orphan containers and volumes
docker rm -f $(docker ps -aq --filter 'name=0_')
docker volume rm $(docker volume ls -q | grep '0_')
# Restart agent
docker restart woodpecker_agent
Per-Step Agent Platform Routing Not Supported
Woodpecker 0.15.4 evaluates when: platform: conditions at pipeline compile time
against the server's platform (amd64). Steps filtered by platform are dropped
before any agent can claim them, so arm64 steps never reach the arm64 agent.
The platform: step-level key (e.g. platform: linux/arm64) is treated as a plugin
attribute and causes Cannot configure both commands and custom attributes [platform].
Workaround: build arm64 artifacts locally via make release-arm64. This is fixed in
Woodpecker 2.x which supports proper per-step label-based agent routing.
Windows DLL Export Ordinal Too Large
/usr/bin/x86_64-w64-mingw32-ld: error: export ordinal too large: 106290
MinGW's ld auto-exports ALL public Rust symbols into the DLL export table. With a
large dependency tree (~106k symbols), this exceeds the 65,535 PE ordinal limit.
Fix: src-tauri/.cargo/config.toml tells ld to suppress auto-export:
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-arg=-Wl,--exclude-all-symbols"]
The desktop main.exe links against rlib (static), so the cdylib export table is
unused at runtime. An empty export table is valid for a DLL.
Gogs OAuth2 Limitation
Gogs 0.14 has no OAuth2 provider support, blocking upgrade to Woodpecker 2.x.
Gogs PostgreSQL Access
docker exec gogs_postgres_db psql -U gogs -d gogsdb -c "SELECT id, lower_name FROM repository;"
Database name is
gogsdb, notgogs.