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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"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() {
|
func main() {
|
||||||
|
|
@ -11,5 +28,213 @@ func main() {
|
||||||
fmt.Fprintln(os.Stderr, "wails-release: only macOS runners are supported")
|
fmt.Fprintln(os.Stderr, "wails-release: only macOS runners are supported")
|
||||||
os.Exit(1)
|
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