fix(workflows): use shell:powershell (PS 5.1) instead of pwsh; fix ciScriptsDir path

- All 3 workflows: pwsh -> powershell (host requires PS 5.1, not Core 7+)
- workflow-example.yml: replace GITHUB_WORKSPACE-relative ciScriptsDir with
  hardcoded host path; use SSH alias URL (host-clone mode) instead of HTTP
- docs: add WORKFLOW-AUTHORING.md reference for AI and developers
This commit is contained in:
Simone
2026-05-11 18:48:07 +02:00
parent db1530cf0f
commit 1c39701534
4 changed files with 575 additions and 7 deletions
+568
View File
@@ -0,0 +1,568 @@
# 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](#1-system-overview)
2. [Runner Labels](#2-runner-labels)
3. [Shell Requirement](#3-shell-requirement)
4. [Environment Variables (Injected by Runner)](#4-environment-variables-injected-by-runner)
5. [Invoke-CIJob.ps1 Parameter Reference](#5-invoke-cijobps1-parameter-reference)
6. [URL Conventions](#6-url-conventions)
7. [Artifact Path Convention](#7-artifact-path-convention)
8. [Workflow Patterns](#8-workflow-patterns)
- [8.1 Windows build (host-clone mode)](#81-windows-build-host-clone-mode)
- [8.2 Linux build (host-clone mode)](#82-linux-build-host-clone-mode)
- [8.3 Windows build (UseGitClone mode)](#83-windows-build-usegitclone-mode)
- [8.4 Linux build (UseGitClone mode)](#84-linux-build-usegitclone-mode)
- [8.5 PSScriptAnalyzer lint](#85-psscriptanalyzer-lint)
- [8.6 Matrix build (multiple configs)](#86-matrix-build-multiple-configs)
9. [Checkout Step](#9-checkout-step)
10. [Timeouts](#10-timeouts)
11. [Common Mistakes](#11-common-mistakes)
---
## 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.
```yaml
# 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-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:
```yaml
path: F:\CI\Artifacts\${{ github.run_id }}\
```
Or for single-file artifact:
```yaml
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.
```yaml
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.
```yaml
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 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.
### 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).
```yaml
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.
```yaml
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.
```yaml
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)
```yaml
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:
```yaml
# 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 }}` |