Codegeist CLI Release Build #3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # release-cli.yml - Codegeist CLI release artifact workflow. | |
| # | |
| # Purpose: | |
| # - Validate release-shaped JVM and native artifacts on GitHub-hosted runners. | |
| # - Publish a GitHub Release only for pushed cli/v* tags. | |
| # | |
| # Inputs and side effects: | |
| # - Branch validation derives the version from release/cli/v* branches, including | |
| # iteration branches and candidate branches. | |
| # - workflow_dispatch may pass release_version for pre-tag validation. | |
| # - Tag runs create or update a published GitHub Release and upload artifacts. | |
| # | |
| # Related files: | |
| # - app/codegeist/cli/pom.xml | |
| # - docs/developer/release/github-release-build.md | |
| # - scripts/install/ | |
| name: Codegeist CLI Release Build | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_version: | |
| description: SemVer without leading v. Leave empty to derive from the selected ref. | |
| required: false | |
| type: string | |
| push: | |
| branches: | |
| - "release/cli/v*" | |
| tags: | |
| - "cli/v*" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: codegeist-cli-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| JAVA_VERSION: "25" | |
| GRAALVM_DISTRIBUTION: graalvm-community | |
| NATIVE_SMOKE_TIMEOUT_SECONDS: "5" | |
| FILE_EDIT_SMOKE_TIMEOUT_SECONDS: "90" | |
| SHELL_ASK_SMOKE_TIMEOUT_SECONDS: "90" | |
| jobs: | |
| metadata: | |
| name: Resolve release metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_version: ${{ steps.resolve.outputs.release_version }} | |
| publish_release: ${{ steps.resolve.outputs.publish_release }} | |
| steps: | |
| - name: Resolve release version | |
| id: resolve | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| INPUT_RELEASE_VERSION: ${{ github.event.inputs.release_version || '' }} | |
| REF_NAME: ${{ github.ref_name }} | |
| REF_TYPE: ${{ github.ref_type }} | |
| run: | | |
| set -euo pipefail | |
| ref_version="" | |
| ref_source="" | |
| if [ "$REF_TYPE" = "tag" ]; then | |
| if [[ "$REF_NAME" =~ ^cli/v(.+)$ ]]; then | |
| ref_version="${BASH_REMATCH[1]}" | |
| ref_source="tag" | |
| else | |
| printf 'Tag %s is not a CLI release tag.\n' "$REF_NAME" >&2 | |
| exit 1 | |
| fi | |
| elif [[ "$REF_NAME" =~ ^release/cli/v(.+)-(github-release-build|codegeist-rc-[0-9]+)$ ]]; then | |
| ref_version="${BASH_REMATCH[1]}" | |
| ref_source="release branch" | |
| elif [[ "$REF_NAME" == release/* ]]; then | |
| printf 'Release branch %s is not a CLI release branch.\n' "$REF_NAME" >&2 | |
| exit 1 | |
| fi | |
| version="${INPUT_RELEASE_VERSION#v}" | |
| source="workflow input" | |
| if [ -n "$version" ]; then | |
| if [ -n "$ref_version" ] && [ "$version" != "$ref_version" ]; then | |
| printf 'Ref %s resolves version %s, not requested version %s.\n' "$REF_NAME" "$ref_version" "$version" >&2 | |
| exit 1 | |
| fi | |
| elif [ -n "$ref_version" ]; then | |
| version="$ref_version" | |
| source="$ref_source" | |
| else | |
| printf 'Could not derive a CLI release version from ref %s; pass release_version.\n' "$REF_NAME" >&2 | |
| exit 1 | |
| fi | |
| semver='^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)([.](0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?$' | |
| if ! [[ "$version" =~ $semver ]]; then | |
| printf 'Release version must be SemVer without leading v: %s\n' "$version" >&2 | |
| exit 1 | |
| fi | |
| publish_release=false | |
| if [ "$REF_TYPE" = "tag" ]; then | |
| expected_ref="cli/v$version" | |
| if [ "$REF_NAME" != "$expected_ref" ]; then | |
| printf 'Tag %s does not match resolved release version %s.\n' "$REF_NAME" "$version" >&2 | |
| exit 1 | |
| fi | |
| if [ "$EVENT_NAME" = "push" ]; then | |
| publish_release=true | |
| fi | |
| fi | |
| printf 'release_version=%s\n' "$version" >> "$GITHUB_OUTPUT" | |
| printf 'publish_release=%s\n' "$publish_release" >> "$GITHUB_OUTPUT" | |
| { | |
| printf '### CLI release metadata\n' | |
| printf '\n' | |
| printf -- '- Version: `%s`\n' "$version" | |
| printf -- '- Source: `%s`\n' "$source" | |
| printf -- '- Ref: `%s`\n' "$REF_NAME" | |
| printf -- '- Published GitHub Release: `%s`\n' "$publish_release" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Require immutable CLI release to be absent | |
| if: steps.resolve.outputs.publish_release == 'true' | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| encoded_tag="$(jq -rn --arg value "$GITHUB_REF_NAME" '$value | @uri')" | |
| status="$(curl --silent --show-error \ | |
| --output /dev/null \ | |
| --write-out '%{http_code}' \ | |
| --header "Authorization: Bearer $GITHUB_TOKEN" \ | |
| --header 'Accept: application/vnd.github+json' \ | |
| --header 'X-GitHub-Api-Version: 2022-11-28' \ | |
| "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/tags/$encoded_tag")" | |
| case "$status" in | |
| 404) ;; | |
| 200) | |
| printf 'Immutable CLI release already exists: %s\n' "$GITHUB_REF_NAME" >&2 | |
| exit 1 | |
| ;; | |
| *) | |
| printf 'Could not prove CLI release absence; GitHub returned HTTP %s.\n' "$status" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| build-jvm: | |
| name: Build JVM jar | |
| runs-on: ubuntu-latest | |
| needs: metadata | |
| env: | |
| RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v7 | |
| - name: Set up GraalVM | |
| uses: graalvm/setup-graalvm@v1 | |
| with: | |
| java-version: ${{ env.JAVA_VERSION }} | |
| distribution: ${{ env.GRAALVM_DISTRIBUTION }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| cache: maven | |
| - name: Run Maven tests | |
| working-directory: app/codegeist/cli | |
| shell: bash | |
| run: mvn --batch-mode --no-transfer-progress -Drevision="$RELEASE_VERSION" test | |
| - name: Build executable jar | |
| working-directory: app/codegeist/cli | |
| shell: bash | |
| run: mvn --batch-mode --no-transfer-progress -Drevision="$RELEASE_VERSION" -DskipTests clean package | |
| - name: Stage JVM jar asset | |
| working-directory: app/codegeist/cli | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p target/dist | |
| cp -p target/codegeist.jar target/dist/codegeist-jvm.jar | |
| - name: Upload JVM jar artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: codegeist-jvm | |
| if-no-files-found: error | |
| path: app/codegeist/cli/target/dist/codegeist-jvm.jar | |
| build-native: | |
| name: Build and smoke native ${{ matrix.platform }} | |
| runs-on: ${{ matrix.os }} | |
| needs: | |
| - metadata | |
| - build-jvm | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux-x64 | |
| os: ubuntu-latest | |
| extension: tar.gz | |
| - platform: windows-x64 | |
| os: windows-latest | |
| extension: zip | |
| - platform: macos-x64 | |
| os: macos-15-intel | |
| extension: tar.gz | |
| env: | |
| RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v7 | |
| - name: Set up GraalVM | |
| uses: graalvm/setup-graalvm@v1 | |
| with: | |
| java-version: ${{ env.JAVA_VERSION }} | |
| distribution: ${{ env.GRAALVM_DISTRIBUTION }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| cache: maven | |
| native-image-job-reports: "true" | |
| - name: Build native executable | |
| if: runner.os != 'Windows' | |
| working-directory: app/codegeist/cli | |
| shell: bash | |
| run: mvn --batch-mode --no-transfer-progress -Drevision="$RELEASE_VERSION" -DskipTests -Pnative clean native:compile | |
| - name: Build native executable with MSVC | |
| if: runner.os == 'Windows' | |
| working-directory: app/codegeist/cli | |
| shell: pwsh | |
| run: | | |
| $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" | |
| if (-not (Test-Path -LiteralPath $vswhere)) { | |
| throw "vswhere.exe was not found: $vswhere" | |
| } | |
| $installationPath = & $vswhere "-latest" "-products" "*" "-requires" "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" "-property" "installationPath" | |
| if (-not $installationPath) { | |
| throw "No Visual Studio installation with MSVC x64 tools was found." | |
| } | |
| $vsDevCmd = Join-Path $installationPath "Common7\Tools\VsDevCmd.bat" | |
| if (-not (Test-Path -LiteralPath $vsDevCmd)) { | |
| throw "VsDevCmd.bat was not found: $vsDevCmd" | |
| } | |
| $command = "`"$vsDevCmd`" -arch=x64 && mvn --batch-mode --no-transfer-progress -Drevision=$env:RELEASE_VERSION -DskipTests -Pnative clean native:compile" | |
| cmd /d /s /c $command | |
| if ($LASTEXITCODE -ne 0) { | |
| exit $LASTEXITCODE | |
| } | |
| $environmentCommand = "`"$vsDevCmd`" -arch=x64 >nul && set VCToolsRedistDir" | |
| $environmentOutput = & cmd /d /s /c $environmentCommand | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to resolve VCToolsRedistDir from the MSVC environment." | |
| } | |
| $redistSetting = $environmentOutput | | |
| Where-Object { $_ -like "VCToolsRedistDir=*" } | | |
| Select-Object -First 1 | |
| if (-not $redistSetting) { | |
| throw "VCToolsRedistDir was not available after activating MSVC." | |
| } | |
| $vcToolsRedistDir = $redistSetting.Substring("VCToolsRedistDir=".Length) | |
| "CODEGEIST_WINDOWS_VC_REDIST_DIR=$vcToolsRedistDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| - name: Package and smoke native archive | |
| working-directory: app/codegeist/cli | |
| shell: pwsh | |
| run: | | |
| ../../../scripts/tests/artifact-smoke.ps1 ` | |
| -Platform "${{ matrix.platform }}" ` | |
| -CliDir (Get-Location).Path ` | |
| -ExpectedVersion $env:RELEASE_VERSION ` | |
| -SmokeRoot (Join-Path (Get-Location).Path "target/smoke-test") ` | |
| -NativeTimeoutSeconds ([int]$env:NATIVE_SMOKE_TIMEOUT_SECONDS) ` | |
| -FileEditTimeoutSeconds ([int]$env:FILE_EDIT_SMOKE_TIMEOUT_SECONDS) ` | |
| -ShellAskTimeoutSeconds ([int]$env:SHELL_ASK_SMOKE_TIMEOUT_SECONDS) ` | |
| -WindowsVcRedistDir $env:CODEGEIST_WINDOWS_VC_REDIST_DIR | |
| - name: Smoke platform install script | |
| working-directory: app/codegeist/cli | |
| shell: pwsh | |
| run: | | |
| ../../../scripts/tests/install-script-smoke.ps1 ` | |
| -Platform "${{ matrix.platform }}" ` | |
| -CliDir (Get-Location).Path ` | |
| -ExpectedVersion $env:RELEASE_VERSION ` | |
| -SmokeRoot (Join-Path (Get-Location).Path "target/smoke-test/install-script/${{ matrix.platform }}") ` | |
| -InstallScriptDir (Resolve-Path "../../../scripts/install").Path | |
| - name: Upload native artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: codegeist-${{ matrix.platform }} | |
| if-no-files-found: error | |
| path: app/codegeist/cli/target/dist/codegeist-${{ matrix.platform }}.${{ matrix.extension }} | |
| stage-install-scripts: | |
| name: Stage install scripts | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v7 | |
| - name: Stage install script assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bash -n \ | |
| scripts/install/codegeist-install-linux.sh \ | |
| scripts/install/codegeist-install-macos.sh | |
| pwsh -NoProfile -Command '"scripts/install/codegeist-install-windows.ps1" | ForEach-Object { $tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile($_, [ref]$tokens, [ref]$errors) > $null; if ($errors.Count -gt 0) { throw $errors[0] } }' | |
| mkdir -p dist | |
| install_scripts=( | |
| codegeist-install-linux.sh | |
| codegeist-install-macos.sh | |
| codegeist-install-windows.ps1 | |
| ) | |
| for script in "${install_scripts[@]}"; do | |
| source="scripts/install/$script" | |
| if [ ! -f "$source" ]; then | |
| printf 'Install script not found: %s\n' "$source" >&2 | |
| exit 1 | |
| fi | |
| cp -p "$source" "dist/$script" | |
| done | |
| - name: Upload install script artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: codegeist-install-scripts | |
| if-no-files-found: error | |
| path: dist/codegeist-install-* | |
| checksums: | |
| name: Generate and verify checksums | |
| runs-on: ubuntu-latest | |
| needs: | |
| - metadata | |
| - build-jvm | |
| - build-native | |
| - stage-install-scripts | |
| steps: | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| - name: Generate SHA256SUMS | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| expected_assets=( | |
| codegeist-jvm.jar | |
| codegeist-linux-x64.tar.gz | |
| codegeist-windows-x64.zip | |
| codegeist-macos-x64.tar.gz | |
| codegeist-install-linux.sh | |
| codegeist-install-macos.sh | |
| codegeist-install-windows.ps1 | |
| ) | |
| shopt -s nullglob | |
| for asset in "${expected_assets[@]}"; do | |
| matches=(artifacts/*/"$asset") | |
| if [ "${#matches[@]}" -ne 1 ] || [ ! -f "${matches[0]}" ]; then | |
| printf 'Expected exactly one workflow artifact named %s, found %s.\n' "$asset" "${#matches[@]}" >&2 | |
| exit 1 | |
| fi | |
| cp -p "${matches[0]}" dist/ | |
| done | |
| shopt -u nullglob | |
| cd dist | |
| checksum_file="SHA256SUMS.txt" | |
| sha256sum "${expected_assets[@]}" > "$checksum_file" | |
| sha256sum -c "$checksum_file" | |
| { | |
| printf '### Release assets\n' | |
| printf '\n' | |
| for asset in *; do | |
| printf -- '- `%s`\n' "$asset" | |
| done | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload checksum artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: codegeist-checksums | |
| if-no-files-found: error | |
| path: dist/SHA256SUMS.txt | |
| release: | |
| name: Create GitHub Release | |
| if: needs.metadata.outputs.publish_release == 'true' | |
| runs-on: ubuntu-latest | |
| needs: | |
| - metadata | |
| - checksums | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| - name: Stage release assets | |
| shell: bash | |
| env: | |
| RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release-assets | |
| expected_assets=( | |
| codegeist-jvm.jar | |
| codegeist-linux-x64.tar.gz | |
| codegeist-windows-x64.zip | |
| codegeist-macos-x64.tar.gz | |
| codegeist-install-linux.sh | |
| codegeist-install-macos.sh | |
| codegeist-install-windows.ps1 | |
| SHA256SUMS.txt | |
| ) | |
| shopt -s nullglob | |
| for asset in "${expected_assets[@]}"; do | |
| matches=(artifacts/*/"$asset") | |
| if [ "${#matches[@]}" -ne 1 ] || [ ! -f "${matches[0]}" ]; then | |
| printf 'Expected exactly one release asset named %s, found %s.\n' "$asset" "${#matches[@]}" >&2 | |
| exit 1 | |
| fi | |
| cp -p "${matches[0]}" release-assets/ | |
| done | |
| shopt -u nullglob | |
| cat > release-notes.md <<EOF | |
| Codegeist CLI $RELEASE_VERSION release. | |
| Validation completed in this workflow run before upload: | |
| - Maven test suite passed before packaging. | |
| - JVM jar was packaged as a release asset without artifact smoke. | |
| - Linux x64, Windows x64, and macOS x64 native archives were built, unpacked, and smoke-tested with --version, --show-config, and ask-driven file-edit plus shell-tool side effects. | |
| - Linux, macOS, and Windows install scripts were run against local release-shaped assets on their matching release runners, then staged as release assets for curl-based downloads. | |
| - The SHA-256 checksum file was generated and verified before upload. | |
| Install scripts are bootstrap helpers for the native archives. This release intentionally excludes package-manager publishing, signing, notarization, SBOM, and SLSA provenance. | |
| EOF | |
| - name: Upload GitHub release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| draft: false | |
| prerelease: ${{ contains(needs.metadata.outputs.release_version, '-') }} | |
| make_latest: false | |
| tag_name: ${{ github.ref_name }} | |
| name: Codegeist CLI ${{ needs.metadata.outputs.release_version }} | |
| body_path: release-notes.md | |
| fail_on_unmatched_files: true | |
| files: release-assets/* |