From 5cdb0cfd6dc6670689f26e1a8cf3ce0589d7cdc2 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Sat, 10 Oct 2026 14:13:20 -0700 Subject: [PATCH 1/7] chore: flutter version bump workflow --- .github/workflows/update_flutter_version.yml | 293 +++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 .github/workflows/update_flutter_version.yml diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml new file mode 100644 index 00000000..0032ac5d --- /dev/null +++ b/.github/workflows/update_flutter_version.yml @@ -0,0 +1,293 @@ +name: Update Flutter Version + +# Manual Flutter bump. Leave "flutter version" empty to use the newest stable +# tag linked from https://github.com/flutter/flutter/blob/master/CHANGELOG.md. +on: + workflow_dispatch: + inputs: + flutter_version: + description: flutter version (leave empty for the latest changelog tag) + required: false + type: string + default: "" + +permissions: + contents: write + pull-requests: write + +concurrency: + group: update-flutter-version + cancel-in-progress: false + +jobs: + update: + name: Update Flutter version + runs-on: macos-latest + timeout-minutes: 60 + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile + + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + submodules: true + + - name: Resolve Flutter version + id: flutter_version + env: + FLUTTER_VERSION_INPUT: ${{ inputs.flutter_version }} + run: | + set -euo pipefail + input="$(printf '%s' "${FLUTTER_VERSION_INPUT}" | tr -d '[:space:]')" + input="${input#v}" + input="${input#V}" + + if [ -n "${input}" ]; then + version="${input}" + else + changelog="$(curl -fsSL --retry 3 https://raw.githubusercontent.com/flutter/flutter/master/CHANGELOG.md)" + version="$(printf '%s\n' "${changelog}" | grep -oE 'releases/tag/[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 | sed 's|.*/||' || true)" + if [ -z "${version}" ]; then + version="$(printf '%s\n' "${changelog}" | grep -oE '^### \[[0-9]+\.[0-9]+\.[0-9]+\]' | head -n 1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)" + fi + fi + + if ! [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid Flutter version: '${version:-}'" >&2 + exit 1 + fi + + echo "version=${version}" >> "${GITHUB_OUTPUT}" + echo "Using Flutter ${version}" + echo "### Flutter ${version}" >> "${GITHUB_STEP_SUMMARY}" + + - name: Point submodules/flutter at the tag + env: + FLUTTER_VERSION: ${{ steps.flutter_version.outputs.version }} + run: | + set -euo pipefail + # Drop the checkout credential header so this public fetch is anonymous. + git -C submodules/flutter -c http.extraheader= fetch --depth 1 origin tag "${FLUTTER_VERSION}" + git -C submodules/flutter checkout --detach "${FLUTTER_VERSION}" + tagged="$(git -C submodules/flutter rev-parse "refs/tags/${FLUTTER_VERSION}^{}")" + head="$(git -C submodules/flutter rev-parse HEAD)" + if [ "${tagged}" != "${head}" ]; then + echo "submodules/flutter HEAD ${head} does not match tag ${FLUTTER_VERSION} (${tagged})" >&2 + exit 1 + fi + submodules/flutter/bin/flutter --version + + - name: Update pubspec.yaml + env: + FLUTTER_VERSION: ${{ steps.flutter_version.outputs.version }} + run: | + set -euo pipefail + python3 - << 'PY' + import os + import re + import sys + from pathlib import Path + + version = os.environ["FLUTTER_VERSION"] + path = Path("pubspec.yaml") + text = path.read_text() + pattern = re.compile( + "^(\\s*flutter:\\s*[\"'])(\\d+\\.\\d+\\.\\d+)([\"'])\\s*$", + re.MULTILINE, + ) + updated, count = pattern.subn(rf"\g<1>{version}\3", text, count=1) + if count != 1: + sys.exit( + f"expected to update 1 flutter pin in pubspec.yaml, updated {count}" + ) + path.write_text(updated) + PY + + - name: Flutter pub get + run: submodules/flutter/bin/flutter pub get + + - name: Set Xcode version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + working-directory: ios + bundler-cache: true + + - name: Expose CocoaPods from the iOS Gemfile + run: | + set -euo pipefail + mkdir -p "${RUNNER_TEMP}/bin" + cat > "${RUNNER_TEMP}/bin/pod" << 'EOF' + #!/bin/bash + exec bundle exec pod "$@" + EOF + chmod +x "${RUNNER_TEMP}/bin/pod" + echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" + + - name: Configure iOS packages + run: | + set -euo pipefail + pod --version + submodules/flutter/bin/flutter build ios --config-only + + - name: Open pull request + env: + FLUTTER_VERSION: ${{ steps.flutter_version.outputs.version }} + BASE_BRANCH: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + if [ "${REF_TYPE}" != "branch" ]; then + echo "Run this workflow from a branch, not a tag." >&2 + exit 1 + fi + + git add -A -- pubspec.yaml pubspec.lock submodules/flutter ios + git restore --staged -- ios/.bundle ios/vendor ios/bin 2>/dev/null || true + + mode="$(git ls-files -s -- submodules/flutter | awk '{print $1}')" + if [ "${mode}" != "160000" ]; then + echo "submodules/flutter was not staged as a gitlink (mode=${mode})" >&2 + exit 1 + fi + + if git diff --cached --quiet; then + echo "Flutter ${FLUTTER_VERSION} did not change any tracked files." + exit 0 + fi + + git diff --cached --stat + + export GIT_AUTHOR_NAME="github-actions[bot]" + export GIT_AUTHOR_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" + export GIT_COMMITTER_NAME="${GIT_AUTHOR_NAME}" + export GIT_COMMITTER_EMAIL="${GIT_AUTHOR_EMAIL}" + + git commit -m "$(cat < Date: Sat, 10 Oct 2026 14:16:30 -0700 Subject: [PATCH 2/7] update --- .github/workflows/update_flutter_version.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml index 0032ac5d..eb2e5ecb 100644 --- a/.github/workflows/update_flutter_version.yml +++ b/.github/workflows/update_flutter_version.yml @@ -285,9 +285,5 @@ jobs: - Refresh \`pubspec.lock\` with \`flutter pub get\`. - Refresh tracked iOS package files with \`flutter build ios --config-only\`. - ## Test plan - - [ ] CI passes on this pull request - - [ ] The iOS project still configures and builds - EOF )" From d746eff935eb26823f6d597ad73ad78d94b8d135 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Sat, 10 Oct 2026 14:19:58 -0700 Subject: [PATCH 3/7] update --- .github/workflows/update_flutter_version.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml index eb2e5ecb..6bab1162 100644 --- a/.github/workflows/update_flutter_version.yml +++ b/.github/workflows/update_flutter_version.yml @@ -277,7 +277,7 @@ jobs: gh pr create \ --base "${BASE_BRANCH}" \ --head "${branch}" \ - --title "chore: bump Flutter version to ${FLUTTER_VERSION}" \ + --title "chore(flutter): bump Flutter version to ${FLUTTER_VERSION}" \ --body "$(cat < Date: Sat, 10 Oct 2026 15:00:17 -0700 Subject: [PATCH 4/7] update --- .github/workflows/update_flutter_version.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml index 6bab1162..1892113f 100644 --- a/.github/workflows/update_flutter_version.yml +++ b/.github/workflows/update_flutter_version.yml @@ -115,7 +115,6 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - working-directory: ios bundler-cache: true - name: Expose CocoaPods from the iOS Gemfile From 8ff3f36cb451973be22c89b97d3f22f40ec99c56 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Sat, 10 Oct 2026 15:12:31 -0700 Subject: [PATCH 5/7] update --- .github/workflows/update_flutter_version.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml index 1892113f..d3fcf9a4 100644 --- a/.github/workflows/update_flutter_version.yml +++ b/.github/workflows/update_flutter_version.yml @@ -147,8 +147,7 @@ jobs: exit 1 fi - git add -A -- pubspec.yaml pubspec.lock submodules/flutter ios - git restore --staged -- ios/.bundle ios/vendor ios/bin 2>/dev/null || true + git add -A -- pubspec.yaml pubspec.lock submodules/flutter ios/Podfile ios/Podfile.lock mode="$(git ls-files -s -- submodules/flutter | awk '{print $1}')" if [ "${mode}" != "160000" ]; then @@ -282,7 +281,7 @@ jobs: - Pin Flutter \`${FLUTTER_VERSION}\` in \`pubspec.yaml\`. - Point \`submodules/flutter\` at tag \`${FLUTTER_VERSION}\`. - Refresh \`pubspec.lock\` with \`flutter pub get\`. - - Refresh tracked iOS package files with \`flutter build ios --config-only\`. + - Refresh \`ios/Podfile\` and \`ios/Podfile.lock\` with \`flutter build ios --config-only\`. EOF )" From bc709eb450906a37a575f3a6adfd90a12c868b5f Mon Sep 17 00:00:00 2001 From: Livinglist Date: Sat, 10 Oct 2026 15:24:04 -0700 Subject: [PATCH 6/7] update --- .github/workflows/update_flutter_version.yml | 58 +++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml index d3fcf9a4..b3347b88 100644 --- a/.github/workflows/update_flutter_version.yml +++ b/.github/workflows/update_flutter_version.yml @@ -1,8 +1,12 @@ name: Update Flutter Version -# Manual Flutter bump. Leave "flutter version" empty to use the newest stable -# tag linked from https://github.com/flutter/flutter/blob/master/CHANGELOG.md. +# Bump Flutter to the newest stable tag in +# https://github.com/flutter/flutter/blob/master/CHANGELOG.md. +# Runs every day at 15:00 UTC, and can also be started by hand. +# Leave "flutter version" empty to use that changelog tag. on: + schedule: + - cron: "0 15 * * *" workflow_dispatch: inputs: flutter_version: @@ -20,8 +24,58 @@ concurrency: cancel-in-progress: false jobs: + check: + name: Check Flutter version + runs-on: ubuntu-latest + outputs: + up_to_date: ${{ steps.compare.outputs.up_to_date }} + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Compare pubspec.yaml with the Flutter changelog + id: compare + run: | + set -euo pipefail + changelog="$(curl -fsSL --retry 3 https://raw.githubusercontent.com/flutter/flutter/master/CHANGELOG.md)" + changelog_version="$(printf '%s\n' "${changelog}" | grep -oE 'releases/tag/[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 | sed 's|.*/||' || true)" + if [ -z "${changelog_version}" ]; then + changelog_version="$(printf '%s\n' "${changelog}" | grep -oE '^### \[[0-9]+\.[0-9]+\.[0-9]+\]' | head -n 1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)" + fi + if ! [[ "${changelog_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Could not read a Flutter version from the changelog." >&2 + exit 1 + fi + + pubspec_version="$(python3 - << 'PY' + import re + import sys + from pathlib import Path + + text = Path("pubspec.yaml").read_text() + pattern = re.compile( + "^(\\s*flutter:\\s*[\"'])(\\d+\\.\\d+\\.\\d+)([\"'])\\s*$", + re.MULTILINE, + ) + match = pattern.search(text) + if match is None: + sys.exit("flutter pin not found in pubspec.yaml") + print(match.group(2)) + PY + )" + + echo "pubspec.yaml pins Flutter ${pubspec_version}; changelog latest is ${changelog_version}." + if [ "${pubspec_version}" = "${changelog_version}" ]; then + echo "up_to_date=true" >> "${GITHUB_OUTPUT}" + echo "Flutter ${pubspec_version} already matches the latest changelog tag." | tee -a "${GITHUB_STEP_SUMMARY}" + else + echo "up_to_date=false" >> "${GITHUB_OUTPUT}" + fi + update: name: Update Flutter version + needs: check + if: needs.check.outputs.up_to_date != 'true' runs-on: macos-latest timeout-minutes: 60 env: From 813be035dd66344491fbafedfc33b950feb12dd4 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Sat, 10 Oct 2026 15:28:37 -0700 Subject: [PATCH 7/7] update --- .github/workflows/update_flutter_version.yml | 53 +++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/update_flutter_version.yml b/.github/workflows/update_flutter_version.yml index b3347b88..a2451100 100644 --- a/.github/workflows/update_flutter_version.yml +++ b/.github/workflows/update_flutter_version.yml @@ -35,6 +35,8 @@ jobs: - name: Compare pubspec.yaml with the Flutter changelog id: compare + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail changelog="$(curl -fsSL --retry 3 https://raw.githubusercontent.com/flutter/flutter/master/CHANGELOG.md)" @@ -68,6 +70,40 @@ jobs: if [ "${pubspec_version}" = "${changelog_version}" ]; then echo "up_to_date=true" >> "${GITHUB_OUTPUT}" echo "Flutter ${pubspec_version} already matches the latest changelog tag." | tee -a "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + existing_pr="$(CHANGELOG_VERSION="${changelog_version}" python3 - << 'PY' + import json + import os + import re + import subprocess + + target = os.environ["CHANGELOG_VERSION"] + pattern = re.compile( + r"^chore(?:\(flutter\))?: bump Flutter version to (\d+\.\d+\.\d+)$" + ) + prs = json.loads( + subprocess.check_output( + [ + "gh", "pr", "list", + "--state", "open", + "--limit", "100", + "--json", "number,title", + ], + text=True, + ) + ) + for pr in prs: + match = pattern.match(pr["title"].strip()) + if match and match.group(1) == target: + print(pr["number"]) + break + PY + )" + if [ -n "${existing_pr}" ]; then + echo "up_to_date=true" >> "${GITHUB_OUTPUT}" + echo "Pull request #${existing_pr} already bumps Flutter to ${changelog_version}." | tee -a "${GITHUB_STEP_SUMMARY}" else echo "up_to_date=false" >> "${GITHUB_OUTPUT}" fi @@ -227,27 +263,28 @@ jobs: EOF )" - branch="chore/flutter-${FLUTTER_VERSION}" + pacific_date="$(TZ=America/Los_Angeles date +%m%d%Y)" + branch="chore/flutter-${FLUTTER_VERSION}-${pacific_date}" action_file="${RUNNER_TEMP}/flutter_pr_action" ACTION_FILE="${action_file}" BRANCH="${branch}" python3 - << 'PY' import json import os + import re import subprocess import sys target_text = os.environ["FLUTTER_VERSION"] target = tuple(int(part) for part in target_text.split(".")) preferred_branch = os.environ["BRANCH"] - prefix = "chore: bump Flutter version to " + title_pattern = re.compile( + r"^chore(?:\(flutter\))?: bump Flutter version to (\d+\.\d+\.\d+)$" + ) def version_of(title): - if not title.startswith(prefix): - return None - raw = title[len(prefix):].strip() - parts = raw.split(".") - if len(parts) != 3 or not all(part.isdigit() for part in parts): + match = title_pattern.match(title.strip()) + if match is None: return None - return tuple(int(part) for part in parts) + return tuple(int(part) for part in match.group(1).split(".")) prs = json.loads( subprocess.check_output(