From 40405f1993413904fa4e0a88d0a4fb35ee6d72a8 Mon Sep 17 00:00:00 2001 From: zhukang <274546966@qq.com> Date: Sat, 14 Mar 2026 22:41:05 +0800 Subject: [PATCH] chore(release): prepare V1.0.2 --- .github/workflows/release.yml | 120 +++++++++++++- .../Sources/AtlasApp/AtlasAppModel.swift | 4 +- Atlas.xcodeproj/project.pbxproj | 16 +- CHANGELOG.md | 16 ++ Docs/Architecture.md | 2 + Docs/DECISIONS.md | 2 + Docs/Execution/Release-Signing.md | 74 +++++++++ README.md | 7 +- README.zh-CN.md | 7 +- project.yml | 8 +- scripts/atlas/package-native.sh | 12 +- scripts/atlas/prepare-release.sh | 105 ++++++++++++ scripts/atlas/setup-release-signing-ci.sh | 151 ++++++++++++++++++ scripts/atlas/signing-preflight.sh | 11 +- 14 files changed, 513 insertions(+), 22 deletions(-) create mode 100755 scripts/atlas/prepare-release.sh create mode 100755 scripts/atlas/setup-release-signing-ci.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d174b6..1b0cc66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,9 +54,102 @@ jobs: path: bin/*-darwin-* retention-days: 1 + native: + name: Build Native Release + runs-on: macos-latest + outputs: + packaging_mode: ${{ steps.mode.outputs.packaging_mode }} + prerelease: ${{ steps.mode.outputs.prerelease }} + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 + + - name: Derive native release version + run: | + echo "ATLAS_VERSION=${GITHUB_REF_NAME#V}" >> "$GITHUB_ENV" + echo "ATLAS_BUILD_NUMBER=${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" + + - name: Select native packaging mode + id: mode + env: + ATLAS_RELEASE_APP_CERT_P12_BASE64: ${{ secrets.ATLAS_RELEASE_APP_CERT_P12_BASE64 }} + ATLAS_RELEASE_APP_CERT_P12_PASSWORD: ${{ secrets.ATLAS_RELEASE_APP_CERT_P12_PASSWORD }} + ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64: ${{ secrets.ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64 }} + ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD: ${{ secrets.ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD }} + ATLAS_NOTARY_KEY_ID: ${{ secrets.ATLAS_NOTARY_KEY_ID }} + ATLAS_NOTARY_ISSUER_ID: ${{ secrets.ATLAS_NOTARY_ISSUER_ID }} + ATLAS_NOTARY_API_KEY_BASE64: ${{ secrets.ATLAS_NOTARY_API_KEY_BASE64 }} + run: | + required_vars=( + ATLAS_RELEASE_APP_CERT_P12_BASE64 + ATLAS_RELEASE_APP_CERT_P12_PASSWORD + ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64 + ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD + ATLAS_NOTARY_KEY_ID + ATLAS_NOTARY_API_KEY_BASE64 + ) + + missing_vars=() + for name in "${required_vars[@]}"; do + if [[ -z "${!name:-}" ]]; then + missing_vars+=("$name") + fi + done + + if [[ ${#missing_vars[@]} -eq 0 ]]; then + echo "packaging_mode=developer-id" >> "$GITHUB_OUTPUT" + echo "prerelease=false" >> "$GITHUB_OUTPUT" + echo "ATLAS_RELEASE_SIGNING_MODE=developer-id" >> "$GITHUB_ENV" + echo "Using Developer ID release packaging" + else + echo "packaging_mode=development" >> "$GITHUB_OUTPUT" + echo "prerelease=true" >> "$GITHUB_OUTPUT" + echo "ATLAS_RELEASE_SIGNING_MODE=development" >> "$GITHUB_ENV" + printf 'Falling back to development packaging; missing secrets: %s\n' "${missing_vars[*]}" + fi + + - name: Configure release signing + if: steps.mode.outputs.packaging_mode == 'developer-id' + env: + ATLAS_RELEASE_APP_CERT_P12_BASE64: ${{ secrets.ATLAS_RELEASE_APP_CERT_P12_BASE64 }} + ATLAS_RELEASE_APP_CERT_P12_PASSWORD: ${{ secrets.ATLAS_RELEASE_APP_CERT_P12_PASSWORD }} + ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64: ${{ secrets.ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64 }} + ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD: ${{ secrets.ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD }} + ATLAS_NOTARY_KEY_ID: ${{ secrets.ATLAS_NOTARY_KEY_ID }} + ATLAS_NOTARY_ISSUER_ID: ${{ secrets.ATLAS_NOTARY_ISSUER_ID }} + ATLAS_NOTARY_API_KEY_BASE64: ${{ secrets.ATLAS_NOTARY_API_KEY_BASE64 }} + run: ./scripts/atlas/setup-release-signing-ci.sh + + - name: Provision local development signing identity + if: steps.mode.outputs.packaging_mode == 'development' + run: ./scripts/atlas/ensure-local-signing-identity.sh + + - name: Validate signing prerequisites + if: steps.mode.outputs.packaging_mode == 'developer-id' + run: ./scripts/atlas/signing-preflight.sh + + - name: Build and package Atlas native app + run: ./scripts/atlas/package-native.sh + + - name: Verify DMG can install to the user Applications folder + run: KEEP_INSTALLED_APP=1 ./scripts/atlas/verify-dmg-install.sh + + - name: Upload native release artifacts + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: atlas-native-release + path: | + dist/native/Atlas-for-Mac.zip + dist/native/Atlas-for-Mac.dmg + dist/native/Atlas-for-Mac.pkg + dist/native/Atlas-for-Mac.sha256 + retention-days: 1 + release: name: Publish Release - needs: build + needs: + - build + - native runs-on: ubuntu-latest permissions: contents: write @@ -70,6 +163,24 @@ jobs: pattern: binaries-* merge-multiple: true + - name: Download native release artifacts + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: atlas-native-release + path: bin + + - name: Generate release body + run: | + if [[ "${{ needs.native.outputs.packaging_mode }}" == "development" ]]; then + { + echo "Native macOS assets in this tag were packaged in development mode because Developer ID release-signing credentials were not configured for this run." + echo + echo "These \`.zip\`, \`.dmg\`, and \`.pkg\` files are intended for internal testing or developer use. macOS Gatekeeper may require \`Open Anyway\` or a right-click \`Open\` flow before launch." + } > RELEASE_BODY.md + else + echo "Native macOS assets in this tag were packaged in CI using Developer ID signing and notarization, then uploaded alongside the existing command-line release artifacts." > RELEASE_BODY.md + fi + - name: Display structure of downloaded files run: ls -R bin/ @@ -91,6 +202,10 @@ jobs: bin/analyze-darwin-* bin/status-darwin-* bin/binaries-darwin-*.tar.gz + bin/Atlas-for-Mac.zip + bin/Atlas-for-Mac.dmg + bin/Atlas-for-Mac.pkg + bin/Atlas-for-Mac.sha256 bin/SHA256SUMS - name: Create Release @@ -99,6 +214,7 @@ jobs: with: name: ${{ github.ref_name }} files: bin/* + body_path: RELEASE_BODY.md generate_release_notes: false draft: false - prerelease: false + prerelease: ${{ needs.native.outputs.prerelease == 'true' }} diff --git a/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift b/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift index c5bb3fb..48daea6 100644 --- a/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift +++ b/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift @@ -100,11 +100,11 @@ final class AtlasAppModel: ObservableObject { } var appVersion: String { - Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.1" + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.2" } var appBuild: String { - Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "2" + Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "3" } func checkForUpdate() async { diff --git a/Atlas.xcodeproj/project.pbxproj b/Atlas.xcodeproj/project.pbxproj index f77247a..7f3767a 100644 --- a/Atlas.xcodeproj/project.pbxproj +++ b/Atlas.xcodeproj/project.pbxproj @@ -458,7 +458,7 @@ buildSettings = { AD_HOC_CODE_SIGNING_ALLOWED = YES; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)"; INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; @@ -467,7 +467,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app.worker; PRODUCT_NAME = AtlasWorkerXPC; SDKROOT = macosx; @@ -535,7 +535,7 @@ buildSettings = { AD_HOC_CODE_SIGNING_ALLOWED = YES; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)"; INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; @@ -544,7 +544,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app.worker; PRODUCT_NAME = AtlasWorkerXPC; SDKROOT = macosx; @@ -557,7 +557,7 @@ AD_HOC_CODE_SIGNING_ALLOWED = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "Atlas for Mac"; INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)"; @@ -569,7 +569,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app; PRODUCT_MODULE_NAME = AtlasApp; PRODUCT_NAME = "Atlas for Mac"; @@ -665,7 +665,7 @@ AD_HOC_CODE_SIGNING_ALLOWED = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "Atlas for Mac"; INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)"; @@ -677,7 +677,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app; PRODUCT_MODULE_NAME = AtlasApp; PRODUCT_NAME = "Atlas for Mac"; diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc34c2..e9eccb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [1.0.2] - 2026-03-14 + +### Added + +- Tag-driven GitHub Releases now publish Atlas native `.zip`, `.dmg`, and `.pkg` assets in addition to the legacy command-line binaries. +- Added `scripts/atlas/prepare-release.sh` to align app version, build number, and changelog scaffolding before tagging a release. + +### Changed + +- Release automation now falls back to a development-signed prerelease path when `Developer ID` signing credentials are unavailable, instead of blocking native packaging entirely. +- README installation guidance now distinguishes signed public releases from development prereleases and recommends local stable signing for developer packaging. + +### Fixed + +- `package-native.sh` and `signing-preflight.sh` now support `notarytool` profiles stored in a non-default keychain, which unblocks CI-based notarization. + ## [1.0.1] - 2026-03-13 ### Added diff --git a/Docs/Architecture.md b/Docs/Architecture.md index e5075de..0766a82 100644 --- a/Docs/Architecture.md +++ b/Docs/Architecture.md @@ -72,6 +72,8 @@ - Distribution target: `Developer ID + Hardened Runtime + Notarization` - Initial release target: direct distribution, not Mac App Store - Native packaging currently uses `xcodegen + xcodebuild`, embeds the helper into `Contents/Helpers/`, and emits `.zip`, `.dmg`, and `.pkg` distribution artifacts. +- Tagged GitHub Releases reuse the same native packaging scripts in CI and publish `.zip`, `.dmg`, `.pkg`, and checksum assets. +- When release signing credentials are configured, CI signs and notarizes those assets; otherwise it falls back to a local development signing identity and marks the GitHub Release as a prerelease. - Local internal packaging now prefers a stable non-ad-hoc app signature when a usable identity is available, so macOS TCC decisions can survive rebuilds more reliably during development. - If Apple release certificates are unavailable, Atlas can fall back to a repo-managed local signing keychain for stable app-bundle identity; public release artifacts still require `Developer ID`. diff --git a/Docs/DECISIONS.md b/Docs/DECISIONS.md index b08bf3d..97f2204 100644 --- a/Docs/DECISIONS.md +++ b/Docs/DECISIONS.md @@ -40,6 +40,8 @@ - Destructive helper actions use a structured executable boundary with path validation - Native MVP packaging uses `xcodegen + xcodebuild`, then embeds the helper into the app bundle +- Tagged GitHub Releases should publish the native `.zip`, `.dmg`, and `.pkg` assets from CI using the same packaging scripts as local release builds +- If CI lacks `Developer ID` release credentials, tagged native assets may still be published as development-signed prereleases instead of blocking the packaging path entirely - Signing and notarization remain optional release-time steps driven by credentials - Internal packaging should prefer a stable local app-signing identity over ad hoc signing whenever possible so macOS permission state does not drift across rebuilds diff --git a/Docs/Execution/Release-Signing.md b/Docs/Execution/Release-Signing.md index 73d52b9..076c310 100644 --- a/Docs/Execution/Release-Signing.md +++ b/Docs/Execution/Release-Signing.md @@ -16,6 +16,7 @@ Turn Atlas for Mac from an installable local build into a publicly distributable - `ATLAS_CODESIGN_KEYCHAIN` - `ATLAS_INSTALLER_SIGN_IDENTITY` - `ATLAS_NOTARY_PROFILE` +- `ATLAS_NOTARY_KEYCHAIN` (optional; required when the notary profile lives in a non-default keychain such as CI) ## Stable Local Signing @@ -43,6 +44,28 @@ Run: If preflight passes, the current machine is ready for signed packaging. +## Version Prep + +Before pushing a release tag, align the app version, build number, and changelog skeleton: + +```bash +./scripts/atlas/prepare-release.sh 1.0.2 +``` + +Optional arguments: + +```bash +./scripts/atlas/prepare-release.sh 1.0.2 3 2026-03-14 +``` + +This updates: + +- `project.yml` +- `Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift` +- `CHANGELOG.md` + +The script increments `CURRENT_PROJECT_VERSION` automatically when you omit the build number. Review the new changelog section before creating the `V1.0.2` tag. + ## Signed Packaging Run: @@ -56,6 +79,12 @@ ATLAS_NOTARY_PROFILE="" \ This signs the app bundle, emits `.zip`, `.dmg`, and `.pkg`, submits artifacts for notarization, and staples results when credentials are available. +If the notary profile is stored in a non-default keychain, also set: + +```bash +ATLAS_NOTARY_KEYCHAIN="/path/to/release.keychain-db" +``` + ## Install Verification After packaging, validate the DMG installation path with: @@ -64,7 +93,52 @@ After packaging, validate the DMG installation path with: KEEP_INSTALLED_APP=1 ./scripts/atlas/verify-dmg-install.sh ``` +## GitHub Tag Release Automation + +Tagged pushes matching `V*` now reuse the same packaging flow in CI and attach native release assets to the GitHub Release created by `.github/workflows/release.yml`. + +Required GitHub Actions secrets: + +- `ATLAS_RELEASE_APP_CERT_P12_BASE64` +- `ATLAS_RELEASE_APP_CERT_P12_PASSWORD` +- `ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64` +- `ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD` +- `ATLAS_NOTARY_KEY_ID` +- `ATLAS_NOTARY_ISSUER_ID` for Team API keys; omit only if you intentionally use an Individual API key +- `ATLAS_NOTARY_API_KEY_BASE64` + +If those secrets are present, the workflow bootstraps a temporary keychain with `./scripts/atlas/setup-release-signing-ci.sh`, stores a `notarytool` profile there, derives `ATLAS_VERSION` from the pushed tag name, then runs `./scripts/atlas/package-native.sh`. + +If those secrets are missing, the workflow automatically falls back to: + +- `./scripts/atlas/ensure-local-signing-identity.sh` +- local development signing for the app bundle +- unsigned installer packaging if no installer identity exists +- no notarization +- GitHub Release marked as `prerelease` + +Release flow: + +```bash +git tag -a V1.0.2 -m "Release V1.0.2" +git push origin V1.0.2 +``` + +That tag creates one GitHub Release containing: + +- legacy Go binaries and Homebrew tarballs from the existing release pipeline +- `Atlas-for-Mac.zip` +- `Atlas-for-Mac.dmg` +- `Atlas-for-Mac.pkg` +- native and aggregate SHA-256 checksum files + +Packaging mode by credential state: + +- `Developer ID secrets present` -> signed and notarized native assets, normal GitHub Release +- `Developer ID secrets missing` -> development-signed native assets, GitHub `prerelease` + ## Current Repo State - Internal packaging can now use a stable local app-signing identity instead of ad hoc signing. - Signed/notarized release artifacts remain blocked only by missing Apple release credentials on this machine. +- Tagged GitHub Releases can still publish development-mode native assets without those credentials. diff --git a/README.md b/README.md index 58ccca5..8eb1b2f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Download the latest release from the [Releases](https://github.com/CSZHK/CleanMy - **`.zip`** — Extract and move Atlas.app to your Applications folder. - **`.pkg`** — Run the installer package for guided installation. +Prefer the latest non-prerelease release if you want the normal public install path. GitHub prereleases may contain development-signed builds intended for testing; those builds can require `Open Anyway` or a right-click `Open` flow before launch. + ### Requirements - macOS 14.0 (Sonoma) or later @@ -51,7 +53,7 @@ xcodegen generate open Atlas.xcodeproj ``` -> **Note**: The app is currently unsigned. On first launch, you may need to right-click and select "Open" to bypass Gatekeeper, or go to System Settings > Privacy & Security to allow it. +> **Note**: Atlas release assets can be either `Developer ID signed + notarized` or `development prerelease` builds, depending on the release. If you install a prerelease or a local build, macOS may require `Open Anyway` or a right-click `Open` flow before launch. ## MVP Modules @@ -115,9 +117,12 @@ open Atlas.xcodeproj ### Package `.zip`, `.dmg`, and `.pkg` artifacts ```bash +./scripts/atlas/ensure-local-signing-identity.sh ./scripts/atlas/package-native.sh ``` +The local signing step is recommended on machines that do not have Apple release certificates. It gives local and prerelease builds a stable development signature instead of falling back to ad hoc packaging. + ### Run focused tests ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 025ff15..0de6c6f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -30,6 +30,8 @@ Atlas for Mac 是一个独立的开源项目,与 Apple、Mole 上游作者或 - **`.zip`** - 解压后将 Atlas.app 移动到 Applications 文件夹。 - **`.pkg`** - 运行安装包,按向导完成安装。 +如果你想要正常的公开安装路径,优先下载最新的非 `prerelease` 版本。GitHub 上的 `prerelease` 版本可能包含用于测试的开发签名构建,这类构建在首次启动时可能需要 `仍要打开` 或右键 `打开`。 + ### 系统要求 - macOS 14.0(Sonoma)或更高版本 @@ -51,7 +53,7 @@ xcodegen generate open Atlas.xcodeproj ``` -> **说明**:应用当前尚未签名。首次启动时,你可能需要右键点击应用并选择“打开”以绕过 Gatekeeper,或前往“系统设置 > 隐私与安全性”手动允许启动。 +> **说明**:Atlas 的发布包可能是 `Developer ID 签名 + 公证` 的正式构建,也可能是开发预发布构建,具体取决于该次发布。如果你安装的是预发布版本或本地构建版本,macOS 在首次启动时可能要求你使用 `仍要打开` 或右键 `打开`。 ## MVP 模块 @@ -115,9 +117,12 @@ open Atlas.xcodeproj ### 打包 `.zip`、`.dmg` 和 `.pkg` 产物 ```bash +./scripts/atlas/ensure-local-signing-identity.sh ./scripts/atlas/package-native.sh ``` +对于没有 Apple 发布证书的机器,建议先执行本地签名初始化。这样本地包和预发布开发包会带有稳定的开发签名,而不是退回到 ad hoc 打包。 + ### 运行聚焦测试 ```bash diff --git a/project.yml b/project.yml index c670e6a..23c323a 100644 --- a/project.yml +++ b/project.yml @@ -38,8 +38,8 @@ targets: base: PRODUCT_BUNDLE_IDENTIFIER: com.atlasformac.app.worker PRODUCT_NAME: AtlasWorkerXPC - MARKETING_VERSION: "1.0.1" - CURRENT_PROJECT_VERSION: 2 + MARKETING_VERSION: "1.0.2" + CURRENT_PROJECT_VERSION: 3 GENERATE_INFOPLIST_FILE: YES INFOPLIST_KEY_CFBundleShortVersionString: $(MARKETING_VERSION) INFOPLIST_KEY_CFBundleVersion: $(CURRENT_PROJECT_VERSION) @@ -64,8 +64,8 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: com.atlasformac.app PRODUCT_NAME: Atlas for Mac PRODUCT_MODULE_NAME: AtlasApp - MARKETING_VERSION: "1.0.1" - CURRENT_PROJECT_VERSION: 2 + MARKETING_VERSION: "1.0.2" + CURRENT_PROJECT_VERSION: 3 GENERATE_INFOPLIST_FILE: YES INFOPLIST_KEY_CFBundleShortVersionString: $(MARKETING_VERSION) INFOPLIST_KEY_CFBundleVersion: $(CURRENT_PROJECT_VERSION) diff --git a/scripts/atlas/package-native.sh b/scripts/atlas/package-native.sh index 80c502c..d26ab43 100755 --- a/scripts/atlas/package-native.sh +++ b/scripts/atlas/package-native.sh @@ -21,6 +21,7 @@ APP_SIGNING_KEYCHAIN="$(atlas_resolve_app_signing_keychain "$APP_SIGN_IDENTITY") APP_SIGNING_MODE="$(atlas_signing_mode_for_identity "$APP_SIGN_IDENTITY")" INSTALLER_SIGN_IDENTITY="$(atlas_resolve_installer_signing_identity)" NOTARY_PROFILE="${ATLAS_NOTARY_PROFILE:-}" +NOTARY_KEYCHAIN="${ATLAS_NOTARY_KEYCHAIN:-}" mkdir -p "$DIST_DIR" @@ -122,10 +123,15 @@ echo "Installer package: $PKG_PATH" echo "Checksums: $SHA_PATH" if [[ -n "$NOTARY_PROFILE" && "$APP_SIGNING_MODE" == "developer-id" && -n "$INSTALLER_SIGN_IDENTITY" ]]; then - xcrun notarytool submit "$PKG_PATH" --keychain-profile "$NOTARY_PROFILE" --wait + notarytool_args=(--keychain-profile "$NOTARY_PROFILE") + if [[ -n "$NOTARY_KEYCHAIN" ]]; then + notarytool_args+=(--keychain "$NOTARY_KEYCHAIN") + fi + + xcrun notarytool submit "$PKG_PATH" "${notarytool_args[@]}" --wait xcrun stapler staple "$PKG_PATH" - xcrun notarytool submit "$DMG_PATH" --keychain-profile "$NOTARY_PROFILE" --wait - xcrun notarytool submit "$ZIP_PATH" --keychain-profile "$NOTARY_PROFILE" --wait + xcrun notarytool submit "$DMG_PATH" "${notarytool_args[@]}" --wait + xcrun notarytool submit "$ZIP_PATH" "${notarytool_args[@]}" --wait xcrun stapler staple "$PACKAGED_APP_PATH" /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$PACKAGED_APP_PATH" "$ZIP_PATH" ( diff --git a/scripts/atlas/prepare-release.sh b/scripts/atlas/prepare-release.sh new file mode 100755 index 0000000..f5b20eb --- /dev/null +++ b/scripts/atlas/prepare-release.sh @@ -0,0 +1,105 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +PROJECT_FILE="$ROOT_DIR/project.yml" +APP_MODEL_FILE="$ROOT_DIR/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift" +CHANGELOG_FILE="$ROOT_DIR/CHANGELOG.md" + +usage() { + cat <<'EOF' +Usage: + ./scripts/atlas/prepare-release.sh [build-number] [release-date] + +Examples: + ./scripts/atlas/prepare-release.sh 1.0.2 + ./scripts/atlas/prepare-release.sh 1.0.2 3 + ./scripts/atlas/prepare-release.sh 1.0.2 3 2026-03-14 + +Behavior: + - updates MARKETING_VERSION and CURRENT_PROJECT_VERSION in project.yml + - updates AtlasApp fallback version/build strings + - inserts a changelog section for the requested version if it does not already exist +EOF +} + +if [[ $# -lt 1 || $# -gt 3 ]]; then + usage >&2 + exit 1 +fi + +VERSION="$1" +BUILD_NUMBER="${2:-}" +RELEASE_DATE="${3:-$(date +%F)}" + +if [[ ! "$VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version: $VERSION" >&2 + exit 1 +fi + +if [[ -n "$BUILD_NUMBER" && ! "$BUILD_NUMBER" =~ ^[0-9]+$ ]]; then + echo "Build number must be numeric: $BUILD_NUMBER" >&2 + exit 1 +fi + +if [[ ! "$RELEASE_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "Release date must use YYYY-MM-DD: $RELEASE_DATE" >&2 + exit 1 +fi + +if [[ ! -f "$PROJECT_FILE" || ! -f "$APP_MODEL_FILE" || ! -f "$CHANGELOG_FILE" ]]; then + echo "Expected release files are missing." >&2 + exit 1 +fi + +if [[ -z "$BUILD_NUMBER" ]]; then + current_build="$( + sed -n 's/.*CURRENT_PROJECT_VERSION: \([0-9][0-9]*\).*/\1/p' "$PROJECT_FILE" | head -1 + )" + if [[ -z "$current_build" ]]; then + echo "Could not determine current build number from project.yml" >&2 + exit 1 + fi + BUILD_NUMBER="$((current_build + 1))" +fi + +current_version="$( + sed -n 's/.*MARKETING_VERSION: "\(.*\)"/\1/p' "$PROJECT_FILE" | head -1 +)" + +perl -0pi -e 's/MARKETING_VERSION: "[^"]+"/MARKETING_VERSION: "'"$VERSION"'"/g' "$PROJECT_FILE" +perl -0pi -e 's/CURRENT_PROJECT_VERSION: \d+/CURRENT_PROJECT_VERSION: '"$BUILD_NUMBER"'/g' "$PROJECT_FILE" +perl -0pi -e 's/(CFBundleShortVersionString"\] as\? String \?\? )"[^"]+"/${1}"'"$VERSION"'"/' "$APP_MODEL_FILE" +perl -0pi -e 's/(CFBundleVersion"\] as\? String \?\? )"[^"]+"/${1}"'"$BUILD_NUMBER"'"/' "$APP_MODEL_FILE" + +if ! grep -Fq "## [$VERSION] - $RELEASE_DATE" "$CHANGELOG_FILE"; then + tmpfile="$(mktemp "${TMPDIR:-/tmp}/atlas-changelog.XXXXXX")" + awk -v version="$VERSION" -v date="$RELEASE_DATE" ' + BEGIN { inserted = 0 } + { + print + if (!inserted && $0 == "## [Unreleased]") { + print "" + print "## [" version "] - " date + print "" + print "### Added" + print "" + print "### Changed" + print "" + print "### Fixed" + print "" + inserted = 1 + } + } + ' "$CHANGELOG_FILE" > "$tmpfile" + mv "$tmpfile" "$CHANGELOG_FILE" +fi + +printf 'Prepared Atlas release files\n' +printf 'Previous version: %s\n' "${current_version:-UNKNOWN}" +printf 'New version: %s\n' "$VERSION" +printf 'Build number: %s\n' "$BUILD_NUMBER" +printf 'Release date: %s\n' "$RELEASE_DATE" +printf 'Updated: %s\n' "$PROJECT_FILE" +printf 'Updated: %s\n' "$APP_MODEL_FILE" +printf 'Updated: %s\n' "$CHANGELOG_FILE" diff --git a/scripts/atlas/setup-release-signing-ci.sh b/scripts/atlas/setup-release-signing-ci.sh new file mode 100755 index 0000000..aec81b6 --- /dev/null +++ b/scripts/atlas/setup-release-signing-ci.sh @@ -0,0 +1,151 @@ +#!/bin/bash +set -euo pipefail + +require_env() { + local name="$1" + if [[ -z "${!name:-}" ]]; then + echo "Missing required environment variable: $name" >&2 + exit 1 + fi +} + +write_env() { + local name="$1" + local value="$2" + + if [[ -n "${GITHUB_ENV:-}" ]]; then + printf '%s=%s\n' "$name" "$value" >> "$GITHUB_ENV" + else + printf 'export %s=%q\n' "$name" "$value" + fi +} + +append_keychain_search_list() { + local keychain_path="$1" + local current_keychains=() + local line="" + + while IFS= read -r line; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%\"}" + line="${line#\"}" + [[ -n "$line" ]] && current_keychains+=("$line") + done < <(security list-keychains -d user 2> /dev/null || true) + + if printf '%s\n' "${current_keychains[@]}" | grep -Fx "$keychain_path" > /dev/null 2>&1; then + return 0 + fi + + security list-keychains -d user -s "$keychain_path" "${current_keychains[@]}" > /dev/null +} + +decode_base64_to_file() { + local encoded="$1" + local destination="$2" + + printf '%s' "$encoded" | base64 --decode > "$destination" +} + +detect_identity() { + local policy="$1" + local prefix="$2" + local keychain_path="$3" + + security find-identity -v -p "$policy" "$keychain_path" 2> /dev/null | + sed -n "s/.*\"\\($prefix.*\\)\"/\\1/p" | + head -1 +} + +require_env ATLAS_RELEASE_APP_CERT_P12_BASE64 +require_env ATLAS_RELEASE_APP_CERT_P12_PASSWORD +require_env ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64 +require_env ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD +require_env ATLAS_NOTARY_KEY_ID +require_env ATLAS_NOTARY_API_KEY_BASE64 + +KEYCHAIN_PATH="${ATLAS_RELEASE_KEYCHAIN_PATH:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/atlas-release.keychain-db}" +KEYCHAIN_PASSWORD="${ATLAS_RELEASE_KEYCHAIN_PASSWORD:-$(uuidgen | tr '[:upper:]' '[:lower:]')}" +NOTARY_PROFILE="${ATLAS_NOTARY_PROFILE:-atlas-release}" + +tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/atlas-release-signing.XXXXXX")" +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +APP_CERT_PATH="$tmpdir/application-cert.p12" +INSTALLER_CERT_PATH="$tmpdir/installer-cert.p12" +NOTARY_KEY_PATH="$tmpdir/AuthKey.p8" + +decode_base64_to_file "$ATLAS_RELEASE_APP_CERT_P12_BASE64" "$APP_CERT_PATH" +decode_base64_to_file "$ATLAS_RELEASE_INSTALLER_CERT_P12_BASE64" "$INSTALLER_CERT_PATH" +decode_base64_to_file "$ATLAS_NOTARY_API_KEY_BASE64" "$NOTARY_KEY_PATH" + +if [[ -f "$KEYCHAIN_PATH" ]]; then + rm -f "$KEYCHAIN_PATH" +fi + +security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" > /dev/null +security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" +security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" +append_keychain_search_list "$KEYCHAIN_PATH" + +security import "$APP_CERT_PATH" \ + -k "$KEYCHAIN_PATH" \ + -P "$ATLAS_RELEASE_APP_CERT_P12_PASSWORD" \ + -f pkcs12 \ + -A \ + -T /usr/bin/codesign \ + -T /usr/bin/security \ + -T /usr/bin/productbuild > /dev/null + +security import "$INSTALLER_CERT_PATH" \ + -k "$KEYCHAIN_PATH" \ + -P "$ATLAS_RELEASE_INSTALLER_CERT_P12_PASSWORD" \ + -f pkcs12 \ + -A \ + -T /usr/bin/codesign \ + -T /usr/bin/security \ + -T /usr/bin/productbuild > /dev/null + +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" > /dev/null + +APP_IDENTITY="${ATLAS_CODESIGN_IDENTITY:-$(detect_identity codesigning 'Developer ID Application:' "$KEYCHAIN_PATH")}" +INSTALLER_IDENTITY="${ATLAS_INSTALLER_SIGN_IDENTITY:-$(detect_identity basic 'Developer ID Installer:' "$KEYCHAIN_PATH")}" + +if [[ -z "$APP_IDENTITY" ]]; then + echo "Developer ID Application identity was not imported successfully." >&2 + exit 1 +fi + +if [[ -z "$INSTALLER_IDENTITY" ]]; then + echo "Developer ID Installer identity was not imported successfully." >&2 + exit 1 +fi + +notarytool_args=( + store-credentials + "$NOTARY_PROFILE" + --key "$NOTARY_KEY_PATH" + --key-id "$ATLAS_NOTARY_KEY_ID" + --keychain "$KEYCHAIN_PATH" + --validate +) + +if [[ -n "${ATLAS_NOTARY_ISSUER_ID:-}" ]]; then + notarytool_args+=(--issuer "$ATLAS_NOTARY_ISSUER_ID") +fi + +xcrun notarytool "${notarytool_args[@]}" > /dev/null + +write_env ATLAS_CODESIGN_KEYCHAIN "$KEYCHAIN_PATH" +write_env ATLAS_CODESIGN_IDENTITY "$APP_IDENTITY" +write_env ATLAS_INSTALLER_SIGN_IDENTITY "$INSTALLER_IDENTITY" +write_env ATLAS_NOTARY_PROFILE "$NOTARY_PROFILE" +write_env ATLAS_NOTARY_KEYCHAIN "$KEYCHAIN_PATH" + +printf 'Configured Atlas release signing\n' +printf 'App identity: %s\n' "$APP_IDENTITY" +printf 'Installer identity: %s\n' "$INSTALLER_IDENTITY" +printf 'Notary profile: %s\n' "$NOTARY_PROFILE" +printf 'Keychain: %s\n' "$KEYCHAIN_PATH" diff --git a/scripts/atlas/signing-preflight.sh b/scripts/atlas/signing-preflight.sh index c27f063..6917868 100755 --- a/scripts/atlas/signing-preflight.sh +++ b/scripts/atlas/signing-preflight.sh @@ -7,6 +7,7 @@ source "$ROOT_DIR/scripts/atlas/signing-common.sh" APP_IDENTITY_OVERRIDE="${ATLAS_CODESIGN_IDENTITY:-}" INSTALLER_IDENTITY_OVERRIDE="${ATLAS_INSTALLER_SIGN_IDENTITY:-}" NOTARY_PROFILE_OVERRIDE="${ATLAS_NOTARY_PROFILE:-}" +NOTARY_KEYCHAIN_OVERRIDE="${ATLAS_NOTARY_KEYCHAIN:-}" codesign_output="$(security find-identity -v -p codesigning 2> /dev/null || true)" basic_output="$(security find-identity -v -p basic 2> /dev/null || true)" @@ -26,6 +27,9 @@ printf '======================\n' printf 'Developer ID Application: %s\n' "${app_identity:-MISSING}" printf 'Developer ID Installer: %s\n' "${installer_identity:-MISSING}" printf 'Notary profile: %s\n' "${NOTARY_PROFILE_OVERRIDE:-MISSING}" +if [[ -n "$NOTARY_KEYCHAIN_OVERRIDE" ]]; then + printf 'Notary keychain: %s\n' "$NOTARY_KEYCHAIN_OVERRIDE" +fi if [[ -n "$local_identity" ]]; then printf 'Stable local app identity: %s\n' "$local_identity" else @@ -47,7 +51,12 @@ if [[ -z "$NOTARY_PROFILE_OVERRIDE" ]]; then fi if [[ -n "$NOTARY_PROFILE_OVERRIDE" ]]; then - if xcrun notarytool history --keychain-profile "$NOTARY_PROFILE_OVERRIDE" > /dev/null 2>&1; then + notarytool_args=(history --keychain-profile "$NOTARY_PROFILE_OVERRIDE") + if [[ -n "$NOTARY_KEYCHAIN_OVERRIDE" ]]; then + notarytool_args+=(--keychain "$NOTARY_KEYCHAIN_OVERRIDE") + fi + + if xcrun notarytool "${notarytool_args[@]}" > /dev/null 2>&1; then echo '✓ notarytool profile is usable' else echo '✗ notarytool profile could not be validated'