Files
Simone 6f51d44f92 feat(build): optional xvfb-run for Linux GUI builds
Add an --xvfb switch to 'build run' and 'job' that wraps the Linux build
command in 'xvfb-run -a sh -c ...', giving GUI toolkits (GTK4/PyGObject)
a headless $DISPLAY. Exported env vars precede xvfb-run so they
propagate into the virtual-display session. No-op (with a notice) for
Windows guests.

The local-ci-build composite action exposes it as the 'xvfb' input
(default false), forwarded as INPUT_XVFB -> --xvfb, mirroring the
use-shared-cache/skip-artifact wiring.

Docs: WORKFLOW-AUTHORING.md parameter table + Linux example note;
LINUX-TEMPLATE-SETUP.md Step 4a (the template ships python3-gi
gir1.2-gtk-4.0 xvfb). Adds build-run unit tests (wrap, plain, Windows
no-op).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:38:34 +02:00

24 KiB

Workflow Authoring Reference

Reference document for AI agents and developers writing Gitea Actions workflows for the Local CI/CD System. Keep this document in sync with runner/config.yaml and scripts/Invoke-CIJob.ps1.


Table of Contents


1. System Overview

The CI system runs entirely on a local Windows 11 host. Gitea triggers jobs; act_runner executes them. Every build step runs inside an ephemeral VMware VM — the runner host itself never invokes compilers or build tools. The entire VM lifecycle is managed by Invoke-CIJob.ps1.

git push → Gitea → act_runner (host) → Invoke-CIJob.ps1 → VM lifecycle
                                                           ├── New-BuildVM
                                                           ├── Wait-VMReady
                                                           ├── Invoke-RemoteBuild
                                                           ├── Get-BuildArtifacts
                                                           └── Remove-BuildVM (always)

Supported guest OS: Windows Server 2025 / 2022 (WinRM transport), Ubuntu 24.04 (SSH transport).


2. Runner Labels

Label Configured as Use for
windows-build windows-build:host Windows VM builds, Windows-only tools
linux-build linux-build:host Linux VM builds (cross-compile, GCC/Clang)
dotnet dotnet:host Alias for windows-build (legacy)
msbuild msbuild:host Alias for windows-build (legacy)

Both windows-build and linux-build run on the same physical host. Only the template VM used differs. Use windows-build unless you specifically need a Linux guest (cross-compile, GCC, CMake, Python/Linux toolchains).


3. Shell Requirement

Always use shell: powershell (Windows PowerShell 5.1).

Never use shell: pwsh — that invokes PowerShell Core 7+ which is not the required runtime and may not be present on the host.

# CORRECT
- name: Build
  shell: powershell
  run: |
    ...

# WRONG — do not use
- name: Build
  shell: pwsh     # PowerShell 7+ — not supported
  run: |
    ...

4. Environment Variables (Injected by Runner)

These variables are automatically available in every job step. Do not redeclare them in the workflow unless you need to override the default.

Variable Value (default) Description
GITEA_CI_TEMPLATE_PATH F:\CI\Templates\WinBuild2025\WinBuild2025.vmx Windows 2025 template VMX path
GITEA_CI_LINUX_TEMPLATE_PATH F:\CI\Templates\LinuxBuild2404\LinuxBuild2404.vmx Ubuntu 24.04 template VMX path
GITEA_CI_CLONE_BASE_DIR F:\CI\BuildVMs Directory for ephemeral VM clones
GITEA_CI_ARTIFACT_DIR F:\CI\Artifacts Root directory for job artifacts
GITEA_CI_GUEST_CRED_TARGET BuildVMGuest Credential Manager target for guest VM
GITEA_CI_SSH_KEY_PATH F:\CI\keys\ci_linux SSH private key for Linux guest access

Invoke-CIJob.ps1 reads GITEA_CI_TEMPLATE_PATH and GITEA_CI_CLONE_BASE_DIR automatically if -TemplatePath and -CloneBaseDir are not passed explicitly.


