# .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: > 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. Auto (default) detects the OS from the VMX guestOS field after cloning. 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":"","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' 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_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: 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. # 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' } # 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)" } }