Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
aa25ddc
workflow: derive the export method from the provisioning profile
Interlap01 Sep 16, 2026
b587320
docs: explain that the export method follows the profile
Interlap01 Sep 16, 2026
c35ddfb
workflow: set manageAppVersionAndBuildNumber with plutil
Interlap01 Sep 16, 2026
94eda37
docs: correct the provider signing doc for the export method
Interlap01 Sep 16, 2026
3833980
config: add build profiles and their resolution
Interlap01 Sep 16, 2026
cd92fbb
build: select a profile with --profile on ios build and ios share
Interlap01 Sep 16, 2026
63da3b6
workflows: apply the profile on the runner and export its env
Interlap01 Sep 16, 2026
7ef5346
docs: describe build profiles
Interlap01 Sep 16, 2026
f5872af
config: reserve the runner's secrets, shell and CI namespaces in prof…
Interlap01 Sep 16, 2026
9644933
build: explain a dispatch rejected by a workflow without the profile …
Interlap01 Sep 16, 2026
3c06c58
workflows: random GITHUB_ENV heredoc delimiter and a type check on env
Interlap01 Sep 16, 2026
3c883da
docs: defaultProfile also needs refreshed workflows; list reserved en…
Interlap01 Sep 16, 2026
636fb52
workflow: tolerate CRLF checkouts in the export-method test
Interlap01 Sep 16, 2026
9880363
asc: add App Store Connect API client
Interlap01 Sep 16, 2026
6bf8b32
auth: store App Store Connect API keys
Interlap01 Sep 16, 2026
364e38f
ios: upload IPAs to App Store Connect
Interlap01 Sep 16, 2026
146e287
ios: submit builds to TestFlight and App Review
Interlap01 Sep 16, 2026
ef25c19
docs: describe the TestFlight and App Store commands
Interlap01 Sep 16, 2026
d617332
build: pass profile settings and build options by pointer
Interlap01 Sep 16, 2026
7845248
asc: make waits injectable, back off status polls, drop unused helpers
Interlap01 Sep 16, 2026
23b9fd7
ios: print no JSON on a nil result and name the ASC_* variables
Interlap01 Sep 16, 2026
7aa74d6
docs: Release configuration prerequisite, drop roadmap item numbers
Interlap01 Sep 16, 2026
5c43085
asc: bundle ID, certificate, device and profile endpoints
Interlap01 Sep 16, 2026
e15399f
signing: provision certificates, devices and profiles through the ASC…
Interlap01 Sep 16, 2026
1abbeb5
signing: make setup automatic with the App Store Connect API key
Interlap01 Sep 16, 2026
ab63330
docs: automatic signing setup is the primary path
Interlap01 Sep 16, 2026
620099e
signing: check devices and write the key before requesting a certificate
Interlap01 Sep 16, 2026
21ea5f8
signing: register only devices whose MobAI ID is a UDID
Interlap01 Sep 16, 2026
d4293b7
docs: export method must follow the profile type
Interlap01 Sep 16, 2026
f66dfdb
Merge branch 'feat/export-method' into feat/signing-sets
Interlap01 Sep 16, 2026
d9974f1
Merge branch 'feat/build-profiles' into feat/signing-sets
Interlap01 Sep 16, 2026
f86a7ab
config: map a profile's distribution to its signing set
Interlap01 Sep 16, 2026
022e1a9
signing: read the profile type locally and name files by type
Interlap01 Sep 16, 2026
f385fdd
signing: upload setup material to the set of its type
Interlap01 Sep 16, 2026
0f8a66c
workflow: select the signing set by distribution and check the profil…
Interlap01 Sep 16, 2026
ce30801
docs: signing sets
Interlap01 Sep 16, 2026
cc0c28d
workflow: require a complete signing set and check the profile before…
Interlap01 Sep 16, 2026
afd81cf
workflow: tolerate CRLF checkouts in the signing set test
Interlap01 Sep 16, 2026
aedb6c9
docs: explain signing sets in the provider secrets guide
Interlap01 Sep 16, 2026
b5ed28b
github: list repository secret names
Interlap01 Sep 16, 2026
be0c51d
config: distribution is the only signing field of a profile
Interlap01 Sep 16, 2026
61de30c
signing: name types after distributions and share the test portal
Interlap01 Sep 16, 2026
8d54df8
build: print the signing set on the signing line
Interlap01 Sep 16, 2026
f4f38bd
signing: setup writes a build profile and ios build provisions a miss…
Interlap01 Sep 16, 2026
a1932ee
workflow: derive signing from the profile's distribution
Interlap01 Sep 16, 2026
a08adcd
docs: signing profiles with distribution
Interlap01 Sep 16, 2026
340d98b
github: explain 403 and 404 when listing secrets
Interlap01 Sep 16, 2026
8743b35
config: point app-store at store
Interlap01 Sep 16, 2026
f70abcd
build: check the signing set of the provider that runs the job
Interlap01 Sep 16, 2026
56b89b1
signing: keep a profile's spelling and report a replaced distribution
Interlap01 Sep 16, 2026
2d62ef9
docs: keep README line endings, say what signing setup leaves alone
Interlap01 Sep 16, 2026
afd4ad2
signing: --provider on setup and a hint when a build's secrets cannot…
Interlap01 Sep 16, 2026
13715af
signing: always print the secret values and treat a failed GitHub upl…
Interlap01 Sep 16, 2026
98cbfad
asc: match MobAI's key check, error window and profile delete
Interlap01 Sep 16, 2026
4f769e4
signing: write the private key as PKCS#8, read PKCS#1 too
Interlap01 Sep 16, 2026
cdb8bb8
docs: walk through creating the App Store Connect API key; drop the c…
Interlap01 Sep 17, 2026
c44fa52
auth: say what auth apple saved
Interlap01 Sep 17, 2026
410325a
Merge branch 'feat/asc-client' into feat/signing-sets
Interlap01 Sep 17, 2026
2e9b6af
workflow: sign the archive with the identity the profile type needs
Interlap01 Sep 17, 2026
be46cb5
workflow: accept legacy iPhone Distribution and Developer certificates
Interlap01 Sep 17, 2026
152608a
docs: explain creating the App Store Connect app record
Interlap01 Sep 17, 2026
87e5daa
share: drop --profile; the simulator build takes no profile
Interlap01 Sep 17, 2026
5534520
signing: reuse the key from signing setup's --out-dir when provisioni…
Interlap01 Sep 17, 2026
dd662e4
signing: satisfy errcheck and unparam in the setup tests
Interlap01 Sep 17, 2026
6919ef9
build: print the failed step and the runner's errors when a run fails
Interlap01 Sep 17, 2026
ac9aef5
workflow: set manual signing on the app target only, not on every Pod…
Interlap01 Sep 17, 2026
896f7eb
workflow: match the Flutter build line by prefix in the signing test
Interlap01 Sep 17, 2026
e921ce7
ci: say why Codemagic or Bitrise rejected a request
Interlap01 Sep 17, 2026
7092e0e
asc: stop claiming the Apple key went to the keychain
Interlap01 Sep 17, 2026
1f66ee8
asc: trim comments and review fixes
Interlap01 Sep 17, 2026
9cd22f3
auth: drop the unrelated tail from the Apple key message
Interlap01 Sep 17, 2026
adf3655
docs: keep README CRLF line endings
Interlap01 Sep 17, 2026
90811ef
signing: trim comments and review fixes
Interlap01 Sep 17, 2026
b0372b2
Merge branch 'feat/asc-client' into feat/signing-sets
Interlap01 Sep 17, 2026
e97f798
asc: page through the app lookup instead of trusting a two-item filter
Interlap01 Sep 17, 2026
555822e
docs: extension targets are not signed yet
Interlap01 Sep 17, 2026
9388923
Merge branch 'feat/asc-client' into feat/signing-sets
Interlap01 Sep 17, 2026
3156445
signing: provision and upload profiles for extension targets
Interlap01 Sep 17, 2026
54eb503
workflow: sign extension targets with their own profiles
Interlap01 Sep 17, 2026
a944716
docs: extension targets are signed with their own profiles
Interlap01 Sep 17, 2026
15d7cab
Merge remote-tracking branch 'origin/main' into feat/signing-sets
Interlap01 Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 108 additions & 45 deletions CLAUDE.md

