- Updated the final master plan to improve clarity and consistency in the status of various capabilities, including fixes and enhancements made as of 2026-05-12. - Revised the opus47 analysis to provide a more structured overview of the system's production readiness, including detailed coverage of OWASP Top 10 risks and operational gaps. - Enhanced the opus47 review of GPT-5.5 by clarifying severity ratings and rationales for various items, while adding new high-severity items that were previously overlooked.
22 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.yamlandscripts/Invoke-CIJob.ps1.
Table of Contents
- Workflow Authoring Reference
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. |
-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-ciis 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 (
GiteaPATtarget). - 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@v4
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@v4
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@v4
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 ifAutodetection is sufficient (reads VMXguestOSfield).-TemplatePath $env:GITEA_CI_LINUX_TEMPLATE_PATHselects the Ubuntu template.-SshKeyPath $env:GITEA_CI_SSH_KEY_PATHuses the injected key path.- The default snapshot name for Linux templates is
BaseClean-Linux. If you rely onAuto, add-SnapshotName 'BaseClean-Linux'explicitly.
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@v4
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@v4
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@v4
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 }} |