mirror of
https://github.com/anomalyco/opentui.git
synced 2026-09-19 01:26:03 +08:00
fa14b96ad4
Keep 26.4.0 as the minimum supported version while allowing later releases.
451 lines
17 KiB
YAML
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
|