Skip to content

Versioned Release

Versioned Release #60

Workflow file for this run

name: Versioned Release
on:
workflow_dispatch:
inputs:
confirm_major:
description: 'Type CONFIRM to allow a major version bump'
required: false
type: string
default: ''
workflow_run:
workflows: ['CI / Docker']
types: [completed]
branches: [main]
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && 'versioned-release' || 'npm-prerelease-main' }}
cancel-in-progress: false
jobs:
# =============================================================================
# NPM Pre-release (successful main-branch CI runs only)
# Publishes @eudiplo/sdk-core and @eudiplo/cli with the "main" dist-tag.
# =============================================================================
npm-prerelease:
name: Publish npm Pre-release
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout successful CI commit
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- uses: pnpm/action-setup@v5
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: 26
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build SDK
run: pnpm --filter @eudiplo/sdk-core build
- name: Build CLI
run: pnpm --filter @eudiplo/cli build
- name: Set pre-release version
working-directory: packages/eudiplo-sdk-core
env:
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# Base the prerelease on the latest repository semver tag (e.g., v5.1.0 -> 5.1.0).
# Fall back to package.json if tags are not available.
LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)
if [ -n "$LATEST_TAG" ]; then
CURRENT_VERSION="${LATEST_TAG#v}"
else
CURRENT_VERSION=$(node -p "require('./package.json').version")
fi
SHORT_SHA=$(echo "$SOURCE_SHA" | cut -c1-7)
PRE_VERSION="${CURRENT_VERSION}-main.${SHORT_SHA}"
echo "Publishing version: ${PRE_VERSION}"
npm version "${PRE_VERSION}" --no-git-tag-version
node ../../scripts/sync-cli-version.js "${PRE_VERSION}"
- name: Publish SDK to npm with main tag
working-directory: packages/eudiplo-sdk-core
run: npm publish --tag main --access public --provenance
- name: Publish CLI to npm with main tag
run: node scripts/publish-cli.js main
prepare-release:
name: Prepare versioned release
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
id-token: write
outputs:
has_release: ${{ steps.version_check.outputs.has_release }}
is_major: ${{ steps.version_check.outputs.is_major }}
next_version: ${{ steps.version_check.outputs.next_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Require the main branch
if: github.ref != 'refs/heads/main'
run: |
echo "Versioned releases must be dispatched from the main branch."
exit 1
- name: Verify successful CI for this commit
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;
const response = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'ci-and-release.yml',
event: 'push',
status: 'completed',
per_page: 100,
});
const successfulRun = response.data.workflow_runs.find(
(run) => run.head_sha === context.sha && run.conclusion === 'success',
);
if (!successfulRun) {
core.setFailed(`No successful CI / Docker push run exists for ${context.sha}.`);
return;
}
core.info(`Releasing artifacts from ${successfulRun.html_url}`);
- uses: pnpm/action-setup@v5
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: 26
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check for major version bump
id: version_check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Running semantic-release dry-run to detect version bump..."
DRY_EXIT=0
DRY_OUTPUT="$(npx semantic-release --dry-run 2>&1)" || DRY_EXIT=$?
echo "$DRY_OUTPUT"
if [ "$DRY_EXIT" -ne 0 ]; then
echo "semantic-release dry-run failed (see output above)" >&2
exit "$DRY_EXIT"
fi
NEXT_VERSION=$(echo "$DRY_OUTPUT" | grep -oP "The next release version is \K[0-9]+\.[0-9]+\.[0-9]+" || echo "")
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
echo "Current version: $CURRENT_VERSION"
echo "Next version: $NEXT_VERSION"
if [ -z "$NEXT_VERSION" ]; then
echo "No release will be created"
echo "has_release=false" >> "$GITHUB_OUTPUT"
echo "is_major=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "has_release=true" >> "$GITHUB_OUTPUT"
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
CURRENT_MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
NEXT_MAJOR=$(echo "$NEXT_VERSION" | cut -d. -f1)
if [ "$NEXT_MAJOR" -gt "$CURRENT_MAJOR" ]; then
echo "⚠️ MAJOR VERSION BUMP DETECTED: $CURRENT_VERSION → $NEXT_VERSION"
echo "is_major=true" >> "$GITHUB_OUTPUT"
else
echo "Minor/patch release: $CURRENT_VERSION → $NEXT_VERSION"
echo "is_major=false" >> "$GITHUB_OUTPUT"
fi
- name: Abort when no release is pending
if: steps.version_check.outputs.has_release != 'true'
run: |
echo "No releasable commits were found since the latest version."
exit 1
- name: Abort if major release not confirmed
if: steps.version_check.outputs.is_major == 'true' && github.event.inputs.confirm_major != 'CONFIRM'
run: |
echo "❌ MAJOR VERSION RELEASE BLOCKED"
echo ""
echo "A major version bump was detected but not confirmed."
echo "To release a major version, trigger this workflow manually with:"
echo " confirm_major: CONFIRM"
exit 1
build-cli-release:
name: Build CLI (${{ matrix.target }})
needs: prepare-release
if: needs.prepare-release.outputs.has_release == 'true'
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
target: linux-x64
- runner: ubuntu-24.04-arm
target: linux-arm64
- runner: macos-15
target: macos-arm64
- runner: windows-2025
target: windows-x64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
- uses: pnpm/action-setup@v5
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: 26
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Sync CLI version
shell: bash
run: node scripts/sync-cli-version.js "${{ needs.prepare-release.outputs.next_version }}"
- name: Install dependencies
run: pnpm --filter @eudiplo/cli... install --frozen-lockfile
- name: Build CLI
shell: bash
run: pnpm --filter @eudiplo/cli build
- name: Build standalone CLI SEA
shell: bash
run: pnpm --filter @eudiplo/cli build:sea
- name: Smoke test executable
shell: bash
run: |
if [[ "${{ runner.os }}" == "Windows" ]]; then
./apps/cli/dist-sea/eudiplo.exe --help
else
./apps/cli/dist-sea/eudiplo --help
fi
- name: Package binary
shell: bash
run: |
set -e
mkdir -p release
ARCHIVE_NAME="eudiplo-v${{ needs.prepare-release.outputs.next_version }}-${{ matrix.target }}"
if [[ "${{ matrix.target }}" == "windows-x64" ]]; then
powershell -NoProfile -ExecutionPolicy Bypass -Command "Compress-Archive -Path 'apps/cli/dist-sea/eudiplo.exe' -DestinationPath 'release/${ARCHIVE_NAME}.zip' -Force"
else
chmod +x apps/cli/dist-sea/eudiplo
tar -C apps/cli/dist-sea -czf "release/${ARCHIVE_NAME}.tar.gz" eudiplo
fi
- name: Upload packaged CLI artifact
uses: actions/upload-artifact@v7
with:
name: cli-${{ matrix.target }}
path: release/*
if-no-files-found: error
retention-days: 1
publish-release:
name: Publish GitHub Release
needs: [prepare-release, build-cli-release]
if: needs.prepare-release.outputs.has_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
pull-requests: write
actions: read
pages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Download packaged CLI artifacts
uses: actions/download-artifact@v8
with:
pattern: cli-*
path: release
merge-multiple: true
- name: Generate checksums
shell: bash
run: |
set -e
mkdir -p release
find release -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) -print0 | sort -z | xargs -0 sha256sum > release/SHA256SUMS.txt
echo "Generated checksums:"
cat release/SHA256SUMS.txt
- uses: pnpm/action-setup@v5
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: 26
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set release version
id: release_version
run: |
echo "version=${{ needs.prepare-release.outputs.next_version }}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify immutable Docker source images
run: |
docker buildx imagetools inspect "ghcr.io/openwallet-foundation/eudiplo:sha-${GITHUB_SHA}"
docker buildx imagetools inspect "ghcr.io/openwallet-foundation/eudiplo-client:sha-${GITHUB_SHA}"
docker buildx imagetools inspect "ghcr.io/openwallet-foundation/eudiplo-demo:sha-${GITHUB_SHA}"
- name: Verify release assets
shell: bash
run: |
set -euo pipefail
VERSION="${{ needs.prepare-release.outputs.next_version }}"
test -s "release/eudiplo-v${VERSION}-linux-x64.tar.gz"
test -s "release/eudiplo-v${VERSION}-linux-arm64.tar.gz"
test -s "release/eudiplo-v${VERSION}-macos-arm64.tar.gz"
test -s "release/eudiplo-v${VERSION}-windows-x64.zip"
test -s "release/SHA256SUMS.txt"
- name: Run semantic-release
id: semantic_release
env:
EUDIPLO_RELEASE_VERSION: ${{ needs.prepare-release.outputs.next_version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
DOCKER_REGISTRY_USER: ${{ github.actor }}
DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
DOCKER_SOURCE_SHA: ${{ github.sha }}
run: |
echo "Running semantic-release with release assets ready..."
npx semantic-release
NEXT_VERSION="${{ needs.prepare-release.outputs.next_version }}"
EXPECTED_TAG="v${NEXT_VERSION}"
if ! git ls-remote --exit-code --tags origin "refs/tags/${EXPECTED_TAG}" >/dev/null; then
echo "semantic-release did not publish the expected tag ${EXPECTED_TAG}"
exit 1
fi
echo "new_release=true" >> "$GITHUB_OUTPUT"
echo "version=${EXPECTED_TAG}" >> "$GITHUB_OUTPUT"
- name: Set up Python for docs
if: steps.semantic_release.outputs.new_release == 'true'
uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Install Python dependencies for docs
if: steps.semantic_release.outputs.new_release == 'true'
run: pip install -r requirements.txt
- name: Copy env file for docs
if: steps.semantic_release.outputs.new_release == 'true'
run: cp .env.example .env
- name: Configure git for mike
if: steps.semantic_release.outputs.new_release == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Build and deploy release docs
if: steps.semantic_release.outputs.new_release == 'true'
run: |
export MASTER_SECRET="$(openssl rand -base64 32)"
export AUTH_CLIENT_ID="ci-docs-client"
export AUTH_CLIENT_SECRET="$(openssl rand -hex 32)"
export ENCRYPTION_KEY="$(openssl rand -hex 32)"
FULL_VERSION="${{ steps.semantic_release.outputs.version }}"
RELEASE_VERSION="${FULL_VERSION#v}"
MINOR_VERSION="v${RELEASE_VERSION%.*}"
pnpm run doc:generate
pnpm run compodoc
git fetch origin gh-pages:gh-pages
mike deploy \
--branch gh-pages \
--deploy-prefix docs \
--alias-type copy \
--update-aliases \
"$MINOR_VERSION" latest
mike set-default \
--branch gh-pages \
--deploy-prefix docs \
latest
git worktree add gh-pages-out gh-pages
rsync -a --delete \
--exclude docs \
--exclude .git \
website/. gh-pages-out/
test -s gh-pages-out/install.sh
bash -n gh-pages-out/install.sh
touch gh-pages-out/.nojekyll
if [ -n "$(git -C gh-pages-out status --porcelain)" ]; then
git -C gh-pages-out add .
git -C gh-pages-out commit -m "docs: sync website for $FULL_VERSION"
fi
git push origin gh-pages
- name: Cleanup temporary docs worktree
if: always()
run: git worktree remove gh-pages-out --force || true