Files
anomalyco__opentui/.github/workflows/release.yml
T
Simon Klee fa14b96ad4 node: support Node.js 26.4 and later (#1410)
Keep 26.4.0 as the minimum supported version while allowing later
releases.
2026-08-23 06:56:30 +02:00

451 lines
17 KiB
YAML

name: Release
on:
push:
tags:
- "v*"
- "*snapshot*"
permissions:
contents: write
env:
NODE_VERSION: 26.7.0
jobs:
# Extract version and check if it's a dry run
prepare:
name: Prepare Release
if: ${{ !contains(github.ref_name, 'snapshot') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
version: ${{ steps.extract.outputs.version }}
fullTag: ${{ steps.extract.outputs.fullTag }}
isDryRun: ${{ steps.extract.outputs.isDryRun }}
releaseTitle: ${{ steps.extract.outputs.releaseTitle }}
steps:
- name: Extract version and dry-run flag
id: extract
run: |
TAG=${GITHUB_REF#refs/tags/v}
echo "Full tag: $TAG"
# Check if this is a dry run (contains -dry.)
if [[ "$TAG" =~ -dry\. ]]; then
IS_DRY_RUN=true
# Extract base version (remove -dry.X suffix for validation)
VERSION=$(echo "$TAG" | sed -E 's/-dry\.[0-9]+$//')
RELEASE_TITLE="Release v$TAG (DRY RUN)"
echo "This is a DRY RUN release"
else
IS_DRY_RUN=false
VERSION="$TAG"
RELEASE_TITLE="Release v$TAG"
echo "This is a PRODUCTION release"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "fullTag=$TAG" >> $GITHUB_OUTPUT
echo "isDryRun=$IS_DRY_RUN" >> $GITHUB_OUTPUT
echo "releaseTitle=$RELEASE_TITLE" >> $GITHUB_OUTPUT
echo "Version (for validation): $VERSION"
echo "Full Tag (for artifacts): $TAG"
echo "Is Dry Run: $IS_DRY_RUN"
echo "Release Title: $RELEASE_TITLE"
# Version validation check
validate-version:
name: Validate Version
if: ${{ !contains(github.ref_name, 'snapshot') }}
needs: prepare
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Check version matches package.json files
run: |
TAG_VERSION="${{ needs.prepare.outputs.version }}"
echo "Validating version: $TAG_VERSION"
# Check package.json versions (exclude web, not published to npm)
FAILED=false
for pkg in packages/*/; do
if [ -f "$pkg/package.json" ]; then
if [ "$(basename "$pkg")" = "web" ]; then
echo "INFO: Skipping $pkg (not published to npm)"
continue
fi
PKG_VERSION=$(node -p "require('./$pkg/package.json').version")
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "❌ Package $pkg version ($PKG_VERSION) does not match tag version ($TAG_VERSION)"
FAILED=true
else
echo "✅ Package $pkg version matches: $PKG_VERSION"
fi
fi
done
if [ "$FAILED" = true ]; then
echo ""
echo "Version validation FAILED!"
echo "Please update package.json versions to match the tag version: $TAG_VERSION"
exit 1
fi
echo ""
echo "✅ All package versions match tag version: $TAG_VERSION"
# Build native libraries and packages
build-native:
name: Build Native
if: ${{ !contains(github.ref_name, 'snapshot') }}
needs: [prepare, validate-version]
uses: ./.github/workflows/build-native.yml
with:
version: ${{ needs.prepare.outputs.fullTag }}
# Build example executables
build-examples:
name: Build Examples
if: ${{ !contains(github.ref_name, 'snapshot') }}
needs: [prepare, sign-windows-artifacts]
uses: ./.github/workflows/build-examples.yml
with:
version: ${{ needs.prepare.outputs.fullTag }}
# Publish to npm
npm-publish:
name: NPM Publish
if: ${{ !contains(github.ref_name, 'snapshot') }}
needs: [prepare, sign-windows-artifacts]
uses: ./.github/workflows/npm-latest-release.yml
permissions:
contents: read
id-token: write
with:
version: ${{ needs.prepare.outputs.fullTag }}
isDryRun: ${{ needs.prepare.outputs.isDryRun == 'true' }}
sign-windows-artifacts:
name: Sign Windows DLL Artifacts
if: ${{ !contains(github.ref_name, 'snapshot') }}
needs: [prepare, build-native]
runs-on: blacksmith-8vcpu-windows-2025
permissions:
id-token: write
contents: read
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
steps:
- name: Skip code signing for dry runs
if: needs.prepare.outputs.isDryRun == 'true'
run: |
echo "Dry run release, skipping Windows code signing"
- name: Download native binaries artifact
if: needs.prepare.outputs.isDryRun != 'true'
uses: actions/download-artifact@v8
with:
name: native-binaries-${{ needs.prepare.outputs.fullTag }}
path: artifacts/native
- name: Download npm packages artifact
if: needs.prepare.outputs.isDryRun != 'true'
uses: actions/download-artifact@v8
with:
name: npm-packages-${{ needs.prepare.outputs.fullTag }}
path: artifacts/npm
- name: Extract Windows DLL artifacts
if: needs.prepare.outputs.isDryRun != 'true'
shell: pwsh
run: |
$artifactRoot = Join-Path $env:GITHUB_WORKSPACE "artifacts"
$signedNativeRoot = Join-Path $env:GITHUB_WORKSPACE "signed-native"
$signedNpmRoot = Join-Path $env:GITHUB_WORKSPACE "signed-npm-packages"
New-Item -ItemType Directory -Force -Path $signedNativeRoot | Out-Null
New-Item -ItemType Directory -Force -Path $signedNpmRoot | Out-Null
Expand-Archive -Path (Join-Path $artifactRoot "native\native-windows-x64.zip") -DestinationPath $signedNativeRoot -Force
Expand-Archive -Path (Join-Path $artifactRoot "native\native-windows-arm64.zip") -DestinationPath $signedNativeRoot -Force
Expand-Archive -Path (Join-Path $artifactRoot "npm\npm-packages.zip") -DestinationPath $signedNpmRoot -Force
- name: Azure login
if: needs.prepare.outputs.isDryRun != 'true'
uses: azure/login@v3
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- name: Sign Windows DLLs
if: needs.prepare.outputs.isDryRun != 'true'
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files: |
${{ github.workspace }}\signed-native\native-windows-x64\opentui.dll
${{ github.workspace }}\signed-native\native-windows-arm64\opentui.dll
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Verify Windows DLL signatures
if: needs.prepare.outputs.isDryRun != 'true'
shell: pwsh
run: |
$files = @(
"${{ github.workspace }}\signed-native\native-windows-x64\opentui.dll",
"${{ github.workspace }}\signed-native\native-windows-arm64\opentui.dll"
)
foreach ($file in $files) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
- name: Copy signed DLLs into npm packages
if: needs.prepare.outputs.isDryRun != 'true'
shell: pwsh
run: |
$pairs = @(
@{
Source = "${{ github.workspace }}\signed-native\native-windows-x64\opentui.dll"
Destination = "${{ github.workspace }}\signed-npm-packages\npm-packages\core-native-packages\core-win32-x64\opentui.dll"
},
@{
Source = "${{ github.workspace }}\signed-native\native-windows-arm64\opentui.dll"
Destination = "${{ github.workspace }}\signed-npm-packages\npm-packages\core-native-packages\core-win32-arm64\opentui.dll"
}
)
foreach ($pair in $pairs) {
Copy-Item -Path $pair.Source -Destination $pair.Destination -Force
$sourceHash = (Get-FileHash $pair.Source -Algorithm SHA256).Hash
$destinationHash = (Get-FileHash $pair.Destination -Algorithm SHA256).Hash
if ($sourceHash -ne $destinationHash) {
throw "Signed DLL copy mismatch: $($pair.Source) -> $($pair.Destination)"
}
}
- name: Repack signed windows outputs
if: needs.prepare.outputs.isDryRun != 'true'
shell: pwsh
run: |
Compress-Archive -Path "${{ github.workspace }}\signed-native\native-windows-x64" -DestinationPath "${{ github.workspace }}\artifacts\native\native-windows-x64.zip" -Force
Compress-Archive -Path "${{ github.workspace }}\signed-native\native-windows-arm64" -DestinationPath "${{ github.workspace }}\artifacts\native\native-windows-arm64.zip" -Force
Compress-Archive -Path "${{ github.workspace }}\signed-npm-packages\npm-packages" -DestinationPath "${{ github.workspace }}\artifacts\npm\npm-packages.zip" -Force
- name: Upload signed native binaries artifact
if: needs.prepare.outputs.isDryRun != 'true'
uses: actions/upload-artifact@v7
with:
name: native-binaries-${{ needs.prepare.outputs.fullTag }}
path: |
artifacts/native/native-darwin-x64.zip
artifacts/native/native-darwin-arm64.zip
artifacts/native/native-linux-x64.zip
artifacts/native/native-linux-arm64.zip
artifacts/native/native-linux-x64-musl.zip
artifacts/native/native-linux-arm64-musl.zip
artifacts/native/native-windows-x64.zip
artifacts/native/native-windows-arm64.zip
if-no-files-found: error
retention-days: 30
overwrite: true
- name: Upload signed npm packages artifact
if: needs.prepare.outputs.isDryRun != 'true'
uses: actions/upload-artifact@v7
with:
name: npm-packages-${{ needs.prepare.outputs.fullTag }}
path: artifacts/npm/npm-packages.zip
if-no-files-found: error
retention-days: 30
overwrite: true
snapshot-prepare:
name: Prepare NPM Snapshot
if: ${{ contains(github.ref_name, 'snapshot') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Generate snapshot version
id: version
run: |
COMMIT_SHA="${GITHUB_SHA::8}"
VERSION="0.0.0-$(date -u +%Y%m%d)-${COMMIT_SHA}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using snapshot version: $VERSION"
snapshot-build:
name: Build NPM Snapshot
if: ${{ contains(github.ref_name, 'snapshot') }}
needs: snapshot-prepare
uses: ./.github/workflows/build-native.yml
with:
version: ${{ needs.snapshot-prepare.outputs.version }}
prepareVersion: ${{ needs.snapshot-prepare.outputs.version }}
# npm supports one Trusted Publisher workflow per package, so snapshots
# and stable releases share this top-level caller and publish workflow.
snapshot-publish:
name: Publish NPM Snapshot
if: ${{ contains(github.ref_name, 'snapshot') }}
needs: [snapshot-prepare, snapshot-build]
uses: ./.github/workflows/npm-latest-release.yml
permissions:
contents: read
id-token: write
with:
version: ${{ needs.snapshot-prepare.outputs.version }}
prepareVersion: ${{ needs.snapshot-prepare.outputs.version }}
isDryRun: false
# Create GitHub release with assets
github-release:
name: Create GitHub Release
if: ${{ !contains(github.ref_name, 'snapshot') }}
needs: [prepare, build-examples, npm-publish]
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
- name: Organize release assets
run: |
set -e
WORK_DIR=$(pwd)
mkdir -p "$WORK_DIR/release-assets"
FULL_TAG="${{ needs.prepare.outputs.fullTag }}"
echo "Organizing release assets for tag: $FULL_TAG"
echo "Working directory: $WORK_DIR"
# Verify artifact directories exist
test -d "artifacts/native-binaries-$FULL_TAG" || (echo "❌ native-binaries artifact directory not found!" && exit 1)
test -d "artifacts/example-executables-$FULL_TAG" || (echo "❌ example-executables artifact directory not found!" && exit 1)
# Copy native binaries (unzip and rezip with versioned names)
echo "Processing native binaries..."
cd "artifacts/native-binaries-$FULL_TAG"
for zip_file in native-*.zip; do
if [ -f "$zip_file" ]; then
platform=$(echo "$zip_file" | sed 's/native-//' | sed 's/.zip//')
echo " Processing $platform..."
unzip -q "$zip_file"
# Repackage with versioned name
dir_name=$(echo "$zip_file" | sed 's/.zip//')
if [ -d "$dir_name" ]; then
cd "$dir_name"
zip -r "$WORK_DIR/release-assets/opentui-native-v${FULL_TAG}-${platform}.zip" .
cd ..
# Verify the output
test -f "$WORK_DIR/release-assets/opentui-native-v${FULL_TAG}-${platform}.zip" || (echo "❌ Failed to create native $platform release asset!" && exit 1)
echo " ✅ opentui-native-v${FULL_TAG}-${platform}.zip created"
else
echo "❌ Directory $dir_name not found after unzip!" && exit 1
fi
fi
done
cd "$WORK_DIR"
# Copy example executables (unzip and rezip with versioned names)
echo "Processing example executables..."
cd "artifacts/example-executables-$FULL_TAG"
for zip_file in examples-*.zip; do
if [ -f "$zip_file" ]; then
platform=$(echo "$zip_file" | sed 's/examples-//' | sed 's/.zip//')
echo " Processing $platform..."
unzip -q "$zip_file"
# Repackage with versioned name
dir_name=$(echo "$zip_file" | sed 's/.zip//')
if [ -d "$dir_name" ]; then
cd "$dir_name"
zip -r "$WORK_DIR/release-assets/opentui-examples-v${FULL_TAG}-${platform}.zip" .
cd ..
# Verify the output
test -f "$WORK_DIR/release-assets/opentui-examples-v${FULL_TAG}-${platform}.zip" || (echo "❌ Failed to create examples $platform release asset!" && exit 1)
echo " ✅ opentui-examples-v${FULL_TAG}-${platform}.zip created"
else
echo "❌ Directory $dir_name not found after unzip!" && exit 1
fi
fi
done
cd "$WORK_DIR"
# Verify all expected release assets exist
echo ""
echo "Verifying all release assets..."
EXPECTED_ASSETS=12 # 8 native targets + 4 examples (no linux-arm64, no windows-arm64)
ACTUAL_ASSETS=$(ls -1 release-assets/*.zip 2>/dev/null | wc -l | tr -d ' ')
if [ "$ACTUAL_ASSETS" -ne "$EXPECTED_ASSETS" ]; then
echo "❌ Expected $EXPECTED_ASSETS release assets, found $ACTUAL_ASSETS"
ls -lah release-assets/ || echo "No release assets found"
exit 1
fi
echo "✅ All $ACTUAL_ASSETS release assets prepared:"
echo " - 8 native binaries (all targets)"
echo " - 4 example executables (darwin-x64, darwin-arm64, linux-x64, windows-x64)"
ls -lah release-assets/
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
name: ${{ needs.prepare.outputs.releaseTitle }}
files: release-assets/*.zip
generate_release_notes: true
draft: false
prerelease: ${{ needs.prepare.outputs.isDryRun == 'true' }}
token: ${{ secrets.GITHUB_TOKEN }}
fail_on_unmatched_files: true