From 0fea7bf446264a9073f789cc76bb18ab74da0d1a Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Sun, 24 May 2026 10:56:46 +0800 Subject: [PATCH 1/7] ci: update unsigned iOS build workflow --- .../resolve-flutter-build-metadata/action.yml | 139 ++++++++++++++++++ .github/actions/setup-flutter/action.yml | 23 +++ .github/workflows/ios-build.yml | 109 ++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 .github/actions/resolve-flutter-build-metadata/action.yml create mode 100644 .github/actions/setup-flutter/action.yml create mode 100644 .github/workflows/ios-build.yml diff --git a/.github/actions/resolve-flutter-build-metadata/action.yml b/.github/actions/resolve-flutter-build-metadata/action.yml new file mode 100644 index 0000000..ded4f8c --- /dev/null +++ b/.github/actions/resolve-flutter-build-metadata/action.yml @@ -0,0 +1,139 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-action.json +name: Resolve Flutter build metadata +description: Resolve build name, build number, release tag metadata, and artifact naming. + +inputs: + platform: + description: Platform name used for platform-specific release tags. + required: true + source-ref: + description: Optional branch, tag, or commit SHA selected from workflow_dispatch. + required: false + default: '' + upload-artifacts: + description: Whether manual workflow_dispatch runs should upload artifacts. + required: false + default: 'false' + +outputs: + build_name: + description: Flutter build name. + value: ${{ steps.metadata.outputs.build_name }} + build_number: + description: Flutter build number. + value: ${{ steps.metadata.outputs.build_number }} + artifact_ref_name: + description: Sanitized ref name for artifact names. + value: ${{ steps.metadata.outputs.artifact_ref_name }} + is_tag_build: + description: Whether the current build resolved to a release tag. + value: ${{ steps.metadata.outputs.is_tag_build }} + upload_artifact: + description: Whether artifacts should be uploaded. + value: ${{ steps.metadata.outputs.upload_artifact }} + +runs: + using: composite + steps: + - name: Resolve metadata + id: metadata + shell: bash + env: + PLATFORM: ${{ inputs.platform }} + SOURCE_REF_INPUT: ${{ inputs.source-ref }} + MANUAL_UPLOAD_ARTIFACTS: ${{ inputs.upload-artifacts }} + run: | + set -euo pipefail + + is_tag_build=false + tag_name="" + + if [[ "${GITHUB_REF_TYPE}" == "tag" && -z "${SOURCE_REF_INPUT}" ]]; then + is_tag_build=true + tag_name="${GITHUB_REF_NAME}" + elif [[ -n "${SOURCE_REF_INPUT}" ]]; then + candidate="${SOURCE_REF_INPUT#refs/tags/}" + if git show-ref --verify --quiet "refs/tags/${candidate}"; then + is_tag_build=true + tag_name="${candidate}" + fi + fi + + if [[ "${is_tag_build}" == "true" ]]; then + if [[ "${tag_name}" =~ ^${PLATFORM}-v([0-9]+)\.([0-9]+)\.([0-9]+)(\+([0-9]+))?$ ]]; then + build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" + build_number="${BASH_REMATCH[5]:-${GITHUB_RUN_NUMBER}}" + elif [[ "${tag_name}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(\+([0-9]+))?$ ]]; then + build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" + build_number="${BASH_REMATCH[5]:-${GITHUB_RUN_NUMBER}}" + else + echo "::error::Invalid ${PLATFORM} release tag '${tag_name}'. Expected vX.Y.Z, vX.Y.Z+B, ${PLATFORM}-vX.Y.Z, or ${PLATFORM}-vX.Y.Z+B." + exit 1 + fi + + tag_commit="$(git rev-list -n 1 "${tag_name}")" + git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' || true + git fetch --no-tags origin '+refs/heads/master:refs/remotes/origin/master' || true + + reachable=false + for branch in origin/main origin/master; do + if git show-ref --verify --quiet "refs/remotes/${branch}" && + git merge-base --is-ancestor "${tag_commit}" "${branch}"; then + reachable=true + break + fi + done + + if [[ "${reachable}" != "true" ]]; then + echo "::error::Release tag must point to a commit reachable from main or master." + exit 1 + fi + + ref_name="${tag_name}" + else + pubspec_version="$(awk '/^version:/ { print $2; exit }' pubspec.yaml)" + build_name="${pubspec_version%%+*}" + if [[ -z "${build_name}" || "${build_name}" == "${pubspec_version}" ]]; then + build_number="${GITHUB_RUN_NUMBER}" + else + build_number="${pubspec_version##*+}" + fi + + if [[ -z "${build_name}" ]]; then + build_name="0.0.0" + fi + if [[ -z "${build_number}" || ! "${build_number}" =~ ^[0-9]+$ ]]; then + build_number="${GITHUB_RUN_NUMBER}" + fi + + if [[ -n "${SOURCE_REF_INPUT}" ]]; then + ref_name="${SOURCE_REF_INPUT#refs/heads/}" + ref_name="${ref_name#refs/tags/}" + else + ref_name="${GITHUB_REF_NAME}" + fi + fi + + artifact_ref_name="$(printf '%s' "${ref_name}" | sed 's#[^A-Za-z0-9_.+-]#-#g')" + + upload_artifact=false + if [[ "${is_tag_build}" == "true" ]]; then + upload_artifact=true + elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${MANUAL_UPLOAD_ARTIFACTS}" == "true" ]]; then + upload_artifact=true + fi + + echo "build_name=${build_name}" >> "${GITHUB_OUTPUT}" + echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}" + echo "artifact_ref_name=${artifact_ref_name}" >> "${GITHUB_OUTPUT}" + echo "is_tag_build=${is_tag_build}" >> "${GITHUB_OUTPUT}" + echo "upload_artifact=${upload_artifact}" >> "${GITHUB_OUTPUT}" + + { + echo "## Build metadata" + echo + echo "- Platform: ${PLATFORM}" + echo "- Build name: ${build_name}" + echo "- Build number: ${build_number}" + echo "- Upload artifacts: ${upload_artifact}" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/actions/setup-flutter/action.yml b/.github/actions/setup-flutter/action.yml new file mode 100644 index 0000000..90c3f0d --- /dev/null +++ b/.github/actions/setup-flutter/action.yml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-action.json +name: Setup Flutter +description: Install a Flutter SDK from Git and add it to PATH. + +inputs: + flutter-repository: + description: Flutter SDK Git repository. + required: false + default: https://github.com/flutter/flutter.git + flutter-ref: + description: Flutter SDK branch, tag, or ref. + required: false + default: stable + +runs: + using: composite + steps: + - name: Install Flutter + shell: bash + run: | + set -euo pipefail + git clone --depth 1 --branch "${{ inputs.flutter-ref }}" "${{ inputs.flutter-repository }}" "${RUNNER_TEMP}/flutter" + echo "${RUNNER_TEMP}/flutter/bin" >> "${GITHUB_PATH}" diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml new file mode 100644 index 0000000..52a81a9 --- /dev/null +++ b/.github/workflows/ios-build.yml @@ -0,0 +1,109 @@ +name: iOS Build + +on: + pull_request: + branches: + - main + - master + - 'dev/**' + push: + branches: + - main + - master + - 'dev/**' + tags: + - 'v*' + - 'ios-v*' + workflow_dispatch: + inputs: + source_ref: + description: 'Optional branch, tag, or commit SHA to build.' + required: false + type: string + upload_artifacts: + description: 'Upload artifacts for this manual run.' + required: true + type: boolean + default: true + +permissions: + contents: read + +concurrency: + group: ios-build-${{ github.event.inputs.source_ref || github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build unsigned iOS app + runs-on: macos-latest + timeout-minutes: 60 + + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.source_ref || github.ref }} + + - name: Install Flutter stable + uses: ./.github/actions/setup-flutter + + - name: Resolve build metadata + id: metadata + uses: ./.github/actions/resolve-flutter-build-metadata + with: + platform: ios + source-ref: ${{ github.event.inputs.source_ref || '' }} + upload-artifacts: ${{ github.event.inputs.upload_artifacts || 'false' }} + + - name: Show Flutter environment + run: flutter --version + + - name: Get Flutter packages + run: flutter pub get + + - name: Install iOS pods + run: | + set -euo pipefail + cd ios + pod install + + - name: Build unsigned iOS app + run: | + set -euo pipefail + flutter build ios --release --no-codesign \ + --build-name "${{ steps.metadata.outputs.build_name }}" \ + --build-number "${{ steps.metadata.outputs.build_number }}" + + - name: Write unsigned iOS artifact notice + if: steps.metadata.outputs.upload_artifact == 'true' + run: | + set -euo pipefail + cat > build/ios/UNSIGNED_ARTIFACT_README.md <<'NOTICE' + # Unsigned iOS build artifact + + This artifact is unsigned. It cannot be uploaded directly to TestFlight and may not install directly on a device. It is only for verifying that the source code builds in a macOS/Xcode environment. + NOTICE + + - name: Package unsigned IPA + if: steps.metadata.outputs.upload_artifact == 'true' + run: | + set -euo pipefail + rm -rf build/ios/ipa + mkdir -p build/ios/ipa/Payload + cp -R build/ios/iphoneos/Runner.app build/ios/ipa/Payload/Runner.app + cd build/ios/ipa + ditto -c -k --sequesterRsrc --keepParent Payload TechPie-unsigned.ipa + + - name: Upload unsigned iOS artifact + if: steps.metadata.outputs.upload_artifact == 'true' + uses: actions/upload-artifact@v4 + with: + name: techpie-ios-unsigned-${{ steps.metadata.outputs.artifact_ref_name }} + if-no-files-found: error + path: | + build/ios/UNSIGNED_ARTIFACT_README.md + build/ios/ipa/TechPie-unsigned.ipa + build/ios/iphoneos/Runner.app + build/ios/archive/ From b84fbeca8225d6a06cbac9f53f3daf7f491ce85c Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Sun, 24 May 2026 22:03:24 +0800 Subject: [PATCH 2/7] ci: precache iOS Flutter artifacts --- .github/workflows/ios-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml index 52a81a9..93a5e99 100644 --- a/.github/workflows/ios-build.yml +++ b/.github/workflows/ios-build.yml @@ -60,6 +60,9 @@ jobs: - name: Show Flutter environment run: flutter --version + - name: Precache iOS Flutter artifacts + run: flutter precache --ios + - name: Get Flutter packages run: flutter pub get From 20ed07e74bd631cfebcd9ead40e1aa561699c2c2 Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Sun, 24 May 2026 22:35:04 +0800 Subject: [PATCH 3/7] fix(ios): guard iOS 26 glass APIs for CI builds --- ios/Runner/NativeGlass/NativeGlassButton.swift | 9 ++++++++- .../NativeGlass/NativeGlassConfirmationButton.swift | 9 +++++++++ ios/Runner/NativeGlass/NativeGlassDropdownMenu.swift | 2 ++ ios/Runner/NativeGlass/NativeGlassSelect.swift | 7 +++++++ ios/Runner/NativeGlass/NativeNavigationBar.swift | 4 ++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ios/Runner/NativeGlass/NativeGlassButton.swift b/ios/Runner/NativeGlass/NativeGlassButton.swift index 30862ab..148d9bf 100644 --- a/ios/Runner/NativeGlass/NativeGlassButton.swift +++ b/ios/Runner/NativeGlass/NativeGlassButton.swift @@ -105,15 +105,21 @@ final class NativeGlassButtonPlatformView: NSObject, FlutterPlatformView { private func applyButtonAppearance() { let image = symbolImage() +#if compiler(>=6.2) if #available(iOS 26.0, *) { applyLiquidGlassAppearance(image: image) - } else if #available(iOS 15.0, *) { + return + } +#endif + + if #available(iOS 15.0, *) { applyModernFallbackAppearance(image: image) } else { applyLegacyFallbackAppearance(image: image) } } +#if compiler(>=6.2) @available(iOS 26.0, *) private func applyLiquidGlassAppearance(image: UIImage?) { var configuration = UIButton.Configuration.prominentGlass() @@ -121,6 +127,7 @@ final class NativeGlassButtonPlatformView: NSObject, FlutterPlatformView { button.configuration = configuration } +#endif @available(iOS 15.0, *) private func applyModernFallbackAppearance(image: UIImage?) { diff --git a/ios/Runner/NativeGlass/NativeGlassConfirmationButton.swift b/ios/Runner/NativeGlass/NativeGlassConfirmationButton.swift index 819162d..3067bdb 100644 --- a/ios/Runner/NativeGlass/NativeGlassConfirmationButton.swift +++ b/ios/Runner/NativeGlass/NativeGlassConfirmationButton.swift @@ -114,10 +114,12 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform } private func applyButtonAppearance() { +#if compiler(>=6.2) if #available(iOS 26.0, *) { applyLiquidGlassAppearance() return } +#endif let image = symbolImage(named: sfSymbol) @@ -139,6 +141,7 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform } } +#if compiler(>=6.2) @available(iOS 26.0, *) private func applyLiquidGlassAppearance() { let image = symbolImage(named: sfSymbol) @@ -155,6 +158,7 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform button.configuration = configuration } +#endif private func symbolImage(named systemName: String) -> UIImage? { return UIImage(systemName: systemName)? @@ -184,12 +188,17 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) if let popover = actionSheet.popoverPresentationController { +#if compiler(>=6.2) if #available(iOS 26.0, *) { popover.sourceItem = button } else { popover.sourceView = rootView popover.sourceRect = rootView.bounds } +#else + popover.sourceView = rootView + popover.sourceRect = rootView.bounds +#endif } controller.present(actionSheet, animated: true) diff --git a/ios/Runner/NativeGlass/NativeGlassDropdownMenu.swift b/ios/Runner/NativeGlass/NativeGlassDropdownMenu.swift index da29273..ec3b395 100644 --- a/ios/Runner/NativeGlass/NativeGlassDropdownMenu.swift +++ b/ios/Runner/NativeGlass/NativeGlassDropdownMenu.swift @@ -123,6 +123,7 @@ final class NativeGlassDropdownMenuPlatformView: NSObject, FlutterPlatformView { private func applyButtonAppearance() { let image = symbolImage(named: sfSymbol) +#if compiler(>=6.2) if #available(iOS 26.0, *) { var configuration = UIButton.Configuration.glass() configuration.image = image @@ -130,6 +131,7 @@ final class NativeGlassDropdownMenuPlatformView: NSObject, FlutterPlatformView { button.configuration = configuration return } +#endif if #available(iOS 15.0, *) { var configuration = UIButton.Configuration.plain() diff --git a/ios/Runner/NativeGlass/NativeGlassSelect.swift b/ios/Runner/NativeGlass/NativeGlassSelect.swift index e2a9be2..40c5bc4 100644 --- a/ios/Runner/NativeGlass/NativeGlassSelect.swift +++ b/ios/Runner/NativeGlass/NativeGlassSelect.swift @@ -135,6 +135,7 @@ final class NativeGlassSelectPlatformView: NSObject, FlutterPlatformView { private func applyButtonAppearance() { let image = symbolImage(named: sfSymbol) +#if compiler(>=6.2) if #available(iOS 26.0, *) { var configuration = UIButton.Configuration.glass() configuration.image = image @@ -142,6 +143,7 @@ final class NativeGlassSelectPlatformView: NSObject, FlutterPlatformView { button.configuration = configuration return } +#endif if #available(iOS 15.0, *) { var configuration = UIButton.Configuration.plain() @@ -246,12 +248,17 @@ final class NativeGlassSelectPlatformView: NSObject, FlutterPlatformView { sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) if let popover = sheet.popoverPresentationController { +#if compiler(>=6.2) if #available(iOS 26.0, *) { popover.sourceItem = button } else { popover.sourceView = rootView popover.sourceRect = rootView.bounds } +#else + popover.sourceView = rootView + popover.sourceRect = rootView.bounds +#endif } controller.present(sheet, animated: true) diff --git a/ios/Runner/NativeGlass/NativeNavigationBar.swift b/ios/Runner/NativeGlass/NativeNavigationBar.swift index b331fa4..478a33d 100644 --- a/ios/Runner/NativeGlass/NativeNavigationBar.swift +++ b/ios/Runner/NativeGlass/NativeNavigationBar.swift @@ -113,10 +113,12 @@ final class NativeNavigationBarPlatformView: NSObject, FlutterPlatformView { navigationItem.title = configuration.title navigationItem.largeTitleDisplayMode = configuration.largeTitleMode ? .always : .never +#if compiler(>=6.2) if #available(iOS 26.0, *) { navigationItem.subtitle = configuration.largeTitleMode ? nil : configuration.subtitle navigationItem.largeSubtitle = configuration.largeTitleMode ? configuration.subtitle : nil } +#endif if #available(iOS 16.0, *) { let leadingItems = configuration.leadingItems.map(makeBarButtonItem) @@ -220,9 +222,11 @@ final class NativeNavigationBarPlatformView: NSObject, FlutterPlatformView { if #available(iOS 16.0, *) { barButtonItem.isHidden = item.hidden } +#if compiler(>=6.2) if #available(iOS 26.0, *) { barButtonItem.identifier = item.id } +#endif return barButtonItem } From 00b0036ed734236be95693850dca3143a4083d5f Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Mon, 25 May 2026 19:27:21 +0800 Subject: [PATCH 4/7] cd: package unsigned iOS artifacts --- .../resolve-flutter-build-metadata/action.yml | 10 ++-- .../{ios-build.yml => ios-unsigned-cd.yml} | 54 +++++++++++++------ 2 files changed, 43 insertions(+), 21 deletions(-) rename .github/workflows/{ios-build.yml => ios-unsigned-cd.yml} (63%) diff --git a/.github/actions/resolve-flutter-build-metadata/action.yml b/.github/actions/resolve-flutter-build-metadata/action.yml index ded4f8c..e6a4983 100644 --- a/.github/actions/resolve-flutter-build-metadata/action.yml +++ b/.github/actions/resolve-flutter-build-metadata/action.yml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=https://json.schemastore.org/github-action.json -name: Resolve Flutter build metadata -description: Resolve build name, build number, release tag metadata, and artifact naming. +name: Resolve Flutter package metadata +description: Resolve Flutter build-name/build-number values, release tag metadata, and artifact naming. inputs: platform: @@ -17,10 +17,10 @@ inputs: outputs: build_name: - description: Flutter build name. + description: Flutter build-name value. value: ${{ steps.metadata.outputs.build_name }} build_number: - description: Flutter build number. + description: Flutter build-number value. value: ${{ steps.metadata.outputs.build_number }} artifact_ref_name: description: Sanitized ref name for artifact names. @@ -130,7 +130,7 @@ runs: echo "upload_artifact=${upload_artifact}" >> "${GITHUB_OUTPUT}" { - echo "## Build metadata" + echo "## Package metadata" echo echo "- Platform: ${PLATFORM}" echo "- Build name: ${build_name}" diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-unsigned-cd.yml similarity index 63% rename from .github/workflows/ios-build.yml rename to .github/workflows/ios-unsigned-cd.yml index 93a5e99..feaa4d7 100644 --- a/.github/workflows/ios-build.yml +++ b/.github/workflows/ios-unsigned-cd.yml @@ -1,4 +1,4 @@ -name: iOS Build +name: iOS Unsigned CD on: pull_request: @@ -11,17 +11,18 @@ on: - main - master - 'dev/**' + - 'cd/**' tags: - 'v*' - 'ios-v*' workflow_dispatch: inputs: source_ref: - description: 'Optional branch, tag, or commit SHA to build.' + description: 'Optional branch, tag, or commit SHA to package.' required: false type: string upload_artifacts: - description: 'Upload artifacts for this manual run.' + description: 'Upload unsigned IPA artifacts for this manual run.' required: true type: boolean default: true @@ -30,14 +31,14 @@ permissions: contents: read concurrency: - group: ios-build-${{ github.event.inputs.source_ref || github.ref }} + group: ios-unsigned-cd-${{ github.event.inputs.source_ref || github.ref }} cancel-in-progress: true jobs: - build: - name: Build unsigned iOS app + package: + name: Build checks and unsigned IPA package runs-on: macos-latest - timeout-minutes: 60 + timeout-minutes: 70 steps: - name: Checkout source @@ -49,7 +50,7 @@ jobs: - name: Install Flutter stable uses: ./.github/actions/setup-flutter - - name: Resolve build metadata + - name: Resolve package metadata id: metadata uses: ./.github/actions/resolve-flutter-build-metadata with: @@ -57,6 +58,13 @@ jobs: source-ref: ${{ github.event.inputs.source_ref || '' }} upload-artifacts: ${{ github.event.inputs.upload_artifacts || 'false' }} + - name: Show Xcode environment + run: | + set -euo pipefail + xcode-select -p + xcodebuild -version + swift --version + - name: Show Flutter environment run: flutter --version @@ -66,6 +74,18 @@ jobs: - name: Get Flutter packages run: flutter pub get + - name: Run static analysis + run: flutter analyze + + - name: Run Flutter tests + run: | + set -euo pipefail + if [[ -d test ]]; then + flutter test + else + echo "No test/ directory found; skipping flutter test." + fi + - name: Install iOS pods run: | set -euo pipefail @@ -78,19 +98,20 @@ jobs: flutter build ios --release --no-codesign \ --build-name "${{ steps.metadata.outputs.build_name }}" \ --build-number "${{ steps.metadata.outputs.build_number }}" + test -d build/ios/iphoneos/Runner.app - - name: Write unsigned iOS artifact notice - if: steps.metadata.outputs.upload_artifact == 'true' + - name: Write unsigned artifact notice + if: steps.metadata.outputs.upload_artifact == 'true' || github.event_name == 'pull_request' run: | set -euo pipefail cat > build/ios/UNSIGNED_ARTIFACT_README.md <<'NOTICE' - # Unsigned iOS build artifact + # Unsigned iOS artifact - This artifact is unsigned. It cannot be uploaded directly to TestFlight and may not install directly on a device. It is only for verifying that the source code builds in a macOS/Xcode environment. + This artifact is unsigned. It cannot be uploaded directly to TestFlight and may not install directly on a device. It is only for verifying that the source code can produce an unsigned iOS package in a macOS/Xcode environment. NOTICE - name: Package unsigned IPA - if: steps.metadata.outputs.upload_artifact == 'true' + if: steps.metadata.outputs.upload_artifact == 'true' || github.event_name == 'pull_request' run: | set -euo pipefail rm -rf build/ios/ipa @@ -98,15 +119,16 @@ jobs: cp -R build/ios/iphoneos/Runner.app build/ios/ipa/Payload/Runner.app cd build/ios/ipa ditto -c -k --sequesterRsrc --keepParent Payload TechPie-unsigned.ipa + test -f TechPie-unsigned.ipa - - name: Upload unsigned iOS artifact - if: steps.metadata.outputs.upload_artifact == 'true' + - name: Upload unsigned IPA artifact + if: steps.metadata.outputs.upload_artifact == 'true' || github.event_name == 'pull_request' uses: actions/upload-artifact@v4 with: name: techpie-ios-unsigned-${{ steps.metadata.outputs.artifact_ref_name }} + retention-days: 7 if-no-files-found: error path: | build/ios/UNSIGNED_ARTIFACT_README.md build/ios/ipa/TechPie-unsigned.ipa build/ios/iphoneos/Runner.app - build/ios/archive/ From f98cb6e69c57b83138274fd4bc0caee7f07eeb3e Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Mon, 25 May 2026 20:05:34 +0800 Subject: [PATCH 5/7] cd: restrict unsigned iOS release to tags --- .../resolve-flutter-build-metadata/action.yml | 103 ++++-------------- ...signed-cd.yml => ios-unsigned-release.yml} | 63 +++-------- 2 files changed, 39 insertions(+), 127 deletions(-) rename .github/workflows/{ios-unsigned-cd.yml => ios-unsigned-release.yml} (62%) diff --git a/.github/actions/resolve-flutter-build-metadata/action.yml b/.github/actions/resolve-flutter-build-metadata/action.yml index e6a4983..d251f82 100644 --- a/.github/actions/resolve-flutter-build-metadata/action.yml +++ b/.github/actions/resolve-flutter-build-metadata/action.yml @@ -6,14 +6,10 @@ inputs: platform: description: Platform name used for platform-specific release tags. required: true - source-ref: - description: Optional branch, tag, or commit SHA selected from workflow_dispatch. + require-tag-build-number: + description: Whether platform release tags must include +BUILD_NUMBER. required: false - default: '' - upload-artifacts: - description: Whether manual workflow_dispatch runs should upload artifacts. - required: false - default: 'false' + default: 'true' outputs: build_name: @@ -28,9 +24,6 @@ outputs: is_tag_build: description: Whether the current build resolved to a release tag. value: ${{ steps.metadata.outputs.is_tag_build }} - upload_artifact: - description: Whether artifacts should be uploaded. - value: ${{ steps.metadata.outputs.upload_artifact }} runs: using: composite @@ -40,100 +33,46 @@ runs: shell: bash env: PLATFORM: ${{ inputs.platform }} - SOURCE_REF_INPUT: ${{ inputs.source-ref }} - MANUAL_UPLOAD_ARTIFACTS: ${{ inputs.upload-artifacts }} + REQUIRE_TAG_BUILD_NUMBER: ${{ inputs.require-tag-build-number }} run: | set -euo pipefail - is_tag_build=false - tag_name="" - - if [[ "${GITHUB_REF_TYPE}" == "tag" && -z "${SOURCE_REF_INPUT}" ]]; then - is_tag_build=true - tag_name="${GITHUB_REF_NAME}" - elif [[ -n "${SOURCE_REF_INPUT}" ]]; then - candidate="${SOURCE_REF_INPUT#refs/tags/}" - if git show-ref --verify --quiet "refs/tags/${candidate}"; then - is_tag_build=true - tag_name="${candidate}" - fi + if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then + echo "::error::This package workflow only supports release tags." + exit 1 fi - if [[ "${is_tag_build}" == "true" ]]; then - if [[ "${tag_name}" =~ ^${PLATFORM}-v([0-9]+)\.([0-9]+)\.([0-9]+)(\+([0-9]+))?$ ]]; then - build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" - build_number="${BASH_REMATCH[5]:-${GITHUB_RUN_NUMBER}}" - elif [[ "${tag_name}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(\+([0-9]+))?$ ]]; then - build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" - build_number="${BASH_REMATCH[5]:-${GITHUB_RUN_NUMBER}}" - else - echo "::error::Invalid ${PLATFORM} release tag '${tag_name}'. Expected vX.Y.Z, vX.Y.Z+B, ${PLATFORM}-vX.Y.Z, or ${PLATFORM}-vX.Y.Z+B." + tag_name="${GITHUB_REF_NAME}" + if [[ "${tag_name}" =~ ^${PLATFORM}-v([0-9]+)\.([0-9]+)\.([0-9]+)\+([0-9]+)$ ]]; then + build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" + build_number="${BASH_REMATCH[4]}" + else + if [[ "${REQUIRE_TAG_BUILD_NUMBER}" == "true" ]]; then + echo "::error::Invalid ${PLATFORM} release tag '${tag_name}'. Expected ${PLATFORM}-vX.Y.Z+B, for example ${PLATFORM}-v0.1.0+5." exit 1 fi - tag_commit="$(git rev-list -n 1 "${tag_name}")" - git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' || true - git fetch --no-tags origin '+refs/heads/master:refs/remotes/origin/master' || true - - reachable=false - for branch in origin/main origin/master; do - if git show-ref --verify --quiet "refs/remotes/${branch}" && - git merge-base --is-ancestor "${tag_commit}" "${branch}"; then - reachable=true - break - fi - done - - if [[ "${reachable}" != "true" ]]; then - echo "::error::Release tag must point to a commit reachable from main or master." + if [[ ! "${tag_name}" =~ ^${PLATFORM}-v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "::error::Invalid ${PLATFORM} release tag '${tag_name}'. Expected ${PLATFORM}-vX.Y.Z+B." exit 1 fi - ref_name="${tag_name}" - else - pubspec_version="$(awk '/^version:/ { print $2; exit }' pubspec.yaml)" - build_name="${pubspec_version%%+*}" - if [[ -z "${build_name}" || "${build_name}" == "${pubspec_version}" ]]; then - build_number="${GITHUB_RUN_NUMBER}" - else - build_number="${pubspec_version##*+}" - fi - - if [[ -z "${build_name}" ]]; then - build_name="0.0.0" - fi - if [[ -z "${build_number}" || ! "${build_number}" =~ ^[0-9]+$ ]]; then - build_number="${GITHUB_RUN_NUMBER}" - fi - - if [[ -n "${SOURCE_REF_INPUT}" ]]; then - ref_name="${SOURCE_REF_INPUT#refs/heads/}" - ref_name="${ref_name#refs/tags/}" - else - ref_name="${GITHUB_REF_NAME}" - fi + build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" + build_number="${GITHUB_RUN_NUMBER}" fi - artifact_ref_name="$(printf '%s' "${ref_name}" | sed 's#[^A-Za-z0-9_.+-]#-#g')" - - upload_artifact=false - if [[ "${is_tag_build}" == "true" ]]; then - upload_artifact=true - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${MANUAL_UPLOAD_ARTIFACTS}" == "true" ]]; then - upload_artifact=true - fi + artifact_ref_name="$(printf '%s' "${tag_name}" | sed 's#[^A-Za-z0-9_.+-]#-#g')" echo "build_name=${build_name}" >> "${GITHUB_OUTPUT}" echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}" echo "artifact_ref_name=${artifact_ref_name}" >> "${GITHUB_OUTPUT}" - echo "is_tag_build=${is_tag_build}" >> "${GITHUB_OUTPUT}" - echo "upload_artifact=${upload_artifact}" >> "${GITHUB_OUTPUT}" + echo "is_tag_build=true" >> "${GITHUB_OUTPUT}" { echo "## Package metadata" echo echo "- Platform: ${PLATFORM}" + echo "- Tag: ${tag_name}" echo "- Build name: ${build_name}" echo "- Build number: ${build_number}" - echo "- Upload artifacts: ${upload_artifact}" } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/ios-unsigned-cd.yml b/.github/workflows/ios-unsigned-release.yml similarity index 62% rename from .github/workflows/ios-unsigned-cd.yml rename to .github/workflows/ios-unsigned-release.yml index feaa4d7..a358198 100644 --- a/.github/workflows/ios-unsigned-cd.yml +++ b/.github/workflows/ios-unsigned-release.yml @@ -1,51 +1,28 @@ -name: iOS Unsigned CD +name: iOS Unsigned Release on: - pull_request: - branches: - - main - - master - - 'dev/**' push: - branches: - - main - - master - - 'dev/**' - - 'cd/**' tags: - - 'v*' - 'ios-v*' - workflow_dispatch: - inputs: - source_ref: - description: 'Optional branch, tag, or commit SHA to package.' - required: false - type: string - upload_artifacts: - description: 'Upload unsigned IPA artifacts for this manual run.' - required: true - type: boolean - default: true permissions: contents: read concurrency: - group: ios-unsigned-cd-${{ github.event.inputs.source_ref || github.ref }} + group: ios-unsigned-release-${{ github.ref }} cancel-in-progress: true jobs: package: - name: Build checks and unsigned IPA package + name: Package unsigned iOS IPA runs-on: macos-latest - timeout-minutes: 70 + timeout-minutes: 60 steps: - name: Checkout source uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.event.inputs.source_ref || github.ref }} - name: Install Flutter stable uses: ./.github/actions/setup-flutter @@ -55,8 +32,7 @@ jobs: uses: ./.github/actions/resolve-flutter-build-metadata with: platform: ios - source-ref: ${{ github.event.inputs.source_ref || '' }} - upload-artifacts: ${{ github.event.inputs.upload_artifacts || 'false' }} + require-tag-build-number: 'true' - name: Show Xcode environment run: | @@ -72,19 +48,20 @@ jobs: run: flutter precache --ios - name: Get Flutter packages - run: flutter pub get - - - name: Run static analysis - run: flutter analyze - - - name: Run Flutter tests run: | set -euo pipefail - if [[ -d test ]]; then - flutter test - else - echo "No test/ directory found; skipping flutter test." - fi + for attempt in 1 2 3; do + if flutter pub get; then + exit 0 + fi + + echo "flutter pub get failed on attempt ${attempt}; retrying after cleaning temporary pub cache files." + rm -rf "${HOME}/.pub-cache/_temp" + sleep $((attempt * 10)) + done + + echo "::error::flutter pub get failed after 3 attempts." + exit 1 - name: Install iOS pods run: | @@ -101,7 +78,6 @@ jobs: test -d build/ios/iphoneos/Runner.app - name: Write unsigned artifact notice - if: steps.metadata.outputs.upload_artifact == 'true' || github.event_name == 'pull_request' run: | set -euo pipefail cat > build/ios/UNSIGNED_ARTIFACT_README.md <<'NOTICE' @@ -111,7 +87,6 @@ jobs: NOTICE - name: Package unsigned IPA - if: steps.metadata.outputs.upload_artifact == 'true' || github.event_name == 'pull_request' run: | set -euo pipefail rm -rf build/ios/ipa @@ -122,13 +97,11 @@ jobs: test -f TechPie-unsigned.ipa - name: Upload unsigned IPA artifact - if: steps.metadata.outputs.upload_artifact == 'true' || github.event_name == 'pull_request' uses: actions/upload-artifact@v4 with: name: techpie-ios-unsigned-${{ steps.metadata.outputs.artifact_ref_name }} - retention-days: 7 + retention-days: 30 if-no-files-found: error path: | build/ios/UNSIGNED_ARTIFACT_README.md build/ios/ipa/TechPie-unsigned.ipa - build/ios/iphoneos/Runner.app From 1b3ac4481dec0159093f3742a180c75dda019459 Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Mon, 25 May 2026 21:14:48 +0800 Subject: [PATCH 6/7] cd: inject iOS bundle identifier for release builds --- .github/workflows/ios-unsigned-release.yml | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/ios-unsigned-release.yml b/.github/workflows/ios-unsigned-release.yml index a358198..0a0e4d2 100644 --- a/.github/workflows/ios-unsigned-release.yml +++ b/.github/workflows/ios-unsigned-release.yml @@ -17,6 +17,8 @@ jobs: name: Package unsigned iOS IPA runs-on: macos-latest timeout-minutes: 60 + env: + IOS_BUNDLE_IDENTIFIER: ${{ vars.IOS_BUNDLE_IDENTIFIER }} steps: - name: Checkout source @@ -69,6 +71,22 @@ jobs: cd ios pod install + - name: Apply release bundle identifier + run: | + set -euo pipefail + if [ -z "${IOS_BUNDLE_IDENTIFIER}" ]; then + echo "::error::Set repository variable IOS_BUNDLE_IDENTIFIER before creating an iOS release tag." + exit 1 + fi + + /usr/libexec/PlistBuddy \ + -c "Set :objects:97C147061CF9000F007C117D:buildSettings:PRODUCT_BUNDLE_IDENTIFIER ${IOS_BUNDLE_IDENTIFIER}" \ + -c "Set :objects:97C147071CF9000F007C117D:buildSettings:PRODUCT_BUNDLE_IDENTIFIER ${IOS_BUNDLE_IDENTIFIER}" \ + -c "Set :objects:249021D4217E4FDB00AE95B9:buildSettings:PRODUCT_BUNDLE_IDENTIFIER ${IOS_BUNDLE_IDENTIFIER}" \ + ios/Runner.xcodeproj/project.pbxproj + + grep -q "PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_IDENTIFIER};" ios/Runner.xcodeproj/project.pbxproj + - name: Build unsigned iOS app run: | set -euo pipefail @@ -76,6 +94,11 @@ jobs: --build-name "${{ steps.metadata.outputs.build_name }}" \ --build-number "${{ steps.metadata.outputs.build_number }}" test -d build/ios/iphoneos/Runner.app + actual_bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' build/ios/iphoneos/Runner.app/Info.plist) + if [ "${actual_bundle_id}" != "${IOS_BUNDLE_IDENTIFIER}" ]; then + echo "::error::Expected bundle identifier ${IOS_BUNDLE_IDENTIFIER}, got ${actual_bundle_id}." + exit 1 + fi - name: Write unsigned artifact notice run: | From 481fc039ed11d161cc0256806c5968670eec248b Mon Sep 17 00:00:00 2001 From: CNDY1390 Date: Mon, 25 May 2026 23:30:47 +0800 Subject: [PATCH 7/7] cd: make iOS bundle identifier injection robust --- .github/workflows/ios-unsigned-release.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ios-unsigned-release.yml b/.github/workflows/ios-unsigned-release.yml index 0a0e4d2..3dee695 100644 --- a/.github/workflows/ios-unsigned-release.yml +++ b/.github/workflows/ios-unsigned-release.yml @@ -79,13 +79,18 @@ jobs: exit 1 fi - /usr/libexec/PlistBuddy \ - -c "Set :objects:97C147061CF9000F007C117D:buildSettings:PRODUCT_BUNDLE_IDENTIFIER ${IOS_BUNDLE_IDENTIFIER}" \ - -c "Set :objects:97C147071CF9000F007C117D:buildSettings:PRODUCT_BUNDLE_IDENTIFIER ${IOS_BUNDLE_IDENTIFIER}" \ - -c "Set :objects:249021D4217E4FDB00AE95B9:buildSettings:PRODUCT_BUNDLE_IDENTIFIER ${IOS_BUNDLE_IDENTIFIER}" \ - ios/Runner.xcodeproj/project.pbxproj + if ! printf '%s\n' "${IOS_BUNDLE_IDENTIFIER}" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$'; then + echo "::error::IOS_BUNDLE_IDENTIFIER has an invalid bundle identifier format." + exit 1 + fi + + perl -0pi -e 's/PRODUCT_BUNDLE_IDENTIFIER = com\.example\.techpie;/PRODUCT_BUNDLE_IDENTIFIER = $ENV{IOS_BUNDLE_IDENTIFIER};/g' ios/Runner.xcodeproj/project.pbxproj - grep -q "PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_IDENTIFIER};" ios/Runner.xcodeproj/project.pbxproj + matches=$(grep -c "PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_IDENTIFIER};" ios/Runner.xcodeproj/project.pbxproj) + if [ "${matches}" -ne 3 ]; then + echo "::error::Expected to update 3 Runner bundle identifier settings, updated ${matches}." + exit 1 + fi - name: Build unsigned iOS app run: |