The shared composite action ran 3 steps under `shell: pwsh`, making every CI job depend on PowerShell on the Linux host. Rewrote them in bash and dropped the dead $IsLinux / F:\CI branches (Phase B left a single Linux host serving both windows-build and linux-build runners). Closes Phase C / idea-3: no CI job or systemd timer requires pwsh on the Linux host anymore. Ticked all C1-C8 subtask checkboxes and set the plans to "C chiusa". pwsh is deliberately left installed (operator choice, not a technical blocker).
488 lines
20 KiB
YAML
488 lines
20 KiB
YAML
# .gitea/actions/local-ci-build/action.yml
|
|
#
|
|
# Reusable composite action — Local CI/CD System
|
|
#
|
|
# Wraps `python -m ci_orchestrator job` (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 the ci_orchestrator
|
|
Python CLI. 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 — both
|
|
labels are served by the same Linux control-plane host; guest-os only picks
|
|
the VM template, never the shell this action itself runs under.
|
|
|
|
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 /var/lib/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 /var/lib/ci/artifacts and are aged
|
|
out by `ci retention run` (see commands/retention.py). 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. Windows guests only (see build.py) — wired via
|
|
Set-TemplateSharedFolders.ps1, which still hardcodes Windows host
|
|
paths (F:\CI\Cache\...) and has not been re-pointed at the Linux
|
|
host post-migration; treat as unverified until that script is ported.
|
|
Requires VMware Tools HGFS driver present in the guest.
|
|
required: false
|
|
default: 'false'
|
|
|
|
cargo-cache:
|
|
description: >
|
|
Reuse compiled Rust dependency artifacts between jobs. "auto" (default)
|
|
caches on branches and pull requests but builds release tags
|
|
(refs/tags/v*) cold; "on" always caches; "off" never does. Only ever
|
|
engages for Linux guests whose workspace has a Cargo.lock, so it is inert
|
|
for every other job. Workspace crates are stripped from an entry before
|
|
it is stored and the job refuses to publish if any survive, so a restored
|
|
cache cannot carry the code under test -- but a release build verifies an
|
|
artifact rather than a change, so "auto" gives it the clean room.
|
|
required: false
|
|
default: 'auto'
|
|
|
|
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 the orchestrator collected artifacts.
|
|
Format: /var/lib/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 ─────────────────────────────────────────────
|
|
# The control-plane host is always Linux (Phase B): both the windows-build
|
|
# and linux-build runner labels are served by act_runner processes running
|
|
# on this same Linux Mint host (one `runner.envs` block per process picks
|
|
# the template; the host OS running act_runner itself never changes).
|
|
# guest-os only selects which VM template gets cloned — it has no bearing
|
|
# on the shell this action runs under.
|
|
- name: Invoke CI Job
|
|
id: invoke-ci
|
|
shell: bash
|
|
# 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_CARGO_CACHE: ${{ inputs.cargo-cache }}
|
|
GITHUB_REF_FOR_CACHE: ${{ github.ref }}
|
|
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: |
|
|
set -euo pipefail
|
|
|
|
# Resolve Python interpreter from venv (Phase A4 — Python orchestrator).
|
|
# Override with CI_VENV_PYTHON env var.
|
|
venv_python="${CI_VENV_PYTHON:-/opt/ci/venv/bin/python}"
|
|
if [[ ! -x "$venv_python" ]]; then
|
|
echo "Python interpreter not found at '$venv_python'. Set CI_VENV_PYTHON or run setup-host-linux.sh." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Build job ID — append suffix when provided (matrix disambiguation)
|
|
job_id="${{ github.run_id }}-${{ github.run_attempt }}"
|
|
if [[ -n "$INPUT_JOB_ID_SUFFIX" ]]; then
|
|
job_id="${job_id}-${INPUT_JOB_ID_SUFFIX}"
|
|
fi
|
|
artifact_path="/var/lib/ci/artifacts/${job_id}"
|
|
|
|
# Resolve artifact name: caller-supplied or default to build-{run_id}-{sha}
|
|
if [[ -n "$INPUT_ARTIFACT_NAME" ]]; then
|
|
artifact_name="$INPUT_ARTIFACT_NAME"
|
|
else
|
|
artifact_name="build-${{ github.run_id }}-${{ github.sha }}"
|
|
fi
|
|
|
|
# Resolve repository URL: explicit override or construct from GitHub context
|
|
if [[ -n "$INPUT_REPO_URL" ]]; then
|
|
repo_url="$INPUT_REPO_URL"
|
|
else
|
|
repo_url="${{ github.server_url }}/${{ github.repository }}.git"
|
|
fi
|
|
|
|
# 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 [[ -n "$INPUT_REPO_BRANCH" ]]; then
|
|
repo_branch="$INPUT_REPO_BRANCH"
|
|
else
|
|
repo_branch="${{ github.ref_name }}"
|
|
fi
|
|
if [[ -n "$INPUT_REPO_COMMIT" ]]; then
|
|
repo_commit="$INPUT_REPO_COMMIT"
|
|
elif [[ -n "$INPUT_REPO_URL" ]]; then
|
|
repo_commit="" # cross-repo: don't pin the workflow's sha
|
|
else
|
|
repo_commit="${{ github.sha }}"
|
|
fi
|
|
|
|
# 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.
|
|
resolved_guest_os="$INPUT_GUEST_OS"
|
|
if [[ "$resolved_guest_os" == "Auto" && ( -z "$INPUT_TEMPLATE_PATH" || -z "$INPUT_SNAPSHOT_NAME" ) ]]; then
|
|
echo "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." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Select template path: explicit input overrides OS-based runner env var
|
|
resolved_template_path="$INPUT_TEMPLATE_PATH"
|
|
if [[ -z "$resolved_template_path" ]]; then
|
|
if [[ "$resolved_guest_os" == "Linux" ]]; then
|
|
resolved_template_path="${GITEA_CI_LINUX_TEMPLATE_PATH:-}"
|
|
else
|
|
resolved_template_path="${GITEA_CI_TEMPLATE_PATH:-}"
|
|
fi
|
|
fi
|
|
|
|
# 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.
|
|
resolved_snapshot_name="$INPUT_SNAPSHOT_NAME"
|
|
if [[ -z "$resolved_snapshot_name" ]]; then
|
|
if [[ "$resolved_guest_os" == "Linux" ]]; then
|
|
resolved_snapshot_name="${GITEA_CI_LINUX_SNAPSHOT_NAME:-BaseClean-Linux}"
|
|
else
|
|
resolved_snapshot_name="${GITEA_CI_SNAPSHOT_NAME:-BaseClean}"
|
|
fi
|
|
fi
|
|
echo "[local-ci-build] template: $resolved_template_path"
|
|
echo "[local-ci-build] snapshot: $resolved_snapshot_name"
|
|
echo "[local-ci-build] guest-os: $resolved_guest_os"
|
|
|
|
# Build argument list for the Python CLI. Long-form kebab-case flags only.
|
|
py_args=(
|
|
-m ci_orchestrator job
|
|
--job-id "$job_id"
|
|
--repo-url "$repo_url"
|
|
--branch "$repo_branch"
|
|
--build-command "$INPUT_BUILD_COMMAND"
|
|
--guest-artifact-source "$INPUT_ARTIFACT_SOURCE"
|
|
--guest-os "$resolved_guest_os"
|
|
--configuration "$INPUT_CONFIGURATION"
|
|
--snapshot-name "$resolved_snapshot_name"
|
|
)
|
|
[[ -n "$resolved_template_path" ]] && py_args+=(--template-path "$resolved_template_path")
|
|
[[ -n "$repo_commit" ]] && py_args+=(--commit "$repo_commit")
|
|
|
|
# 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 [[ "$INPUT_SUBMODULES" == "false" ]]; then
|
|
py_args+=(--no-submodules)
|
|
else
|
|
py_args+=(--submodules)
|
|
fi
|
|
if [[ "$INPUT_USE_GIT_CLONE" == "false" ]]; then
|
|
py_args+=(--host-clone)
|
|
else
|
|
py_args+=(--use-git-clone)
|
|
fi
|
|
[[ "$INPUT_USE_SHARED_CACHE" == "true" ]] && py_args+=(--use-shared-cache)
|
|
[[ "$INPUT_SKIP_ARTIFACT" == "true" ]] && py_args+=(--skip-artifact)
|
|
|
|
# Release tags build cold: they verify a release artifact, not a
|
|
# change, so the clean room is worth the two minutes (furyrs#13).
|
|
cargo_cache_mode="${INPUT_CARGO_CACHE:-auto}"
|
|
case "$cargo_cache_mode" in
|
|
on) use_cargo_cache=true ;;
|
|
off) use_cargo_cache=false ;;
|
|
auto)
|
|
if [[ "$GITHUB_REF_FOR_CACHE" == refs/tags/v* ]]; then
|
|
use_cargo_cache=false
|
|
else
|
|
use_cargo_cache=true
|
|
fi
|
|
;;
|
|
*)
|
|
echo "cargo-cache must be one of 'auto', 'on', 'off' - got '$cargo_cache_mode'." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
[[ "$use_cargo_cache" == false ]] && py_args+=(--no-cargo-cache)
|
|
if [[ "$use_cargo_cache" == true ]]; then
|
|
echo "[job] Cargo cache: on"
|
|
else
|
|
echo "[job] Cargo cache: off (cold)"
|
|
fi
|
|
|
|
[[ "$INPUT_XVFB" == "true" ]] && py_args+=(--xvfb)
|
|
|
|
# VMX overrides: forward only when > 0 (0 = keep template value).
|
|
if (( INPUT_GUEST_CPU > 0 )); then
|
|
py_args+=(--guest-cpu "$INPUT_GUEST_CPU")
|
|
fi
|
|
if (( INPUT_GUEST_MEMORY_MB > 0 )); then
|
|
py_args+=(--guest-memory-mb "$INPUT_GUEST_MEMORY_MB")
|
|
fi
|
|
if [[ -n "$INPUT_EXTRA_GUEST_ENV_JSON" && "$INPUT_EXTRA_GUEST_ENV_JSON" != "{}" ]]; then
|
|
py_args+=(--extra-env-json "$INPUT_EXTRA_GUEST_ENV_JSON")
|
|
fi
|
|
|
|
"$venv_python" "${py_args[@]}"
|
|
|
|
# Emit outputs consumed by subsequent steps.
|
|
if [[ -z "$artifact_path" ]]; then
|
|
echo "artifact_path is empty; refusing to emit." >&2
|
|
exit 1
|
|
fi
|
|
{
|
|
echo "artifact-path=${artifact_path}"
|
|
echo "artifact-name=${artifact_name}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
# ── 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 /var/lib/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: bash
|
|
run: |
|
|
set -euo pipefail
|
|
p='${{ steps.invoke-ci.outputs.artifact-path }}'
|
|
if [[ ! -e "$p" ]] || [[ -z "$(ls -A "$p")" ]]; then
|
|
echo "no artifacts at $p" >&2
|
|
exit 1
|
|
fi
|
|
echo "Artifacts on host: $p"
|
|
find "$p" -type f -print | sed 's/^/ /'
|
|
|
|
- name: Report diagnostic log location on failure
|
|
if: failure()
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
p='${{ steps.invoke-ci.outputs.artifact-path }}'
|
|
if [[ -n "$p" && -e "$p" ]]; then
|
|
echo "Diagnostic logs (if any) on host: $p"
|
|
find "$p" -type f -name '*.log' -print 2>/dev/null | sed 's/^/ /'
|
|
fi
|