Skip to content

Codegeist CLI Release Build #3

Codegeist CLI Release Build

Codegeist CLI Release Build #3

Workflow file for this run

# 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/*