Add notarytool wrapper supporting api-key and apple-id
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
83904d8f8c
commit
708cc0c864
92
internal/notarize/notarize.go
Normal file
92
internal/notarize/notarize.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package notarize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/leonmika/wails-release/internal/runner"
|
||||
)
|
||||
|
||||
// APIKey is the App Store Connect API key credential set.
|
||||
type APIKey struct {
|
||||
KeyPath string
|
||||
KeyID string
|
||||
IssuerID string
|
||||
}
|
||||
|
||||
// AppleID is the legacy Apple ID + app-specific password credential set.
|
||||
type AppleID struct {
|
||||
Username string
|
||||
Password string
|
||||
TeamID string
|
||||
}
|
||||
|
||||
// Opts configures a notarization submission. Exactly one of APIKey or
|
||||
// AppleID must be set.
|
||||
type Opts struct {
|
||||
ZipPath string
|
||||
APIKey *APIKey
|
||||
AppleID *AppleID
|
||||
}
|
||||
|
||||
// Submit ships ZipPath to Apple's notary service and waits for the result.
|
||||
// Rejected submissions cause the per-submission log to be fetched and
|
||||
// embedded in the returned error.
|
||||
func Submit(ctx context.Context, r runner.Runner, opts Opts) error {
|
||||
if (opts.APIKey == nil) == (opts.AppleID == nil) {
|
||||
return fmt.Errorf("notarize: exactly one of APIKey or AppleID must be set")
|
||||
}
|
||||
credArgs := credentialArgs(opts)
|
||||
|
||||
args := append([]string{"notarytool", "submit", opts.ZipPath}, credArgs...)
|
||||
args = append(args, "--wait", "--output-format", "json")
|
||||
|
||||
out, err := r.Run(ctx, runner.Spec{Name: "xcrun", Args: args})
|
||||
if err != nil {
|
||||
return fmt.Errorf("notarytool submit: %w", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if jerr := json.Unmarshal(out, &result); jerr != nil {
|
||||
return fmt.Errorf("notarytool submit: parse output: %w (raw: %s)", jerr, string(out))
|
||||
}
|
||||
if result.Status == "Accepted" {
|
||||
return nil
|
||||
}
|
||||
|
||||
logOut, _ := r.Run(ctx, runner.Spec{
|
||||
Name: "xcrun",
|
||||
Args: append([]string{"notarytool", "log", result.ID}, credArgs...),
|
||||
})
|
||||
return fmt.Errorf("notarization %s: %s\n%s", result.Status, result.ID, string(logOut))
|
||||
}
|
||||
|
||||
// Staple runs `xcrun stapler staple` against the bundle.
|
||||
func Staple(ctx context.Context, r runner.Runner, appPath string) error {
|
||||
if _, err := r.Run(ctx, runner.Spec{
|
||||
Name: "xcrun",
|
||||
Args: []string{"stapler", "staple", appPath},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("stapler staple: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func credentialArgs(opts Opts) []string {
|
||||
if opts.APIKey != nil {
|
||||
return []string{
|
||||
"--key", opts.APIKey.KeyPath,
|
||||
"--key-id", opts.APIKey.KeyID,
|
||||
"--issuer", opts.APIKey.IssuerID,
|
||||
}
|
||||
}
|
||||
return []string{
|
||||
"--apple-id", opts.AppleID.Username,
|
||||
"--password", opts.AppleID.Password,
|
||||
"--team-id", opts.AppleID.TeamID,
|
||||
}
|
||||
}
|
||||
120
internal/notarize/notarize_test.go
Normal file
120
internal/notarize/notarize_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package notarize_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/leonmika/wails-release/internal/notarize"
|
||||
"github.com/leonmika/wails-release/internal/runner"
|
||||
)
|
||||
|
||||
func TestSubmit_APIKeyArgs(t *testing.T) {
|
||||
f := &runner.Fake{}
|
||||
f.On("xcrun", nil).Return([]byte(`{"id":"sub-1","status":"Accepted"}`), nil)
|
||||
|
||||
err := notarize.Submit(context.Background(), f, notarize.Opts{
|
||||
ZipPath: "/dist/MyApp.app.zip",
|
||||
APIKey: ¬arize.APIKey{KeyPath: "/tmp/key.p8", KeyID: "ABCD1234", IssuerID: "iss"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %v", err)
|
||||
}
|
||||
want := []string{
|
||||
"notarytool", "submit", "/dist/MyApp.app.zip",
|
||||
"--key", "/tmp/key.p8",
|
||||
"--key-id", "ABCD1234",
|
||||
"--issuer", "iss",
|
||||
"--wait",
|
||||
"--output-format", "json",
|
||||
}
|
||||
if !reflect.DeepEqual(f.Calls[0].Args, want) {
|
||||
t.Fatalf("args got %v want %v", f.Calls[0].Args, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmit_AppleIDArgs(t *testing.T) {
|
||||
f := &runner.Fake{}
|
||||
f.On("xcrun", nil).Return([]byte(`{"id":"1","status":"Accepted"}`), nil)
|
||||
|
||||
err := notarize.Submit(context.Background(), f, notarize.Opts{
|
||||
ZipPath: "/dist/MyApp.app.zip",
|
||||
AppleID: ¬arize.AppleID{Username: "u@x", Password: "pw", TeamID: "TEAM1234"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %v", err)
|
||||
}
|
||||
want := []string{
|
||||
"notarytool", "submit", "/dist/MyApp.app.zip",
|
||||
"--apple-id", "u@x",
|
||||
"--password", "pw",
|
||||
"--team-id", "TEAM1234",
|
||||
"--wait",
|
||||
"--output-format", "json",
|
||||
}
|
||||
if !reflect.DeepEqual(f.Calls[0].Args, want) {
|
||||
t.Fatalf("args got %v want %v", f.Calls[0].Args, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmit_RejectionFetchesLogAndReturnsError(t *testing.T) {
|
||||
f := &runner.Fake{}
|
||||
// First call: submit returns Invalid status with id.
|
||||
f.On("xcrun", []string{
|
||||
"notarytool", "submit", "/dist/MyApp.app.zip",
|
||||
"--key", "/tmp/key.p8", "--key-id", "K", "--issuer", "I",
|
||||
"--wait", "--output-format", "json",
|
||||
}).Return([]byte(`{"id":"sub-1","status":"Invalid"}`), nil)
|
||||
|
||||
// Second call: log fetch.
|
||||
f.On("xcrun", []string{
|
||||
"notarytool", "log", "sub-1",
|
||||
"--key", "/tmp/key.p8", "--key-id", "K", "--issuer", "I",
|
||||
}).Return([]byte(`{"issues":[{"message":"missing entitlement"}]}`), nil)
|
||||
|
||||
err := notarize.Submit(context.Background(), f, notarize.Opts{
|
||||
ZipPath: "/dist/MyApp.app.zip",
|
||||
APIKey: ¬arize.APIKey{KeyPath: "/tmp/key.p8", KeyID: "K", IssuerID: "I"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing entitlement") {
|
||||
t.Fatalf("expected log content in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmit_NoCredentialsErrors(t *testing.T) {
|
||||
err := notarize.Submit(context.Background(), &runner.Fake{}, notarize.Opts{ZipPath: "/x"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmit_ToolFailureWraps(t *testing.T) {
|
||||
f := &runner.Fake{}
|
||||
f.On("xcrun", nil).Return(nil, errors.New("network down"))
|
||||
err := notarize.Submit(context.Background(), f, notarize.Opts{
|
||||
ZipPath: "/x",
|
||||
APIKey: ¬arize.APIKey{KeyPath: "/k", KeyID: "K", IssuerID: "I"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaple_BuildsCorrectArgs(t *testing.T) {
|
||||
f := &runner.Fake{}
|
||||
f.On("xcrun", nil).Return(nil, nil)
|
||||
|
||||
err := notarize.Staple(context.Background(), f, "/build/MyApp.app")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %v", err)
|
||||
}
|
||||
want := []string{"stapler", "staple", "/build/MyApp.app"}
|
||||
if !reflect.DeepEqual(f.Calls[0].Args, want) {
|
||||
t.Fatalf("args got %v want %v", f.Calls[0].Args, want)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue