act_runner does not expose the job's runs-on label to the workflow: v1.0.4 provides only RUNNER_OS/ARCH/TEMP/TOOL_CACHE/DEBUG/VERSION, verified against the binary. The `$env:RUNNER_LABELS -match 'linux'` check was therefore always false, so guest-os: 'Auto' silently resolved to Windows and selected the Windows template for Linux jobs. It was masked only because every caller passes guest-os explicitly or overrides template-path. 'Auto' cannot pick a template on its own -- the VMX guestOS field is only readable after the clone -- so it now requires template-path and snapshot-name and errors otherwise, instead of falling back to the wrong template. Add GITEA_CI_LINUX_SNAPSHOT_NAME so a runner instance dedicated to a Linux variant can declare its own snapshot instead of every workflow repeating it; the Linux snapshot was hardcoded to BaseClean-Linux. BREAKING: a workflow using the composite with neither guest-os nor template-path now fails instead of silently using the Windows template.
444 lines
19 KiB
YAML
444 lines
19 KiB
YAML
# .gitea/actions/local-ci-build/action.yml
|
||
#
|
||
# Reusable composite action — Local CI/CD System
|
||
#
|
||
# Wraps Invoke-CIJob.ps1 (ephemeral VMware VM pipeline) into a single callable
|
||
# step, usable from any Gitea repository without duplicating the orchestration
|
||
# logic.
|
||
#
|
||
# Stored in: Simone/local-ci-cd-system / .gitea/actions/local-ci-build/action.yml
|
||
#
|
||
# Usage from a calling repo's workflow:
|
||
#
|
||
# jobs:
|
||
# build:
|
||
# runs-on: windows-build # or linux-build
|
||
# steps:
|
||
# - uses: actions/checkout@v4
|
||
# - uses: Simone/local-ci-cd-system/.gitea/actions/local-ci-build@main
|
||
# with:
|
||
# build-command: 'python build_plugin.py --final --dist-dir dist'
|
||
# artifact-source: 'dist'
|
||
# submodules: 'true'
|
||
# # xvfb: 'true' # Linux: run the build under a headless X server
|
||
#
|
||
# The template comes from the `guest-os` input, NOT from the runs-on label:
|
||
# act_runner does not expose the job's label to the action, so this action
|
||
# cannot see which runner picked the job.
|
||
#
|
||
# guest-os: 'Windows' -> GITEA_CI_TEMPLATE_PATH (WinRM/HTTPS)
|
||
# guest-os: 'Linux' -> GITEA_CI_LINUX_TEMPLATE_PATH (SSH)
|
||
# guest-os: 'Auto' -> requires template-path + snapshot-name; the guest
|
||
# OS is then sniffed from the cloned VMX
|
||
#
|
||
# Routing a label to a different template is therefore a *runner-side* concern:
|
||
# register a second runner instance whose envs point at that template (this is
|
||
# how rust-build:host -> LinuxRust2404 works), or pass template-path per job.
|
||
|
||
name: Local CI Build
|
||
description: >
|
||
Builds a project using an ephemeral VMware VM managed by Invoke-CIJob.ps1.
|
||
Handles the full VM lifecycle: clone template -> start VM -> run build ->
|
||
collect artifacts -> destroy VM.
|
||
Compatible with windows-build (WinRM) and linux-build (SSH) runners.
|
||
|
||
inputs:
|
||
build-command:
|
||
description: >
|
||
Shell command to execute inside the build VM.
|
||
Example: 'python build_plugin.py --final --dist-dir dist'
|
||
required: true
|
||
|
||
artifact-source:
|
||
description: >
|
||
Path inside VM where build outputs are located, relative to the CI work
|
||
directory. Collected to F:\CI\Artifacts\{JobId}\ on the host.
|
||
required: false
|
||
default: 'dist'
|
||
|
||
submodules:
|
||
description: >
|
||
Recurse submodules when cloning. Default "true". Set to "false"
|
||
to skip submodule init/update.
|
||
required: false
|
||
default: 'true'
|
||
|
||
guest-os:
|
||
description: >
|
||
Guest OS type: Windows, Linux, or Auto.
|
||
Windows/Linux select the template and snapshot from the runner env vars.
|
||
Auto defers OS detection to the VMX guestOS field after cloning, which
|
||
means it cannot pick the template itself — with Auto you must also pass
|
||
template-path and snapshot-name, or the action fails fast.
|
||
required: false
|
||
default: 'Auto'
|
||
|
||
use-git-clone:
|
||
description: >
|
||
Transport for the repo source. Default "true": clone inside the
|
||
build VM (requires Git in the template — see
|
||
Install-CIToolchain-WinBuild2025.ps1 Step 12 /
|
||
Install-CIToolchain-Linux2404.sh). Set to "false" for the
|
||
host-side variant: the host clones the repo and transfers a zip
|
||
to the guest (useful when the guest cannot reach Gitea).
|
||
required: false
|
||
default: 'true'
|
||
|
||
configuration:
|
||
description: >
|
||
Build configuration string forwarded to the build command (Release/Debug).
|
||
required: false
|
||
default: 'Release'
|
||
|
||
template-path:
|
||
description: >
|
||
Override the VMX template path. Leave empty (default) to use the value
|
||
from the GITEA_CI_TEMPLATE_PATH runner environment variable set in
|
||
runner/config.yaml.
|
||
required: false
|
||
default: ''
|
||
|
||
snapshot-name:
|
||
description: >
|
||
Override the snapshot name to clone from. Leave empty (default) to use
|
||
GITEA_CI_SNAPSHOT_NAME runner env var, or "BaseClean" if that is also
|
||
unset.
|
||
required: false
|
||
default: ''
|
||
|
||
artifact-name:
|
||
description: >
|
||
Name for the artifact uploaded to Gitea. Leave empty to use the default:
|
||
build-{run_id}-{sha}.
|
||
required: false
|
||
default: ''
|
||
|
||
artifact-retention-days:
|
||
description: >
|
||
Deprecated / no-op. Artifacts are no longer uploaded to the Gitea
|
||
Actions store (that needs the github.com-hosted upload-artifact
|
||
action). They stay on the host under F:\CI\Artifacts and are aged
|
||
out by scripts/Invoke-RetentionPolicy.ps1. Kept for caller
|
||
back-compat.
|
||
required: false
|
||
default: '14'
|
||
|
||
job-id-suffix:
|
||
description: >
|
||
Optional suffix appended to the job ID: run_id-run_attempt-SUFFIX.
|
||
Use to disambiguate parallel matrix legs so each leg writes to its own
|
||
artifact directory (e.g. set to matrix.target: windows, linux).
|
||
Leave empty (default) for single-job workflows.
|
||
required: false
|
||
default: ''
|
||
|
||
repo-url:
|
||
description: >
|
||
Override the repository URL passed to Invoke-CIJob.ps1.
|
||
Defaults to github.server_url/repository.git (HTTPS).
|
||
Set to an SSH URL (e.g. ssh://gitea-ci/Org/repo.git) when the host
|
||
runner uses an SSH alias for Gitea.
|
||
required: false
|
||
default: ''
|
||
|
||
repo-branch:
|
||
description: >
|
||
Branch to clone in the target repo. Leave empty (default) to use
|
||
the workflow's own ref (github.ref_name) — correct only when
|
||
repo-url is the same repo as the workflow. Set explicitly (e.g.
|
||
"main") for cross-repo builds where repo-url points elsewhere.
|
||
required: false
|
||
default: ''
|
||
|
||
repo-commit:
|
||
description: >
|
||
Commit SHA to check out in the target repo. Leave empty (default)
|
||
to use github.sha only when repo-url is NOT overridden; for a
|
||
cross-repo build (repo-url set) an empty value means "branch tip,
|
||
no pin". Set explicitly to pin a specific commit of the target.
|
||
required: false
|
||
default: ''
|
||
|
||
extra-guest-env-json:
|
||
description: >
|
||
JSON object of extra environment variables to inject into the build VM
|
||
guest before the build command runs. Use for signing keys and secrets.
|
||
Example: '{"SIGN_PASS":"<secrets.SIGN_PASS>","GPG_KEY_ID":"0xABCD1234"}'
|
||
Values are passed via ExtraGuestEnv hashtable in Invoke-CIJob.ps1 and
|
||
are never echoed to the log.
|
||
required: false
|
||
default: '{}'
|
||
|
||
guest-cpu:
|
||
description: >
|
||
Override the build VM vCPU count (numvcpus in the clone VMX).
|
||
"0" (default) keeps the template value.
|
||
required: false
|
||
default: '0'
|
||
|
||
guest-memory-mb:
|
||
description: >
|
||
Override the build VM RAM in MB (memsize in the clone VMX).
|
||
"0" (default) keeps the template value. Raise for memory-heavy
|
||
parallel builds (e.g. the Linux MinGW cross-compile thrashes at
|
||
the 4 GB template default).
|
||
required: false
|
||
default: '0'
|
||
|
||
use-shared-cache:
|
||
description: >
|
||
Set to "true" to redirect NuGet and pip caches to VMware shared folders
|
||
on the host (F:\CI\Cache\NuGet and F:\CI\Cache\pip).
|
||
Requires Set-TemplateSharedFolders.ps1 to have been run on the template
|
||
and VMware Tools HGFS driver present in the guest.
|
||
required: false
|
||
default: 'false'
|
||
|
||
skip-artifact:
|
||
description: >
|
||
Set to "true" to skip artifact packaging and collection entirely.
|
||
Use for jobs that intentionally produce no output (lint, static analysis,
|
||
test-only). When absent (default), the build MUST produce artifact-source
|
||
or the job fails.
|
||
required: false
|
||
default: 'false'
|
||
|
||
xvfb:
|
||
description: >
|
||
Linux only. Set to "true" to run the build command under xvfb-run, a
|
||
headless X server providing $DISPLAY for GUI toolkits (e.g. GTK4 /
|
||
PyGObject). Requires xvfb in the Linux template (installed by
|
||
Install-CIToolchain-Linux2404.sh). Ignored on Windows guests.
|
||
required: false
|
||
default: 'false'
|
||
|
||
outputs:
|
||
artifact-path:
|
||
description: >
|
||
Host-side directory where Invoke-CIJob.ps1 placed collected artifacts.
|
||
Format: F:\CI\Artifacts\{run_id}-{run_attempt}
|
||
value: ${{ steps.invoke-ci.outputs.artifact-path }}
|
||
|
||
artifact-name:
|
||
description: 'Name used when uploading the artifact to Gitea.'
|
||
value: ${{ steps.invoke-ci.outputs.artifact-name }}
|
||
|
||
runs:
|
||
using: composite
|
||
steps:
|
||
|
||
# ── Invoke the CI orchestrator ─────────────────────────────────────────────
|
||
- name: Invoke CI Job
|
||
id: invoke-ci
|
||
shell: pwsh
|
||
# Inputs are forwarded via environment variables to avoid shell injection
|
||
# from user-supplied build-command or path strings.
|
||
env:
|
||
INPUT_BUILD_COMMAND: ${{ inputs.build-command }}
|
||
INPUT_ARTIFACT_SOURCE: ${{ inputs.artifact-source }}
|
||
INPUT_SUBMODULES: ${{ inputs.submodules }}
|
||
INPUT_GUEST_OS: ${{ inputs.guest-os }}
|
||
INPUT_USE_GIT_CLONE: ${{ inputs.use-git-clone }}
|
||
INPUT_CONFIGURATION: ${{ inputs.configuration }}
|
||
INPUT_TEMPLATE_PATH: ${{ inputs.template-path }}
|
||
INPUT_SNAPSHOT_NAME: ${{ inputs.snapshot-name }}
|
||
INPUT_ARTIFACT_NAME: ${{ inputs.artifact-name }}
|
||
INPUT_JOB_ID_SUFFIX: ${{ inputs.job-id-suffix }}
|
||
INPUT_REPO_URL: ${{ inputs.repo-url }}
|
||
INPUT_REPO_BRANCH: ${{ inputs.repo-branch }}
|
||
INPUT_REPO_COMMIT: ${{ inputs.repo-commit }}
|
||
INPUT_EXTRA_GUEST_ENV_JSON: ${{ inputs.extra-guest-env-json }}
|
||
INPUT_USE_SHARED_CACHE: ${{ inputs.use-shared-cache }}
|
||
INPUT_SKIP_ARTIFACT: ${{ inputs.skip-artifact }}
|
||
INPUT_XVFB: ${{ inputs.xvfb }}
|
||
INPUT_GUEST_CPU: ${{ inputs.guest-cpu }}
|
||
INPUT_GUEST_MEMORY_MB: ${{ inputs.guest-memory-mb }}
|
||
run: |
|
||
#Requires -Version 5.1
|
||
Set-StrictMode -Version Latest
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
# Resolve Python interpreter from venv (Phase A4 — Python orchestrator).
|
||
# Override with CI_VENV_PYTHON env var; OS-specific defaults otherwise.
|
||
$venvPython = $env:CI_VENV_PYTHON
|
||
if (-not $venvPython) {
|
||
if ($IsLinux) { $venvPython = '/opt/ci/venv/bin/python' }
|
||
else { $venvPython = 'F:\CI\python\venv\Scripts\python.exe' }
|
||
}
|
||
if (-not (Test-Path -LiteralPath $venvPython)) {
|
||
throw "Python interpreter not found at '$venvPython'. Set CI_VENV_PYTHON or run Setup-Host.ps1."
|
||
}
|
||
|
||
# Build job ID — append suffix when provided (matrix disambiguation)
|
||
$jobId = '${{ github.run_id }}-${{ github.run_attempt }}'
|
||
if ($env:INPUT_JOB_ID_SUFFIX) { $jobId = "$jobId-$($env:INPUT_JOB_ID_SUFFIX)" }
|
||
if ($IsLinux) { $artifactPath = "/var/lib/ci/artifacts/$jobId" }
|
||
else { $artifactPath = "F:\CI\Artifacts\$jobId" }
|
||
|
||
# Resolve artifact name: caller-supplied or default to build-{run_id}-{sha}
|
||
if ($env:INPUT_ARTIFACT_NAME) {
|
||
$artifactName = $env:INPUT_ARTIFACT_NAME
|
||
}
|
||
else {
|
||
$artifactName = "build-${{ github.run_id }}-${{ github.sha }}"
|
||
}
|
||
|
||
# Resolve repository URL: explicit override or construct from GitHub context
|
||
if ($env:INPUT_REPO_URL) {
|
||
$repoUrl = $env:INPUT_REPO_URL
|
||
}
|
||
else {
|
||
$repoUrl = '${{ github.server_url }}/${{ github.repository }}.git'
|
||
}
|
||
|
||
# Resolve target branch/commit. github.ref_name/github.sha refer
|
||
# to the workflow's own repo — only valid when repo-url is NOT
|
||
# overridden. For a cross-repo build the caller must give
|
||
# repo-branch (and optionally repo-commit); an unset commit means
|
||
# "branch tip, no pin".
|
||
if ($env:INPUT_REPO_BRANCH) {
|
||
$repoBranch = $env:INPUT_REPO_BRANCH
|
||
}
|
||
else {
|
||
$repoBranch = '${{ github.ref_name }}'
|
||
}
|
||
if ($env:INPUT_REPO_COMMIT) {
|
||
$repoCommit = $env:INPUT_REPO_COMMIT
|
||
}
|
||
elseif ($env:INPUT_REPO_URL) {
|
||
$repoCommit = '' # cross-repo: don't pin the workflow's sha
|
||
}
|
||
else {
|
||
$repoCommit = '${{ github.sha }}'
|
||
}
|
||
|
||
# Resolve guest OS.
|
||
#
|
||
# 'Auto' is NOT resolved here: act_runner exposes no RUNNER_LABELS
|
||
# variable (v1.0.4 provides only RUNNER_OS/ARCH/TEMP/TOOL_CACHE/DEBUG/
|
||
# VERSION), so the job's runs-on label is invisible to this action.
|
||
# The previous label sniff always fell through to 'Windows', which
|
||
# silently selected the Windows template for Linux jobs.
|
||
#
|
||
# 'Auto' is instead forwarded to the Python CLI, which reads the
|
||
# guestOS field from the cloned VMX — but that happens *after* the
|
||
# clone, so the template and snapshot must already be known. Hence:
|
||
# Auto requires both template-path and snapshot-name.
|
||
$resolvedGuestOS = $env:INPUT_GUEST_OS
|
||
if ($resolvedGuestOS -eq 'Auto' -and (-not $env:INPUT_TEMPLATE_PATH -or -not $env:INPUT_SNAPSHOT_NAME)) {
|
||
throw ("guest-os: 'Auto' cannot select a template or snapshot on its own — " +
|
||
"act_runner does not expose the job's runs-on label to the action. " +
|
||
"Either set guest-os to 'Windows'/'Linux' (uses the runner env defaults), " +
|
||
"or pass both template-path and snapshot-name explicitly.")
|
||
}
|
||
|
||
# Select template path: explicit input overrides OS-based runner env var
|
||
$resolvedTemplatePath = $env:INPUT_TEMPLATE_PATH
|
||
if (-not $resolvedTemplatePath) {
|
||
if ($resolvedGuestOS -eq 'Linux') {
|
||
$resolvedTemplatePath = $env:GITEA_CI_LINUX_TEMPLATE_PATH
|
||
} else {
|
||
$resolvedTemplatePath = $env:GITEA_CI_TEMPLATE_PATH
|
||
}
|
||
}
|
||
|
||
# Select snapshot name: explicit input > OS-based default.
|
||
# GITEA_CI_SNAPSHOT_NAME applies to Windows (template-refresh override);
|
||
# GITEA_CI_LINUX_SNAPSHOT_NAME is the Linux equivalent, which lets a
|
||
# runner instance dedicated to a Linux variant (e.g. the rust-build
|
||
# runner pointing at LinuxRust2404) declare its own snapshot instead of
|
||
# every workflow repeating it.
|
||
$resolvedSnapshotName = $env:INPUT_SNAPSHOT_NAME
|
||
if (-not $resolvedSnapshotName) {
|
||
if ($resolvedGuestOS -eq 'Linux') {
|
||
$resolvedSnapshotName = if ($env:GITEA_CI_LINUX_SNAPSHOT_NAME) { $env:GITEA_CI_LINUX_SNAPSHOT_NAME } else { 'BaseClean-Linux' }
|
||
} else {
|
||
$resolvedSnapshotName = if ($env:GITEA_CI_SNAPSHOT_NAME) { $env:GITEA_CI_SNAPSHOT_NAME } else { 'BaseClean' }
|
||
}
|
||
}
|
||
Write-Host "[local-ci-build] template: $resolvedTemplatePath"
|
||
Write-Host "[local-ci-build] snapshot: $resolvedSnapshotName"
|
||
Write-Host "[local-ci-build] guest-os: $resolvedGuestOS"
|
||
|
||
# Build argument list for the Python CLI. Long-form kebab-case flags only.
|
||
# NOTE: never put an empty string in the splatted array — PowerShell
|
||
# 5.1 drops zero-length args when calling a native exe, which would
|
||
# shift the flag/value pairing. Add optional args conditionally.
|
||
$pyArgs = @(
|
||
'-m', 'ci_orchestrator', 'job',
|
||
'--job-id', $jobId,
|
||
'--repo-url', $repoUrl,
|
||
'--branch', $repoBranch,
|
||
'--build-command', $env:INPUT_BUILD_COMMAND,
|
||
'--guest-artifact-source', $env:INPUT_ARTIFACT_SOURCE,
|
||
'--guest-os', $resolvedGuestOS,
|
||
'--configuration', $env:INPUT_CONFIGURATION,
|
||
'--snapshot-name', $resolvedSnapshotName
|
||
)
|
||
if ($resolvedTemplatePath) { $pyArgs += @('--template-path', $resolvedTemplatePath) }
|
||
if ($repoCommit) { $pyArgs += @('--commit', $repoCommit) }
|
||
|
||
# Submodules and transport default to ON; pass the explicit
|
||
# negative flag when the caller opted out so the Python default
|
||
# (also ON) is overridden deterministically.
|
||
if ($env:INPUT_SUBMODULES -eq 'false') { $pyArgs += '--no-submodules' }
|
||
else { $pyArgs += '--submodules' }
|
||
if ($env:INPUT_USE_GIT_CLONE -eq 'false') { $pyArgs += '--host-clone' }
|
||
else { $pyArgs += '--use-git-clone' }
|
||
if ($env:INPUT_USE_SHARED_CACHE -eq 'true') { $pyArgs += '--use-shared-cache' }
|
||
if ($env:INPUT_SKIP_ARTIFACT -eq 'true') { $pyArgs += '--skip-artifact' }
|
||
if ($env:INPUT_XVFB -eq 'true') { $pyArgs += '--xvfb' }
|
||
# VMX overrides: forward only when > 0 (0 = keep template value).
|
||
if ([int]($env:INPUT_GUEST_CPU) -gt 0) { $pyArgs += @('--guest-cpu', $env:INPUT_GUEST_CPU) }
|
||
if ([int]($env:INPUT_GUEST_MEMORY_MB) -gt 0) { $pyArgs += @('--guest-memory-mb', $env:INPUT_GUEST_MEMORY_MB) }
|
||
if ($env:INPUT_EXTRA_GUEST_ENV_JSON -and $env:INPUT_EXTRA_GUEST_ENV_JSON -ne '{}') {
|
||
$pyArgs += @('--extra-env-json', $env:INPUT_EXTRA_GUEST_ENV_JSON)
|
||
}
|
||
|
||
& $venvPython @pyArgs
|
||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||
|
||
# Emit outputs consumed by subsequent steps.
|
||
# PowerShell 5.1 `Out-File -Encoding utf8` writes a BOM; the act_runner
|
||
# GITHUB_OUTPUT parser then reads the key as "artifact-path" and
|
||
# the output resolves empty. An empty artifact-path made the upload
|
||
# step glob the entire F: drive. Write UTF-8 without BOM.
|
||
if (-not $artifactPath) { throw "artifactPath is empty; refusing to emit." }
|
||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||
[System.IO.File]::AppendAllText(
|
||
$env:GITHUB_OUTPUT, "artifact-path=$artifactPath`n", $utf8NoBom)
|
||
[System.IO.File]::AppendAllText(
|
||
$env:GITHUB_OUTPUT, "artifact-name=$artifactName`n", $utf8NoBom)
|
||
|
||
# ── Report artifact location ──────────────────────────────────────────────
|
||
# No actions/upload-artifact: that action's code is fetched from
|
||
# github.com (and v4 is GHES-incompatible, v3 deprecated). The runner
|
||
# host is single and its filesystem is shared across jobs, so the
|
||
# orchestrator's host-side collect dir IS the artifact handoff.
|
||
# Downstream jobs read F:\CI\Artifacts\{run_id}-{run_attempt}[-suffix]
|
||
# directly (see build-nsInnoUnp.yml release job).
|
||
- name: Report artifact location
|
||
if: success() && inputs.skip-artifact != 'true'
|
||
shell: pwsh
|
||
run: |
|
||
$p = '${{ steps.invoke-ci.outputs.artifact-path }}'
|
||
if (-not (Test-Path -LiteralPath $p) -or
|
||
-not (Get-ChildItem -Force -LiteralPath $p)) {
|
||
throw "no artifacts at $p"
|
||
}
|
||
Write-Host "Artifacts on host: $p"
|
||
Get-ChildItem -Recurse -File -LiteralPath $p |
|
||
ForEach-Object { Write-Host " $($_.FullName)" }
|
||
|
||
- name: Report diagnostic log location on failure
|
||
if: failure()
|
||
shell: pwsh
|
||
run: |
|
||
$p = '${{ steps.invoke-ci.outputs.artifact-path }}'
|
||
if ($p -and (Test-Path -LiteralPath $p)) {
|
||
Write-Host "Diagnostic logs (if any) on host: $p"
|
||
Get-ChildItem -Recurse -File -Filter '*.log' -LiteralPath $p `
|
||
-ErrorAction SilentlyContinue |
|
||
ForEach-Object { Write-Host " $($_.FullName)" }
|
||
}
|