22 KiB
Gitea Actions Workflows - Line-by-Line Explanation
This document provides a detailed explanation of every line in the CI/CD workflows, explaining what each line does, why it's needed, and how it works.
Table of Contents
CI Workflow (ci.yml)
Header Section
name: CI
What: Defines the workflow name displayed in the Gitea Actions UI
Why: Helps identify this workflow among multiple workflows
How: Gitea reads this and displays "CI" in the Actions tab
Trigger Configuration
on:
What: Starts the trigger configuration section
Why: Tells Gitea when to run this workflow
How: Gitea monitors repository events and matches them against these triggers
push:
branches:
- '**'
What: Triggers workflow on push to any branch
Why: We want to test every commit on every branch
How: '**' is a glob pattern matching all branch names (main, develop, feature/*, etc.)
Alternative: Use - main to only run on main branch
pull_request:
types: [opened, synchronize, reopened]
What: Triggers workflow on pull request events
Why: Test code before merging into main branch
How:
opened- When PR is first createdsynchronize- When new commits are pushed to the PRreopened- When a closed PR is reopened
Note: Does not run onclosedto save resources
Job Definition
jobs:
What: Starts the jobs section
Why: Workflows contain one or more jobs that run tasks
How: Each job runs in a fresh virtual environment
build-and-test:
What: Job identifier (internal name)
Why: Unique ID for this job, used in logs and dependencies
How: Must be unique within the workflow, use kebab-case
name: Build & Test
What: Human-readable job name
Why: Displayed in the Gitea UI for better readability
How: Shows in the Actions tab instead of "build-and-test"
runs-on: ubuntu-latest
What: Specifies which runner to use
Why: Determines the OS and environment for the job
How: Gitea matches this label with available runners
Note: Your runner must have the ubuntu-latest label configured
Steps Section
steps:
What: Starts the list of steps to execute
Why: Steps are the individual tasks that make up a job
How: Steps run sequentially in order
Step 1: Checkout Code
- name: Checkout code
What: Human-readable step name
Why: Helps identify this step in logs
How: Displayed in the workflow run UI
uses: actions/checkout@v4
What: Uses a pre-built action to checkout code
Why: Clones your repository into the runner's workspace
How:
actions/checkout- GitHub's official checkout action (Gitea compatible)@v4- Pins to version 4 for stability
What it does:
- Clones the repository
- Checks out the commit that triggered the workflow
- Sets up git configuration
Why needed: Without this, the runner has no access to your code
Step 2: Setup Rust
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
What: Installs Rust toolchain on the runner
Why: Needed to compile Rust code
How: Downloads and installs rustc, cargo, and related tools
What it does:
- Installs Rust stable version
- Adds cargo to PATH
- Configures cargo home directory
with:
toolchain: stable
What: Specifies which Rust version to install
Why: stable ensures we use the latest stable Rust release
How: Downloads from rust-lang.org
Alternatives: nightly, beta, or specific version like 1.75.0
components: rustfmt, clippy
What: Installs additional Rust components
Why:
rustfmt- Code formatter (needed for formatting check)clippy- Linter (needed for lint check)
How: Installed via rustup alongside the toolchain
Step 3-5: Caching
Why caching is needed:
- Rust compilation is slow
- Dependencies rarely change
- Caching speeds up builds from ~15 minutes to ~2 minutes
- name: Cache cargo registry
uses: actions/cache@v4
What: Caches the Cargo registry
Why: Stores downloaded crate metadata to avoid re-downloading
How: Uses GitHub's cache action (Gitea compatible)
with:
path: ~/.cargo/registry
What: Directory to cache
Why: This is where Cargo stores downloaded crate files
How: Entire directory is compressed and stored
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
What: Unique cache key
Why: Identifies this specific cache
How:
${{ runner.os }}- OS name (e.g., "Linux")cargo-registry- Cache identifier${{ hashFiles('**/Cargo.lock') }}- Hash of Cargo.lock
Result: Key likeLinux-cargo-registry-a1b2c3d4
When it changes: When Cargo.lock changes (dependencies updated)
restore-keys: |
${{ runner.os }}-cargo-registry-
What: Fallback cache keys
Why: If exact match not found, use partial match
How: Tries to find any cache starting with Linux-cargo-registry-
Benefit: Even with dependency changes, most cache is still valid
Same pattern repeats for:
- Cargo index cache (
~/.cargo/git) - Build cache (
target/)
Step 6: Check Code
- name: Check code
run: cargo check --workspace --verbose
What: Runs cargo check command
Why: Fast compilation check without producing binaries
How:
cargo check- Compiles code but doesn't generate executables--workspace- Check all packages in the workspace--verbose- Show detailed output
Benefit: Catches compilation errors quickly (~2x faster than full build)
Exit code: Non-zero if compilation fails, which fails the workflow
Step 7: Run Tests
- name: Run tests
run: cargo test --workspace --verbose
What: Runs all tests in the workspace
Why: Ensures code changes don't break functionality
How:
- Compiles test code
- Runs all
#[test]functions - Runs integration tests in
tests/directory
Exit code: Non-zero if any test fails
Step 8: Run Clippy
- name: Run clippy
run: cargo clippy --workspace -- -D warnings
What: Runs Rust linter
Why: Catches common mistakes and enforces best practices
How:
cargo clippy- Runs the clippy linter--workspace- Lint all packages--- Separator between cargo args and clippy args-D warnings- Treat warnings as errors
Result: Fails if any clippy warnings are found
Examples of what clippy catches:- Unused variables
- Inefficient code patterns
- Potential bugs
Step 9: Check Formatting
- name: Check formatting
run: cargo fmt --all -- --check
What: Checks if code is properly formatted
Why: Enforces consistent code style
How:
cargo fmt- Rust formatter--all- Check all packages--- Separator--check- Don't modify files, just check
Exit code: Non-zero if any file is not formatted
To fix locally: Runcargo fmt --allwithout--check
Step 10: Build Release Binaries
- name: Build release binaries
run: cargo build --workspace --release --verbose
What: Builds all binaries with optimizations
Why: Ensures release builds work and produces artifacts
How:
cargo build- Compile code--workspace- Build all packages--release- Enable optimizations (from Cargo.toml profile.release)--verbose- Show detailed output
Result: Binaries intarget/release/directory
Time: ~5-10 minutes (first run), ~2-5 minutes (cached)
Step 11: List Binaries
- name: List built binaries
run: |
echo "Built binaries:"
ls -lh target/release/ | grep -E '^-.*x.*'
What: Lists built executable files
Why: Helps verify all binaries were built successfully
How:
ls -lh- List files with human-readable sizesgrep -E '^-.*x.*'- Filter for executable files
Output: Shows binary names and sizes in logs
Step 12: Upload Artifacts
- name: Upload build artifacts
uses: actions/upload-artifact@v4
What: Uploads files to Gitea for download
Why: Makes binaries available for testing without creating a release
How: Uses GitHub's upload action (Gitea compatible)
with:
name: binaries-${{ github.sha }}
What: Artifact name
Why: Unique name for this set of binaries
How: ${{ github.sha }} - Git commit SHA (e.g., binaries-a1b2c3d4)
path: |
target/release/supervisor
target/release/coordinator
target/release/horus
target/release/osiris
target/release/herorunner
target/release/runner_osiris
target/release/runner_sal
What: Files to upload
Why: These are the 7 binaries we want to preserve
How: Each line is a file path, | allows multi-line list
retention-days: 7
What: How long to keep artifacts
Why: Saves storage space by auto-deleting old artifacts
How: Gitea automatically deletes after 7 days
if-no-files-found: warn
What: What to do if files don't exist
Why: warn logs a warning but doesn't fail the workflow
How: Useful if some binaries fail to build
Alternatives: error (fail workflow), ignore (silent)
Release Workflow (release.yml)
Header and Triggers
name: Release
What: Workflow name
Why: Identifies this as the release workflow
on:
push:
tags:
- 'v*.*.*'
What: Triggers on version tags
Why: Only create releases for version tags
How:
tags:- Watches for tag pushes'v*.*.*'- Glob pattern matching semantic versions
Matches:v1.0.0,v2.1.3,v0.1.0
Doesn't match:v1.0,1.0.0,release-1.0.0
Job Setup
jobs:
build-release:
name: Build Release Binaries
runs-on: ubuntu-latest
Same as CI workflow - See above for explanation
Steps 1-2: Checkout and Setup
Same as CI workflow - See above for explanation
Step 3: Extract Version
- name: Extract version from tag
id: version
What: Names this step and gives it an ID
Why: id allows other steps to reference this step's outputs
How: Use ${{ steps.version.outputs.VERSION }} in later steps
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
What: Extracts version from tag name
Why: Needed for naming release files
How:
GITHUB_REF- Full ref likerefs/tags/v1.0.0${GITHUB_REF#refs/tags/}- Bash parameter expansion, removesrefs/tags/prefix- Result:
v1.0.0 >> $GITHUB_OUTPUT- Sets output variable
Usage: Later steps access via${{ steps.version.outputs.VERSION }}
Step 4: Build Release Binaries
- name: Build release binaries
run: cargo build --workspace --release --verbose
Same as CI workflow - Builds optimized binaries
Why here: Need fresh release builds for distribution
Step 5: Strip Binaries
- name: Strip binaries
run: |
strip target/release/supervisor || true
strip target/release/coordinator || true
# ... etc for all 7 binaries
What: Removes debug symbols from binaries
Why: Reduces binary size by 50-90%
How:
strip- Linux command that removes debugging symbols|| true- Don't fail if strip fails (some binaries might not exist)
Result: Smaller binaries, faster downloads
Example: 50MB binary → 5MB binary
Step 6: Create Release Directory
- name: Create release directory
run: mkdir -p release-artifacts
What: Creates directory for release files
Why: Organize artifacts before uploading
How: mkdir -p creates directory (doesn't fail if exists)
Step 7: Package Binaries
- name: Package binaries
run: |
# Package each binary as a tarball
for binary in supervisor coordinator horus osiris herorunner runner_osiris runner_sal; do
What: Loops through all binary names
Why: Package each binary separately
How: Bash for loop
if [ -f "target/release/$binary" ]; then
What: Checks if binary file exists
Why: Skip if binary wasn't built
How: -f tests if file exists
tar -czf "release-artifacts/${binary}-${{ steps.version.outputs.VERSION }}-linux-x86_64.tar.gz" \
-C target/release "$binary"
What: Creates compressed tarball
Why: Standard distribution format for Linux binaries
How:
tar- Archive tool-c- Create archive-z- Compress with gzip-f- Output filename-C target/release- Change to this directory first"$binary"- File to archive
Result: File likesupervisor-v1.0.0-linux-x86_64.tar.gz
Naming convention:{name}-{version}-{platform}-{arch}.tar.gz
echo "Packaged $binary"
else
echo "Warning: $binary not found, skipping"
fi
done
What: Logs success or warning
Why: Helps debug if binaries are missing
How: Simple echo statements
Step 8: Generate Checksums
- name: Generate checksums
run: |
cd release-artifacts
sha256sum *.tar.gz > checksums.txt
cat checksums.txt
What: Creates SHA256 checksums for all tarballs
Why: Allows users to verify download integrity
How:
cd release-artifacts- Change to artifact directorysha256sum *.tar.gz- Calculate SHA256 hash for each tarball> checksums.txt- Save to filecat checksums.txt- Display in logs
Result: File with lines like:
a1b2c3d4... supervisor-v1.0.0-linux-x86_64.tar.gz
e5f6g7h8... coordinator-v1.0.0-linux-x86_64.tar.gz
Step 9: Create Release
- name: Create Release
uses: actions/gitea-release@v1
What: Uses Gitea's release action
Why: Creates a release with attached files
How: Calls Gitea API to create release
with:
files: release-artifacts/*
What: Files to attach to release
Why: Makes binaries downloadable
How: Glob pattern uploads all files in directory
token: ${{ secrets.GITHUB_TOKEN }}
What: Authentication token
Why: Needed to create releases via API
How: Gitea automatically provides this secret
Security: Token is scoped to this repository only
tag_name: ${{ steps.version.outputs.VERSION }}
What: Tag to create release for
Why: Associates release with the tag
How: Uses version extracted in step 3
name: Release ${{ steps.version.outputs.VERSION }}
What: Release title
Why: Displayed in releases page
How: Combines "Release" + version (e.g., "Release v1.0.0")
body: |
## Horus Release ${{ steps.version.outputs.VERSION }}
### Binaries
This release includes the following binaries for Linux x86_64:
- `supervisor` - Hero Supervisor service
# ... etc
What: Release description (markdown)
Why: Provides context and instructions
How: Multi-line string with markdown formatting
Result: Rendered as formatted text in release page
draft: false
What: Publish immediately
Why: Make release public right away
How: false = published, true = draft
Alternative: Set to true to review before publishing
prerelease: false
What: Mark as stable release
Why: Indicates this is production-ready
How: false = stable, true = pre-release (beta, alpha)
When to use true: For tags like v1.0.0-beta.1
Testing Guide
Testing CI Workflow Locally
Before pushing, test locally:
# 1. Check compilation
cargo check --workspace --verbose
# 2. Run tests
cargo test --workspace --verbose
# 3. Run clippy
cargo clippy --workspace -- -D warnings
# 4. Check formatting
cargo fmt --all -- --check
# 5. Build release
cargo build --workspace --release --verbose
# 6. Verify binaries exist
ls -lh target/release/ | grep -E '^-.*x.*'
Expected result: All commands should succeed with exit code 0
Testing CI Workflow in Gitea
# 1. Create a test branch
git checkout -b test-ci
# 2. Make a small change (e.g., add a comment)
echo "// Test CI" >> bin/supervisor/src/main.rs
# 3. Commit and push
git add .
git commit -m "test: Trigger CI workflow"
git push origin test-ci
# 4. Check Gitea Actions
# Navigate to: https://git.ourworld.tf/peternashaat/horus/actions
Expected result:
- Workflow appears in Actions tab
- All steps complete successfully (green checkmarks)
- Artifacts are uploaded
To download artifacts:
- Click on the workflow run
- Scroll to "Artifacts" section
- Click to download
Testing Release Workflow Locally
Simulate release build:
# 1. Build release binaries
cargo build --workspace --release --verbose
# 2. Strip binaries
strip target/release/supervisor || true
strip target/release/coordinator || true
# ... etc
# 3. Create test directory
mkdir -p test-release
# 4. Package binaries
for binary in supervisor coordinator horus osiris herorunner runner_osiris runner_sal; do
if [ -f "target/release/$binary" ]; then
tar -czf "test-release/${binary}-v0.0.1-test-linux-x86_64.tar.gz" \
-C target/release "$binary"
echo "Packaged $binary"
fi
done
# 5. Generate checksums
cd test-release
sha256sum *.tar.gz > checksums.txt
cat checksums.txt
cd ..
# 6. Test extraction
cd test-release
tar -xzf supervisor-v0.0.1-test-linux-x86_64.tar.gz
./supervisor --help
cd ..
Expected result:
- All binaries package successfully
- Checksums are generated
- Binary extracts and runs
Testing Release Workflow in Gitea
# 1. Ensure code is ready
git checkout main
git pull
# 2. Create a test tag
git tag v0.1.0-test
# 3. Push the tag
git push origin v0.1.0-test
# 4. Check Gitea
# Navigate to: https://git.ourworld.tf/peternashaat/horus/releases
Expected result:
- Release appears in Releases tab
- All 7 binaries are attached as
.tar.gzfiles checksums.txtis attached- Release notes are properly formatted
To test download:
# Download a binary
wget https://git.ourworld.tf/peternashaat/horus/releases/download/v0.1.0-test/supervisor-v0.1.0-test-linux-x86_64.tar.gz
# Extract
tar -xzf supervisor-v0.1.0-test-linux-x86_64.tar.gz
# Test
chmod +x supervisor
./supervisor --help
Cleanup test release:
- Go to Releases page
- Click on the test release
- Click "Delete" button
Common Patterns Explained
Why || true?
strip target/release/supervisor || true
What: Bash OR operator
Why: Prevents step from failing if command fails
How:
- If
stripsucceeds (exit 0), continue - If
stripfails (exit non-zero),|| trueruns and returns 0
Use case: Some binaries might not exist, don't fail the whole workflow
Why ${{ }} Syntax?
${{ github.sha }}
${{ steps.version.outputs.VERSION }}
What: GitHub Actions expression syntax
Why: Access variables and context
How: Gitea evaluates these at runtime
Types:
github.*- Workflow context (sha, ref, actor, etc.)secrets.*- Secret variablessteps.*- Output from previous stepsrunner.*- Runner information (os, arch, etc.)
Why --workspace?
cargo build --workspace
What: Cargo flag to include all workspace members
Why: Your project is a workspace with multiple packages
How: Cargo reads Cargo.toml [workspace] section
Without it: Only builds the root package
With it: Builds all 7 binaries
Why Separate CI and Release Workflows?
CI Workflow:
- Runs frequently (every push)
- Fast feedback
- Doesn't create releases
Release Workflow:
- Runs rarely (only on tags)
- Slower (includes packaging)
- Creates permanent artifacts
Benefit: Fast CI doesn't slow down development, releases are deliberate
Why Cache Three Directories?
-
~/.cargo/registry- Downloaded crate files- Changes when: Dependencies are added/updated
- Size: ~500MB - 2GB
-
~/.cargo/git- Git dependencies- Changes when: Git dependencies are updated
- Size: ~100MB - 500MB
-
target/- Compiled artifacts- Changes when: Code or dependencies change
- Size: ~2GB - 10GB
Together: Reduce build time from 15 minutes to 2 minutes
Why hashFiles('**/Cargo.lock')?
What: Generates hash of Cargo.lock
Why: Cache key changes when dependencies change
How:
**/Cargo.lock- Find all Cargo.lock fileshashFiles()- Generate SHA256 hash
Result: Different hash = different cache = rebuild dependencies
Benefit: Cache is invalidated when dependencies change
Summary
Both workflows follow best practices:
✅ Fast feedback - CI runs on every push
✅ Comprehensive testing - Check, test, lint, format
✅ Optimized builds - Caching reduces build time
✅ Automated releases - Tag-based release creation
✅ Secure - Uses scoped tokens, no manual secrets
✅ Reproducible - Pinned action versions
✅ User-friendly - Clear release notes and instructions
For more information, see README.md.