Large diffs are not rendered by default.

1,620 changes: 883 additions & 737 deletions README.md

Large diffs are not rendered by default.

80 changes: 76 additions & 4 deletions cmd/builder/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,44 @@ func detectIOSPath() (string, string) {
return "", ""
}

// bundleIDRe matches PRODUCT_BUNDLE_IDENTIFIER assignments in a project.pbxproj.
var bundleIDRe = regexp.MustCompile(`PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";\s]+)"?\s*;`)

// detectBundleID reads the app's bundle identifier from the Xcode project
// under iosPath, skipping test targets and $(…) values. Anything still
// ambiguous yields "" so init leaves the field for `signing setup` to resolve.
func detectBundleID(iosPath string) string {
if iosPath == "" {
iosPath = "."
}
projects, _ := filepath.Glob(filepath.Join(iosPath, "*.xcodeproj", "project.pbxproj"))
var found []string
for _, path := range projects {
data, err := os.ReadFile(path)
if err != nil {
continue
}
found = append(found, bundleIDsFromPbxproj(string(data))...)
}
if len(found) == 1 {
return found[0]
}
return ""
}

// bundleIDsFromPbxproj returns the distinct app bundle identifiers in pbxproj text.
func bundleIDsFromPbxproj(text string) []string {
var ids []string
for _, m := range bundleIDRe.FindAllStringSubmatch(text, -1) {
id := m[1]
if strings.Contains(id, "$") || strings.HasSuffix(id, "Tests") || slices.Contains(ids, id) {
continue
}
ids = append(ids, id)
}
return ids
}

