Versioned Release #60
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
| 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 |