From e9a09eeb8f288ebc31e9e80abe7148a361c52f04 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Thu, 23 Oct 2025 16:39:11 -0400 Subject: [PATCH] chore(ci): automatic release creation, packaging, and publishing --- .github/workflows/finalize-release.yml | 204 ------------- .../{package-and-release.yml => package.yml} | 36 +-- .github/workflows/release.yml | 42 +++ pyproject.toml | 23 ++ scripts/release.sh | 277 ------------------ src-tauri/tauri.conf.json | 2 +- 6 files changed, 84 insertions(+), 500 deletions(-) delete mode 100644 .github/workflows/finalize-release.yml rename .github/workflows/{package-and-release.yml => package.yml} (72%) create mode 100644 .github/workflows/release.yml delete mode 100755 scripts/release.sh diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml deleted file mode 100644 index 8b7300f..0000000 --- a/.github/workflows/finalize-release.yml +++ /dev/null @@ -1,204 +0,0 @@ -name: "Finalize Release" -on: - workflow_run: - workflows: ["Package and Release"] - types: [completed] - branches: [release] - workflow_dispatch: - inputs: - version: - description: "Version to finalize (e.g., 0.1.0)" - required: true - type: string - -jobs: - finalize-release: - runs-on: ubuntu-latest - if: - ${{ github.event.workflow_run.conclusion == 'success' || github.event_name - == 'workflow_dispatch' }} - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: release - lfs: true - fetch-depth: 0 # Fetch all history for changelog generation - fetch-tags: true # Explicitly fetch all tags - - - name: Get version from files - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - # Manual trigger - use provided version - VERSION="${{ github.event.inputs.version }}" - else - # Automatic trigger - read from files - VERSION=$(python -c "from e3sm_quickview import __version__; print(__version__)") - fi - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "APP_TAG=v$VERSION" >> $GITHUB_ENV - echo "Using version: $VERSION" - - - name: Wait for all platform builds to complete - id: wait_for_builds - uses: actions/github-script@v7 - with: - script: | - const maxWaitTime = 30 * 60 * 1000; // 30 minutes - const pollInterval = 30 * 1000; // 30 seconds - const startTime = Date.now(); - - let releaseReady = false; - let releaseId = null; - - while (Date.now() - startTime < maxWaitTime) { - // Get the draft release - const releases = await github.rest.repos.listReleases({ - owner: context.repo.owner, - repo: context.repo.repo, - }); - - const draftRelease = releases.data.find(release => - release.draft && release.tag_name === '${{ env.APP_TAG }}' - ); - - if (!draftRelease) { - console.log('Draft release not found yet, waiting...'); - await new Promise(resolve => setTimeout(resolve, pollInterval)); - continue; - } - - // Check if we have assets for both macOS targets - const expectedAssets = 2; // aarch64 and x86_64 for macOS - - if (draftRelease.assets.length >= expectedAssets) { - console.log(`Found ${draftRelease.assets.length} assets, proceeding with release`); - releaseId = draftRelease.id; - releaseReady = true; - break; - } else { - console.log(`Found ${draftRelease.assets.length}/${expectedAssets} assets, waiting for more...`); - await new Promise(resolve => setTimeout(resolve, pollInterval)); - } - } - - if (!releaseReady) { - throw new Error('Timeout waiting for all platform builds to complete'); - } - - return releaseId; - - - name: Generate comprehensive release notes - run: | - # Fetch all tags from all branches to ensure we have complete history - git fetch --all --tags - - # Get all tags sorted by version - ALL_TAGS=$(git tag -l "v*" --sort=-version:refname) - echo "All tags found: $ALL_TAGS" - - # Get the current tag (the one we're creating) - CURRENT_TAG="${{ env.APP_TAG }}" - - # Find the previous tag (excluding the current one if it exists) - PREV_TAG="" - for tag in $ALL_TAGS; do - if [ "$tag" != "$CURRENT_TAG" ]; then - PREV_TAG=$tag - break - fi - done - - echo "Previous tag: ${PREV_TAG:-none}" - echo "Current tag: $CURRENT_TAG" - - # Generate changelog - if [ -z "$PREV_TAG" ]; then - echo "No previous tag found, including all commits" - # For first release, get all commits - CHANGELOG=$(git log --oneline --pretty=format:"- %s" --reverse HEAD 2>/dev/null || echo "- Initial release") - else - echo "Generating changelog from $PREV_TAG to $CURRENT_TAG" - # Since tags are on master, get commits between tags - # Use the current tag if it exists, otherwise use HEAD - if git rev-parse "$CURRENT_TAG" >/dev/null 2>&1; then - CHANGELOG=$(git log --oneline --pretty=format:"- %s" ${PREV_TAG}..${CURRENT_TAG} 2>/dev/null || echo "- Release updates") - else - CHANGELOG=$(git log --oneline --pretty=format:"- %s" ${PREV_TAG}..HEAD 2>/dev/null || echo "- Release updates") - fi - fi - - # Filter out version bump commits and merge commits - CHANGELOG=$(echo "$CHANGELOG" | grep -v "^- Bump version:" | grep -v "^- Merge" | grep -v "^- \[pre-commit\]" || true) - - # If changelog is empty, use a default message - if [ -z "$CHANGELOG" ]; then - CHANGELOG="- Release updates" - fi - - echo "Generated changelog:" - echo "$CHANGELOG" - - cat > release_notes.md << EOF - ## What's New in v${{ env.VERSION }} - - $CHANGELOG - - ## Downloads - - This release includes distributables for the following platforms: - - ### macOS - - **Apple Silicon (M1/M2/M3)**: Download the \`aarch64\` version - - **Intel Macs**: Download the \`x64\` version - - Reminder: After download, use the following command in Terminal to unblock the app for macOS - ``` - xattr -d com.apple.quarantine .dmg - ``` - - ## Support - - If you encounter any issues: - 1. Check the [Issues](https://github.com/${{ github.repository }}/issues) page - 2. Create a new issue with details about your problem - 3. Include your operating system and version information - - --- - - **Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG:-"Initial"}...${{ env.APP_TAG }} - EOF - - - name: Publish the release - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const releaseNotes = fs.readFileSync('release_notes.md', 'utf8'); - - const releaseId = '${{ steps.wait_for_builds.outputs.result }}'; - - await github.rest.repos.updateRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: parseInt(releaseId), - draft: false, - body: releaseNotes, - name: `QuickView release v${{ env.VERSION }}`, - }); - - console.log('Release v${{ env.VERSION }} published successfully!'); - - - name: Create release summary - run: | - echo "## Release Published!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Trigger**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Version**: v${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY - echo "**Tag**: ${{ env.APP_TAG }}" >> $GITHUB_STEP_SUMMARY - echo "**Release URL**: https://github.com/${{ github.repository }}/releases/tag/${{ env.APP_TAG }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The release is now live with all platform distributables attached!" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/package-and-release.yml b/.github/workflows/package.yml similarity index 72% rename from .github/workflows/package-and-release.yml rename to .github/workflows/package.yml index 43a3650..6e23902 100644 --- a/.github/workflows/package-and-release.yml +++ b/.github/workflows/package.yml @@ -1,10 +1,14 @@ -name: "Package and Release" +name: "Package" + on: - push: - branches: [release*] -# This workflow will trigger on each push to the `master` branch to create or update a GitHub release, build your app, and upload the artifacts to the release. + workflow_dispatch: # manually triggered + workflow_run: + workflows: ["Release"] + types: [completed] + branches: [master] + jobs: - publish-tauri: + package: permissions: contents: write strategy: @@ -13,10 +17,12 @@ jobs: include: - platform: "macos-latest" # for Arm based macs (M1 and above). args: "--target aarch64-apple-darwin" - - platform: "macos-latest" # for Intel based macs. - args: "--target x86_64-apple-darwin" - # - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04. - # args: '' + - platform: 'macos-latest' # for Intel based macs. + args: '--target x86_64-apple-darwin' + - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04. + args: '' + # - platform: 'windows-latest' + # args: '' runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 @@ -49,11 +55,7 @@ jobs: - name: Install dependencies and get version run: | python -m pip install --upgrade pip - pip install . # Assumes installable via setup.py or pyproject.toml - # Now we can read the version - VERSION=$(python -c "from e3sm_quickview import __version__; print(__version__)") - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "Found version: $VERSION" + pip install .[tauri] shell: bash -l {0} - name: Package using PyInstaller run: | @@ -82,9 +84,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tagName: v${{ env.VERSION }} - releaseName: "QuickView release v${{ env.VERSION }}" - releaseBody: "See the assets to download this version and install." - releaseDraft: true + tagName: v__VERSION__ + releaseDraft: false prerelease: false args: ${{ matrix.args }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8461e63 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: "Release" +on: + workflow_dispatch: # manually triggered + +env: + # Many color libraries just need this to be set to any value, but at least + # one distinguishes color depth, where "3" -> "256-bit color". + FORCE_COLOR: 3 + +jobs: + # https://docs.pypi.org/trusted-publishers/using-a-publisher/ + release: + needs: [] + name: Distribution build and publish to PyPI + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + contents: write + environment: + name: pypi + url: https://pypi.org/p/e3sm-quickview + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Python Semantic Release + id: release + uses: python-semantic-release/python-semantic-release@v10.4.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate artifact attestation for sdist and wheel + if: steps.release.outputs.released == 'true' + uses: actions/attest-build-provenance@v2.2.3 + with: + subject-path: "dist/*" + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + if: steps.release.outputs.released == 'true' diff --git a/pyproject.toml b/pyproject.toml index 7fc5744..aea2646 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,3 +67,26 @@ quickview = "e3sm_quickview.app2:main" # Ignore star import issues in ParaView plugins "e3sm_quickview/plugins/*.py" = ["F403", "F405"] "e3sm_quickview/pipeline.py" = ["F821"] # Plugin classes loaded dynamically + +[tool.semantic_release] +version_toml = [ + "pyproject.toml:project.version", + "src-tauri/Cargo.toml:package.version", +] +version_variables = [ + "src/e3sm_quickview/__init__.py:__version__", + "src-tauri/tauri.conf.json:version", +] +build_command = """ + python -m venv .venv + source .venv/bin/activate + pip install -U pip build + python -m build . +""" + +[tool.semantic_release.branches.master] +match = "master" + +[tool.semantic_release.publish] +dist_glob_patterns = ["dist/*"] +upload_to_vcs_release = true diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index 3690034..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,277 +0,0 @@ -#!/bin/bash - -# Function to print output -print_status() { - echo "[INFO] $1" >&2 -} - -print_success() { - echo "[SUCCESS] $1" >&2 -} - -print_warning() { - echo "[WARNING] $1" >&2 -} - -print_error() { - echo "[ERROR] $1" >&2 -} - -# Function to show usage -show_usage() { - echo "Usage: $0 [patch|minor|major]" - echo "" - echo "This script will:" - echo " 1. Bump the version using bump2version" - echo " 2. Commit the changes to main" - echo " 3. Create a release PR from main to release branch" - echo "" - echo "Examples:" - echo " $0 patch # 0.1.0 -> 0.1.1" - echo " $0 minor # 0.1.0 -> 0.2.0" - echo " $0 major # 0.1.0 -> 1.0.0" -} - -# Function to check prerequisites -check_prerequisites() { - print_status "Checking prerequisites..." - - # Check if bump2version is installed - if ! command -v bump2version &> /dev/null; then - print_error "bump2version is not installed. Install it with: pip install bump2version" - exit 1 - fi - - # Check if gh CLI is installed - if ! command -v gh &> /dev/null; then - print_error "GitHub CLI is not installed. Install it from: https://cli.github.com/" - exit 1 - fi - - # Check if we're in a git repository - if ! git rev-parse --is-inside-work-tree &> /dev/null; then - print_error "Not in a git repository" - exit 1 - fi - - # Check if we're on main/master branch - current_branch=$(git branch --show-current) - if [[ "$current_branch" != "main" && "$current_branch" != "master" ]]; then - print_error "Must be on main or master branch. Currently on: $current_branch" - exit 1 - fi - - # Check if working directory is clean - if ! git diff-index --quiet HEAD --; then - print_error "Working directory is not clean. Please commit or stash your changes." - exit 1 - fi - - print_success "All prerequisites met" -} - -# Function to get current version from pyproject.toml -get_current_version() { - python -c "import tomllib; f=open(\"pyproject.toml\",\"rb\"); data=tomllib.load(f); f.close(); print(data[\"project\"][\"version\"])" -} - -# Function to get previous version from git tags -get_previous_version() { - git describe --tags --abbrev=0 2>/dev/null | sed "s/^v//" || echo "0.0.0" -} - -# Function to generate changelog -generate_changelog() { - local prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - - if [ -z "$prev_tag" ]; then - echo "- Initial release" - else - # Get commits since last tag, excluding merge commits - git log --oneline --pretty=format:"- %s" --no-merges ${prev_tag}..HEAD | head -15 - fi -} - -# Function to bump version -bump_version() { - local version_type=$1 - - print_status "Getting current version..." - local old_version=$(get_current_version) - print_status "Current version: v$old_version" - - # First, get what the new version will be - local new_version=$(bump2version --list $version_type --dry-run 2>/dev/null | grep "^new_version=" | cut -d= -f2) - if [ -z "$new_version" ]; then - print_error "Failed to determine new version" - exit 1 - fi - - print_status "Bumping $version_type version..." - # Now actually bump the version - bump2version $version_type --verbose >&2 - if [ $? -ne 0 ]; then - print_error "Failed to bump version" - exit 1 - fi - - print_success "Version bumped: v$old_version -> v$new_version" - - # Return just the clean version number - echo "$new_version" -} - -# Function to commit and push changes -commit_and_push() { - local version=$1 - local branch=$(git branch --show-current) - - # Debug: show what version we received - print_status "Received version for push: '$version'" >&2 - - print_status "Pushing changes to $branch..." - if ! git push origin $branch; then - print_error "Failed to push changes" - exit 1 - fi - - # Push the new tag (bump2version already created it) - print_status "Pushing new tag v${version}..." - if ! git push origin v${version}; then - # Check if tag already exists on remote - if git ls-remote --tags origin | grep -q "refs/tags/v${version}$"; then - print_warning "Tag v${version} already exists on remote" - else - print_error "Failed to push tag v${version}" - exit 1 - fi - fi - - print_success "Changes and tags pushed to $branch" -} - -# Function to create release PR -create_release_pr() { - local current_version=$1 - local previous_version=$(get_previous_version) - - # Determine release type - IFS="." read -ra CURRENT <<< "$current_version" - IFS="." read -ra PREVIOUS <<< "$previous_version" - - if [ "${CURRENT[0]}" != "${PREVIOUS[0]}" ]; then - release_type="major" - elif [ "${CURRENT[1]}" != "${PREVIOUS[1]}" ]; then - release_type="minor" - else - release_type="patch" - fi - - print_status "Generating changelog..." - local changelog=$(generate_changelog) - - print_status "Creating release PR..." - - # Create PR using GitHub CLI - gh pr create \ - --base release \ - --head $(git branch --show-current) \ - --title "Release v${current_version}" \ - --body "$(cat << EOF -## Release v${current_version} - -This pull request contains all changes for the **v${current_version}** release. - -### Release Information -- **Previous version**: v${previous_version} -- **New version**: v${current_version} -- **Release type**: ${release_type} -- **Release date**: $(date +%Y-%m-%d) - -### What Changed - -${changelog} - -### Release Checklist -- [x] Version bumped in all files -- [ ] All tests passing -- [ ] Documentation updated (if needed) -- [ ] Breaking changes documented (if any) -- [ ] Ready for production deployment - -### Files Updated -- quickview/__init__.py - Version updated to ${current_version} -- pyproject.toml - Version updated to ${current_version} -- src-tauri/tauri.conf.json - Version updated to ${current_version} -- src-tauri/Cargo.toml - Version updated to ${current_version} - -### Post-Merge Actions -After merging this PR: -1. Git tag v${current_version} will be available -2. Tauri build will trigger automatically -3. Draft release will be created with distributables -4. Release will be published automatically with release notes - -### Important Notes -- This is a **${release_type}** release -- Review all changes carefully before merging -- Ensure all CI checks pass before merging - ---- - -**Release prepared by**: @$(git config user.name) -**Release type**: ${release_type} -**Target branch**: release -**Commit count**: $(git rev-list --count HEAD ^$(git merge-base HEAD release) 2>/dev/null || echo "N/A") -EOF -)" \ - --assignee @me \ - --label "release,${release_type}" || { - print_error "Failed to create PR" - exit 1 - } - - print_success "Release PR created successfully!" -} - -# Main function -main() { - # Check if version type is provided - if [ $# -eq 0 ]; then - show_usage - exit 1 - fi - - local version_type=$1 - - # Validate version type - if [[ ! "$version_type" =~ ^(patch|minor|major)$ ]]; then - print_error "Invalid version type: $version_type" - show_usage - exit 1 - fi - - echo "QuickView Release Tool" - echo "======================" - - # Run checks - check_prerequisites - - # Bump version - local new_version=$(bump_version $version_type) - - # Commit and push changes - commit_and_push $new_version - - # Create release PR - create_release_pr $new_version - - echo "" - print_success "Release process completed!" - echo "Version: v$new_version ($version_type)" - echo "Review and merge the PR to trigger the release pipeline" - echo "PR URL: $(gh pr view --json url --jq .url 2>/dev/null || echo Check GitHub for PR link)" -} - -# Run main function -main "$@" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dda2fb2..aae753d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -51,7 +51,7 @@ }, "resources": ["server"], "shortDescription": "", - "targets": ["dmg"], + "targets": ["dmg", "deb", "rpm", "msi"], "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256",