Files
horus/.gitea/workflows/WORKFLOW_EXPLAINED.md
2025-11-19 08:45:56 +00:00

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

  1. CI Workflow (ci.yml)
  2. Release Workflow (release.yml)
  3. Testing Guide
  4. Common Patterns Explained

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 created
  • synchronize - When new commits are pushed to the PR
  • reopened - When a closed PR is reopened
    Note: Does not run on closed to 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:
  1. Clones the repository
  2. Checks out the commit that triggered the workflow
  3. 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:

  1. Installs Rust stable version
  2. Adds cargo to PATH
  3. 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 like Linux-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: Run cargo fmt --all without --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 in target/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 sizes
  • grep -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 like refs/tags/v1.0.0
  • ${GITHUB_REF#refs/tags/} - Bash parameter expansion, removes refs/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 like supervisor-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 directory
  • sha256sum *.tar.gz - Calculate SHA256 hash for each tarball
  • > checksums.txt - Save to file
  • cat 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:

  1. Click on the workflow run
  2. Scroll to "Artifacts" section
  3. 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.gz files
  • checksums.txt is 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:

  1. Go to Releases page
  2. Click on the test release
  3. 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 strip succeeds (exit 0), continue
  • If strip fails (exit non-zero), || true runs 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 variables
  • steps.* - Output from previous steps
  • runner.* - 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?

  1. ~/.cargo/registry - Downloaded crate files

    • Changes when: Dependencies are added/updated
    • Size: ~500MB - 2GB
  2. ~/.cargo/git - Git dependencies

    • Changes when: Git dependencies are updated
    • Size: ~100MB - 500MB
  3. 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 files
  • hashFiles() - 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.