From 51f09cdadef7bbd87435c1614cc0d172e2e361a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Thu, 11 Jun 2026 10:13:58 +0200 Subject: [PATCH] Fix prerelease version-bump job: add PR fallback for protected branches (#2224) * Fix prerelease version bump workflow * Fix prerelease version-bump job: add PR fallback for protected branches ### Motivation - The prerelease version bump job was failing when it could not push to protected release branches, causing the workflow to error instead of progressing. - The job needs permission and robust push logic so prerelease automation can update `.sure-version` and `charts/sure/Chart.yaml` even when direct pushes are blocked. ### Description - Grant the `bump-pre_release-version` job `pull-requests: write` permission so it may open an automated PR when direct pushes are disallowed. - Harden the bump step: enable `set -euo pipefail`, pass `GH_TOKEN` into the job environment, and use fully-qualified branch refs for pushes. - Add retry + rebase logic for direct pushes and a fallback path that pushes the changes to an `automation/bump-version-after-...` branch and opens a PR with `gh pr create` when direct push attempts fail. - Preserve existing validations that ensure `.sure-version` and `charts/sure/Chart.yaml` exist and the prerelease portion is parsed before writing the bump. ### Testing - Verified workflow YAML parses with `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/publish.yml"); puts "YAML OK"'` and it succeeded. - Confirmed the bump job has the expected permission with `ruby -e 'require "yaml"; workflow=YAML.load_file(".github/workflows/publish.yml"); abort("missing bump job") unless workflow.dig("jobs", "bump-pre_release-version", "permissions", "pull-requests") == "write"; puts "bump permissions OK"'` and it succeeded. - Ran ActionLint via `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.8 .github/workflows/publish.yml` and received no actionable errors. - Ran `git diff --check` to ensure no whitespace/format issues and it succeeded. * Harden pre-release bump job: validation, retries and PR fallback ### Motivation - Make the `bump-pre_release-version` job more robust by validating prerelease version formats and failing early on unexpected inputs. - Handle protected branches gracefully by attempting direct pushes with retries and falling back to creating a pull request when direct push is blocked. - Ensure the workflow has the permissions required to open PRs when needed. ### Description - Added `pull-requests: write` to the job `permissions` so the workflow can create PRs when required. - Hardened the bump script with `set -euo pipefail` and a strict regex that validates and extracts `BASE_VERSION`, prerelease tag, and number using `BASH_REMATCH` before incrementing the prerelease counter. - Simplified and made file reads safer by using input redirection for `tr` and improved error messages for missing or malformed version files. - Reworked commit/push logic to set a commit message variable, attempt direct pushes with exponential backoff and detection of branch-protection errors, and when rejected create a dedicated bump branch and open a PR via `gh pr create`. ### Testing - Validated the modified workflow YAML with a YAML linter; no syntax errors were reported. - Executed the updated bump logic in a CI dry-run (workflow syntax validation on GitHub Actions) and the workflow file was accepted by the Actions syntax checks. * Add manual workflow triggers and robust pre-release bump with protected-branch fallback ### Motivation - Allow manual invocation of PR and chart CI workflows via `workflow_dispatch` for on-demand runs. - Make the pre-release version bump process more robust and reliable when `refs/tags/v*` releases are published. - Ensure the bump can proceed even when the target branch is protected by creating an automated pull request and running PR checks. ### Description - Added `workflow_dispatch` to `.github/workflows/pr.yml` and `.github/workflows/chart-ci.yml` to support manual runs. - Extended `bump-pre_release-version` job in `.github/workflows/publish.yml` to request `actions: write` and `pull-requests: write` permissions. - Rewrote the bump script to use `set -euo pipefail` and a single regex validation to parse and increment prerelease versions (e.g. `1.2.3-alpha.4`), update `.sure-version`, and update `charts/sure/Chart.yaml` reliably. - Improved commit and push flow by adding `GH_TOKEN`, using a consistent commit message variable, attempting direct pushes with retry and rebase, detecting branch-protection failures, and falling back to creating a bumped branch and an automated PR via `gh pr create`; the workflow also dispatches `pr.yml` and `chart-ci.yml` runs for the created branch. ### Testing - No automated tests were run as part of this change; behavior will be exercised when the workflow creates a bump PR or when workflows are manually triggered via the new `workflow_dispatch` events. --- .github/workflows/chart-ci.yml | 1 + .github/workflows/pr.yml | 1 + .github/workflows/publish.yml | 189 +++++++++++++++++++++------------ 3 files changed, 121 insertions(+), 70 deletions(-) diff --git a/.github/workflows/chart-ci.yml b/.github/workflows/chart-ci.yml index 8ca9111d4..a80af679f 100644 --- a/.github/workflows/chart-ci.yml +++ b/.github/workflows/chart-ci.yml @@ -1,6 +1,7 @@ name: Chart CI on: + workflow_dispatch: pull_request: paths: - 'charts/**' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index edd1e2f6e..5c9e4fd50 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,6 +1,7 @@ name: Pull Request on: + workflow_dispatch: pull_request: types: [opened, synchronize, reopened, labeled] paths-ignore: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9a30609b0..32c1e617d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -199,6 +199,7 @@ jobs: env: TAGS: ${{ needs.build.outputs.tags }} DIGESTS_DIR: ${{ runner.temp }}/digests + REF_NAME: ${{ github.ref_name }} shell: bash -xeuo pipefail {0} run: | tag_args=() @@ -216,7 +217,7 @@ jobs: "index:org.opencontainers.image.created=$(date -Iseconds)" 'index:org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}' 'index:org.opencontainers.image.revision=${{ github.sha }}' - 'index:org.opencontainers.image.ref.name=${{ github.ref_name }}' + "index:org.opencontainers.image.ref.name=${REF_NAME}" 'index:org.opencontainers.image.vendor=we-promise' 'index:org.opencontainers.image.licenses=AGPL-3.0' 'index:org.opencontainers.image.title=Sure' @@ -294,6 +295,8 @@ jobs: path: ${{ runner.temp }}/helm-artifacts - name: Prepare release assets + env: + REF_NAME: ${{ github.ref_name }} run: | mkdir -p ${{ runner.temp }}/release-assets @@ -306,13 +309,13 @@ jobs: # Copy debug APK if it exists if [ -f "${{ runner.temp }}/mobile-artifacts/app-debug.apk" ]; then - cp "${{ runner.temp }}/mobile-artifacts/app-debug.apk" "${{ runner.temp }}/release-assets/sure-${{ github.ref_name }}-debug.apk" + cp "${{ runner.temp }}/mobile-artifacts/app-debug.apk" "${{ runner.temp }}/release-assets/sure-${REF_NAME}-debug.apk" echo "✓ Debug APK prepared" fi # Copy release APK if it exists if [ -f "${{ runner.temp }}/mobile-artifacts/app-release.apk" ]; then - cp "${{ runner.temp }}/mobile-artifacts/app-release.apk" "${{ runner.temp }}/release-assets/sure-${{ github.ref_name }}.apk" + cp "${{ runner.temp }}/mobile-artifacts/app-release.apk" "${{ runner.temp }}/release-assets/sure-${REF_NAME}.apk" echo "✓ Release APK prepared" fi @@ -320,7 +323,7 @@ jobs: # Path preserves directory structure from artifact upload if [ -d "${{ runner.temp }}/ios-build/ios/iphoneos/Runner.app" ]; then cd "${{ runner.temp }}/ios-build/ios/iphoneos" - zip -r "${{ runner.temp }}/release-assets/sure-${{ github.ref_name }}-ios-unsigned.zip" Runner.app + zip -r "${{ runner.temp }}/release-assets/sure-${REF_NAME}-ios-unsigned.zip" Runner.app echo "✓ iOS build archive prepared" fi @@ -398,7 +401,9 @@ jobs: timeout-minutes: 10 permissions: + actions: write contents: write + pull-requests: write steps: - name: Determine source branch for tag @@ -433,71 +438,66 @@ jobs: - name: Bump pre-release version run: | - VERSION_FILE=".sure-version" - CHART_FILE="charts/sure/Chart.yaml" + set -euo pipefail - # Ensure version file exists - if [ ! -f "$VERSION_FILE" ]; then - echo "ERROR: Version file not found: $VERSION_FILE" - exit 1 - fi + VERSION_FILE=".sure-version" + CHART_FILE="charts/sure/Chart.yaml" - # Ensure chart file exists - if [ ! -f "$CHART_FILE" ]; then - echo "ERROR: Chart file not found: $CHART_FILE" - exit 1 - fi - - # Extract current version - CURRENT_VERSION=$(cat "$VERSION_FILE" | tr -d '[:space:]') - if [ -z "$CURRENT_VERSION" ]; then - echo "ERROR: Could not extract version from $VERSION_FILE" - exit 1 - fi - echo "Current version: $CURRENT_VERSION" + # Ensure version file exists + if [ ! -f "$VERSION_FILE" ]; then + echo "ERROR: Version file not found: $VERSION_FILE" + exit 1 + fi - # Extract the pre-release tag and number, then increment it - PRE_RELEASE_TAG=$(echo "$CURRENT_VERSION" | grep -oP '(alpha|beta|rc)') - if [ -z "$PRE_RELEASE_TAG" ]; then - echo "ERROR: Could not extract pre-release tag from $CURRENT_VERSION" - exit 1 - fi + # Ensure chart file exists + if [ ! -f "$CHART_FILE" ]; then + echo "ERROR: Chart file not found: $CHART_FILE" + exit 1 + fi - PRE_RELEASE_NUM=$(echo "$CURRENT_VERSION" | grep -oP '(alpha|beta|rc)\.\K[0-9]+') - if [ -z "$PRE_RELEASE_NUM" ]; then - echo "ERROR: Could not extract pre-release number from $CURRENT_VERSION" - exit 1 - fi - NEW_PRE_RELEASE_NUM=$((PRE_RELEASE_NUM + 1)) - - # Create new version string - BASE_VERSION=$(echo "$CURRENT_VERSION" | grep -oP '^[0-9]+\.[0-9]+\.[0-9]+') - if [ -z "$BASE_VERSION" ]; then - echo "ERROR: Could not extract base version from $CURRENT_VERSION" - exit 1 - fi - NEW_VERSION="${BASE_VERSION}-${PRE_RELEASE_TAG}.${NEW_PRE_RELEASE_NUM}" - echo "New version: $NEW_VERSION" + # Extract current version + CURRENT_VERSION=$(tr -d '[:space:]' < "$VERSION_FILE") + if [ -z "$CURRENT_VERSION" ]; then + echo "ERROR: Could not extract version from $VERSION_FILE" + exit 1 + fi + echo "Current version: $CURRENT_VERSION" - # Update the version file - echo "$NEW_VERSION" > "$VERSION_FILE" + if [[ ! "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(alpha|beta|rc)\.([0-9]+)$ ]]; then + echo "ERROR: Expected prerelease version like 1.2.3-alpha.4, got $CURRENT_VERSION" + exit 1 + fi - # Verify the change - echo "Updated .sure-version:" - cat "$VERSION_FILE" + BASE_VERSION="${BASH_REMATCH[1]}" + PRE_RELEASE_TAG="${BASH_REMATCH[2]}" + PRE_RELEASE_NUM="${BASH_REMATCH[3]}" + NEW_PRE_RELEASE_NUM=$((PRE_RELEASE_NUM + 1)) + NEW_VERSION="${BASE_VERSION}-${PRE_RELEASE_TAG}.${NEW_PRE_RELEASE_NUM}" + echo "New version: $NEW_VERSION" - # Update Helm chart version and appVersion - sed -i -E "s/^version: .*/version: ${NEW_VERSION}/" "$CHART_FILE" - sed -i -E "s/^appVersion: .*/appVersion: \"${NEW_VERSION}\"/" "$CHART_FILE" + # Update the version file + echo "$NEW_VERSION" > "$VERSION_FILE" - # Verify the change - echo "Updated Chart.yaml:" - grep -E "^(version|appVersion):" "$CHART_FILE" + # Verify the change + echo "Updated .sure-version:" + cat "$VERSION_FILE" + + # Update Helm chart version and appVersion + sed -i -E "s/^version: .*/version: ${NEW_VERSION}/" "$CHART_FILE" + sed -i -E "s/^appVersion: .*/appVersion: \"${NEW_VERSION}\"/" "$CHART_FILE" + + # Verify the change + echo "Updated Chart.yaml:" + grep -E "^(version|appVersion):" "$CHART_FILE" - name: Commit and push version bump env: SOURCE_BRANCH: ${{ steps.source_branch.outputs.branch }} + GH_TOKEN: ${{ github.token }} + REF_NAME: ${{ github.ref_name }} run: | + set -euo pipefail + git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -510,20 +510,69 @@ jobs: exit 0 fi - git commit -m "Bump version to next iteration after ${{ github.ref_name }} release" + COMMIT_MESSAGE="Bump version to next iteration after ${REF_NAME} release" + git commit -m "$COMMIT_MESSAGE" echo "Pushing to branch: $SOURCE_BRANCH" - # Push with retry logic - attempts=0 - until git push origin HEAD:$SOURCE_BRANCH; do - attempts=$((attempts + 1)) - if [[ $attempts -ge 4 ]]; then - echo "ERROR: Failed to push after 4 attempts." >&2 - exit 1 - fi - delay=$((2 ** attempts)) - echo "Push failed (attempt $attempts). Retrying in ${delay} seconds..." - sleep ${delay} - git pull --rebase origin $SOURCE_BRANCH - done + # Push directly when allowed. Main is protected and must be updated + # through a pull request. Other branches get a direct-push attempt, but + # protected-branch rejections fall back to a pull request immediately. + push_succeeded=false + if [[ "$SOURCE_BRANCH" == "main" || "$SOURCE_BRANCH" == "master" ]]; then + echo "$SOURCE_BRANCH is protected; creating a pull request instead of pushing directly." + else + direct_push_output="" + for attempt in 1 2 3 4; do + set +e + direct_push_output=$(git push origin HEAD:"refs/heads/${SOURCE_BRANCH}" 2>&1) + push_status=$? + set -e + + echo "$direct_push_output" + + if [[ $push_status -eq 0 ]]; then + push_succeeded=true + break + fi + + if grep -Eq 'GH006|Protected branch update failed|Changes must be made through a pull request' <<< "$direct_push_output"; then + echo "Direct push is blocked by branch protection; creating a pull request instead." + break + fi + + if [[ $attempt -eq 4 ]]; then + echo "Direct push failed after 4 attempts; creating a pull request instead." + break + fi + + delay=$((2 ** attempt)) + echo "Push failed (attempt $attempt). Retrying in ${delay} seconds..." + sleep "$delay" + git fetch origin "${SOURCE_BRANCH}" + git rebase "origin/${SOURCE_BRANCH}" + done + fi + + if [[ "$push_succeeded" == "true" ]]; then + exit 0 + fi + + SAFE_TAG=$(printf '%s' "${REF_NAME}" | tr -c '[:alnum:]._-' '-') + BUMP_BRANCH="automation/bump-version-after-${SAFE_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + + git push --force-with-lease origin HEAD:"refs/heads/${BUMP_BRANCH}" + + PR_BODY="This automated PR bumps .sure-version and charts/sure/Chart.yaml after the ${REF_NAME} pre-release. It was opened because the workflow could not push directly to ${SOURCE_BRANCH}." + + PR_URL=$(gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --head "$BUMP_BRANCH" \ + --base "$SOURCE_BRANCH" \ + --title "$COMMIT_MESSAGE" \ + --body "$PR_BODY") + + echo "Created version bump PR: $PR_URL" + echo "Dispatching PR checks for $BUMP_BRANCH" + gh workflow run pr.yml --repo "$GITHUB_REPOSITORY" --ref "$BUMP_BRANCH" + gh workflow run chart-ci.yml --repo "$GITHUB_REPOSITORY" --ref "$BUMP_BRANCH"