5. Invoke-CIJob.ps1 Parameter Reference

Script location (fixed on host): N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1

Mandatory parameters

Parameter Type Description
-JobId string Unique job ID. Use ${{ github.run_id }} or run_id-run_attempt.
-RepoUrl string Git URL of the repository to clone/build. See Section 6.
-Branch string Branch to build. Use ${{ github.ref_name }}.

Optional parameters

Parameter Type Default Description
-Commit string '' Specific commit SHA. Use ${{ github.sha }}. Empty = HEAD.
-TemplatePath string $env:GITEA_CI_TEMPLATE_PATH Full path to template VMX.
-SnapshotName string BaseClean Template snapshot name to clone from.
-GuestOS string Auto Windows, Linux, or Auto (detected from VMX guestOS field).
-BuildCommand string '' Shell command to run inside VM. Empty = dotnet build.
-GuestArtifactSource string dist Path inside VM guest from which artifacts are collected.
-Xvfb switch off Linux only: run the build under xvfb-run (headless X for GUI toolkits, e.g. GTK4/PyGObject). Needs xvfb in the template (installed by the toolchain). Ignored for Windows.
-Submodules switch off Pass to clone with --recurse-submodules.
-UseGitClone switch off Skip host-side clone; let the guest VM clone directly via HTTPS.
-GiteaCredentialTarget string GiteaPAT Credential Manager target for Gitea PAT (UseGitClone only).
-GuestCredentialTarget string BuildVMGuest Credential Manager target for Windows guest VM login.
-SshKeyPath string F:\CI\keys\ci_linux SSH key for Linux guest (ignored for Windows).
-SshUser string ci_build SSH username for Linux guest (ignored for Windows).
-CloneBaseDir string $env:GITEA_CI_CLONE_BASE_DIR Directory for VM clone files.
-ArtifactBaseDir string F:\CI\Artifacts Root artifact directory on host.
-VMIPAddress string '' Override guest IP. Empty = auto-detected via vmrun.
-LogDir string F:\CI\Logs Directory for per-job transcripts and JSONL logs.
-LogRetentionDays int 30 Days before log directories are purged. 0 = disabled.

Exit codes: 0 = success, 1 = failure (VM is always destroyed regardless).


6. URL Conventions

The -RepoUrl value depends on the build mode.

Host-clone mode (default, no -UseGitClone)

The host runner clones the repo using the SSH alias defined in ~/.ssh/config:

ssh://gitea-ci/<owner>/<repo>.git
  • gitea-ci is a Host alias in the runner's SSH config pointing to Gitea's SSH port.
  • This URL is reachable only from the host, not from inside guest VMs.
  • After clone, source is transferred to the guest via WinRM (Windows) or tar+scp (Linux).

UseGitClone mode (-UseGitClone)

The guest VM clones the repo directly via HTTPS:

https://gitea.emulab.it/<owner>/<repo>.git
  • This URL must be reachable from inside the VM's NAT network.
  • For private repos, the Gitea PAT is injected from Credential Manager (GiteaPAT target).
  • Do not use the SSH alias URL with -UseGitClone — it is not accessible inside VMs.

Quick reference

Mode RepoUrl to use
Default (Windows) ssh://gitea-ci/<owner>/<repo>.git
Default (Linux) ssh://gitea-ci/<owner>/<repo>.git
UseGitClone https://gitea.emulab.it/<owner>/<repo>.git

7. Artifact Path Convention

Artifacts are written to:

F:\CI\Artifacts\<JobId>\

Where <JobId> is the value passed to -JobId.

In upload-artifact steps use:

path: F:\CI\Artifacts\${{ github.run_id }}\

Or for single-file artifact:

path: F:\CI\Artifacts\${{ github.run_id }}\artifacts.zip

Invoke-CIJob.ps1 always produces an artifacts.zip at the job artifact root, plus the raw files extracted from the guest under the same directory.


