From 0857b7569fdf883d3008e4efc59c7f21e4422bb4 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 26 Jul 2026 11:47:22 +0200 Subject: [PATCH] Split CI into format and build_test, drop the ios_ prefix `ios_main.yml` said nothing about what it runs, and every job in it repeated the same ten steps to get odrcore resolved. Both problems are fixed the way OpenDocument.core and OpenDocument.droid already do it: * one workflow per concern - `format`, `build_test`, `release` - so a failing check names itself in the PR view * the shared conan bootstrap moves into a composite action, `.github/actions/setup-odrcore`, which the three jobs that compile the app now call with a profile and a configuration `build_test` also gains `paths-ignore` for markdown and store metadata, so a doc change no longer triggers a full native build. `format` keeps running unconditionally, because it checks files `build_test` ignores. `release` stops writing the conan cache: it only ever consumed what `build_test` had already built, and an untested release should not be able to poison the entry the other workflows restore. Its pinned action versions were a release behind and are bumped to match. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011MhKU2kWm1cPW4GBq9gon5 --- .github/actions/setup-odrcore/action.yml | 100 ++++++++++ .github/workflows/build_test.yml | 97 ++++++++++ .github/workflows/format.yml | 39 ++++ .github/workflows/ios_main.yml | 181 ------------------ .../{ios_release.yml => release.yml} | 57 ++---- README.md | 19 +- 6 files changed, 266 insertions(+), 227 deletions(-) create mode 100644 .github/actions/setup-odrcore/action.yml create mode 100644 .github/workflows/build_test.yml create mode 100644 .github/workflows/format.yml delete mode 100644 .github/workflows/ios_main.yml rename .github/workflows/{ios_release.yml => release.yml} (68%) diff --git a/.github/actions/setup-odrcore/action.yml b/.github/actions/setup-odrcore/action.yml new file mode 100644 index 0000000..9cb0544 --- /dev/null +++ b/.github/actions/setup-odrcore/action.yml @@ -0,0 +1,100 @@ +name: setup odrcore +description: > + Installs the toolchain and resolves odrcore and its dependencies with conan, + leaving the generated xcconfigs in conan-output and the runtime assets in + conan-assets. Every job that compiles the app needs the exact same ten steps, + so they live here instead of being copied into each workflow. + +inputs: + xcode-version: + description: Xcode version to select on the runner + required: true + profile: + description: conan host profile under conan/profiles + required: true + configuration: + description: Xcode configuration to resolve the dependencies for + required: true + cache-flavor: + description: > + Cache namespace. The simulator and the device slice resolve a different + dependency set, so they must not overwrite each other's cache entry. + required: true + save-cache: + description: > + Whether this job may write the conan cache back. Jobs that only consume + what another job built restore without saving. + required: false + default: "true" + +outputs: + conan-cache-key: + description: Cache key of the conan package cache, to restore it elsewhere + value: ${{ steps.conan-cache-key.outputs.key }} + +runs: + using: composite + steps: + - name: checkout conan-odr-index + shell: bash + run: git submodule update --init --depth 1 conan-odr-index + + # 3.12+ is required by the conan-odr-index helper scripts + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ inputs.xcode-version }} + + # conan itself is pinned by conan-odr-index so recipes and client stay in sync + - name: install conan + shell: bash + run: pip3 install -r conan-odr-index/requirements.txt + + - name: conan profile + shell: bash + run: conan profile detect + + - name: conan cache key + id: conan-cache-key + shell: bash + run: echo "key=conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}-index$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conan/conanfile.py', 'conan/profiles/*') }}" >> "$GITHUB_OUTPUT" + + # restore before exporting the recipes: the export must run on top of the + # restored cache so current recipes win over stale ones from the archive + - name: conan cache + if: inputs.save-cache == 'true' + uses: actions/cache@v6 + with: + path: ~/.conan2/p + key: ${{ steps.conan-cache-key.outputs.key }} + restore-keys: | + conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}- + + - name: restore conan cache + if: inputs.save-cache != 'true' + uses: actions/cache/restore@v6 + with: + path: ~/.conan2/p + key: ${{ steps.conan-cache-key.outputs.key }} + restore-keys: | + conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}- + + # recipes are exported from the submodule into the local cache, which is + # why no private remote has to be configured + - name: export conan-odr-index + shell: bash + run: python3 conan-odr-index/scripts/conan_export_all_packages.py + + - name: conan install + shell: bash + run: > + conan install conan/ + --output-folder=conan-output + --build=missing + --profile:host=conan/profiles/${{ inputs.profile }} + --deployer=conan/conandeployer.py + --deployer-folder=conan-assets + -o "configuration=${{ inputs.configuration }}" diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml new file mode 100644 index 0000000..a70080c --- /dev/null +++ b/.github/workflows/build_test.yml @@ -0,0 +1,97 @@ +name: build_test + +on: + workflow_dispatch: + # branch pushes are covered by the pull_request trigger, so only main is built + # directly - otherwise every push to a branch with an open PR builds twice + push: + branches: + - main + paths-ignore: + - '**/*.md' + - 'fastlane/metadata/**' + pull_request: + paths-ignore: + - '**/*.md' + - 'fastlane/metadata/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # the runner image only ships the iOS platform bundle for its default Xcode; + # older ones fail in ibtool with "iOS 26.0 Platform Not Installed" + xcode_version: "26.5" + +jobs: + test: + runs-on: macos-26 + steps: + - name: checkout + uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - uses: ./.github/actions/setup-odrcore + with: + xcode-version: ${{ env.xcode_version }} + profile: ios-simulator-arm64 + configuration: Debug + cache-flavor: simulator + + - name: run tests + run: bundle exec fastlane tests + + # drop temporary download/source/build folders so only built packages get cached + - name: clean conan cache + run: conan cache clean "*" + + - uses: actions/upload-artifact@v7 + if: always() + with: + name: logs + path: | + /Users/runner/Library/Developer/Xcode/DerivedData/OpenDocumentReader-*/ + + # the test job only ever builds the simulator slice of one configuration, so + # release-only and device-only breakage used to surface at deploy time + build: + runs-on: macos-26 + strategy: + fail-fast: false + matrix: + include: + - { scheme: "ODR Full", configuration: "Release" } + - { scheme: "ODR Lite", configuration: "Release Lite" } + steps: + - name: checkout + uses: actions/checkout@v7 + + - uses: ./.github/actions/setup-odrcore + with: + xcode-version: ${{ env.xcode_version }} + profile: ios + configuration: ${{ matrix.configuration }} + cache-flavor: device + + # signing needs secrets this workflow does not have, so this only proves + # that the device slice compiles and links + - name: build + run: > + xcodebuild + -project OpenDocumentReader.xcodeproj + -scheme "${{ matrix.scheme }}" + -configuration "${{ matrix.configuration }}" + -sdk iphoneos + -destination 'generic/platform=iOS' + CODE_SIGNING_ALLOWED=NO + build + + - name: clean conan cache + run: conan cache clean "*" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..f93eb21 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,39 @@ +name: format + +# swift-format comes with the Xcode toolchain, so this needs neither conan nor +# ruby and reports style breakage in a minute instead of after a full build. +# Unlike build_test this has no paths-ignore: every file the formatter touches +# gets checked. + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # the runner image only ships the iOS platform bundle for its default Xcode; + # older ones fail in ibtool with "iOS 26.0 Platform Not Installed" + xcode_version: "26.5" + +jobs: + swift-format: + runs-on: macos-26 + steps: + - name: checkout + uses: actions/checkout@v7 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.xcode_version }} + + - name: check formatting + run: scripts/format.sh --check diff --git a/.github/workflows/ios_main.yml b/.github/workflows/ios_main.yml deleted file mode 100644 index 5e140b2..0000000 --- a/.github/workflows/ios_main.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: OpenDocumentReader-iOS - -on: - workflow_dispatch: - push: - branches: - - main - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - # the runner image only ships the iOS platform bundle for its default Xcode; - # older ones fail in ibtool with "iOS 26.0 Platform Not Installed" - xcode_version: "26.5" - -jobs: - # swift-format comes with the toolchain, so this needs neither conan nor ruby - # and reports style breakage in a minute instead of after a full build - format: - runs-on: macos-26 - steps: - - name: checkout - uses: actions/checkout@v7 - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.xcode_version }} - - - name: check formatting - run: scripts/format.sh --check - - test: - runs-on: macos-26 - steps: - - name: checkout - uses: actions/checkout@v7 - - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - - # 3.12+ is required by the conan-odr-index helper scripts - - uses: actions/setup-python@v7 - with: - python-version: "3.12" - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.xcode_version }} - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - # conan itself is pinned by conan-odr-index so recipes and client stay in sync - - name: install conan - run: pip3 install -r conan-odr-index/requirements.txt - - - name: conan profile - run: conan profile detect - - - name: conan cache key - id: conan-cache-key - run: echo "key=conan2-${{ runner.os }}-xcode${{ env.xcode_version }}-simulator-index$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conan/conanfile.py', 'conan/profiles/*') }}" >> "$GITHUB_OUTPUT" - - # restore before exporting the recipes: the export must run on top of the - # restored cache so current recipes win over stale ones from the archive - - name: conan cache - uses: actions/cache@v6 - with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ env.xcode_version }}-simulator- - - # recipes are exported from the submodule into the local cache, which is - # why no private remote has to be configured - - name: export conan-odr-index - run: python3 conan-odr-index/scripts/conan_export_all_packages.py - - - name: conan install - run: > - conan install conan/ - --output-folder=conan-output - --build=missing - --profile:host=conan/profiles/ios-simulator-arm64 - --deployer=conan/conandeployer.py - --deployer-folder=conan-assets - -o "configuration=Debug" - - - name: run tests - run: bundle exec fastlane tests - - # drop temporary download/source/build folders so only built packages get cached - - name: clean conan cache - run: conan cache clean "*" - - - uses: actions/upload-artifact@v7 - if: always() - with: - name: logs - path: | - /Users/runner/Library/Developer/Xcode/DerivedData/OpenDocumentReader-*/ - - # the test job only ever builds the simulator slice of one configuration, so - # release-only and device-only breakage used to surface at deploy time - build: - runs-on: macos-26 - strategy: - fail-fast: false - matrix: - include: - - { scheme: "ODR Full", configuration: "Release" } - - { scheme: "ODR Lite", configuration: "Release Lite" } - steps: - - name: checkout - uses: actions/checkout@v7 - - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - - - uses: actions/setup-python@v7 - with: - python-version: "3.12" - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.xcode_version }} - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: install conan - run: pip3 install -r conan-odr-index/requirements.txt - - - name: conan profile - run: conan profile detect - - - name: conan cache key - id: conan-cache-key - run: echo "key=conan2-${{ runner.os }}-xcode${{ env.xcode_version }}-device-index$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conan/conanfile.py', 'conan/profiles/*') }}" >> "$GITHUB_OUTPUT" - - - name: conan cache - uses: actions/cache@v6 - with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ env.xcode_version }}-device- - - - name: export conan-odr-index - run: python3 conan-odr-index/scripts/conan_export_all_packages.py - - - name: conan install - run: > - conan install conan/ - --output-folder=conan-output - --build=missing - --profile:host=conan/profiles/ios - --deployer=conan/conandeployer.py - --deployer-folder=conan-assets - -o "configuration=${{ matrix.configuration }}" - - # signing needs secrets this workflow does not have, so this only proves - # that the device slice compiles and links - - name: build - run: > - xcodebuild - -project OpenDocumentReader.xcodeproj - -scheme "${{ matrix.scheme }}" - -configuration "${{ matrix.configuration }}" - -sdk iphoneos - -destination 'generic/platform=iOS' - CODE_SIGNING_ALLOWED=NO - build - - - name: clean conan cache - run: conan cache clean "*" diff --git a/.github/workflows/ios_release.yml b/.github/workflows/release.yml similarity index 68% rename from .github/workflows/ios_release.yml rename to .github/workflows/release.yml index f15ba24..85baca9 100644 --- a/.github/workflows/ios_release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: OpenDocumentReader-iOS-release +name: release # Manual only. Releasing stays a deliberate act, this just moves it off # whoever's laptop happened to have the Apple ID signed in. @@ -18,6 +18,9 @@ concurrency: group: ${{ github.workflow }} cancel-in-progress: false +permissions: + contents: read + env: xcode_version: "26.5" @@ -48,53 +51,21 @@ jobs: fi - name: checkout - uses: actions/checkout@v4 - - - name: checkout conan-odr-index - run: git submodule update --init --depth 1 conan-odr-index - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.xcode_version }} + uses: actions/checkout@v7 - uses: ruby/setup-ruby@v1 with: bundler-cache: true - - name: install conan - run: pip3 install -r conan-odr-index/requirements.txt - - - name: conan profile - run: conan profile detect - - - name: conan cache key - id: conan-cache-key - run: echo "key=conan2-${{ runner.os }}-xcode${{ env.xcode_version }}-device-index$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conan/conanfile.py', 'conan/profiles/*') }}" >> "$GITHUB_OUTPUT" - - - name: conan cache - uses: actions/cache/restore@v4 + # a release consumes what build_test already built and never writes the + # cache back, so an untested release cannot poison it + - uses: ./.github/actions/setup-odrcore with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ env.xcode_version }}-device- - - - name: export conan-odr-index - run: python3 conan-odr-index/scripts/conan_export_all_packages.py - - - name: conan install - run: > - conan install conan/ - --output-folder=conan-output - --build=missing - --profile:host=conan/profiles/ios - --deployer=conan/conandeployer.py - --deployer-folder=conan-assets - -o "configuration=${{ matrix.flavor == 'pro' && 'Release' || 'Release Lite' }}" + xcode-version: ${{ env.xcode_version }} + profile: ios + configuration: ${{ matrix.flavor == 'pro' && 'Release' || 'Release Lite' }} + cache-flavor: device + save-cache: "false" # a throwaway keychain so the distribution certificate never outlives the job - name: import signing certificate @@ -127,7 +98,7 @@ jobs: ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }} run: bundle exec fastlane ${{ matrix.flavor == 'pro' && 'deployPro' || 'deployLite' }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() with: name: build-log-${{ matrix.flavor }} diff --git a/README.md b/README.md index 0dcc515..38e894b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OpenDocument.ios ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/ios_main.yml/badge.svg) +# OpenDocument.ios ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/build_test.yml/badge.svg) ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/format.yml/badge.svg) It's Android's first OpenOffice Document Reader... for iOS! This is an iOS frontend for our C++ [OpenDocument.core](https://github.com/opendocument-app/OpenDocument.core) library. @@ -32,10 +32,23 @@ Swift sources are formatted with `swift-format` from the active Xcode toolchain, configured in `.swift-format`. Run `scripts/format.sh` before committing; CI runs `scripts/format.sh --check` and fails on any difference. +## Continuous integration + +| workflow | what it does | +| --- | --- | +| `format` | `scripts/format.sh --check`, on every push and pull request | +| `build_test` | unit tests on the simulator plus a device build of both flavors | +| `release` | manual upload to App Store Connect, see below | + +`format` needs nothing but the Xcode toolchain and reports style breakage in a +minute, so it is kept apart from the native build. Everything that does need +odrcore shares `.github/actions/setup-odrcore`, which resolves the C++ +dependencies with conan and caches them per Xcode version and profile. + ## Releasing -The `OpenDocumentReader-iOS-release` workflow uploads a build to App Store -Connect. It is manual (`workflow_dispatch`) and never submits for review, so +The `release` workflow uploads a build to App Store Connect. It is manual +(`workflow_dispatch`) and never submits for review, so promoting a build stays a deliberate step in App Store Connect. Build numbers come from whatever TestFlight already has, so nothing needs to be committed to cut a release.