Wire orchestrator end-to-end
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9c4e4675c7
commit
97fb47d023
|
|
@ -1,9 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
"github.com/leonmika/wails-release/internal/actions"
|
||||
"github.com/leonmika/wails-release/internal/archive"
|
||||
"github.com/leonmika/wails-release/internal/cleanup"
|
||||
"github.com/leonmika/wails-release/internal/codesign"
|
||||
"github.com/leonmika/wails-release/internal/config"
|
||||
"github.com/leonmika/wails-release/internal/notarize"
|
||||
"github.com/leonmika/wails-release/internal/runner"
|
||||
"github.com/leonmika/wails-release/internal/upload"
|
||||
"github.com/leonmika/wails-release/internal/version"
|
||||
"github.com/leonmika/wails-release/internal/wails"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -11,5 +28,213 @@ func main() {
|
|||
fmt.Fprintln(os.Stderr, "wails-release: only macOS runners are supported")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("wails-release: stub")
|
||||
if err := run(context.Background()); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "wails-release:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
cfg := config.Load(os.Getenv)
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maskSecrets(cfg)
|
||||
|
||||
r := runner.Real{}
|
||||
var cs cleanup.Stack
|
||||
defer func() {
|
||||
for _, e := range cs.Run() {
|
||||
fmt.Fprintln(os.Stderr, "cleanup:", e)
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. Resolve version + app name
|
||||
resolvedVersion := version.Resolve(cfg.Version, os.Getenv("GITHUB_REF"), os.Getenv("GITHUB_SHA"))
|
||||
|
||||
project, err := wails.ReadProject(cfg.WorkingDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appName := cfg.AppName
|
||||
if appName == "" {
|
||||
appName = project.Name
|
||||
}
|
||||
|
||||
// 2. Wails CLI
|
||||
wailsVersion := cfg.WailsVersion
|
||||
if wailsVersion == "" {
|
||||
wailsVersion, err = wails.ProjectWailsVersion(cfg.WorkingDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := wails.EnsureCLI(ctx, r, wailsVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Build
|
||||
if err := wails.Build(ctx, r, wails.BuildOpts{
|
||||
Dir: cfg.WorkingDirectory,
|
||||
ExtraFlags: cfg.ExtraBuildFlags,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
appPath := filepath.Join(cfg.WorkingDirectory, "build", "bin", appName+".app")
|
||||
|
||||
// 4. Codesign — temp keychain + import + sign
|
||||
tmpDir, err := os.MkdirTemp("", "wails-release-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("mktemp: %w", err)
|
||||
}
|
||||
cs.Add(func() error { return os.RemoveAll(tmpDir) })
|
||||
|
||||
keychainPath := filepath.Join(tmpDir, "build.keychain-db")
|
||||
keychainPW := randomHex(16)
|
||||
kc, err := codesign.CreateKeychain(ctx, r, keychainPath, keychainPW)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cs.Add(func() error { return codesign.DeleteKeychain(ctx, r, *kc) })
|
||||
|
||||
p12Path := filepath.Join(tmpDir, "cert.p12")
|
||||
if err := writeBase64(p12Path, cfg.DeveloperIDCertBase64); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := codesign.ImportP12(ctx, r, *kc, p12Path, cfg.DeveloperIDCertPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
identity, err := codesignIdentityFromP12(ctx, r, *kc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := codesign.Sign(ctx, r, codesign.SignOpts{
|
||||
AppPath: appPath, Identity: identity, KeychainPath: kc.Path,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := codesign.Verify(ctx, r, appPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Archive (pre-staple zip for notary submission)
|
||||
distDir := filepath.Join(cfg.WorkingDirectory, "build", "bin")
|
||||
artifactName := fmt.Sprintf("%s-%s.app.zip", appName, resolvedVersion)
|
||||
zipPath := filepath.Join(distDir, artifactName)
|
||||
if err := archive.ZipApp(ctx, r, appPath, zipPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 6. Notarize + staple + re-archive
|
||||
notarOpts := notarize.Opts{ZipPath: zipPath}
|
||||
switch cfg.ResolvedNotarizationMethod() {
|
||||
case "api-key":
|
||||
p8Path := filepath.Join(tmpDir, "key.p8")
|
||||
if err := writeBase64(p8Path, cfg.NotarizationAPIKeyBase64); err != nil {
|
||||
return err
|
||||
}
|
||||
notarOpts.APIKey = ¬arize.APIKey{
|
||||
KeyPath: p8Path,
|
||||
KeyID: cfg.NotarizationAPIKeyID,
|
||||
IssuerID: cfg.NotarizationAPIIssuerID,
|
||||
}
|
||||
case "apple-id":
|
||||
notarOpts.AppleID = ¬arize.AppleID{
|
||||
Username: cfg.NotarizationAppleID,
|
||||
Password: cfg.NotarizationApplePassword,
|
||||
TeamID: cfg.NotarizationTeamID,
|
||||
}
|
||||
}
|
||||
if err := notarize.Submit(ctx, r, notarOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notarize.Staple(ctx, r, appPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := archive.ZipApp(ctx, r, appPath, zipPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. Upload (optional)
|
||||
s3URL := ""
|
||||
if cfg.S3Bucket != "" {
|
||||
client, err := upload.NewClient(ctx, cfg.S3Region, cfg.S3EndpointURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := upload.RenderKey(cfg.S3Key, resolvedVersion, artifactName)
|
||||
s3URL, err = upload.Upload(ctx, client, upload.Opts{
|
||||
Bucket: cfg.S3Bucket, Key: key, FilePath: zipPath,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Outputs
|
||||
outFile := os.Getenv("GITHUB_OUTPUT")
|
||||
for k, v := range map[string]string{
|
||||
"version": resolvedVersion,
|
||||
"app-name": appName,
|
||||
"artifact-path": zipPath,
|
||||
"artifact-filename": artifactName,
|
||||
"s3-url": s3URL,
|
||||
} {
|
||||
if err := actions.SetOutput(outFile, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maskSecrets(c *config.Config) {
|
||||
for _, v := range []string{
|
||||
c.DeveloperIDCertPassword,
|
||||
c.NotarizationAPIKeyBase64,
|
||||
c.NotarizationApplePassword,
|
||||
} {
|
||||
actions.AddMask(os.Stdout, v)
|
||||
}
|
||||
}
|
||||
|
||||
func writeBase64(path, encoded string) error {
|
||||
raw, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode base64 → %s: %w", path, err)
|
||||
}
|
||||
return os.WriteFile(path, raw, 0o600)
|
||||
}
|
||||
|
||||
func randomHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// extremely unlikely; fall back to a less-strong but functional value
|
||||
return "fallback-keychain-pw"
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
var devIDRE = regexp.MustCompile(`"(Developer ID Application: [^"]+)"`)
|
||||
|
||||
// codesignIdentityFromP12 finds the imported "Developer ID Application: …"
|
||||
// identity in the keychain so the caller does not have to thread it through
|
||||
// inputs.
|
||||
func codesignIdentityFromP12(ctx context.Context, r runner.Runner, kc codesign.Keychain) (string, error) {
|
||||
out, err := r.Run(ctx, runner.Spec{
|
||||
Name: "security",
|
||||
Args: []string{"find-identity", "-p", "codesigning", "-v", kc.Path},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find-identity: %w", err)
|
||||
}
|
||||
return parseDeveloperIDIdentity(out)
|
||||
}
|
||||
|
||||
func parseDeveloperIDIdentity(out []byte) (string, error) {
|
||||
if m := devIDRE.FindStringSubmatch(string(out)); m != nil {
|
||||
return m[1], nil
|
||||
}
|
||||
return "", fmt.Errorf("no Developer ID Application identity in keychain (output: %s)", string(out))
|
||||
}
|
||||
|
|
|
|||
22
cmd/wails-release/main_test.go
Normal file
22
cmd/wails-release/main_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseDeveloperIDIdentity(t *testing.T) {
|
||||
out := []byte(` 1) ABCD1234ABCDEF "Developer ID Application: Acme Inc (TEAM1234)"
|
||||
1 valid identities found
|
||||
`)
|
||||
got, err := parseDeveloperIDIdentity(out)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %v", err)
|
||||
}
|
||||
if got != "Developer ID Application: Acme Inc (TEAM1234)" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeveloperIDIdentity_NoneFound(t *testing.T) {
|
||||
if _, err := parseDeveloperIDIdentity([]byte("0 valid identities found")); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue