From 62af5130bde0b5c3400beb1050ff12622d3ca1e8 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 17 Oct 2025 12:39:26 -0500 Subject: [PATCH 1/6] Add status checks workflow to main --- .github/workflows/status-checks.yml | 236 +++++++++++++++------------- 1 file changed, 124 insertions(+), 112 deletions(-) diff --git a/.github/workflows/status-checks.yml b/.github/workflows/status-checks.yml index 4928195..53653cd 100644 --- a/.github/workflows/status-checks.yml +++ b/.github/workflows/status-checks.yml @@ -16,22 +16,64 @@ env: CI_CD: true jobs: - run-unit-tests-linux: + run-unit-tests-debian: runs-on: ubuntu-latest + permissions: + contents: read + packages: read + container: + image: ghcr.io/jmr-dev/android-file-handler-debian-builder:debian13-trixie + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' + - name: Install dependencies + run: | + poetry install --no-interaction - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Run tests + run: | + poetry run pytest tests/ -v + + run-unit-tests-arch: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + container: + image: ghcr.io/jmr-dev/android-file-handler-arch-builder:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 - name: Install dependencies run: | - poetry install + poetry install --no-interaction + + - name: Run tests + run: | + poetry run pytest tests/ -v + + run-unit-tests-rhel: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + container: + image: ghcr.io/jmr-dev/android-file-handler-rhel-builder:fedora42 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + poetry install --no-interaction - name: Run tests run: | @@ -45,7 +87,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - name: Install Poetry uses: snok/install-poetry@v1 @@ -70,16 +112,16 @@ jobs: poetry run pytest tests/ -v build-windows: - needs: [run-unit-tests-linux, run-unit-tests-windows] + needs: [run-unit-tests-windows] runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.12 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - name: Install Poetry uses: snok/install-poetry@v1 @@ -113,65 +155,35 @@ jobs: dist/android-file-handler.exe build-debian: - needs: [run-unit-tests-linux, run-unit-tests-windows] + needs: [run-unit-tests-debian, run-unit-tests-arch, run-unit-tests-rhel] + permissions: + contents: read + packages: read env: DISTRO_TYPE: debian runs-on: ubuntu-latest container: - image: python:3.12-slim + image: ghcr.io/jmr-dev/android-file-handler-debian-builder:debian13-trixie + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - - name: Install system dependencies & gem fpm (include Tcl/Tk) - run: | - set -euo pipefail - apt-get update - # Install Tcl/Tk runtimes, dev headers and common X libraries required by tkinter - apt-get install -y --no-install-recommends \ - curl git build-essential ruby ruby-dev gcc make zlib1g-dev ca-certificates python3-tk \ - tcl8.6 tk8.6 tcl8.6-dev tk8.6-dev libx11-6 libxext6 libxrender1 libxcb1 - # install pinned fpm to the system gem dir (will be available under gem env's EXECUTABLE DIRECTORY or /usr/local/bin) - gem install --no-document -v "${FPM_VERSION}" fpm - - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: latest - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Configure Poetry + - name: Build executable run: | - echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV - export PATH="$HOME/.local/bin:$PATH" - poetry config virtualenvs.create true - poetry config virtualenvs.in-project true - - - name: Install dependencies & build executable - run: | - export PATH="$HOME/.local/bin:$PATH" - poetry env use python3.12 || true - # Debug: show tkinter/_tkinter and Tcl library discovery in the Poetry venv - poetry run python -c 'import tkinter, _tkinter, sys; print("tkinter=", getattr(tkinter, "__file__", None)); print("_tkinter=", getattr(_tkinter, "__file__", None)); import tkinter as tk; print("TCL_LIBRARY=", tk.Tcl().eval("info library"))' - - # Build distro-specific package layout using the build script via Poetry + # Container has Poetry and all dependencies pre-installed + poetry install --no-interaction poetry run python scripts/build_package_linux.py - name: Package .deb (fpm) shell: bash run: | set -euo pipefail - export PATH="$HOME/.local/bin:$PATH" VERSION="$(poetry version -s)" PKG_DIR="pkg_dist_debian" mkdir -p dist - # Debug listing echo "Packaging from $PKG_DIR" ls -la "$PKG_DIR" || true @@ -184,7 +196,7 @@ jobs: fi fpm -s dir -t deb -n android-file-handler -v "$VERSION" \ - --architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ + --architecture amd64 --deb-user root --deb-group root \ --after-install scripts/debian_postinst.sh \ -p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" "${PKG_ITEMS[@]}" @@ -197,72 +209,70 @@ jobs: pkg_dist_debian/** build-arch: - needs: [run-unit-tests-linux, run-unit-tests-windows] + needs: [run-unit-tests-debian, run-unit-tests-arch, run-unit-tests-rhel] + permissions: + contents: read + packages: read env: DISTRO_TYPE: arch runs-on: ubuntu-latest - container: - image: archlinux:latest steps: - - name: Install system dependencies (Arch) and system Ruby - run: | - set -euo pipefail - pacman -Syu --noconfirm - pacman -S --noconfirm ruby base-devel curl git tar ca-certificates tk tcl libx11 libxext libxrender libxcb - # Install fpm system-wide and pin version so fpm will be in /usr/in - gem install --no-document erb - gem install --no-document -v "${FPM_VERSION}" fpm --bindir /usr/bin - # persist system bindir to subsequent steps (usually already on PATH) - echo "/usr/local/bin" >> $GITHUB_PATH - - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 with: - python-version: '3.12' + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CI_CD_PAT }} # Personal Access Token with read:packages + - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: latest - virtualenvs-create: true - virtualenvs-in-project: true + - name: Pull Docker image + run: docker pull ghcr.io/jmr-dev/android-file-handler-arch-builder:latest - - name: Configure Poetry + - name: Build executable inside container run: | - echo 'export PATH="$HOME/bin:$PATH"' >> $GITHUB_ENV - export PATH="$HOME/bin:$PATH" + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + -e DISTRO_TYPE=${{ env.DISTRO_TYPE }} \ + -e FPM_VERSION=${{ env.FPM_VERSION }} \ + -e CI_CD=${{ env.CI_CD }} \ + ghcr.io/jmr-dev/android-file-handler-arch-builder:latest \ + sh -c "poetry install --no-interaction && poetry run python scripts/build_package_linux.py" - - name: Install dependencies & build executable + - name: Package pacman (fpm) inside container run: | - export PATH="$HOME/bin:/usr/bin:$PATH" - # Use unified build script to produce pkg_dist_arch layout via Poetry - poetry run python scripts/build_package_linux.py - - - name: Package pacman (fpm) - shell: bash - run: | - set -euo pipefail - export PATH="$HOME/.local/bin:/usr/bin:$PATH" - VERSION="$(poetry version -s)" - PKG_DIR="pkg_dist_arch" - mkdir -p dist - echo "Packaging from $PKG_DIR" - ls -la "$PKG_DIR" || true - - ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" - PKG_ITEMS=( "usr/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" ) - if [ -f "$ICON_PATH" ]; then - PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" ) - else - echo "Note: icon not present, packaging without icon" - fi - - fpm -s dir -t pacman -n android-file-handler -v "$VERSION" \ - --architecture x86_64 --prefix /usr/bin \ - -p "dist/android-file-handler-${VERSION}-1-x86_64.pkg.tar.zst" -C "$PKG_DIR" "${PKG_ITEMS[@]}" + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + -e FPM_VERSION=${{ env.FPM_VERSION }} \ + ghcr.io/jmr-dev/android-file-handler-arch-builder:latest \ + sh -c 'set -euo pipefail && \ + VERSION="$(poetry version -s)" && \ + PKG_DIR="pkg_dist_arch" && \ + mkdir -p dist && \ + echo "Packaging from $PKG_DIR" && \ + ls -la "$PKG_DIR" || true && \ + ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" && \ + if [ -f "$ICON_PATH" ]; then \ + fpm -s dir -t pacman -n android-file-handler -v "$VERSION" \ + --architecture x86_64 \ + -p "dist/android-file-handler-${VERSION}-1-x86_64.pkg.tar.zst" \ + -C "$PKG_DIR" \ + "usr/bin/android-file-handler" \ + "usr/share/applications/android-file-handler.desktop" \ + "usr/share/icons/hicolor/256x256/apps/android-file-handler.png"; \ + else \ + echo "Note: icon not present, packaging without icon" && \ + fpm -s dir -t pacman -n android-file-handler -v "$VERSION" \ + --architecture x86_64 \ + -p "dist/android-file-handler-${VERSION}-1-x86_64.pkg.tar.zst" \ + -C "$PKG_DIR" \ + "usr/bin/android-file-handler" \ + "usr/share/applications/android-file-handler.desktop"; \ + fi' - name: Upload Arch package uses: actions/upload-artifact@v4 @@ -273,7 +283,7 @@ jobs: pkg_dist_arch/** build-rhel: - needs: [run-unit-tests-linux, run-unit-tests-windows] + needs: [run-unit-tests-debian, run-unit-tests-arch, run-unit-tests-rhel] permissions: contents: read packages: read @@ -281,7 +291,10 @@ jobs: DISTRO_TYPE: rhel runs-on: ubuntu-latest container: - image: ghcr.io/jmr-dev/android-file-handler-adb:v0.1.0 + image: ghcr.io/jmr-dev/android-file-handler-rhel-builder:fedora42 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -289,8 +302,7 @@ jobs: - name: Build RHEL package run: | set -euo pipefail - export CI_CD=true - export DISTRO_TYPE=rhel + # Container has Poetry and all dependencies pre-installed poetry install --no-interaction poetry run python scripts/build_package_linux.py -- 2.47.3 From 44c5f3cb1f497a4165057f3584ab809cc408eb7c Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 17 Oct 2025 13:17:12 -0500 Subject: [PATCH 2/6] make release workflow usable --- .github/workflows/release-scrapyard.yml | 108 +++- .github/workflows/release.yml | 657 +----------------------- .github/workflows/status-checks.yml | 1 + 3 files changed, 125 insertions(+), 641 deletions(-) diff --git a/.github/workflows/release-scrapyard.yml b/.github/workflows/release-scrapyard.yml index 3132c06..7789f74 100644 --- a/.github/workflows/release-scrapyard.yml +++ b/.github/workflows/release-scrapyard.yml @@ -630,4 +630,110 @@ jobs: echo "✅ GitHub release created" echo "⏭️ AUR publishing skipped (dry run mode)" echo "" - echo "To publish to AUR, re-run with dry_run=false" \ No newline at end of file + echo "To publish to AUR, re-run with dry_run=false" + + + + + inputs: + new_version: + description: "New version to release (e.g., 0.2.0)" + required: false + type: string + pr_check_timeout: + description: "Timeout in seconds for PR status checks (default: 1800)" + required: false + type: number + default: 1800 + jobs: + description: "Comma-separated jobs to run (e.g., build-windows,build-debian,build-arch,build-rhel)" + required: true + default: "build-windows,build-debian,build-arch,build-rhel" + start_from_step: + description: "Start workflow from this step (all subsequent steps will run)" + required: false + type: choice + default: "bump-version" + options: + - "bump-version" + - "merge-develop-to-main" + + + + rollback-on-build-failure: + needs: [merge-develop-to-main, wait-for-main-pr, build-windows, build-debian, build-arch, build-rhel] + if: | + ${{ + always() && + (github.event.inputs.start_from_step == 'bump-version' || + github.event.inputs.start_from_step == 'merge-develop-to-main') && + needs.wait-for-main-pr.result == 'success' && + (needs.build-windows.result == 'failure' || + needs.build-debian.result == 'failure' || + needs.build-arch.result == 'failure' || + needs.build-rhel.result == 'failure') + }} + runs-on: ubuntu-latest + permissions: + contents: write + # Prevent multiple rollback operations from running concurrently + concurrency: + group: main-branch-rollback + cancel-in-progress: false + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Rollback merge commit on build failure + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + echo "::error::One or more build jobs failed - initiating rollback" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Fetch latest + git fetch origin main + + # Get the previous main SHA (before the merge) + PREVIOUS_MAIN_SHA="${{ needs.merge-develop-to-main.outputs.previous_main_sha }}" + echo "Resetting main to previous commit: $PREVIOUS_MAIN_SHA" + + # Checkout main and reset to previous commit + git checkout main + git reset --hard "$PREVIOUS_MAIN_SHA" + + # Force push the rollback + if ! git push --force origin main; then + git notes -m 'release commit could not be rolled back' + echo "::error::Failed to push rollback to main" + exit 1 + fi + git notes -m 'release commit rolled back' + echo "::error::Reset main branch to commit $PREVIOUS_MAIN_SHA" + echo "::error::Build failures detected:" + + # Report which builds failed + if [ "${{ needs.build-windows.result }}" = "failure" ]; then + echo "::error:: - build-windows: FAILED" + fi + if [ "${{ needs.build-debian.result }}" = "failure" ]; then + echo "::error:: - build-debian: FAILED" + fi + if [ "${{ needs.build-arch.result }}" = "failure" ]; then + echo "::error:: - build-arch: FAILED" + fi + if [ "${{ needs.build-rhel.result }}" = "failure" ]; then + echo "::error:: - build-rhel: FAILED" + fi + + exit 1 + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ae8bac..153f7cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,28 +11,6 @@ name: Build Multi-Platform Binaries on: workflow_dispatch: - inputs: - new_version: - description: "New version to release (e.g., 0.2.0)" - required: false - type: string - pr_check_timeout: - description: "Timeout in seconds for PR status checks (default: 1800)" - required: false - type: number - default: 1800 - jobs: - description: "Comma-separated jobs to run (e.g., build-windows,build-debian,build-arch,build-rhel)" - required: true - default: "build-windows,build-debian,build-arch,build-rhel" - start_from_step: - description: "Start workflow from this step (all subsequent steps will run)" - required: false - type: choice - default: "bump-version" - options: - - "bump-version" - - "merge-develop-to-main" permissions: contents: write @@ -42,7 +20,7 @@ permissions: # This is critical to prevent concurrent rollbacks concurrency: group: release-workflow - cancel-in-progress: false + cancel-in-progress: true env: # change this if you prefer a different pinned fpm version @@ -52,497 +30,12 @@ env: CI_CD: true jobs: - bump-version: - if: ${{ github.event.inputs.start_from_step == 'bump-version' }} - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - outputs: - pr_number: ${{ steps.create-pr.outputs.pr_number }} - steps: - - name: Validate version input - run: | - VERSION="${{ github.event.inputs.new_version }}" - if [ -z "$VERSION" ]; then - echo "::error::Version number is required for the bump-version task" - echo "::error::Please provide a version number in the 'new_version' input field" - echo "::error::Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)" - exit 1 - fi - # Full semver regex supporting: - # - Basic: 1.2.3 - # - Prerelease: 1.2.3-beta, 1.2.3-rc.1, 1.2.3-alpha.1.2 - # - Build metadata: 1.2.3+build, 1.2.3+20130313144700 - # - Combined: 1.2.3-beta.1+build.123 - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$ ]]; then - echo "::error::Invalid version format: $VERSION" - echo "::error::Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)" - exit 1 - fi - echo "Version format is valid: $VERSION" - - name: Checkout develop branch - uses: actions/checkout@v4 - with: - ref: develop - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Set up Python 3.13 - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install Poetry - uses: snok/install-poetry@v1 - - - name: Check for existing PR or branch - id: check-existing - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - VERSION="${{ github.event.inputs.new_version }}" - BRANCH_NAME="release/v${VERSION}" - - # Check if branch already exists - if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then - echo "::warning::Branch $BRANCH_NAME already exists" - - # Check if there's an open PR for this branch - EXISTING_PR=$(gh pr list --base develop --head "$BRANCH_NAME" --state open --json number --jq '.[0].number' || echo "") - - if [ -n "$EXISTING_PR" ]; then - echo "::error::PR #$EXISTING_PR already exists for version $VERSION" - echo "::error::Please close or merge the existing PR before creating a new release" - exit 1 - fi - - echo "::error::Branch $BRANCH_NAME exists but no open PR found" - echo "::error::Please delete the branch or use a different version number" - exit 1 - fi - - echo "✓ No existing branch or PR found for version $VERSION" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - - name: Create release branch and bump version - id: bump - run: | - set -e - VERSION="${{ github.event.inputs.new_version }}" - BRANCH_NAME="${{ steps.check-existing.outputs.branch_name }}" - - # Create and checkout release branch - git checkout -b "$BRANCH_NAME" - - # Update version in pyproject.toml - poetry version "$VERSION" - - # Commit the version change - git add pyproject.toml - git commit -m "Bump version to ${VERSION}" - - # Push the branch with error handling - if ! git push origin "$BRANCH_NAME"; then - echo "::error::Failed to push branch $BRANCH_NAME" - exit 1 - fi - - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - - name: Create PR to develop - id: create-pr - env: - GH_TOKEN: ${{ secrets.CI_CD_PAT }} - run: | - set -e - VERSION="${{ github.event.inputs.new_version }}" - BRANCH_NAME="${{ steps.bump.outputs.branch_name }}" - - # Create PR with auto-merge enabled - PR_URL=$(gh pr create \ - --base develop \ - --head "$BRANCH_NAME" \ - --title "Release v${VERSION}" \ - --body "This PR bumps the version to ${VERSION} as part of the release process. - - **Auto-generated by release workflow** - - Once status checks pass, this PR will be automatically merged." \ - --repo ${{ github.repository }} || { - echo "::error::Failed to create PR" - exit 1 - }) - - # Extract PR number from URL - PR_NUMBER=$(echo "$PR_URL" | grep -oP '\d+$') - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - echo "Created PR #$PR_NUMBER: $PR_URL" - - # Enable auto-merge (rebase) - if ! gh pr merge "$PR_NUMBER" --auto --rebase --repo ${{ github.repository }}; then - echo "::error::Failed to enable auto-merge for PR #$PR_NUMBER" - exit 1 - fi - echo "Auto-merge enabled for PR #$PR_NUMBER" - - wait-for-version-pr: - needs: [bump-version] - if: ${{ github.event.inputs.start_from_step == 'bump-version' }} - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Wait for PR status checks and merge - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - PR_NUMBER="${{ needs.bump-version.outputs.pr_number }}" - echo "Monitoring PR #$PR_NUMBER for status checks..." - - MAX_WAIT=${{ github.event.inputs.pr_check_timeout || 1800 }} - INITIAL_INTERVAL=10 - MAX_INTERVAL=300 # 5 minutes maximum - BACKOFF_MULTIPLIER=1.5 - SLEEP_INTERVAL=$INITIAL_INTERVAL - ELAPSED=0 - PR_STATE_RETRY_COUNT=0 - STATUS_CHECK_RETRY_COUNT=0 - MAX_RETRIES=3 - echo "Max wait time: ${MAX_WAIT}s, using exponential backoff (max interval: ${MAX_INTERVAL}s)" - - while [ $ELAPSED -lt $MAX_WAIT ]; do - # Get PR status with retry logic - if ! PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo ${{ github.repository }} 2>&1); then - PR_STATE_RETRY_COUNT=$((PR_STATE_RETRY_COUNT + 1)) - if [ $PR_STATE_RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "::error::Failed to fetch PR status after $MAX_RETRIES retries" - exit 1 - fi - echo "::warning::Failed to fetch PR status (attempt $PR_STATE_RETRY_COUNT/$MAX_RETRIES), retrying..." - sleep $((2 ** PR_STATE_RETRY_COUNT)) - continue - fi - PR_STATE_RETRY_COUNT=0 - - if [ "$PR_STATE" = "MERGED" ]; then - echo "✓ PR #$PR_NUMBER has been merged successfully!" - exit 0 - fi - - if [ "$PR_STATE" = "CLOSED" ]; then - echo "::error::PR #$PR_NUMBER was closed without merging" - exit 1 - fi - - # Check status checks with retry logic - if ! STATUS_JSON=$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '.statusCheckRollup' --repo ${{ github.repository }} 2>&1); then - STATUS_CHECK_RETRY_COUNT=$((STATUS_CHECK_RETRY_COUNT + 1)) - if [ $STATUS_CHECK_RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "::error::Failed to fetch status checks after $MAX_RETRIES retries" - exit 1 - fi - echo "::warning::Failed to fetch status checks (attempt $STATUS_CHECK_RETRY_COUNT/$MAX_RETRIES), retrying..." - sleep $((2 ** STATUS_CHECK_RETRY_COUNT)) - continue - fi - STATUS_CHECK_RETRY_COUNT=0 - - # Count check states - TOTAL=$(echo "$STATUS_JSON" | jq 'length') - COMPLETED=$(echo "$STATUS_JSON" | jq '[.[] | select(.conclusion != null)] | length') - SUCCESS=$(echo "$STATUS_JSON" | jq '[.[] | select(.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED")] | length') - FAILED=$(echo "$STATUS_JSON" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT")] | length') - - echo "Status checks: $COMPLETED/$TOTAL completed, $SUCCESS passed, $FAILED failed (interval: ${SLEEP_INTERVAL}s)" - - # Check for failures - if [ "$FAILED" -gt 0 ]; then - echo "::error::Status checks failed for PR #$PR_NUMBER" - gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT") | "- " + .name + ": " + .conclusion' --repo ${{ github.repository }} - exit 1 - fi - - echo "Waiting for checks to complete... (${ELAPSED}s elapsed)" - sleep $SLEEP_INTERVAL - ELAPSED=$((ELAPSED + SLEEP_INTERVAL)) - - # Calculate next interval with exponential backoff (capped at MAX_INTERVAL) - NEXT_INTERVAL=$(awk "BEGIN {printf \"%.0f\", $SLEEP_INTERVAL * $BACKOFF_MULTIPLIER}") - if [ $NEXT_INTERVAL -gt $MAX_INTERVAL ]; then - SLEEP_INTERVAL=$MAX_INTERVAL - else - SLEEP_INTERVAL=$NEXT_INTERVAL - fi - done - - echo "::error::Timeout waiting for PR #$PR_NUMBER to merge" - exit 1 - - merge-develop-to-main: - needs: [wait-for-version-pr] - if: | - ${{ - always() && - (github.event.inputs.start_from_step == 'bump-version' || github.event.inputs.start_from_step == 'merge-develop-to-main') && - (needs.wait-for-version-pr.result == 'success' || needs.wait-for-version-pr.result == 'skipped') - }} - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - # Ensure only one merge operation runs at a time - concurrency: - group: main-branch-merge - cancel-in-progress: false - outputs: - pr_number: ${{ steps.create-pr.outputs.pr_number }} - previous_main_sha: ${{ steps.check-branches.outputs.previous_main_sha }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: develop - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Set up Python 3.13 - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install Poetry - uses: snok/install-poetry@v1 - - - name: Check branches and verify merge readiness - id: check-branches - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - git fetch origin main develop - - # Store current main SHA for reference - PREVIOUS_MAIN_SHA=$(git rev-parse origin/main) - echo "previous_main_sha=$PREVIOUS_MAIN_SHA" >> $GITHUB_OUTPUT - echo "Previous main SHA: $PREVIOUS_MAIN_SHA" - - # Verify develop is ahead of main - MERGE_BASE=$(git merge-base origin/main origin/develop) - MAIN_SHA=$(git rev-parse origin/main) - - if [ "$MERGE_BASE" != "$MAIN_SHA" ]; then - echo "::error::Main branch has commits not in develop. Cannot fast-forward." - echo "::error::Please merge or rebase main into develop first." - exit 1 - fi - - # Ensure develop is actually ahead - DEVELOP_SHA=$(git rev-parse origin/develop) - if [ "$MAIN_SHA" = "$DEVELOP_SHA" ]; then - echo "::warning::Main is already up to date with develop. Nothing to merge." - exit 0 - fi - - # Check for existing open PR from develop to main - EXISTING_PR=$(gh pr list --base main --head develop --state open --json number --jq '.[0].number' || echo "") - if [ -n "$EXISTING_PR" ]; then - echo "::error::PR #$EXISTING_PR already exists from develop to main" - echo "::error::Please close or merge the existing PR before creating a new one" - exit 1 - fi - - echo "✓ Ready to create PR from develop to main" - - - name: Create PR from develop to main - id: create-pr - env: - GH_TOKEN: ${{ secrets.CI_CD_PAT }} - run: | - set -e - # Read version from pyproject.toml (single source of truth) - if ! VERSION="$(poetry version -s 2>&1)"; then - echo "::error::Failed to read version from pyproject.toml" - echo "::error::Poetry output: $VERSION" - exit 1 - fi - if [ -z "$VERSION" ]; then - echo "::error::Version is empty in pyproject.toml" - exit 1 - fi - # Validate semver format - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$ ]]; then - echo "::error::Invalid version format in pyproject.toml: $VERSION" - echo "::error::Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)" - exit 1 - fi - echo "Using version from pyproject.toml: $VERSION" - - # Create PR with auto-merge enabled - PR_URL=$(gh pr create \ - --base main \ - --head develop \ - --title "Release v${VERSION} - Merge develop into main" \ - --body "This PR merges develop into main for release v${VERSION}. - - **Auto-generated by release workflow** - - Once status checks pass, this PR will be automatically merged." \ - --repo ${{ github.repository }} || { - echo "::error::Failed to create PR" - exit 1 - }) - - # Extract PR number from URL - PR_NUMBER=$(echo "$PR_URL" | grep -oP '\d+$') - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - echo "Created PR #$PR_NUMBER: $PR_URL" - - # Enable auto-merge (rebase for fast-forward) - if ! gh pr merge "$PR_NUMBER" --auto --rebase --repo ${{ github.repository }}; then - echo "::error::Failed to enable auto-merge for PR #$PR_NUMBER" - exit 1 - fi - echo "Auto-merge enabled for PR #$PR_NUMBER" - - wait-for-main-pr: - needs: [merge-develop-to-main] - if: | - ${{ - always() && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.merge-develop-to-main.result == 'success' - }} - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - merge_commit_sha: ${{ steps.wait-merge.outputs.merge_commit_sha }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Wait for PR status checks and merge - id: wait-merge - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - PR_NUMBER="${{ needs.merge-develop-to-main.outputs.pr_number }}" - echo "Monitoring PR #$PR_NUMBER for status checks..." - - MAX_WAIT=${{ github.event.inputs.pr_check_timeout || 1800 }} - INITIAL_INTERVAL=10 - MAX_INTERVAL=300 # 5 minutes maximum - BACKOFF_MULTIPLIER=1.5 - SLEEP_INTERVAL=$INITIAL_INTERVAL - ELAPSED=0 - PR_STATE_RETRY_COUNT=0 - STATUS_CHECK_RETRY_COUNT=0 - MAX_RETRIES=3 - echo "Max wait time: ${MAX_WAIT}s, using exponential backoff (max interval: ${MAX_INTERVAL}s)" - - while [ $ELAPSED -lt $MAX_WAIT ]; do - # Get PR status with retry logic - if ! PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo ${{ github.repository }} 2>&1); then - PR_STATE_RETRY_COUNT=$((PR_STATE_RETRY_COUNT + 1)) - if [ $PR_STATE_RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "::error::Failed to fetch PR status after $MAX_RETRIES retries" - exit 1 - fi - echo "::warning::Failed to fetch PR status (attempt $PR_STATE_RETRY_COUNT/$MAX_RETRIES), retrying..." - sleep $((2 ** PR_STATE_RETRY_COUNT)) - continue - fi - PR_STATE_RETRY_COUNT=0 - - if [ "$PR_STATE" = "MERGED" ]; then - echo "✓ PR #$PR_NUMBER has been merged successfully!" - - # Get the merge commit SHA - MERGE_COMMIT_SHA=$(gh pr view "$PR_NUMBER" --json mergeCommit --jq '.mergeCommit.oid' --repo ${{ github.repository }}) - echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT - echo "Merge commit SHA: $MERGE_COMMIT_SHA" - exit 0 - fi - - if [ "$PR_STATE" = "CLOSED" ]; then - echo "::error::PR #$PR_NUMBER was closed without merging" - exit 1 - fi - - # Check status checks with retry logic - if ! STATUS_JSON=$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '.statusCheckRollup' --repo ${{ github.repository }} 2>&1); then - STATUS_CHECK_RETRY_COUNT=$((STATUS_CHECK_RETRY_COUNT + 1)) - if [ $STATUS_CHECK_RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "::error::Failed to fetch status checks after $MAX_RETRIES retries" - exit 1 - fi - echo "::warning::Failed to fetch status checks (attempt $STATUS_CHECK_RETRY_COUNT/$MAX_RETRIES), retrying..." - sleep $((2 ** STATUS_CHECK_RETRY_COUNT)) - continue - fi - STATUS_CHECK_RETRY_COUNT=0 - - # Count check states - TOTAL=$(echo "$STATUS_JSON" | jq 'length') - COMPLETED=$(echo "$STATUS_JSON" | jq '[.[] | select(.conclusion != null)] | length') - SUCCESS=$(echo "$STATUS_JSON" | jq '[.[] | select(.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED")] | length') - FAILED=$(echo "$STATUS_JSON" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT")] | length') - - echo "Status checks: $COMPLETED/$TOTAL completed, $SUCCESS passed, $FAILED failed (interval: ${SLEEP_INTERVAL}s)" - - # Check for failures - if [ "$FAILED" -gt 0 ]; then - echo "::error::Status checks failed for PR #$PR_NUMBER" - gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT") | "- " + .name + ": " + .conclusion' --repo ${{ github.repository }} - exit 1 - fi - - echo "Waiting for checks to complete... (${ELAPSED}s elapsed)" - sleep $SLEEP_INTERVAL - ELAPSED=$((ELAPSED + SLEEP_INTERVAL)) - - # Calculate next interval with exponential backoff (capped at MAX_INTERVAL) - NEXT_INTERVAL=$(awk "BEGIN {printf \"%.0f\", $SLEEP_INTERVAL * $BACKOFF_MULTIPLIER}") - if [ $NEXT_INTERVAL -gt $MAX_INTERVAL ]; then - SLEEP_INTERVAL=$MAX_INTERVAL - else - SLEEP_INTERVAL=$NEXT_INTERVAL - fi - done - - echo "::error::Timeout waiting for PR #$PR_NUMBER to merge" - exit 1 + status-checks: + uses: ./.github/workflows/status-checks.yml build-windows: - needs: [wait-for-main-pr] - if: | - ${{ - always() && - contains(github.event.inputs.jobs, 'build-windows') && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.wait-for-main-pr.result == 'success' - }} + needs: status-checks runs-on: windows-latest steps: - name: Checkout code @@ -628,15 +121,7 @@ jobs: dist/android-file-handler-windows.sha256 build-debian: - needs: [wait-for-main-pr] - if: | - ${{ - always() && - contains(github.event.inputs.jobs, 'build-debian') && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.wait-for-main-pr.result == 'success' - }} + needs: status-checks permissions: contents: read packages: read @@ -744,15 +229,7 @@ jobs: pkg_dist_debian/** build-arch: - needs: [wait-for-main-pr] - if: | - ${{ - always() && - contains(github.event.inputs.jobs, 'build-arch') && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.wait-for-main-pr.result == 'success' - }} + needs: status-checks permissions: contents: read packages: read @@ -878,15 +355,7 @@ jobs: build-rhel: - needs: [wait-for-main-pr] - if: | - ${{ - always() && - contains(github.event.inputs.jobs, 'build-rhel') && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.wait-for-main-pr.result == 'success' - }} + needs: status-checks permissions: contents: read packages: read @@ -991,96 +460,14 @@ jobs: dist/android-file-handler-rhel.sha256 pkg_dist_rhel/** - rollback-on-build-failure: - needs: [merge-develop-to-main, wait-for-main-pr, build-windows, build-debian, build-arch, build-rhel] - if: | - ${{ - always() && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.wait-for-main-pr.result == 'success' && - (needs.build-windows.result == 'failure' || - needs.build-debian.result == 'failure' || - needs.build-arch.result == 'failure' || - needs.build-rhel.result == 'failure') - }} - runs-on: ubuntu-latest - permissions: - contents: write - # Prevent multiple rollback operations from running concurrently - concurrency: - group: main-branch-rollback - cancel-in-progress: false - steps: - - name: Checkout main branch - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Rollback merge commit on build failure - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - echo "::error::One or more build jobs failed - initiating rollback" - - # Configure git - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Fetch latest - git fetch origin main - - # Get the previous main SHA (before the merge) - PREVIOUS_MAIN_SHA="${{ needs.merge-develop-to-main.outputs.previous_main_sha }}" - echo "Resetting main to previous commit: $PREVIOUS_MAIN_SHA" - - # Checkout main and reset to previous commit - git checkout main - git reset --hard "$PREVIOUS_MAIN_SHA" - - # Force push the rollback - if ! git push --force origin main; then - git notes -m 'release commit could not be rolled back' - echo "::error::Failed to push rollback to main" - exit 1 - fi - git notes -m 'release commit rolled back' - echo "::error::Reset main branch to commit $PREVIOUS_MAIN_SHA" - echo "::error::Build failures detected:" - - # Report which builds failed - if [ "${{ needs.build-windows.result }}" = "failure" ]; then - echo "::error:: - build-windows: FAILED" - fi - if [ "${{ needs.build-debian.result }}" = "failure" ]; then - echo "::error:: - build-debian: FAILED" - fi - if [ "${{ needs.build-arch.result }}" = "failure" ]; then - echo "::error:: - build-arch: FAILED" - fi - if [ "${{ needs.build-rhel.result }}" = "failure" ]; then - echo "::error:: - build-rhel: FAILED" - fi - - exit 1 - - + do-release: - needs: [rollback-on-build-failure, build-windows, build-debian, build-arch, build-rhel] - if: | - ${{ - always() && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.rollback-on-build-failure.result != 'failure' && - (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && - (needs.build-debian.result == 'success' || needs.build-debian.result == 'skipped') && - (needs.build-arch.result == 'success' || needs.build-arch.result == 'skipped') && - (needs.build-rhel.result == 'success' || needs.build-rhel.result == 'skipped') - }} + needs: + - status-checks + - build-windows + - build-debian + - build-arch + - build-rhel runs-on: ubuntu-latest steps: - name: Checkout code @@ -1146,19 +533,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} upload-s3: - needs: [rollback-on-build-failure, build-windows, build-debian, build-arch, build-rhel] - if: | - ${{ - always() && - (github.event.inputs.start_from_step == 'bump-version' || - github.event.inputs.start_from_step == 'merge-develop-to-main') && - needs.rollback-on-build-failure.result != 'failure' && - (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && - (needs.build-debian.result == 'success' || needs.build-debian.result == 'skipped') && - (needs.build-arch.result == 'success' || needs.build-arch.result == 'skipped') && - (needs.build-rhel.result == 'success' || needs.build-rhel.result == 'skipped') - }} runs-on: ubuntu-latest + needs: do-release + if: needs.do-release.result == 'success' steps: - name: Checkout code uses: actions/checkout@v4 @@ -1195,7 +572,7 @@ jobs: sync-wiki: needs: do-release - if: always() && needs.do-release.result == 'success' + if: needs.do-release.result == 'success' uses: ./.github/workflows/sync-wiki.yml with: branch: main diff --git a/.github/workflows/status-checks.yml b/.github/workflows/status-checks.yml index 53653cd..f55c731 100644 --- a/.github/workflows/status-checks.yml +++ b/.github/workflows/status-checks.yml @@ -5,6 +5,7 @@ name: Status Checks on: pull_request: branches: [ main, develop ] + workflow_call: permissions: contents: read -- 2.47.3 From 47d22e45565b8fef99a52085d4436498609ae51d Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 17 Oct 2025 13:27:45 -0500 Subject: [PATCH 3/6] make release workflow usable --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 153f7cd..9d795e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,9 @@ on: workflow_dispatch: permissions: - contents: write - pull-requests: write + contents: read + packages: read + # Prevent multiple release workflows from running simultaneously # This is critical to prevent concurrent rollbacks -- 2.47.3 From 94351ddb532b7e6ea93984ba49276a26da1e87e5 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 17 Oct 2025 13:31:55 -0500 Subject: [PATCH 4/6] remove unecessary permissions from status check workflow --- .github/workflows/status-checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/status-checks.yml b/.github/workflows/status-checks.yml index f55c731..33341cc 100644 --- a/.github/workflows/status-checks.yml +++ b/.github/workflows/status-checks.yml @@ -10,7 +10,6 @@ on: permissions: contents: read packages: read - pull-requests: write env: FPM_VERSION: "1.16.0" -- 2.47.3 From b7aa6a08987959cee5a13eea1548a7a14e818ef7 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 17 Oct 2025 13:38:11 -0500 Subject: [PATCH 5/6] fix sync wiki --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d795e8..1f7f359 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -574,6 +574,9 @@ jobs: sync-wiki: needs: do-release if: needs.do-release.result == 'success' + permissions: + contents: write + pull-requests: write uses: ./.github/workflows/sync-wiki.yml with: branch: main -- 2.47.3 From ea9d9ea83597a7aee4073523d5bfd2dd99804630 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 17 Oct 2025 14:02:57 -0500 Subject: [PATCH 6/6] fixing workflow permissions --- .github/workflows/release.yml | 4 ++++ .github/workflows/status-checks.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f7f359..7f1c294 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,10 @@ env: jobs: status-checks: + permissions: + contents: read + packages: read + uses: ./.github/workflows/status-checks.yml build-windows: diff --git a/.github/workflows/status-checks.yml b/.github/workflows/status-checks.yml index 33341cc..9b34ffa 100644 --- a/.github/workflows/status-checks.yml +++ b/.github/workflows/status-checks.yml @@ -42,6 +42,7 @@ jobs: permissions: contents: read packages: read + container: image: ghcr.io/jmr-dev/android-file-handler-arch-builder:latest credentials: -- 2.47.3