8. Workflow Patterns

Place workflow files in .gitea/workflows/ in the project repository.

8.1 Windows build (host-clone mode)

Standard Windows build. Host clones the repo and transfers source to the Windows VM via WinRM/zip transfer.

name: Build (Windows)

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

jobs:
  build:
    runs-on: windows-build
    timeout-minutes: 90

    steps:
      - name: Run CI Job
        shell: powershell
        run: |
          & 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
            -JobId               '${{ github.run_id }}' `
            -RepoUrl             'ssh://gitea-ci/<owner>/<repo>.git' `
            -Branch              '${{ github.ref_name }}' `
            -Commit              '${{ github.sha }}' `
            -Submodules `
            -BuildCommand        'python build_plugin.py --final --dist-dir dist' `
            -GuestArtifactSource 'dist'
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Upload artifacts
        if: success()
        uses: actions/upload-artifact@v3
        with:
          name: build-${{ github.ref_name }}
          path: F:\CI\Artifacts\${{ github.run_id }}\artifacts.zip
          if-no-files-found: error

      - name: Upload logs on failure
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: build-logs-${{ github.run_id }}
          path: F:\CI\Artifacts\${{ github.run_id }}\*.log
          if-no-files-found: ignore
          retention-days: 3

8.2 Linux build (host-clone mode)

Host clones the repo, compresses it with tar, and transfers it to the Ubuntu 24.04 VM via scp. Build runs under ci_build user.

name: Build (Linux)

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

