From 934c3fd3f2de15d0a3208b6e714b06f0cca95423 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 17 May 2026 02:51:36 +0200 Subject: [PATCH] ci: add local-ci-build composite action to main Required so `uses: .../local-ci-build@main` resolves: the action only existed on feature/python-rewrite-and-linux-migration. Brings just the action.yml (host-side orchestrator unchanged) without merging the full migration branch. Co-Authored-By: Claude Opus 4.7 --- .gitea/actions/local-ci-build/action.yml | 300 +++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 .gitea/actions/local-ci-build/action.yml diff --git a/.gitea/actions/local-ci-build/action.yml b/.gitea/actions/local-ci-build/action.yml new file mode 100644 index 0000000..8ad8658 --- /dev/null +++ b/.gitea/actions/local-ci-build/action.yml @@ -0,0 +1,300 @@ +# .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 + "artifact-path=$artifactPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + "artifact-name=$artifactName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + # ── Upload build artifacts on success ───────────────────────────────────── + - name: Upload artifacts + if: success() && inputs.skip-artifact != 'true' + uses: actions/upload-artifact@v4 + 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@v4 + with: + name: build-logs-${{ github.sha }} + path: ${{ steps.invoke-ci.outputs.artifact-path }}\*.log + retention-days: 3 + if-no-files-found: ignore