func detectGitHubRepo(remoteName string) (owner, repo string, err error) {
// Try to get GitHub remote URL from git
cmd := exec.Command("git", "remote", "get-url", remoteName)
Expand Down Expand Up @@ -432,6 +470,10 @@ func runInit(cmd *cobra.Command, args []string) error {
cfg.Project, cfg.Platform = projectName, "ios"
cfg.GitHub = config.GitHubConfig{Owner: githubOwner, Repo: repoName}
cfg.IOS.Path, cfg.IOS.Scheme = iosPath, scheme
if cfg.IOS.BundleID == "" {
cfg.IOS.BundleID = detectBundleID(iosPath)
}
syncExtensions(cfg, os.Stdout)
if flutterVersion != "" {
cfg.Flutter.Version = flutterVersion
}
Expand Down Expand Up @@ -506,7 +548,7 @@ func runInit(cmd *cobra.Command, args []string) error {

if buildErr == nil {
fmt.Println()
return runBuild(context.Background(), cfg, build.BuildOptions{
return runBuild(context.Background(), cfg, &build.BuildOptions{
OutputDir: "dist",
Timeout: build.DefaultTimeout,
Remote: remoteName,
Expand Down Expand Up @@ -587,6 +629,7 @@ func init() {
iosBuildCmd.Flags().Bool("unsigned", false, "Build unsigned IPA (skip code signing even if configured)")
iosBuildCmd.Flags().StringP("remote", "r", "origin", "Git remote to push the working-tree snapshot to")
iosBuildCmd.Flags().String("provider", "", "Override CI provider (default github or builder.json provider)")
iosBuildCmd.Flags().String("profile", "", "Build profile from builder.json (default: defaultProfile, else the top-level ios settings)")
iosCmd.AddCommand(iosBuildCmd)

// iOS share command flags
Expand All @@ -596,6 +639,20 @@ func init() {
iosCmd.AddCommand(iosShareCmd)
}

// effectiveProvider is the --provider flag, else the selected profile's
// provider, else builder.json's. The coordinator resolves the same chain; this
// exists so the GitHub client and signal handling agree with it.
func effectiveProvider(cfg *config.Config, profile, flag string) (string, error) {
if flag != "" {
return flag, nil
}
s, err := cfg.ResolveProfile(profile)
if err != nil {
return "", err
}
return s.Provider, nil
}

func runIOSBuild(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
Expand All @@ -610,13 +667,18 @@ func runIOSBuild(cmd *cobra.Command, args []string) error {
timeout, _ := cmd.Flags().GetDuration("timeout")
unsigned, _ := cmd.Flags().GetBool("unsigned")
remote, _ := cmd.Flags().GetString("remote")
provider, _ := cmd.Flags().GetString("provider")
providerFlag, _ := cmd.Flags().GetString("provider")
profile, _ := cmd.Flags().GetString("profile")

ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}

provider, err := effectiveProvider(cfg, profile, providerFlag)
if err != nil {
return err
}
name, err := cfg.ProviderName(provider)
if err != nil {
return err
Expand All @@ -626,8 +688,9 @@ func runIOSBuild(cmd *cobra.Command, args []string) error {
ctx, stop = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
}
return runBuild(ctx, cfg, build.BuildOptions{
return runBuild(ctx, cfg, &build.BuildOptions{
Provider: provider,
Profile: profile,
OutputDir: outputDir,
Timeout: timeout,
Unsigned: unsigned,
Expand All @@ -646,6 +709,8 @@ func runIOSShare(cmd *cobra.Command, args []string) error {

duration, _ := cmd.Flags().GetDuration("duration")
remote, _ := cmd.Flags().GetString("remote")
// A simulator build takes no profile, so the provider is the flag, else
// builder.json's.
provider, _ := cmd.Flags().GetString("provider")

ctx := cmd.Context()
Expand Down Expand Up @@ -688,11 +753,18 @@ func runIOSShare(cmd *cobra.Command, args []string) error {
return nil
}

func runBuild(ctx context.Context, cfg *config.Config, opts build.BuildOptions) error {
func runBuild(ctx context.Context, cfg *config.Config, opts *build.BuildOptions) error {
ghClient, err := clientForProvider(cfg, opts.Provider)
if err != nil {
return err
}
// A GitHub build with a distribution needs its signing set in the
// repository; ensureSigningSecrets leaves Codemagic and Bitrise alone.
if ghClient != nil && !opts.Unsigned {
if err := ensureSigningSecrets(ctx, cfg, ghClient, getASCClient, opts.Profile, opts.Provider, os.Stdout); err != nil {
return err
}
}

coordinator := build.NewCoordinator(cfg, ghClient)

Expand Down
66 changes: 66 additions & 0 deletions cmd/builder/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"os"
"path/filepath"
"testing"
)

const flutterPbxproj = `
97C147061CF9000F007C117D /* Debug */ = {
buildSettings = {
PRODUCT_BUNDLE_IDENTIFIER = com.example.myApp;
PRODUCT_NAME = "$(TARGET_NAME)";
};
};
97C147071CF9000F007C117D /* Release */ = {
buildSettings = {
PRODUCT_BUNDLE_IDENTIFIER = com.example.myApp;
};
};
331C8088294A63A400263BE5 /* Debug */ = {
buildSettings = {
PRODUCT_BUNDLE_IDENTIFIER = com.example.myApp.RunnerTests;
};
};
`

func TestBundleIDsFromPbxproj(t *testing.T) {
if got := bundleIDsFromPbxproj(flutterPbxproj); len(got) != 1 || got[0] != "com.example.myApp" {
t.Errorf("bundleIDsFromPbxproj = %v, want [com.example.myApp]", got)
}
quoted := `PRODUCT_BUNDLE_IDENTIFIER = "com.example.my-app"; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).app";`
if got := bundleIDsFromPbxproj(quoted); len(got) != 1 || got[0] != "com.example.my-app" {
t.Errorf("bundleIDsFromPbxproj(quoted) = %v", got)
}
two := `PRODUCT_BUNDLE_IDENTIFIER = com.example.free; PRODUCT_BUNDLE_IDENTIFIER = com.example.pro;`
if got := bundleIDsFromPbxproj(two); len(got) != 2 {
t.Errorf("bundleIDsFromPbxproj(two apps) = %v", got)
}
}

func TestDetectBundleID(t *testing.T) {
dir := t.TempDir()
proj := filepath.Join(dir, "ios", "Runner.xcodeproj")
if err := os.MkdirAll(proj, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(proj, "project.pbxproj"), []byte(flutterPbxproj), 0644); err != nil {
t.Fatal(err)
}
if got := detectBundleID(filepath.Join(dir, "ios")); got != "com.example.myApp" {
t.Errorf("detectBundleID = %q", got)
}
if got := detectBundleID(filepath.Join(dir, "missing")); got != "" {
t.Errorf("detectBundleID(missing) = %q, want empty", got)
}
// Two app targets: ambiguous, leave it to signing setup.
two := filepath.Join(dir, "two", "App.xcodeproj")
if err := os.MkdirAll(two, 0755); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(two, "project.pbxproj"), []byte(`PRODUCT_BUNDLE_IDENTIFIER = com.example.free; PRODUCT_BUNDLE_IDENTIFIER = com.example.pro;`), 0644)
if got := detectBundleID(filepath.Join(dir, "two")); got != "" {
t.Errorf("detectBundleID(two apps) = %q, want empty", got)
}
}
Loading
Loading