jobs:
  build:
    runs-on: linux-build
    timeout-minutes: 60

    steps:
      - name: Run CI Job (Linux)
        shell: powershell
        run: |
          & 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
            -JobId               '${{ github.run_id }}' `
            -RepoUrl             'ssh://gitea-ci/<owner>/<repo>.git' `
            -Branch              '${{ github.ref_name }}' `
            -Commit              '${{ github.sha }}' `
            -Submodules `
            -GuestOS             'Linux' `
            -TemplatePath        $env:GITEA_CI_LINUX_TEMPLATE_PATH `
            -SshKeyPath          $env:GITEA_CI_SSH_KEY_PATH `
            -BuildCommand        'python3 build_plugin.py --host linux --verbose' `
            -GuestArtifactSource 'dist'
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Upload artifacts
        if: success()
        uses: actions/upload-artifact@v3
        with:
          name: build-linux-${{ github.ref_name }}
          path: F:\CI\Artifacts\${{ github.run_id }}\artifacts.zip
          if-no-files-found: error

Notes:

  • -GuestOS 'Linux' can be omitted if Auto detection is sufficient (reads VMX guestOS field).
  • -TemplatePath $env:GITEA_CI_LINUX_TEMPLATE_PATH selects the Ubuntu template.
  • -SshKeyPath $env:GITEA_CI_SSH_KEY_PATH uses the injected key path.
  • The default snapshot name for Linux templates is BaseClean-Linux. If you rely on Auto, add -SnapshotName 'BaseClean-Linux' explicitly.
  • For builds or tests that need a display (GUI toolkits such as GTK4/PyGObject), add the -Xvfb switch — the build command then runs under xvfb-run with a headless X server providing $DISPLAY. Example:
              -BuildCommand        'xvfb-aware-tests.sh' `
              -Xvfb `
    
    The Ubuntu template ships python3-gi, gir1.2-gtk-4.0 and xvfb (installed by the toolchain). -Xvfb is ignored for Windows guests.

8.3 Windows build (UseGitClone mode)

The Windows guest VM clones the repository directly via HTTPS. Requires Git for Windows in the template and the GiteaPAT Credential Manager entry on the host (for private repos).

name: Build (Windows, in-VM clone)

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: windows-build
    timeout-minutes: 90

    steps:
      - name: Run CI Job
        shell: powershell
        run: |
          & 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
            -JobId               '${{ github.run_id }}' `
            -RepoUrl             'https://gitea.emulab.it/<owner>/<repo>.git' `
            -Branch              '${{ github.ref_name }}' `
            -Commit              '${{ github.sha }}' `
            -Submodules `
            -UseGitClone `
            -GiteaCredentialTarget 'GiteaPAT' `
            -BuildCommand        'python build_plugin.py --final --dist-dir dist' `
            -GuestArtifactSource 'dist'
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Upload artifacts
        if: success()
        uses: actions/upload-artifact@v3
        with:
          name: build-${{ github.ref_name }}
          path: F:\CI\Artifacts\${{ github.run_id }}\artifacts.zip
          if-no-files-found: error

8.4 Linux build (UseGitClone mode)

Linux guest clones directly. Useful when the host SSH agent is unavailable or for testing the full HTTPS clone path.

name: Build (Linux, in-VM clone)

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: linux-build
    timeout-minutes: 60

    steps:
      - name: Run CI Job (Linux, UseGitClone)
        shell: powershell
        run: |
          & 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
            -JobId               '${{ github.run_id }}' `
            -RepoUrl             'https://gitea.emulab.it/<owner>/<repo>.git' `
            -Branch              '${{ github.ref_name }}' `
            -Commit              '${{ github.sha }}' `
            -Submodules `
            -UseGitClone `
            -GiteaCredentialTarget 'GiteaPAT' `
            -GuestOS             'Linux' `
            -TemplatePath        $env:GITEA_CI_LINUX_TEMPLATE_PATH `
            -SshKeyPath          $env:GITEA_CI_SSH_KEY_PATH `
            -SnapshotName        'BaseClean-Linux' `
            -BuildCommand        'python3 build_plugin.py --host linux --verbose' `
            -GuestArtifactSource 'dist'
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Upload artifacts
        if: success()
        uses: actions/upload-artifact@v3
        with:
          name: build-linux-${{ github.ref_name }}
          path: F:\CI\Artifacts\${{ github.run_id }}\artifacts.zip
          if-no-files-found: error

8.5 PSScriptAnalyzer lint

Runs on every push or PR that touches .ps1 / .psm1 files.

name: Lint (PSScriptAnalyzer)

on:
  push:
    paths:
      - '**.ps1'
      - '**.psm1'
  pull_request:
    paths:
      - '**.ps1'
      - '**.psm1'

jobs:
  lint:
    runs-on: windows-build
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run PSScriptAnalyzer
        shell: powershell
        run: |
          $ErrorActionPreference = 'Stop'

          if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) {
            Install-Module PSScriptAnalyzer -Scope CurrentUser -Force -AllowClobber
          }

          $paths = @('scripts', 'template', 'runner', 'Setup-Host.ps1')
          $results = $paths | ForEach-Object {
            if (Test-Path $_) {
              Invoke-ScriptAnalyzer -Path $_ -Recurse -Severity Error,Warning
            }
          }

          if ($results) {
            $results | Format-Table -AutoSize
            Write-Error "PSScriptAnalyzer found $($results.Count) issue(s)."
            exit 1
          }
          Write-Host "PSScriptAnalyzer: no issues found."

Note: The lint job uses actions/checkout@v4 because it analyzes files inside the project repo (the checked-out workspace), not the CI scripts repo.

8.6 Matrix build (multiple configs)

name: Build (matrix)

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: windows-build
    timeout-minutes: 90

    strategy:
      matrix:
        config: [x86-ansi, x86-unicode, x64-unicode]
      fail-fast: false

    steps:
      - name: Run CI Job
        shell: powershell
        run: |
          & 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
            -JobId               '${{ github.run_id }}-${{ matrix.config }}' `
            -RepoUrl             'ssh://gitea-ci/<owner>/<repo>.git' `
            -Branch              '${{ github.ref_name }}' `
            -Commit              '${{ github.sha }}' `
            -Submodules `
            -BuildCommand        "python build_plugin.py --config ${{ matrix.config }}" `
            -GuestArtifactSource 'dist'
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Upload artifacts
        if: success()
        uses: actions/upload-artifact@v3
        with:
          name: build-${{ matrix.config }}-${{ github.ref_name }}
          path: F:\CI\Artifacts\${{ github.run_id }}-${{ matrix.config }}\artifacts.zip
          if-no-files-found: error

Note: With runner.capacity: 4, up to 4 matrix jobs run in parallel. Each gets its own ephemeral VM. Ensure -JobId is unique per matrix cell.


9. Checkout Step

Do not add actions/checkout@v4 to build steps unless the workflow itself needs access to repository files on the host (e.g. lint, static analysis).

For build jobs, Invoke-CIJob.ps1 handles all source acquisition — either by cloning on the host and transferring to the VM, or by having the VM clone directly.

The CI scripts are at a fixed path on the runner host and do not need a checkout:

# CORRECT — direct path to CI scripts, no checkout needed
- name: Run CI Job
  shell: powershell
  run: |
    & 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' ...

# WRONG — CI scripts are not in the project repo
- name: Checkout
  uses: actions/checkout@v4   # this checks out the PROJECT repo, not CI scripts
- name: Run CI Job
  shell: powershell
  run: |
    & "$env:GITHUB_WORKSPACE\scripts\Invoke-CIJob.ps1" ...  # path doesn't exist

10. Timeouts

Build type Recommended timeout-minutes
Windows build (typical) 90
Windows build (large/slow) 120
Linux build (typical) 60
Lint / static analysis 10
Template provisioning 180

The runner-level timeout is 2h (from runner/config.yaml). Keep job timeouts below that value. If a job timeout fires, the VM may not be cleanly destroyed — the cleanup task Cleanup-OrphanedBuildVMs.ps1 handles these cases.


11. Common Mistakes

Mistake Correct approach
shell: pwsh Use shell: powershell (PS 5.1, not Core)
SSH alias URL with -UseGitClone Use https://gitea.emulab.it/... for UseGitClone; SSH alias is host-only
HTTPS URL without -UseGitClone SSH alias ssh://gitea-ci/... for host-clone mode
$env:GITHUB_WORKSPACE to find CI scripts Use hardcoded N:\Code\Workspace\Local-CI-CD-System\scripts\
actions/checkout@v4 in build jobs Omit it; source is handled by Invoke-CIJob.ps1
runs-on: ubuntu-latest or windows-latest Use windows-build or linux-build — cloud runners are not available
&& in PowerShell run blocks Not valid in PS 5.1; use if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Hardcoded IP in -VMIPAddress Omit it; auto-detected via vmrun getGuestIPAddress
-TemplatePath for Linux without GITEA_CI_LINUX_TEMPLATE_PATH Pass $env:GITEA_CI_LINUX_TEMPLATE_PATH or the full path explicitly
Forgetting -SnapshotName 'BaseClean-Linux' for Linux Linux template uses a different snapshot name than Windows (BaseClean)
Same -JobId for matrix jobs Append matrix key: ${{ github.run_id }}-${{ matrix.config }}
actions/upload-artifact@v4 / download-artifact@v4 Use @v3. Gitea reports as GHES; @v4 (@actions/artifact v2 / Twirp API) fails with GHESNotSupportedError.
Short action ref Simone/local-ci-cd-system/...@main Use the full URL https://gitea.emulab.it/Simone/local-ci-cd-system/.gitea/actions/local-ci-build@main; with [actions] DEFAULT_ACTIONS_URL = github in Gitea's app.ini this keeps GitHub actions (checkout, upload-artifact) resolvable while pointing the composite action at the local instance. The action repo must be on @main (the ref the runner clones).
Composite action referenced from a private/internal repo The runner clones the action repo unauthenticated. Make local-ci-cd-system public, or the clone fails with authentication required: Unauthorized.