diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 423d0b02..42dd3295 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache/restore@v4 @@ -145,7 +145,7 @@ jobs: if: ${{ ! matrix.legacy_win7 }} uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache/restore@v4 @@ -160,7 +160,7 @@ jobs: with: path: | ~/go/go_win7 - key: go_win7_1266 + key: go_win7_1267 - name: Setup Go for Windows 7 if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' env: @@ -488,7 +488,7 @@ jobs: if: ${{ ! matrix.legacy_osx }} uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache/restore@v4 @@ -503,7 +503,7 @@ jobs: with: path: | ~/go/go_osx - key: go_osx_1266 + key: go_osx_1267 - name: Setup Go for macOS 10.13 if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true' env: @@ -587,7 +587,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache@v4 @@ -690,7 +690,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache@v4 @@ -754,7 +754,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache/restore@v4 @@ -862,7 +862,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache/restore@v4 @@ -972,7 +972,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache@v4 @@ -1081,7 +1081,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules uses: actions/cache@v4 @@ -1238,7 +1238,7 @@ jobs: if: matrix.build uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules if: matrix.build @@ -1252,8 +1252,8 @@ jobs: uses: actions/cache@v4 with: path: ~/Library/Caches/go-build - key: libbox-go-build-${{ matrix.artifact }}-1.26.6-${{ hashFiles('go.sum') }} - restore-keys: libbox-go-build-${{ matrix.artifact }}-1.26.6- + key: libbox-go-build-${{ matrix.artifact }}-1.26.7-${{ hashFiles('go.sum') }} + restore-keys: libbox-go-build-${{ matrix.artifact }}-1.26.7- - name: Set tag if: matrix.build run: git tag v${{ needs.calculate_version.outputs.version }} -f @@ -1293,6 +1293,8 @@ jobs: destination: '' archive: '' upload: '' + upload_extension: '' + altool_platform: '' - name: iOS-jailbreak if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'iOS-jailbreak' }} type: jailbreak @@ -1303,6 +1305,8 @@ jobs: destination: '' archive: '' upload: '' + upload_extension: '' + altool_platform: '' steps: &build_apple_steps - name: Checkout if: matrix.if @@ -1316,7 +1320,7 @@ jobs: if: matrix.if uses: actions/setup-go@v5 with: - go-version: 1.26.6 + go-version: 1.26.7 cache: false - name: Cache Go modules if: matrix.if @@ -1367,21 +1371,23 @@ jobs: - name: Setup App Store Connect key if: matrix.if && matrix.type != 'jailbreak' && github.event_name == 'workflow_dispatch' run: |- - ASC_KEY_PATH=$RUNNER_TEMP/AuthKey.p8 + # altool selects individual key authentication by the ApiKey_ file name prefix; + # an AuthKey_ prefixed file is signed as a team key and rejected. + ASC_PRIVATE_KEYS_DIR=$RUNNER_TEMP/private_keys + mkdir -p $ASC_PRIVATE_KEYS_DIR + ASC_KEY_PATH=$ASC_PRIVATE_KEYS_DIR/ApiKey_$ASC_KEY_ID.p8 echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH + echo "API_PRIVATE_KEYS_DIR=$ASC_PRIVATE_KEYS_DIR" >> "$GITHUB_ENV" echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" - echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" env: ASC_KEY: ${{ secrets.ASC_KEY }} ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }} - name: Install Apple provisioning profiles if: matrix.if && matrix.type != 'jailbreak' run: |- PROFILES_ZIP_PATH=$RUNNER_TEMP/AppleProvisioningProfiles.zip - for index in $(seq 1 16); do - variable="PROVISIONING_PROFILES_$index" + for variable in $(compgen -v | grep -E '^PROVISIONING_PROFILES_[0-9]+$' | sort -t_ -k3 -n); do printf '%s' "${!variable}" done | base64 --decode -o $PROFILES_ZIP_PATH PROFILES_PATH="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" @@ -1442,14 +1448,33 @@ jobs: DEVELOPER_ID_CERTIFICATES_P12: ${{ secrets.DEVELOPER_ID_CERTIFICATES_P12 }} P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Setup Apple Distribution certificate + if: matrix.if && matrix.type == 'app-store' + run: |- + CERTIFICATE_PATH=$RUNNER_TEMP/AppleDistributionCertificates.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/apple-distribution.keychain-db + echo -n "$APPLE_DISTRIBUTION_CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychains -d user -s $KEYCHAIN_PATH $APPLE_DEVELOPMENT_KEYCHAIN_PATH + security default-keychain -d user -s $KEYCHAIN_PATH + security find-identity -v -p codesigning $KEYCHAIN_PATH + env: + APPLE_DISTRIBUTION_CERTIFICATES_P12: ${{ secrets.APPLE_DISTRIBUTION_CERTIFICATES_P12 }} + P12_PASSWORD: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} - name: Setup notarization credentials if: matrix.if && matrix.type == 'standalone' && github.event_name == 'workflow_dispatch' run: |- security unlock-keychain -p "$KEYCHAIN_PASSWORD" $SIGNING_KEYCHAIN_PATH - xcrun notarytool store-credentials "notarytool-password" \ + # store-credentials prompts for the issuer ID even when --issuer is absent; + # an empty answer selects individual key authentication. + printf '\n' | xcrun notarytool store-credentials "notarytool-password" \ --key $ASC_KEY_PATH \ --key-id $ASC_KEY_ID \ - --issuer $ASC_KEY_ISSUER_ID \ --keychain $SIGNING_KEYCHAIN_PATH env: KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} @@ -1483,16 +1508,17 @@ jobs: go run ./cmd/internal/merge_apple_xcframework \ -output clients/apple/Libbox.xcframework \ "${library_inputs[@]}" - - name: Update macOS version - if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch' + - name: Update project version + if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' run: |- - MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version) - echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" - echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV" + PROJECT_VERSION_VARIABLE=$(echo "${{ matrix.platform }}" | tr '[:lower:]' '[:upper:]')_PROJECT_VERSION + PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_project_version ${{ matrix.platform }}) + echo "$PROJECT_VERSION_VARIABLE=$PROJECT_VERSION" + echo "$PROJECT_VERSION_VARIABLE=$PROJECT_VERSION" >> "$GITHUB_ENV" - name: Update version - if: matrix.if && matrix.name != 'iOS' + if: matrix.if run: |- - go run -v ./cmd/internal/update_apple_version --ci + go run -v ./cmd/internal/update_apple_version --ci ${{ matrix.type == 'app-store' && '--testflight' || '' }} - name: Archive App Store build if: matrix.if && matrix.type == 'app-store' run: |- @@ -1513,10 +1539,14 @@ jobs: cd clients/apple xcodebuild -exportArchive \ -archivePath "${{ matrix.archive }}" \ - -exportOptionsPlist ${{ matrix.upload }} \ - -authenticationKeyPath $ASC_KEY_PATH \ - -authenticationKeyID $ASC_KEY_ID \ - -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID + -exportPath "$RUNNER_TEMP/export" \ + -exportOptionsPlist ${{ matrix.upload }} + # altool requires --api-issuer even for an individual key, and ignores its value. + xcrun altool --upload-app \ + -f "$RUNNER_TEMP"/export/*.${{ matrix.upload_extension }} \ + -t ${{ matrix.altool_platform }} \ + --api-key $ASC_KEY_ID \ + --api-issuer 00000000-0000-0000-0000-000000000000 - name: Publish to TestFlight if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/testing' run: |- @@ -1591,7 +1621,9 @@ jobs: scheme: SFI destination: 'generic/platform=iOS' archive: build/SFI.xcarchive - upload: SFI/Upload.plist + upload: SFI/AppStoreExport.plist + upload_extension: ipa + altool_platform: ios variant: '' library_arch: '' - name: macOS @@ -1601,7 +1633,9 @@ jobs: scheme: SFM destination: 'generic/platform=macOS' archive: build/SFM.xcarchive - upload: SFI/Upload.plist + upload: SFI/AppStoreExport.plist + upload_extension: pkg + altool_platform: macos variant: '' library_arch: '' - name: tvOS @@ -1611,7 +1645,9 @@ jobs: scheme: SFT destination: 'generic/platform=tvOS' archive: build/SFT.xcarchive - upload: SFI/Upload.plist + upload: SFI/AppStoreExport.plist + upload_extension: ipa + altool_platform: appletvos variant: '' library_arch: '' steps: *build_apple_steps diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go index 0a1ab61a..8c143dbe 100644 --- a/cmd/internal/app_store_connect/main.go +++ b/cmd/internal/app_store_connect/main.go @@ -19,8 +19,11 @@ import ( func main() { ctx := context.Background() switch os.Args[1] { - case "next_macos_project_version": - err := fetchMacOSVersion(ctx) + case "next_project_version": + if len(os.Args) < 3 { + log.Fatal("platform required: ios, macos, or tvos") + } + err := fetchNextProjectVersion(ctx, os.Args[2]) if err != nil { log.Fatal(err) } @@ -50,8 +53,8 @@ func main() { } const ( - appID = "6673731168" - groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda" + appID = "6785326793" + groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4" ) func createClient(expireDuration time.Duration) *asc.Client { @@ -66,36 +69,49 @@ func createClient(expireDuration time.Duration) *asc.Client { return asc.NewClient(tokenConfig.Client()) } -func fetchMacOSVersion(ctx context.Context) error { +func fetchNextProjectVersion(ctx context.Context, platformName string) error { + var platform asc.Platform + switch platformName { + case "ios": + platform = asc.PlatformIOS + case "macos": + platform = asc.PlatformMACOS + case "tvos": + platform = asc.PlatformTVOS + default: + return E.New("unknown platform: ", platformName) + } + + query := &asc.ListBuildsQuery{ + FilterApp: []string{appID}, + FilterPreReleaseVersionPlatform: []string{string(platform)}, + Limit: 200, + } + if platform != asc.PlatformMACOS { + tagVersion, err := build_shared.ReadTagVersion() + if err != nil { + return err + } + query.FilterPreReleaseVersionVersion = []string{build_shared.TestFlightVersion(tagVersion)} + } + client := createClient(time.Minute) - versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ - FilterPlatform: []string{"MAC_OS"}, - }) + builds, _, err := client.Builds.ListBuilds(ctx, query) if err != nil { return err } - var versionID string -findVersion: - for _, version := range versions.Data { - switch *version.Attributes.AppStoreState { - case asc.AppStoreVersionStateReadyForSale, - asc.AppStoreVersionStatePendingDeveloperRelease: - versionID = version.ID - break findVersion + nextProjectVersion := 1 + var projectVersion int + for _, build := range builds.Data { + projectVersion, err = strconv.Atoi(*build.Attributes.Version) + if err != nil { + return E.Cause(err, "parse version code") + } + if projectVersion >= nextProjectVersion { + nextProjectVersion = projectVersion + 1 } } - if versionID == "" { - return E.New("no version found") - } - latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{}) - if err != nil { - return err - } - versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version) - if err != nil { - return E.Cause(err, "parse version code") - } - os.Stdout.WriteString(F.ToString(versionInt+1, "\n")) + os.Stdout.WriteString(F.ToString(nextProjectVersion, "\n")) return nil } diff --git a/cmd/internal/build_shared/tag.go b/cmd/internal/build_shared/tag.go index 15f05090..a4971a23 100644 --- a/cmd/internal/build_shared/tag.go +++ b/cmd/internal/build_shared/tag.go @@ -3,6 +3,7 @@ package build_shared import ( "github.com/sagernet/sing-box/common/badversion" "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/shell" ) @@ -36,3 +37,7 @@ func ReadTagVersion() (badversion.Version, error) { } return version, nil } + +func TestFlightVersion(version badversion.Version) string { + return F.ToString(version.Major, ".", version.Minor, ".10") +} diff --git a/cmd/internal/update_apple_version/main.go b/cmd/internal/update_apple_version/main.go index c365c051..cbfd5507 100644 --- a/cmd/internal/update_apple_version/main.go +++ b/cmd/internal/update_apple_version/main.go @@ -15,10 +15,14 @@ import ( "howett.net/plist" ) -var flagRunInCI bool +var ( + flagRunInCI bool + flagTestFlight bool +) func init() { flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") + flag.BoolVar(&flagTestFlight, "testflight", false, "Override the App Store marketing version with the version reserved for TestFlight") } func main() { @@ -41,19 +45,39 @@ func main() { common.Must(decoder.Decode(&project)) objectsMap := project["objects"].(map[string]any) projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj"))) - newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString()) - newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String()) - if updated0 || updated1 { - log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")") - } - var updated2 bool - if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" { - newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion) - if updated2 { - log.Info("updated macos project version to ", macProjectVersion) + newContent := projectContent + var marketingVersionUpdated bool + if flagTestFlight { + testFlightVersion := build_shared.TestFlightVersion(newVersion) + newContent, marketingVersionUpdated = findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfamt"}, testFlightVersion) + if marketingVersionUpdated { + log.Info("updated App Store version to ", testFlightVersion) } } - if updated0 || updated1 || updated2 { + var standaloneVersionUpdated bool + newContent, standaloneVersionUpdated = findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfamt.standalone", "io.nekohasekai.sfamt.system"}, newVersion.String()) + if standaloneVersionUpdated { + marketingVersionUpdated = true + log.Info("updated version to ", newVersion.String()) + } + var projectVersionUpdated bool + for environmentName, directory := range map[string]string{ + "IOS_PROJECT_VERSION": "SFI", + "MACOS_PROJECT_VERSION": "SFM", + "TVOS_PROJECT_VERSION": "SFT", + } { + projectVersion := os.Getenv(environmentName) + if projectVersion == "" { + continue + } + var updated bool + newContent, updated = findAndReplaceProjectVersion(objectsMap, newContent, []string{directory}, projectVersion) + if updated { + projectVersionUpdated = true + log.Info("updated ", directory, " project version to ", projectVersion) + } + } + if marketingVersionUpdated || projectVersionUpdated { common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644)) } } diff --git a/go.mod b/go.mod index 7620d2db..a2baaa74 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/openai/openai-go/v3 v3.26.0 github.com/oschwald/maxminddb-golang v1.13.1 github.com/pkg/sftp v1.13.10 - github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 + github.com/sagernet/asc-go v0.0.0-20260827085112-8f8226245b0d github.com/sagernet/bbolt v0.0.0-20260823094646-e24805439c9c github.com/sagernet/cors v1.2.1 github.com/sagernet/cronet-go v0.0.0-20260824154604-9247da29c718 diff --git a/go.sum b/go.sum index 33d65aa0..ca87bfd4 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9q github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= -github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 h1:qi+ijeREa0yfAaO+NOcZ81gv4uzOfALUIdhkiIFvmG4= -github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1/go.mod h1:JULDuzTMn2gyZFcjpTVZP4/UuwAdbHJ0bum2RdjXojU= +github.com/sagernet/asc-go v0.0.0-20260827085112-8f8226245b0d h1:r3JBMjxwI8BPLprJpklmcgoPT3r7rJfUg4uuGWky48A= +github.com/sagernet/asc-go v0.0.0-20260827085112-8f8226245b0d/go.mod h1:JULDuzTMn2gyZFcjpTVZP4/UuwAdbHJ0bum2RdjXojU= github.com/sagernet/bbolt v0.0.0-20260823094646-e24805439c9c h1:JdEDkgiwsDxiZb4+dQYgVOU/lND+jZxFmN5y/q49EXk= github.com/sagernet/bbolt v0.0.0-20260823094646-e24805439c9c/go.mod h1:7/qFPFH5fqK5XQeY8EA1Qpaly08Av77p38Zt50ieQww= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=