# .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":"","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