Files
local-ci-cd-system/.gitea/actions/local-ci-build/action.yml
T
Simone d73c7ff0ff fix(ci): write GITHUB_OUTPUT without BOM; harden artifact path
PowerShell 5.1 `Out-File -Encoding utf8` prepends a UTF-8 BOM. The
act_runner GITHUB_OUTPUT parser then read the key as "artifact-path",
so steps.invoke-ci.outputs.artifact-path resolved empty. Combined with
the trailing backslash, the upload path became "\" -> the F: drive root,
causing upload-artifact to ingest the entire drive (hundreds of GB of
unrelated files).

- Append outputs with UTF8Encoding($false) (no BOM).
- Throw if artifactPath is empty instead of emitting nothing.
- Drop the trailing backslash; use /*.log for the diagnostic glob.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 12:13:22 +02:00

309 lines
12 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# .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'
#
# Runner label determines the guest OS template:
# windows-build -> WinBuild2025 (WinRM/HTTPS transport)
# linux-build -> LinuxBuild2404 (SSH transport)
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: >
Set to "true" to clone the repository with --recurse-submodules.
required: false
default: 'false'
guest-os:
description: >
Guest OS type: Windows, Linux, or Auto.
Auto (default) detects the OS from the VMX guestOS field after cloning.
required: false
default: 'Auto'
use-git-clone:
description: >
Set to "true" to clone the repository inside the VM rather than
transferring a zip from the host. Requires Git in the template (see
Install-CIToolchain-WinBuild2025.ps1 Step 12 / Install-CIToolchain-Linux2404.sh).
Preferred for repositories larger than ~200 MB.
required: false
default: 'false'
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: 'Days to retain uploaded artifacts in Gitea.'
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: ''
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: '{}'
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'
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: powershell
# 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_EXTRA_GUEST_ENV_JSON: ${{ inputs.extra-guest-env-json }}
INPUT_USE_SHARED_CACHE: ${{ inputs.use-shared-cache }}
INPUT_SKIP_ARTIFACT: ${{ inputs.skip-artifact }}
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; default matches Setup-Host.ps1 layout.
$venvPython = $env:CI_VENV_PYTHON
if (-not $venvPython) { $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)" }
$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 guest OS: honor explicit input; for Auto, infer from RUNNER_LABELS
$resolvedGuestOS = $env:INPUT_GUEST_OS
if ($resolvedGuestOS -eq 'Auto') {
if ($env:RUNNER_LABELS -match 'linux') {
$resolvedGuestOS = 'Linux'
} else {
$resolvedGuestOS = 'Windows'
}
}
# 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)
$resolvedSnapshotName = $env:INPUT_SNAPSHOT_NAME
if (-not $resolvedSnapshotName) {
if ($resolvedGuestOS -eq 'Linux') {
$resolvedSnapshotName = 'BaseClean-Linux'
} else {
$resolvedSnapshotName = if ($env:GITEA_CI_SNAPSHOT_NAME) { $env:GITEA_CI_SNAPSHOT_NAME } else { 'BaseClean' }
}
}
# Build argument list for the Python CLI. Long-form kebab-case flags only.
$pyArgs = @(
'-m', 'ci_orchestrator', 'job',
'--job-id', $jobId,
'--repo-url', $repoUrl,
'--branch', '${{ github.ref_name }}',
'--commit', '${{ github.sha }}',
'--build-command', $env:INPUT_BUILD_COMMAND,
'--guest-artifact-source', $env:INPUT_ARTIFACT_SOURCE,
'--guest-os', $resolvedGuestOS,
'--configuration', $env:INPUT_CONFIGURATION,
'--template-path', $resolvedTemplatePath,
'--snapshot-name', $resolvedSnapshotName
)
if ($env:INPUT_SUBMODULES -eq 'true') { $pyArgs += '--submodules' }
if ($env:INPUT_USE_GIT_CLONE -eq 'true') { $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_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)
# ── Upload build artifacts on success ─────────────────────────────────────
- name: Upload artifacts
if: success() && inputs.skip-artifact != 'true'
uses: actions/upload-artifact@v3
with:
name: ${{ steps.invoke-ci.outputs.artifact-name }}
path: ${{ steps.invoke-ci.outputs.artifact-path }}
retention-days: ${{ inputs.artifact-retention-days }}
if-no-files-found: error
# ── Upload diagnostic logs on failure ─────────────────────────────────────
- name: Upload diagnostic logs on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: build-logs-${{ github.sha }}
path: ${{ steps.invoke-ci.outputs.artifact-path }}/*.log
retention-days: 3
if-no-files-found: ignore