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.