[GH-ISSUE #86] Add signing files fetch workflow #29

Closed
opened 2026-02-26 21:32:52 +03:00 by kerem · 2 comments
Owner

Originally created by @rudrankriyam on GitHub (Jan 24, 2026).
Original GitHub issue: https://github.com/rudrankriyam/App-Store-Connect-CLI/issues/86

Summary

Add a one‑shot signing files fetch command that collects certificates + profiles for an app and writes them to disk (optionally creating missing resources).

Proposed CLI (brainstorm)

asc signing fetch --app APP_ID --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing
asc signing fetch --app APP_ID --bundle-id com.example.app --profile-type IOS_APP_DEVELOPMENT --device DEVICE_ID[,DEVICE_ID...] --create-missing

Behavior (brainstorm)

  • Fetch existing certificates and profiles first.
  • If --create-missing is set, create certificates/profiles when absent.
  • Write output files using secure creation (no overwrite by default).
  • Optional --keychain integration to import .p12 if provided.

API Endpoints

  • Bundle IDs: /v1/bundleIds
  • Certificates: /v1/certificates
  • Devices: /v1/devices
  • Profiles: /v1/profiles

Implementation Plan

  1. Build on signing CRUD (bundle IDs / certificates / devices / profiles).
  2. Add a signing fetch command that orchestrates:
    • resolve bundle ID
    • resolve certificate(s)
    • create/fetch profile
    • write .mobileprovision and certs to output dir

Tests

  • CLI validation (required flags, conflicts).
  • Orchestration tests with fake client.
  • File write safety tests.

Acceptance Criteria

  • Single command produces required signing assets for a target app/profile type.
  • Safe, deterministic outputs with JSON metadata summary.
  • No interactive prompts.
Originally created by @rudrankriyam on GitHub (Jan 24, 2026). Original GitHub issue: https://github.com/rudrankriyam/App-Store-Connect-CLI/issues/86 ## Summary Add a one‑shot signing files fetch command that collects certificates + profiles for an app and writes them to disk (optionally creating missing resources). ## Proposed CLI (brainstorm) ``` asc signing fetch --app APP_ID --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing asc signing fetch --app APP_ID --bundle-id com.example.app --profile-type IOS_APP_DEVELOPMENT --device DEVICE_ID[,DEVICE_ID...] --create-missing ``` ## Behavior (brainstorm) - Fetch existing certificates and profiles first. - If `--create-missing` is set, create certificates/profiles when absent. - Write output files using secure creation (no overwrite by default). - Optional `--keychain` integration to import .p12 if provided. ## API Endpoints - Bundle IDs: `/v1/bundleIds` - Certificates: `/v1/certificates` - Devices: `/v1/devices` - Profiles: `/v1/profiles` ## Implementation Plan 1) Build on signing CRUD (bundle IDs / certificates / devices / profiles). 2) Add a `signing fetch` command that orchestrates: - resolve bundle ID - resolve certificate(s) - create/fetch profile - write `.mobileprovision` and certs to output dir ## Tests - CLI validation (required flags, conflicts). - Orchestration tests with fake client. - File write safety tests. ## Acceptance Criteria - Single command produces required signing assets for a target app/profile type. - Safe, deterministic outputs with JSON metadata summary. - No interactive prompts.
kerem closed this issue 2026-02-26 21:32:53 +03:00
Author
Owner

@rudrankriyam commented on GitHub (Jan 24, 2026):

@cursor

Implementation Guide

Codebase Context

This is an orchestration command that builds on Issue #71 (Signing & Provisioning). It should be implemented AFTER #71 is complete.

Prerequisites

  • Issue #71 (Signing & Provisioning) must be implemented first
  • Reuses: bundle IDs, certificates, devices, profiles client methods

File Structure

1. cmd/signing_fetch.go (~300-350 lines)

func SigningFetchCommand() *ffcli.Command {
    fs := flag.NewFlagSet("signing fetch", flag.ExitOnError)

    appID := fs.String("app", "", "App ID (optional, used to filter by app)")
    bundleID := fs.String("bundle-id", "", "Bundle identifier (e.g., com.example.app) - required")
    profileType := fs.String("profile-type", "", "Profile type: IOS_APP_STORE, IOS_APP_DEVELOPMENT, MAC_APP_STORE, etc. (required)")
    deviceIDs := fs.String("device", "", "Device ID(s), comma-separated (required for development profiles)")
    certType := fs.String("certificate-type", "", "Certificate type filter (optional)")
    outputPath := fs.String("output", "./signing", "Output directory for signing files")
    createMissing := fs.Bool("create-missing", false, "Create missing profiles/certificates")
    output := fs.String("format", "json", "Output format for metadata")
    pretty := fs.Bool("pretty", false, "Pretty-print JSON")

    return &ffcli.Command{
        Name:       "fetch",
        ShortUsage: "asc signing fetch [flags]",
        ShortHelp:  "Fetch signing files (certificates + profiles) for an app.",
        LongHelp: `Fetch signing certificates and provisioning profiles for an app.

This command resolves the bundle ID, finds matching certificates and profiles,
and writes them to the output directory.

With --create-missing, it will create new profiles if none exist for the
specified configuration.

Examples:
  asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing
  asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_DEVELOPMENT --device "DEVICE1,DEVICE2"
  asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --create-missing`,
        FlagSet:   fs,
        UsageFunc: DefaultUsageFunc,
        Exec: func(ctx context.Context, args []string) error {
            // Validation
            bundle := strings.TrimSpace(*bundleID)
            if bundle == "" {
                fmt.Fprintln(os.Stderr, "Error: --bundle-id is required")
                return flag.ErrHelp
            }

            profType := strings.TrimSpace(*profileType)
            if profType == "" {
                fmt.Fprintln(os.Stderr, "Error: --profile-type is required")
                return flag.ErrHelp
            }

            // Check if development profile needs devices
            if isDevelopmentProfile(profType) && *deviceIDs == "" {
                fmt.Fprintln(os.Stderr, "Error: --device is required for development profiles")
                return flag.ErrHelp
            }

            client, err := getASCClient()
            if err \!= nil {
                return fmt.Errorf("signing fetch: %w", err)
            }

            requestCtx, cancel := contextWithTimeout(ctx)
            defer cancel()

            result := &SigningFetchResult{
                BundleID:    bundle,
                ProfileType: profType,
                OutputPath:  *outputPath,
            }

            // Step 1: Resolve bundle ID
            bundleIDResp, err := findBundleID(requestCtx, client, bundle)
            if err \!= nil {
                return fmt.Errorf("signing fetch: %w", err)
            }
            result.BundleIDResource = bundleIDResp.Data.ID

            // Step 2: Find certificates
            certs, err := findCertificates(requestCtx, client, profType, *certType)
            if err \!= nil {
                return fmt.Errorf("signing fetch: %w", err)
            }
            result.CertificateIDs = extractIDs(certs.Data)

            // Step 3: Find or create profile
            profile, err := findOrCreateProfile(requestCtx, client, bundleIDResp.Data.ID, profType, result.CertificateIDs, splitCSV(*deviceIDs), *createMissing)
            if err \!= nil {
                return fmt.Errorf("signing fetch: %w", err)
            }
            result.ProfileID = profile.Data.ID

            // Step 4: Write files to output directory
            if err := os.MkdirAll(*outputPath, 0o755); err \!= nil {
                return fmt.Errorf("signing fetch: create output dir: %w", err)
            }

            // Write profile (.mobileprovision)
            profilePath := filepath.Join(*outputPath, profile.Data.Attributes.Name+".mobileprovision")
            if err := writeProfileFile(profilePath, profile.Data.Attributes.ProfileContent); err \!= nil {
                return fmt.Errorf("signing fetch: write profile: %w", err)
            }
            result.ProfileFile = profilePath

            // Write certificates (.cer)
            for _, cert := range certs.Data {
                certPath := filepath.Join(*outputPath, cert.Attributes.SerialNumber+".cer")
                if err := writeCertificateFile(certPath, cert.Attributes.CertificateContent); err \!= nil {
                    return fmt.Errorf("signing fetch: write certificate: %w", err)
                }
                result.CertificateFiles = append(result.CertificateFiles, certPath)
            }

            return printOutput(result, *output, *pretty)
        },
    }
}

// Helper functions
func findBundleID(ctx context.Context, client *asc.Client, identifier string) (*asc.BundleIDResponse, error) {
    // List bundle IDs and filter by identifier
    resp, err := client.GetBundleIDs(ctx, asc.WithBundleIDsFilterIdentifier(identifier))
    if err \!= nil {
        return nil, err
    }
    if len(resp.Data) == 0 {
        return nil, fmt.Errorf("bundle ID not found: %s", identifier)
    }
    return &asc.BundleIDResponse{Data: resp.Data[0]}, nil
}

func findCertificates(ctx context.Context, client *asc.Client, profileType, certType string) (*asc.CertificatesResponse, error) {
    // Infer certificate type from profile type if not specified
    if certType == "" {
        certType = inferCertificateType(profileType)
    }
    return client.GetCertificates(ctx, asc.WithCertificatesFilterType(certType))
}

func findOrCreateProfile(ctx context.Context, client *asc.Client, bundleIDResourceID, profileType string, certIDs, deviceIDs []string, createMissing bool) (*asc.ProfileResponse, error) {
    // Try to find existing profile
    profiles, err := client.GetProfiles(ctx,
        asc.WithProfilesFilterBundleID(bundleIDResourceID),
        asc.WithProfilesFilterType(profileType))
    if err \!= nil {
        return nil, err
    }

    // Return first valid profile
    for _, p := range profiles.Data {
        if p.Attributes.ProfileState == "ACTIVE" {
            return &asc.ProfileResponse{Data: p}, nil
        }
    }

    if \!createMissing {
        return nil, fmt.Errorf("no active profile found; use --create-missing to create one")
    }

    // Create new profile
    return client.CreateProfile(ctx, asc.ProfileCreateAttributes{
        Name:        fmt.Sprintf("%s-%s", profileType, time.Now().Format("20060102")),
        ProfileType: profileType,
        BundleIDID:  bundleIDResourceID,
        CertificateIDs: certIDs,
        DeviceIDs:   deviceIDs,
    })
}

func isDevelopmentProfile(profileType string) bool {
    return strings.Contains(profileType, "DEVELOPMENT") || strings.Contains(profileType, "AD_HOC")
}

func inferCertificateType(profileType string) string {
    switch {
    case strings.Contains(profileType, "DEVELOPMENT"):
        return "IOS_DEVELOPMENT"
    case strings.Contains(profileType, "APP_STORE"):
        return "IOS_DISTRIBUTION"
    case strings.Contains(profileType, "AD_HOC"):
        return "IOS_DISTRIBUTION"
    default:
        return ""
    }
}

func writeProfileFile(path, base64Content string) error {
    data, err := base64.StdEncoding.DecodeString(base64Content)
    if err \!= nil {
        return fmt.Errorf("decode profile: %w", err)
    }

    // Use secure file creation (no overwrite, no symlink)
    file, err := openNewFileNoFollow(path, 0o644)
    if err \!= nil {
        return err
    }
    defer file.Close()

    if _, err := file.Write(data); err \!= nil {
        return err
    }
    return file.Sync()
}

func writeCertificateFile(path, base64Content string) error {
    data, err := base64.StdEncoding.DecodeString(base64Content)
    if err \!= nil {
        return fmt.Errorf("decode certificate: %w", err)
    }

    file, err := openNewFileNoFollow(path, 0o644)
    if err \!= nil {
        return err
    }
    defer file.Close()

    if _, err := file.Write(data); err \!= nil {
        return err
    }
    return file.Sync()
}

2. internal/asc/signing_fetch.go (~50 lines)

// Result type for signing fetch
type SigningFetchResult struct {
    BundleID          string   `json:"bundleId"`
    BundleIDResource  string   `json:"bundleIdResourceId"`
    ProfileType       string   `json:"profileType"`
    ProfileID         string   `json:"profileId"`
    ProfileFile       string   `json:"profileFile"`
    CertificateIDs    []string `json:"certificateIds"`
    CertificateFiles  []string `json:"certificateFiles"`
    OutputPath        string   `json:"outputPath"`
    Created           bool     `json:"created,omitempty"` // true if profile was created
}

3. Add output formatting in internal/asc/signing_output.go

func printSigningFetchResultTable(result *SigningFetchResult) error
func printSigningFetchResultMarkdown(result *SigningFetchResult) error

4. Add to output_core.go switch statements

5. Register as subcommand of SigningCommand()
In cmd/signing.go (from #71), add SigningFetchCommand() to Subcommands.

Profile Types Reference

iOS:
- IOS_APP_DEVELOPMENT
- IOS_APP_STORE
- IOS_APP_ADHOC
- IOS_APP_INHOUSE

macOS:
- MAC_APP_DEVELOPMENT  
- MAC_APP_STORE
- MAC_APP_DIRECT

tvOS:
- TVOS_APP_DEVELOPMENT
- TVOS_APP_STORE
- TVOS_APP_ADHOC

CLI Usage Examples

# Fetch App Store signing files
asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing

# Fetch development signing files (requires devices)
asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_DEVELOPMENT --device "UDID1,UDID2" --output ./signing

# Create missing profile if needed
asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing --create-missing

# Specify certificate type
asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --certificate-type IOS_DISTRIBUTION --output ./signing

Output Files

./signing/
├── MyApp-Distribution.mobileprovision
├── ABC123DEF456.cer
└── metadata.json  (optional: summary of what was fetched)

Testing

  • Run make test && make lint
  • Test development profile requires --device flag
  • Test file writes use secure creation (no overwrite)
  • Test --create-missing flow
<!-- gh-comment-id:3795332225 --> @rudrankriyam commented on GitHub (Jan 24, 2026): @cursor ## Implementation Guide ### Codebase Context This is an orchestration command that builds on Issue #71 (Signing & Provisioning). It should be implemented AFTER #71 is complete. ### Prerequisites - Issue #71 (Signing & Provisioning) must be implemented first - Reuses: bundle IDs, certificates, devices, profiles client methods ### File Structure **1. `cmd/signing_fetch.go`** (~300-350 lines) ```go func SigningFetchCommand() *ffcli.Command { fs := flag.NewFlagSet("signing fetch", flag.ExitOnError) appID := fs.String("app", "", "App ID (optional, used to filter by app)") bundleID := fs.String("bundle-id", "", "Bundle identifier (e.g., com.example.app) - required") profileType := fs.String("profile-type", "", "Profile type: IOS_APP_STORE, IOS_APP_DEVELOPMENT, MAC_APP_STORE, etc. (required)") deviceIDs := fs.String("device", "", "Device ID(s), comma-separated (required for development profiles)") certType := fs.String("certificate-type", "", "Certificate type filter (optional)") outputPath := fs.String("output", "./signing", "Output directory for signing files") createMissing := fs.Bool("create-missing", false, "Create missing profiles/certificates") output := fs.String("format", "json", "Output format for metadata") pretty := fs.Bool("pretty", false, "Pretty-print JSON") return &ffcli.Command{ Name: "fetch", ShortUsage: "asc signing fetch [flags]", ShortHelp: "Fetch signing files (certificates + profiles) for an app.", LongHelp: `Fetch signing certificates and provisioning profiles for an app. This command resolves the bundle ID, finds matching certificates and profiles, and writes them to the output directory. With --create-missing, it will create new profiles if none exist for the specified configuration. Examples: asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_DEVELOPMENT --device "DEVICE1,DEVICE2" asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --create-missing`, FlagSet: fs, UsageFunc: DefaultUsageFunc, Exec: func(ctx context.Context, args []string) error { // Validation bundle := strings.TrimSpace(*bundleID) if bundle == "" { fmt.Fprintln(os.Stderr, "Error: --bundle-id is required") return flag.ErrHelp } profType := strings.TrimSpace(*profileType) if profType == "" { fmt.Fprintln(os.Stderr, "Error: --profile-type is required") return flag.ErrHelp } // Check if development profile needs devices if isDevelopmentProfile(profType) && *deviceIDs == "" { fmt.Fprintln(os.Stderr, "Error: --device is required for development profiles") return flag.ErrHelp } client, err := getASCClient() if err \!= nil { return fmt.Errorf("signing fetch: %w", err) } requestCtx, cancel := contextWithTimeout(ctx) defer cancel() result := &SigningFetchResult{ BundleID: bundle, ProfileType: profType, OutputPath: *outputPath, } // Step 1: Resolve bundle ID bundleIDResp, err := findBundleID(requestCtx, client, bundle) if err \!= nil { return fmt.Errorf("signing fetch: %w", err) } result.BundleIDResource = bundleIDResp.Data.ID // Step 2: Find certificates certs, err := findCertificates(requestCtx, client, profType, *certType) if err \!= nil { return fmt.Errorf("signing fetch: %w", err) } result.CertificateIDs = extractIDs(certs.Data) // Step 3: Find or create profile profile, err := findOrCreateProfile(requestCtx, client, bundleIDResp.Data.ID, profType, result.CertificateIDs, splitCSV(*deviceIDs), *createMissing) if err \!= nil { return fmt.Errorf("signing fetch: %w", err) } result.ProfileID = profile.Data.ID // Step 4: Write files to output directory if err := os.MkdirAll(*outputPath, 0o755); err \!= nil { return fmt.Errorf("signing fetch: create output dir: %w", err) } // Write profile (.mobileprovision) profilePath := filepath.Join(*outputPath, profile.Data.Attributes.Name+".mobileprovision") if err := writeProfileFile(profilePath, profile.Data.Attributes.ProfileContent); err \!= nil { return fmt.Errorf("signing fetch: write profile: %w", err) } result.ProfileFile = profilePath // Write certificates (.cer) for _, cert := range certs.Data { certPath := filepath.Join(*outputPath, cert.Attributes.SerialNumber+".cer") if err := writeCertificateFile(certPath, cert.Attributes.CertificateContent); err \!= nil { return fmt.Errorf("signing fetch: write certificate: %w", err) } result.CertificateFiles = append(result.CertificateFiles, certPath) } return printOutput(result, *output, *pretty) }, } } // Helper functions func findBundleID(ctx context.Context, client *asc.Client, identifier string) (*asc.BundleIDResponse, error) { // List bundle IDs and filter by identifier resp, err := client.GetBundleIDs(ctx, asc.WithBundleIDsFilterIdentifier(identifier)) if err \!= nil { return nil, err } if len(resp.Data) == 0 { return nil, fmt.Errorf("bundle ID not found: %s", identifier) } return &asc.BundleIDResponse{Data: resp.Data[0]}, nil } func findCertificates(ctx context.Context, client *asc.Client, profileType, certType string) (*asc.CertificatesResponse, error) { // Infer certificate type from profile type if not specified if certType == "" { certType = inferCertificateType(profileType) } return client.GetCertificates(ctx, asc.WithCertificatesFilterType(certType)) } func findOrCreateProfile(ctx context.Context, client *asc.Client, bundleIDResourceID, profileType string, certIDs, deviceIDs []string, createMissing bool) (*asc.ProfileResponse, error) { // Try to find existing profile profiles, err := client.GetProfiles(ctx, asc.WithProfilesFilterBundleID(bundleIDResourceID), asc.WithProfilesFilterType(profileType)) if err \!= nil { return nil, err } // Return first valid profile for _, p := range profiles.Data { if p.Attributes.ProfileState == "ACTIVE" { return &asc.ProfileResponse{Data: p}, nil } } if \!createMissing { return nil, fmt.Errorf("no active profile found; use --create-missing to create one") } // Create new profile return client.CreateProfile(ctx, asc.ProfileCreateAttributes{ Name: fmt.Sprintf("%s-%s", profileType, time.Now().Format("20060102")), ProfileType: profileType, BundleIDID: bundleIDResourceID, CertificateIDs: certIDs, DeviceIDs: deviceIDs, }) } func isDevelopmentProfile(profileType string) bool { return strings.Contains(profileType, "DEVELOPMENT") || strings.Contains(profileType, "AD_HOC") } func inferCertificateType(profileType string) string { switch { case strings.Contains(profileType, "DEVELOPMENT"): return "IOS_DEVELOPMENT" case strings.Contains(profileType, "APP_STORE"): return "IOS_DISTRIBUTION" case strings.Contains(profileType, "AD_HOC"): return "IOS_DISTRIBUTION" default: return "" } } func writeProfileFile(path, base64Content string) error { data, err := base64.StdEncoding.DecodeString(base64Content) if err \!= nil { return fmt.Errorf("decode profile: %w", err) } // Use secure file creation (no overwrite, no symlink) file, err := openNewFileNoFollow(path, 0o644) if err \!= nil { return err } defer file.Close() if _, err := file.Write(data); err \!= nil { return err } return file.Sync() } func writeCertificateFile(path, base64Content string) error { data, err := base64.StdEncoding.DecodeString(base64Content) if err \!= nil { return fmt.Errorf("decode certificate: %w", err) } file, err := openNewFileNoFollow(path, 0o644) if err \!= nil { return err } defer file.Close() if _, err := file.Write(data); err \!= nil { return err } return file.Sync() } ``` **2. `internal/asc/signing_fetch.go`** (~50 lines) ```go // Result type for signing fetch type SigningFetchResult struct { BundleID string `json:"bundleId"` BundleIDResource string `json:"bundleIdResourceId"` ProfileType string `json:"profileType"` ProfileID string `json:"profileId"` ProfileFile string `json:"profileFile"` CertificateIDs []string `json:"certificateIds"` CertificateFiles []string `json:"certificateFiles"` OutputPath string `json:"outputPath"` Created bool `json:"created,omitempty"` // true if profile was created } ``` **3. Add output formatting** in `internal/asc/signing_output.go` ```go func printSigningFetchResultTable(result *SigningFetchResult) error func printSigningFetchResultMarkdown(result *SigningFetchResult) error ``` **4. Add to output_core.go switch statements** **5. Register as subcommand of SigningCommand()** In `cmd/signing.go` (from #71), add `SigningFetchCommand()` to Subcommands. ### Profile Types Reference ``` iOS: - IOS_APP_DEVELOPMENT - IOS_APP_STORE - IOS_APP_ADHOC - IOS_APP_INHOUSE macOS: - MAC_APP_DEVELOPMENT - MAC_APP_STORE - MAC_APP_DIRECT tvOS: - TVOS_APP_DEVELOPMENT - TVOS_APP_STORE - TVOS_APP_ADHOC ``` ### CLI Usage Examples ```bash # Fetch App Store signing files asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing # Fetch development signing files (requires devices) asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_DEVELOPMENT --device "UDID1,UDID2" --output ./signing # Create missing profile if needed asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output ./signing --create-missing # Specify certificate type asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --certificate-type IOS_DISTRIBUTION --output ./signing ``` ### Output Files ``` ./signing/ ├── MyApp-Distribution.mobileprovision ├── ABC123DEF456.cer └── metadata.json (optional: summary of what was fetched) ``` ### Testing - Run `make test && make lint` - Test development profile requires --device flag - Test file writes use secure creation (no overwrite) - Test --create-missing flow
Author
Owner

@cursor[bot] commented on GitHub (Jan 24, 2026):

Summary:

  • Added the new asc signing fetch command with secure file writes, bundle/profile/certificate orchestration, and optional profile creation.
  • Implemented signing client support (bundle IDs, certificates, devices, profiles) plus output formatting for signing fetch results.
  • Wired the signing command into the root CLI and added tests for validation and safe file creation.

Tests:

  • make test && make lint

Note:

  • --create-missing currently creates profiles only; if no certificates exist for the resolved type, the command returns an error.

View PR
Open in Cursor Open in Web

<!-- gh-comment-id:3795332307 --> @cursor[bot] commented on GitHub (Jan 24, 2026): Summary: - Added the new `asc signing fetch` command with secure file writes, bundle/profile/certificate orchestration, and optional profile creation. - Implemented signing client support (bundle IDs, certificates, devices, profiles) plus output formatting for signing fetch results. - Wired the signing command into the root CLI and added tests for validation and safe file creation. Tests: - `make test && make lint` Note: - `--create-missing` currently creates profiles only; if no certificates exist for the resolved type, the command returns an error. <a href="https://github.com/rudrankriyam/App-Store-Connect-CLI/pull/107"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/view-pr-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/view-pr-light.svg"><img alt="View PR" src="https://cursor.com/view-pr-light.svg"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-f4da6314-2817-47e7-aadc-ab4ced5ccf9a"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"><img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?id=bc-f4da6314-2817-47e7-aadc-ab4ced5ccf9a"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"><img alt="Open in Web" src="https://cursor.com/open-in-web.svg"></picture></a>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/App-Store-Connect-CLI#29
No description provided.