diff --git a/.claude/rules/ci.md b/.claude/rules/ci.md index 7b78a38646..574f2dfe0a 100644 --- a/.claude/rules/ci.md +++ b/.claude/rules/ci.md @@ -42,7 +42,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ - Triggers: pull request, push to main or `release/**`, dispatch. A PR keeps only its newest run; each pushed commit has its own concurrency group and is never cancelled. - `Changes (macOS)` outputs `run` (detector `macos`) and `plugins` (detector `plugins`). The test job has a 120-minute limit and sets `NSUnbufferedIO=YES`. -- Setup, in order: checkout, setup-xcode, setup-xcodegen, `scripts/download-libs.sh`, `generate-project.sh macos`, SPM restore, resolve, SPM save. +- Setup, in order: checkout, setup-xcode, prepare-build (setup-xcodegen, `scripts/download-libs.sh`, `generate-project.sh macos`, SPM restore), resolve, SPM save. - One `build-for-testing` with no `-testPlan` builds both plans and writes one `.xctestrun` per plan, with signing, the index store and the compilation cache off. - `COMPILE_PLUGINS` is true on every push and dispatch, and on a PR when the `plugins` detector is not false. It adds Go, the Dameng and HANA bridge builds, `xcodebuild build -scheme AllPlugins`, then `scripts/ci/verify-plugin-loads.sh`. - `test-without-building -testPlan Unit`, then `-testPlan UI`. The UI plan runs after a Unit failure too, so one round trip reports both. @@ -59,11 +59,11 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ ## Release Build (`release-build.yml`) -- Push to main or `release/**`, and dispatch; never a PR. Each commit has its own concurrency group and is never cancelled. Not a required check. -- `Changes (Release)` outputs `run` (detector `release`). `Release Build` runs on `xcode-27` with a 150-minute limit. -- Setup as macOS Tests up to the SPM restore, which it never saves; then Go, `build-dameng.sh both`, `build-hana.sh both` and create-dmg. +- Push to main or `release/**`, and dispatch; never a PR. Never cancelled. Not a required check. +- `Changes (Release)` outputs `run` (detector `release`). `Release Build` runs on `xcode-27` with a 150-minute limit, with VERSION 0.0.0-dev and BUILD 9999999, so build.sh passes the version settings a tag passes. +- Setup as macOS Tests up to the SPM restore, which it never saves; then Go, `build-dameng.sh both` and `build-hana.sh both`. - `scripts/release/build.sh app --unsigned` archives the `TablePro` scheme; `build.sh plugins --unsigned` builds `RegistryPlugins` and copies the publish set, the targets `scripts/release/registry.py targets` reads from `.github/plugin-registry.json`, to `build/release/plugins`. Both build universal. They run `lipo -verify_arch` once per arch (arm64, x86_64), since Xcode 27's `lipo` takes one arch per call, then `verify-plugin-loads.sh`. -- `scripts/release/package.sh app 0.0.0-dev --unsigned` writes the release assets: the DMG, the `.tar.xz` (extracted and compared with the app) and the dSYMs zip; `package.sh plugins` writes one zip per plugin and the plugin dSYMs zip. +- `scripts/release/package.sh app --unsigned` writes the release assets: the DMG, the `.tar.xz` (extracted and compared with the app) and the dSYMs zip; `package.sh plugins` writes one zip per plugin and the plugin dSYMs zip. - The plugins build runs after an app failure too. The xcodebuild logs in `build/release/logs` upload only on failure. ## Release, Withdraw and Pages @@ -114,11 +114,11 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/ - `detect-changed-paths.sh [--base ] macos|plugins|ios|oracle|hana|release` prints `true` or `false`. An unknown suite or flag exits 2. - A PR passes its base SHA, a push `github.event.before`. No base prints true. The all-zero SHA of a new ref diffs against `git merge-base HEAD origin/main`, so a branch cut from main skips what main already ran; with no merge base it prints true. -- `macos` watches the app, test, plugin, package, native, config and libs folders, `TestPlans/`, `project.yml`, the app's `Package.resolved`, the build scripts and CI scripts it runs, both setup actions and its workflow. `Packages/` covers TableProMacCore. +- `macos` watches the app, test, plugin, package, native, config and libs folders, `TestPlans/`, `project.yml`, the app's `Package.resolved`, the build scripts and CI scripts it runs, the setup actions and its workflow. `Packages/` covers TableProMacCore. - `plugins` watches `Plugins/`, `Packages/TableProCore/`, `Packages/TableProOracle/`, `Native/`, `Libs/`, `Configs/`, `project.yml`, the bridge scripts and `verify-plugin-loads.sh`. - `ios` watches `TableProMobile/`, `Libs/`, `Plugins/TableProPluginKit/` (the package target is a symlink to it), and every `../` path it reads out of `TableProMobile/project.yml`. - `oracle` watches `Packages/TableProOracle/`; `hana` watches `Native/HanaBridge/`. Each suite also watches its own workflow. -- `release` watches what the Release build reads: the app, plugin, package, native, config and libs folders, `project.yml`, the app's `Package.resolved`, `.github/plugin-registry.json`, `scripts/release/`, the build and CI scripts it runs and both setup actions. It skips `Plugins/*Tests/` and `Packages/*/Tests/`. +- `release` watches what the Release build reads: the app, plugin, package, native, config and libs folders, `project.yml`, the app's `Package.resolved`, `.github/plugin-registry.json`, the release scripts it runs (`build.sh`, `package.sh`, `registry.py`, `version.py`, `notes.py`), the build and CI scripts it runs and the setup actions. It skips `Plugins/*Tests/` and `Packages/*/Tests/`. - A path a suite newly reads joins that suite's pattern in the same change, or the suite skips on it. - `CHANGELOG.md` and `changelog.d/` are on no suite's pattern: the What's New phase writes nothing at `0.0.0-dev`, and Repo Hygiene renders every fragment and section instead. diff --git a/.claude/rules/native-bridges.md b/.claude/rules/native-bridges.md index a77c344421..b42b88184e 100644 --- a/.claude/rules/native-bridges.md +++ b/.claude/rules/native-bridges.md @@ -12,4 +12,4 @@ paths: - **First-party bridge code lives in `Native/Bridge`**, built by `scripts/build-.sh`, with output in a gitignored folder beside the source, never in `Libs/`. - **Rust may run in the app process; Go may not.** The Dameng staticlib is built with `panic = "unwind"`, and every export that does driver work wraps it in `catch_unwind`; a new export that does work takes the same guard. Go's `recover()` cannot catch a panic on a goroutine go-hdb starts, so HANA runs out of process as `tablepro-hana-helper`, one per driver session. - **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves. -- **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `project.yml` signs the HANA helper with the plugin's identity, and `scripts/release/build.sh plugins` checks every executable with `codesign -dvvv`. +- **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `project.yml` signs the HANA helper with the plugin's identity; the plugins' notary submission rejects any executable signed otherwise, and `scripts/release/notarize.sh` prints the log. diff --git a/.github/actions/apple-signing/action.yml b/.github/actions/apple-signing/action.yml index ba9f90e1bb..9a230d9909 100644 --- a/.github/actions/apple-signing/action.yml +++ b/.github/actions/apple-signing/action.yml @@ -79,17 +79,5 @@ runs: run: | set -euo pipefail profiles="$HOME/Library/MobileDevice/Provisioning Profiles" - decoded="$RUNNER_TEMP/profile.plist" mkdir -p "$profiles" - echo "$PROVISIONING_PROFILE" | base64 --decode > "$RUNNER_TEMP/tablepro.provisionprofile" - security cms -D -i "$RUNNER_TEMP/tablepro.provisionprofile" > "$decoded" - name="$(plutil -extract Name raw -o - "$decoded")" - uuid="$(plutil -extract UUID raw -o - "$decoded")" - expiry="$(plutil -extract ExpirationDate raw -o - "$decoded")" - mv "$RUNNER_TEMP/tablepro.provisionprofile" "$profiles/$uuid.provisionprofile" - rm -f "$decoded" - # Both are ISO 8601 in UTC, so they compare as strings. - if [[ "$expiry" < "$(date -u -v+60d +%Y-%m-%dT%H:%M:%SZ)" ]]; then - echo "::warning::The provisioning profile '$name' expires on $expiry" - fi - echo "Installed the provisioning profile '$name' ($uuid), valid until $expiry" + printf '%s' "$PROVISIONING_PROFILE" | base64 --decode > "$profiles/tablepro.provisionprofile" diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml new file mode 100644 index 0000000000..28f2061c03 --- /dev/null +++ b/.github/actions/prepare-build/action.yml @@ -0,0 +1,34 @@ +name: Prepare build +description: Installs XcodeGen, downloads the static libraries, generates TablePro.xcodeproj and restores the Swift package checkouts. + +outputs: + cache-hit: + description: The restore step's cache-hit output. + value: ${{ steps.spm.outputs.cache-hit }} + cache-primary-key: + description: The restore step's cache-primary-key output. + value: ${{ steps.spm.outputs.cache-primary-key }} + +runs: + using: composite + steps: + - uses: ./.github/actions/setup-xcodegen + + - name: Download static libraries + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: scripts/download-libs.sh + + - name: Generate the Xcode project + shell: bash + run: scripts/generate-project.sh macos + + # macos-tests.yml saves this cache. + - name: Restore Swift package checkouts + id: spm + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.spm-cache + key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: spm-${{ runner.os }}- diff --git a/.github/workflows/macos-tests.yml b/.github/workflows/macos-tests.yml index 71750d9bc0..f0d2cdb270 100644 --- a/.github/workflows/macos-tests.yml +++ b/.github/workflows/macos-tests.yml @@ -54,24 +54,8 @@ jobs: - uses: ./.github/actions/setup-xcode - - uses: ./.github/actions/setup-xcodegen - - - name: Download static libraries - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-libs.sh - - - name: Generate the Xcode project - run: scripts/generate-project.sh macos - - # Saved on a push only: a cache saved on a pull request ref is visible to that PR alone. - - name: Restore Swift package checkouts - id: spm - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.spm-cache - key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-${{ runner.os }}- + - uses: ./.github/actions/prepare-build + id: prepare - name: Resolve Swift packages run: | @@ -79,12 +63,13 @@ jobs: -project TablePro.xcodeproj -scheme TablePro \ -clonedSourcePackagesDirPath ~/.spm-cache -skipPackagePluginValidation + # Saved on a push only: a cache saved on a pull request ref is visible to that PR alone. - name: Save Swift package checkouts - if: github.event_name == 'push' && steps.spm.outputs.cache-hit != 'true' + if: github.event_name == 'push' && steps.prepare.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.spm-cache - key: ${{ steps.spm.outputs.cache-primary-key }} + key: ${{ steps.prepare.outputs.cache-primary-key }} # One build covers both test plans: build-for-testing writes an .xctestrun per plan. - name: Build for testing diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 6f0cc9ce04..2456ecebc5 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -8,11 +8,6 @@ on: branches: [main, 'release/**'] workflow_dispatch: -# Each commit has a group of its own and is never cancelled, as in macos-tests.yml. -concurrency: - group: release-build-${{ github.sha }} - cancel-in-progress: false - permissions: contents: read @@ -44,28 +39,16 @@ jobs: if: ${{ !cancelled() && needs.changes.outputs.run != 'false' }} runs-on: xcode-27 timeout-minutes: 150 + # The tag's version settings run here first; 9999999 is the largest build at MAJOR 0 (RELEASING.md, Versions). + env: + VERSION: 0.0.0-dev + BUILD: "9999999" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup-xcode - - uses: ./.github/actions/setup-xcodegen - - - name: Download static libraries - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-libs.sh - - - name: Generate the Xcode project - run: scripts/generate-project.sh macos - - # macos-tests.yml saves this cache. - - name: Restore Swift package checkouts - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.spm-cache - key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-${{ runner.os }}- + - uses: ./.github/actions/prepare-build - name: Set up Go for the SAP HANA helper uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 @@ -79,9 +62,6 @@ jobs: scripts/build-dameng.sh both scripts/build-hana.sh both - - name: Install create-dmg - run: brew list create-dmg > /dev/null 2>&1 || brew install create-dmg - - name: Build the app id: app run: scripts/release/build.sh app --unsigned @@ -94,11 +74,11 @@ jobs: - name: Package the app if: ${{ !cancelled() && steps.app.outcome == 'success' }} - run: scripts/release/package.sh app 0.0.0-dev --unsigned + run: scripts/release/package.sh app --unsigned - name: Package the plugins if: ${{ !cancelled() && steps.plugins.outcome == 'success' }} - run: scripts/release/package.sh plugins 0.0.0-dev --unsigned + run: scripts/release/package.sh plugins --unsigned - name: Upload the build logs if: ${{ failure() }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a07bdddf8..1400bacf6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,33 +108,14 @@ jobs: - name: Check the Production CloudKit schema env: CLOUDKIT_MANAGEMENT_TOKEN: ${{ secrets.CLOUDKIT_MANAGEMENT_TOKEN }} - run: | - set -euo pipefail - xcrun cktool save-token --type management --method file --force "$CLOUDKIT_MANAGEMENT_TOKEN" - scripts/export-cloudkit-schema.sh --check - - - uses: ./.github/actions/setup-xcodegen - - - name: Download static libraries - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-libs.sh + run: scripts/export-cloudkit-schema.sh --check - name: Write Secrets.xcconfig env: ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }} run: printf 'ANALYTICS_HMAC_SECRET = %s\n' "$ANALYTICS_HMAC_SECRET" > Configs/Secrets.xcconfig - - name: Generate the Xcode project - run: scripts/generate-project.sh macos - - # macos-tests.yml saves this cache; a save in tag scope would be unreadable from other refs. - - name: Restore Swift package checkouts - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.spm-cache - key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-${{ runner.os }}- + - uses: ./.github/actions/prepare-build - uses: ./.github/actions/apple-signing with: @@ -145,9 +126,6 @@ jobs: asc-issuer-id: ${{ secrets.ASC_ISSUER_ID }} asc-key-p8: ${{ secrets.ASC_KEY_P8 }} - - name: Install create-dmg - run: brew list create-dmg > /dev/null 2>&1 || brew install create-dmg - - name: Build the app run: scripts/release/build.sh app @@ -155,7 +133,7 @@ jobs: run: scripts/release/notarize.sh build/release/TablePro.app - name: Package the app - run: scripts/release/package.sh app "$VERSION" + run: scripts/release/package.sh app - name: Notarize the disk image run: scripts/release/notarize.sh "dist/TablePro-$VERSION.dmg" @@ -185,22 +163,7 @@ jobs: with: require-release: ${{ needs.verify.outputs.channel == 'stable' }} - - uses: ./.github/actions/setup-xcodegen - - - name: Download static libraries - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-libs.sh - - - name: Generate the Xcode project - run: scripts/generate-project.sh macos - - - name: Restore Swift package checkouts - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.spm-cache - key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} - restore-keys: spm-${{ runner.os }}- + - uses: ./.github/actions/prepare-build - name: Set up Go for the SAP HANA helper uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 @@ -229,7 +192,7 @@ jobs: run: scripts/release/notarize.sh build/release/plugins/*.tableplugin - name: Package the plugins - run: scripts/release/package.sh plugins "$VERSION" + run: scripts/release/package.sh plugins - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/Configs/Base.xcconfig b/Configs/Base.xcconfig index 9f76303ed8..eea62f3c4b 100644 --- a/Configs/Base.xcconfig +++ b/Configs/Base.xcconfig @@ -62,9 +62,7 @@ STRINGS_FILE_OUTPUT_ENCODING = binary SWIFT_VERSION = 6.0 CODE_SIGN_STYLE = Automatic -// Signing knobs. The app targets and the plugin bundles sign with this team; the helper tool -// signs ad hoc. A contributor overrides these in Configs/Secrets.xcconfig without touching a -// tracked file, and the release scripts pass their own values on the xcodebuild command line. +// A contributor overrides these in Configs/Secrets.xcconfig. TABLEPRO_DEVELOPMENT_TEAM = D7HJ5TFYCU TABLEPRO_APP_BUNDLE_IDENTIFIER = com.TablePro diff --git a/RELEASING.md b/RELEASING.md index bf3b904bc9..f7ea803e3c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -82,7 +82,7 @@ python3 scripts/release/notes.py docs X.Y.Z --ref vX.Y.Z --mdx ../tablepro-docs/ A `v*` tag runs `release.yml` and nothing else. A run is never cancelled, and every job reads the version, build and channel from `verify`. - `verify`: `version.py check` refuses a tag that breaks the grammar or the bounds; that `origin/release/` does not contain; whose previous tag on the line is not its ancestor; a stable X.Y.0 with no published beta, or a beta after its X.Y.0; a `CHANGELOG.md` missing the stable's heading, or a published stable of this or the previous line below it. Then it needs macOS Tests, iOS and Package Tests and Release Build green on the tag commit or the nearest first-parent commit that ran them, waiting on a pending run. Last, it writes the notes. -- `app` (`signing`): a GA Xcode for a stable (`setup-xcode` `require-release`), the CloudKit Production schema check, `build.sh app` (archive, export with `scripts/release/ExportOptions.plist`, universal, signature and plugin load checks), `notarize.sh` on the app, `package.sh app`, `notarize.sh` on the DMG. +- `app` (`signing`): a GA Xcode for a stable (`setup-xcode` `require-release`), the CloudKit Production schema check, `build.sh app` (archive, export with `scripts/release/ExportOptions.plist`, universal and plugin load checks), `notarize.sh` on the app, `package.sh app`, `notarize.sh` on the DMG. - `plugins` (`signing`): the Dameng and HANA bridges, `build.sh plugins` (each target `registry.py targets` prints, universal, signed with Developer ID, the hardened runtime and a timestamp), one notary submission for all, `package.sh plugins` (the zips, described as the manifest will read them). - `github-release`: `publish.py draft` puts every asset on a draft and checks each digest, `attest-build-provenance` attests them, `publish.py release` publishes. - `feed` (`release`): `feed.py add` downloads the archive and the zips from the release, checks their digests, runs `generate_appcast` and `registry.py add`, runs `feed.py check`, and pushes `feeds`, retrying from a fresh fetch up to 3 times. @@ -114,7 +114,7 @@ gh workflow run withdraw.yml --ref v -f version=X.Y.Z | `github-pages` | tags `v*` | none | - No repo-level secret or deploy key is left, and no environment has required reviewers: admin-only, immutable tags are the gate. Rulesets: `.claude/rules/ci.md`, Rulesets. -- Notarization uses the App Store Connect API key, which `.github/actions/apple-signing` stores as the `TablePro` notarytool profile. The action warns when the certificate or the provisioning profile expires within 60 days. +- Notarization uses the App Store Connect API key, which `.github/actions/apple-signing` stores as the `TablePro` notarytool profile. The action warns when the certificate expires within 60 days. - An offline backup of the Sparkle EdDSA key exists and the maintainer holds it. The key never rotates without a signed transition, since every app from 0.82 verifies the feed with it. - `feeds` takes pushes from `github-actions[bot]` with `GITHUB_TOKEN` and has no required checks; the signed feed proves its content. It was seeded once with the legacy `appcast.xml` and an empty v3 manifest. @@ -141,10 +141,11 @@ git add Libs/release Libs/checksums.sha256 Libs/ios/checksums.sha256 && git comm ## Reproducing a Release build ```bash -scripts/download-libs.sh && scripts/generate-project.sh macos && brew install create-dmg -scripts/release/build.sh app --unsigned && scripts/release/package.sh app 0.0.0-dev --unsigned +export VERSION=0.0.0-dev BUILD=9999999 +scripts/download-libs.sh && scripts/generate-project.sh macos +scripts/release/build.sh app --unsigned && scripts/release/package.sh app --unsigned scripts/build-dameng.sh both && scripts/build-hana.sh both -scripts/release/build.sh plugins --unsigned && scripts/release/package.sh plugins 0.0.0-dev --unsigned +scripts/release/build.sh plugins --unsigned && scripts/release/package.sh plugins --unsigned ``` -- `build.sh` writes `build/release/` (xcodebuild logs in `build/release/logs/`), `package.sh` writes `dist/`. Release Build (`release-build.yml`) runs the same on every push to `main` and `release/**`. Signed: drop `--unsigned`, set `VERSION` and `BUILD`, and have the Developer ID identity and the provisioning profile `ExportOptions.plist` names. `notarize.sh ...` needs the profile `xcrun notarytool store-credentials TablePro --key --key-id --issuer ` creates. +- `build.sh` writes `build/release/` (xcodebuild logs in `build/release/logs/`), `package.sh` writes `dist/`. CI builds the same in Release Build (`.claude/rules/ci.md`, Release Build). Signed: drop `--unsigned`, export the tag's `VERSION` and `BUILD`, and have the Developer ID identity and the provisioning profile `ExportOptions.plist` names. `notarize.sh ...` needs the profile `xcrun notarytool store-credentials TablePro --key --key-id --issuer ` creates. diff --git a/scripts/ci/detect-changed-paths.sh b/scripts/ci/detect-changed-paths.sh index 6eab0add36..03c2f64cf4 100755 --- a/scripts/ci/detect-changed-paths.sh +++ b/scripts/ci/detect-changed-paths.sh @@ -24,7 +24,8 @@ case "$SUITE" in 'TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$' 'scripts/(generate-project|download-libs|create-openssl-dylibs|build-dameng|build-hana)\.sh$' 'scripts/lib/' 'scripts/ci/(detect-changed-paths|summarize-xcresult|verify-plugin-loads)\.sh$' - 'scripts/release/notes\.py$' '\.github/actions/setup-xcode(gen)?/' '\.github/workflows/macos-tests\.yml$') ;; + 'scripts/release/notes\.py$' '\.github/actions/(setup-xcode(gen)?|prepare-build)/' + '\.github/workflows/macos-tests\.yml$') ;; plugins) # Registry plugins link the TableProCore and TableProOracle products. PATTERNS=('Plugins/' 'Packages/TableProCore/' 'Packages/TableProOracle/' 'Native/' 'Libs/' @@ -57,8 +58,9 @@ case "$SUITE" in 'TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$' '\.github/plugin-registry\.json$' 'scripts/(generate-project|download-libs|create-openssl-dylibs|build-dameng|build-hana)\.sh$' - 'scripts/lib/' 'scripts/release/' 'scripts/ci/(detect-changed-paths|verify-plugin-loads)\.sh$' - '\.github/actions/setup-xcode(gen)?/' '\.github/workflows/release-build\.yml$') + 'scripts/lib/' 'scripts/release/(build|package)\.sh$' 'scripts/release/(notes|registry|version)\.py$' + 'scripts/ci/(detect-changed-paths|verify-plugin-loads)\.sh$' + '\.github/actions/(setup-xcode(gen)?|prepare-build)/' '\.github/workflows/release-build\.yml$') SKIP='Plugins/[^/]+Tests/|Packages/[^/]+/Tests/' ;; *) echo "detect-changed-paths.sh: expected macos, plugins, ios, oracle, hana or release, got '$SUITE'" >&2 diff --git a/scripts/ci/test_detect_changed_paths.py b/scripts/ci/test_detect_changed_paths.py index 66a2725d05..606f0b0ddd 100644 --- a/scripts/ci/test_detect_changed_paths.py +++ b/scripts/ci/test_detect_changed_paths.py @@ -88,10 +88,14 @@ def test_release_runs_on_what_the_release_build_reads(self): ".github/plugin-registry.json", "scripts/release/build.sh", "scripts/release/package.sh", + "scripts/release/registry.py", + "scripts/release/notes.py", + "scripts/release/version.py", "scripts/build-hana.sh", "scripts/lib/common.sh", "scripts/ci/verify-plugin-loads.sh", ".github/actions/setup-xcode/action.yml", + ".github/actions/prepare-build/action.yml", ".github/workflows/release-build.yml", ]: with self.subTest(path=path): @@ -109,6 +113,11 @@ def test_release_skips_tests_docs_and_other_workflows(self): "README.md", "scripts/ci/summarize-xcresult.sh", ".github/workflows/macos-tests.yml", + "scripts/release/feed.py", + "scripts/release/notarize.sh", + "scripts/release/ExportOptions.plist", + "scripts/release/test_feed.py", + "scripts/release/requirements.txt", ]: with self.subTest(path=path): self.assertEqual(self.verdict_after("release", path), "false") @@ -119,6 +128,9 @@ def test_macos_keeps_its_test_folders_and_ignores_the_release_workflow(self): self.assertEqual(self.verdict_after("macos", ".github/workflows/release-build.yml"), "false") self.assertEqual(self.verdict_after("macos", "scripts/release/build.sh"), "false") + def test_macos_runs_on_the_build_setup_action(self): + self.assertEqual(self.verdict_after("macos", ".github/actions/prepare-build/action.yml"), "true") + def test_macos_runs_on_the_notes_script_its_build_phase_reads(self): self.assertEqual(self.verdict_after("macos", "scripts/release/notes.py"), "true") self.assertEqual(self.verdict_after("macos", "scripts/release/feed.py"), "false") diff --git a/scripts/export-cloudkit-schema.sh b/scripts/export-cloudkit-schema.sh index 3e3927b058..d0942e6b52 100755 --- a/scripts/export-cloudkit-schema.sh +++ b/scripts/export-cloudkit-schema.sh @@ -4,8 +4,7 @@ set -euo pipefail # Refresh the checked-in snapshot of the Production CloudKit schema. When to run it: # .claude/rules/data-sync-storage.md. # -# Requires a CloudKit management token: -# xcrun cktool save-token --type management +# Reads the management token from CLOUDKIT_MANAGEMENT_TOKEN, or from `xcrun cktool save-token --type management`. # # Usage: # scripts/export-cloudkit-schema.sh # overwrite the snapshot diff --git a/scripts/release/ExportOptions.plist b/scripts/release/ExportOptions.plist index 204ba1bd5c..01339a72d5 100644 --- a/scripts/release/ExportOptions.plist +++ b/scripts/release/ExportOptions.plist @@ -4,10 +4,6 @@ method developer-id - signingStyle - manual - signingCertificate - Developer ID Application iCloudContainerEnvironment Production provisioningProfiles diff --git a/scripts/release/build.sh b/scripts/release/build.sh index 702e8265ee..af14baacd3 100755 --- a/scripts/release/build.sh +++ b/scripts/release/build.sh @@ -38,7 +38,7 @@ EXPORT_OPTIONS="scripts/release/ExportOptions.plist" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT -SETTINGS=(COMPILATION_CACHE_ENABLE_CACHING=NO COMPILER_INDEX_STORE_ENABLE=NO) +SETTINGS=(COMPILATION_CACHE_ENABLE_CACHING=NO) [ -z "${VERSION:-}" ] || SETTINGS+=("MARKETING_VERSION=$VERSION") [ -z "${BUILD:-}" ] || SETTINGS+=("CURRENT_PROJECT_VERSION=$BUILD") if [ "$SIGNED" = true ]; then @@ -72,36 +72,13 @@ xcodebuild_project() { "$@" "${SETTINGS[@]}" } -is_macho() { - [ -f "$1" ] && [ ! -L "$1" ] && case "$(file -b "$1")" in *Mach-O*) true ;; *) false ;; esac -} - check_universal() { - local bundle="$1" binary arch found=0 - for binary in "$bundle/Contents/MacOS"/* "$bundle/Contents/Helpers"/*; do - is_macho "$binary" || continue + local binary arch + for binary in "$1/Contents/MacOS"/*; do # Xcode 27's lipo refuses a second arch after -verify_arch, so each is checked alone. for arch in arm64 x86_64; do lipo "$binary" -verify_arch "$arch" || die "$binary has no $arch slice: $(lipo -archs "$binary")" done - found=1 - done - [ "$found" = 1 ] || die "$bundle has no executable" -} - -# codesign --verify --strict --deep also accepts an ad-hoc signed helper, which notarization -# rejects, so each executable is checked for what notarization needs. -check_signature() { - local bundle="$1" binary details - codesign --verify --strict --deep "$bundle" || die "$bundle fails codesign --verify" - for binary in "$bundle" "$bundle/Contents/MacOS"/* "$bundle/Contents/Helpers"/*; do - [ "$binary" = "$bundle" ] || is_macho "$binary" || continue - details="$(codesign -dvvv "$binary" 2>&1)" || die "$binary is not signed: $details" - grep -qE '^CodeDirectory .*flags=0x[0-9a-f]+\([^)]*runtime' <<< "$details" \ - || die "$binary is not signed with the hardened runtime" - grep -q '^Authority=Developer ID Application: ' <<< "$details" \ - || die "$binary is not signed with a Developer ID Application identity" - grep -q '^Timestamp=' <<< "$details" || die "$binary has no secure timestamp" done } @@ -130,7 +107,6 @@ build_app() { fi check_universal "$app" - [ "$SIGNED" = false ] || check_signature "$app" scripts/ci/verify-plugin-loads.sh "$app/Contents/PlugIns" "$app/Contents/Frameworks" echo "Built $app ($(plutil -extract CFBundleShortVersionString raw -o - "$app/Contents/Info.plist"))" } @@ -151,7 +127,6 @@ build_plugins() { ditto "$plugin" "$OUT/plugins/$target.tableplugin" ditto "$plugin.dSYM" "$OUT/plugins-dSYMs/$target.tableplugin.dSYM" check_universal "$OUT/plugins/$target.tableplugin" - [ "$SIGNED" = false ] || check_signature "$OUT/plugins/$target.tableplugin" done < "$targets" mkdir "$kit" diff --git a/scripts/release/notarize.sh b/scripts/release/notarize.sh index 3ed7c70426..17a89287e7 100755 --- a/scripts/release/notarize.sh +++ b/scripts/release/notarize.sh @@ -2,7 +2,7 @@ set -euo pipefail # Usage: notarize.sh ... -# One notary submission for every path, then on each: a stapled ticket, stapler validate and spctl. +# One notary submission for every path, then on each: a stapled ticket and spctl. # A path is a bundle, or a single disk image on its own. Credentials come from the TablePro keychain # profile that .github/actions/apple-signing stores. @@ -54,19 +54,16 @@ cat "$result" echo id="$(plutil -extract id raw -o - "$result" 2> /dev/null || true)" state="$(plutil -extract status raw -o - "$result" 2> /dev/null || true)" -# notarytool has exited 0 on a rejected submission, so the status is read as well. -if [ "$status" -ne 0 ] || [ "$state" != "Accepted" ]; then - if [ -n "$id" ]; then - echo "Notary log for $id:" >&2 - xcrun notarytool log "$id" --keychain-profile "$PROFILE" >&2 || true - fi - die "the submission ended '${state:-without a status}' (notarytool exit $status)" +if [ -n "$id" ]; then + echo "Notary log for $id:" + xcrun notarytool log "$id" --keychain-profile "$PROFILE" || true fi +# notarytool has exited 0 on a rejected submission, so the status is read as well. +[ "$status" -eq 0 ] && [ "$state" = "Accepted" ] || die "the submission ended '${state:-without a status}' (notarytool exit $status)" for path in "${PATHS[@]}"; do name="$(basename "$path")" xcrun stapler staple "$path" || die "stapling $name failed" - xcrun stapler validate "$path" || die "the ticket stapled to $name does not validate" case "$path" in *.app) assessment=(-t exec) ;; *) assessment=(-t open --context context:primary-signature) ;; diff --git a/scripts/release/package.sh b/scripts/release/package.sh index dd93b90ed8..ef7b33a87f 100755 --- a/scripts/release/package.sh +++ b/scripts/release/package.sh @@ -1,12 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -# Usage: package.sh app|plugins [--unsigned] -# Turns what scripts/release/build.sh wrote to build/release into the release assets in dist/. -# BUILD is the build number build.sh got; unset, it is 1, the CURRENT_PROJECT_VERSION project.yml sets. +# Usage: package.sh app|plugins [--unsigned] +# Turns what scripts/release/build.sh wrote to build/release into the release assets in dist/, named by VERSION. usage() { - echo "Usage: package.sh app|plugins [--unsigned]" >&2 + echo "Usage: package.sh app|plugins [--unsigned]" >&2 exit 2 } @@ -16,27 +15,18 @@ die() { } KIND="" -VERSION="" SIGNED=true for arg in "$@"; do case "$arg" in + app | plugins) [ -z "$KIND" ] || usage; KIND="$arg" ;; --unsigned) SIGNED=false ;; - -*) usage ;; - *) - if [ -z "$KIND" ]; then - KIND="$arg" - elif [ -z "$VERSION" ]; then - VERSION="$arg" - else - usage - fi - ;; + *) usage ;; esac done -case "$KIND" in app | plugins) ;; *) usage ;; esac -case "$VERSION" in '' | *[!0-9A-Za-z.-]*) usage ;; esac -BUILD="${BUILD:-1}" -case "$BUILD" in '' | *[!0-9]*) echo "package.sh: BUILD must be an integer, got '$BUILD'" >&2; exit 2 ;; esac +[ -n "$KIND" ] || usage +case "${VERSION:-}" in + '' | *[!0-9A-Za-z.-]*) echo "package.sh: set VERSION, got '${VERSION:-}'" >&2; exit 2 ;; +esac cd "$(dirname "${BASH_SOURCE[0]}")/../.." @@ -51,22 +41,17 @@ package_app() { local symbols="$DIST/TablePro-$VERSION-dSYMs.zip" extracted="$WORK/extracted" [ -d "$app" ] || die "no $app; run scripts/release/build.sh app first" [ -d "$dsyms" ] || die "no $dsyms; run scripts/release/build.sh app first" - command -v create-dmg > /dev/null || die "create-dmg is not installed: brew install create-dmg" mkdir -p "$DIST" rm -f "$dmg" "$update" "$symbols" mkdir "$WORK/dmg" ditto "$app" "$WORK/dmg/TablePro.app" - create-dmg --volname "TablePro $VERSION" --volicon "$app/Contents/Resources/AppIcon.icns" \ - --window-pos 200 120 --window-size 600 400 --icon-size 80 --icon TablePro.app 150 190 \ - --hide-extension TablePro.app --app-drop-link 450 190 --format ULMO "$dmg" "$WORK/dmg" - if [ "$SIGNED" = true ]; then - codesign --sign "Developer ID Application" --timestamp "$dmg" - codesign --verify --strict "$dmg" || die "$dmg fails codesign --verify" - fi + ln -s /Applications "$WORK/dmg/Applications" + hdiutil create -volname "TablePro $VERSION" -srcfolder "$WORK/dmg" -ov -format ULFO "$dmg" + [ "$SIGNED" = false ] || codesign --sign "Developer ID Application" --timestamp "$dmg" # tar keeps the framework symlinks codesign seals; a signed bundle needs no extended attributes. - /usr/bin/tar --no-mac-metadata --no-xattrs --no-acls --uid 0 --gid 0 \ + /usr/bin/tar --no-mac-metadata --no-xattrs --uid 0 --gid 0 \ --options xz:compression-level=9 -cJf "$update" -C "$OUT" TablePro.app # Sparkle's SUPipedUnarchiver pipes a .tar.xz into this same command. @@ -86,6 +71,7 @@ package_plugins() { local plugin target symbols="$DIST/TablePro-$VERSION-Plugins-dSYMs.zip" count=0 kit [ -d "$OUT/plugins" ] || die "no $OUT/plugins; run scripts/release/build.sh plugins first" [ -d "$OUT/plugins-dSYMs" ] || die "no $OUT/plugins-dSYMs; run scripts/release/build.sh plugins first" + case "${BUILD:-}" in '' | *[!0-9]*) die "set BUILD to an integer, got '${BUILD:-}'" ;; esac mkdir -p "$DIST" rm -f "$DIST"/*-"$VERSION".zip "$symbols"