Add config loader and validator
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2115507d52
commit
5c72dd2a97
150
internal/config/config.go
Normal file
150
internal/config/config.go
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the complete set of inputs to the action, parsed from env.
|
||||||
|
type Config struct {
|
||||||
|
WorkingDirectory string
|
||||||
|
AppName string
|
||||||
|
Version string
|
||||||
|
WailsVersion string
|
||||||
|
ExtraBuildFlags string
|
||||||
|
|
||||||
|
DeveloperIDCertBase64 string
|
||||||
|
DeveloperIDCertPassword string
|
||||||
|
|
||||||
|
NotarizationMethod string // "api-key" | "apple-id" | "auto"
|
||||||
|
NotarizationAPIKeyBase64 string
|
||||||
|
NotarizationAPIKeyID string
|
||||||
|
NotarizationAPIIssuerID string
|
||||||
|
NotarizationAppleID string
|
||||||
|
NotarizationApplePassword string
|
||||||
|
NotarizationTeamID string
|
||||||
|
|
||||||
|
S3Bucket string
|
||||||
|
S3Key string
|
||||||
|
S3EndpointURL string
|
||||||
|
S3Region string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads the action's INPUT_* environment variables.
|
||||||
|
func Load(get func(string) string) *Config {
|
||||||
|
c := &Config{
|
||||||
|
WorkingDirectory: getOr(get, "INPUT_WORKING_DIRECTORY", "."),
|
||||||
|
AppName: get("INPUT_APP_NAME"),
|
||||||
|
Version: get("INPUT_VERSION"),
|
||||||
|
WailsVersion: get("INPUT_WAILS_VERSION"),
|
||||||
|
ExtraBuildFlags: get("INPUT_EXTRA_BUILD_FLAGS"),
|
||||||
|
DeveloperIDCertBase64: get("INPUT_DEVELOPER_ID_CERT_BASE64"),
|
||||||
|
DeveloperIDCertPassword: get("INPUT_DEVELOPER_ID_CERT_PASSWORD"),
|
||||||
|
NotarizationMethod: getOr(get, "INPUT_NOTARIZATION_METHOD", "auto"),
|
||||||
|
NotarizationAPIKeyBase64: get("INPUT_NOTARIZATION_API_KEY_BASE64"),
|
||||||
|
NotarizationAPIKeyID: get("INPUT_NOTARIZATION_API_KEY_ID"),
|
||||||
|
NotarizationAPIIssuerID: get("INPUT_NOTARIZATION_API_ISSUER_ID"),
|
||||||
|
NotarizationAppleID: get("INPUT_NOTARIZATION_APPLE_ID"),
|
||||||
|
NotarizationApplePassword: get("INPUT_NOTARIZATION_APPLE_PASSWORD"),
|
||||||
|
NotarizationTeamID: get("INPUT_NOTARIZATION_TEAM_ID"),
|
||||||
|
S3Bucket: get("INPUT_S3_BUCKET"),
|
||||||
|
S3Key: get("INPUT_S3_KEY"),
|
||||||
|
S3EndpointURL: get("INPUT_S3_ENDPOINT_URL"),
|
||||||
|
S3Region: getOr(get, "INPUT_S3_REGION", "us-east-1"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOr(get func(string) string, key, def string) string {
|
||||||
|
if v := get(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks structural rules. It does NOT touch the filesystem.
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
var missing []string
|
||||||
|
if c.DeveloperIDCertBase64 == "" {
|
||||||
|
missing = append(missing, "developer-id-cert-base64")
|
||||||
|
}
|
||||||
|
if c.DeveloperIDCertPassword == "" {
|
||||||
|
missing = append(missing, "developer-id-cert-password")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return fmt.Errorf("missing required input(s): %s", strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFields := []struct{ name, value string }{
|
||||||
|
{"notarization-api-key-base64", c.NotarizationAPIKeyBase64},
|
||||||
|
{"notarization-api-key-id", c.NotarizationAPIKeyID},
|
||||||
|
{"notarization-api-issuer-id", c.NotarizationAPIIssuerID},
|
||||||
|
}
|
||||||
|
appleFields := []struct{ name, value string }{
|
||||||
|
{"notarization-apple-id", c.NotarizationAppleID},
|
||||||
|
{"notarization-apple-password", c.NotarizationApplePassword},
|
||||||
|
{"notarization-team-id", c.NotarizationTeamID},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFilled, apiMissing := groupStatus(apiFields)
|
||||||
|
appleFilled, appleMissing := groupStatus(appleFields)
|
||||||
|
|
||||||
|
switch c.NotarizationMethod {
|
||||||
|
case "api-key":
|
||||||
|
if !apiFilled {
|
||||||
|
return fmt.Errorf("notarization-method=api-key requires: %s (missing: %s)",
|
||||||
|
joinNames(apiFields), strings.Join(apiMissing, ", "))
|
||||||
|
}
|
||||||
|
case "apple-id":
|
||||||
|
if !appleFilled {
|
||||||
|
return fmt.Errorf("notarization-method=apple-id requires: %s (missing: %s)",
|
||||||
|
joinNames(appleFields), strings.Join(appleMissing, ", "))
|
||||||
|
}
|
||||||
|
case "auto", "":
|
||||||
|
switch {
|
||||||
|
case apiFilled && appleFilled:
|
||||||
|
return fmt.Errorf("notarization credentials are ambiguous: both api-key and apple-id groups are populated; set notarization-method explicitly")
|
||||||
|
case !apiFilled && !appleFilled:
|
||||||
|
return fmt.Errorf("no notarization credentials supplied: populate either the api-key group (%s) or the apple-id group (%s)",
|
||||||
|
joinNames(apiFields), joinNames(appleFields))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("notarization-method must be one of api-key, apple-id, auto (got %q)", c.NotarizationMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.S3Bucket != "" && c.S3Key == "" {
|
||||||
|
return fmt.Errorf("s3-bucket is set but s3-key is empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvedNotarizationMethod returns "api-key" or "apple-id" once Validate has succeeded.
|
||||||
|
func (c *Config) ResolvedNotarizationMethod() string {
|
||||||
|
if c.NotarizationMethod == "api-key" || c.NotarizationMethod == "apple-id" {
|
||||||
|
return c.NotarizationMethod
|
||||||
|
}
|
||||||
|
if c.NotarizationAPIKeyID != "" {
|
||||||
|
return "api-key"
|
||||||
|
}
|
||||||
|
return "apple-id"
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupStatus(fields []struct{ name, value string }) (filled bool, missing []string) {
|
||||||
|
any := false
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.value != "" {
|
||||||
|
any = true
|
||||||
|
} else {
|
||||||
|
missing = append(missing, f.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return any && len(missing) == 0, missing
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinNames(fields []struct{ name, value string }) string {
|
||||||
|
names := make([]string, len(fields))
|
||||||
|
for i, f := range fields {
|
||||||
|
names[i] = f.name
|
||||||
|
}
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
153
internal/config/config_test.go
Normal file
153
internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/leonmika/wails-release/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad_PopulatesAllFieldsFromEnv(t *testing.T) {
|
||||||
|
env := map[string]string{
|
||||||
|
"INPUT_WORKING_DIRECTORY": "apps/desktop",
|
||||||
|
"INPUT_APP_NAME": "MyApp",
|
||||||
|
"INPUT_VERSION": "1.2.3",
|
||||||
|
"INPUT_WAILS_VERSION": "v2.11.0",
|
||||||
|
"INPUT_EXTRA_BUILD_FLAGS": "-tags release",
|
||||||
|
"INPUT_DEVELOPER_ID_CERT_BASE64": "Y2VydA==",
|
||||||
|
"INPUT_DEVELOPER_ID_CERT_PASSWORD": "pw",
|
||||||
|
"INPUT_NOTARIZATION_METHOD": "api-key",
|
||||||
|
"INPUT_NOTARIZATION_API_KEY_BASE64": "a2V5",
|
||||||
|
"INPUT_NOTARIZATION_API_KEY_ID": "ABCD1234",
|
||||||
|
"INPUT_NOTARIZATION_API_ISSUER_ID": "12345678-aaaa-bbbb-cccc-111111111111",
|
||||||
|
"INPUT_S3_BUCKET": "my-bucket",
|
||||||
|
"INPUT_S3_KEY": "releases/{version}/{filename}",
|
||||||
|
"INPUT_S3_REGION": "us-east-1",
|
||||||
|
}
|
||||||
|
c := config.Load(envGetter(env))
|
||||||
|
if c.WorkingDirectory != "apps/desktop" || c.AppName != "MyApp" || c.Version != "1.2.3" {
|
||||||
|
t.Fatalf("basic fields wrong: %+v", c)
|
||||||
|
}
|
||||||
|
if c.NotarizationMethod != "api-key" || c.NotarizationAPIKeyID != "ABCD1234" {
|
||||||
|
t.Fatalf("notarization fields wrong: %+v", c)
|
||||||
|
}
|
||||||
|
if c.S3Bucket != "my-bucket" || c.S3Key != "releases/{version}/{filename}" {
|
||||||
|
t.Fatalf("s3 fields wrong: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_AppliesDefaults(t *testing.T) {
|
||||||
|
c := config.Load(envGetter(nil))
|
||||||
|
if c.WorkingDirectory != "." {
|
||||||
|
t.Fatalf("expected default working-directory '.', got %q", c.WorkingDirectory)
|
||||||
|
}
|
||||||
|
if c.S3Region != "us-east-1" {
|
||||||
|
t.Fatalf("expected default s3-region us-east-1, got %q", c.S3Region)
|
||||||
|
}
|
||||||
|
if c.NotarizationMethod != "auto" {
|
||||||
|
t.Fatalf("expected default notarization-method auto, got %q", c.NotarizationMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
base := func() *config.Config {
|
||||||
|
return &config.Config{
|
||||||
|
WorkingDirectory: ".",
|
||||||
|
DeveloperIDCertBase64: "x",
|
||||||
|
DeveloperIDCertPassword: "x",
|
||||||
|
NotarizationMethod: "auto",
|
||||||
|
S3Region: "us-east-1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
mutate func(*config.Config)
|
||||||
|
errMsg string // substring expected in error; empty means must succeed
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid with api-key group",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid with apple-id group",
|
||||||
|
mutate: func(c *config.Config) { fillAppleID(c) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing cert base64",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c); c.DeveloperIDCertBase64 = "" },
|
||||||
|
errMsg: "developer-id-cert-base64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing cert password",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c); c.DeveloperIDCertPassword = "" },
|
||||||
|
errMsg: "developer-id-cert-password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "method=api-key with missing field",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c); c.NotarizationMethod = "api-key"; c.NotarizationAPIKeyID = "" },
|
||||||
|
errMsg: "notarization-api-key-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "method=apple-id with missing field",
|
||||||
|
mutate: func(c *config.Config) { fillAppleID(c); c.NotarizationMethod = "apple-id"; c.NotarizationTeamID = "" },
|
||||||
|
errMsg: "notarization-team-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auto with both groups populated is ambiguous",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c); fillAppleID(c) },
|
||||||
|
errMsg: "ambiguous",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auto with no group populated",
|
||||||
|
mutate: func(c *config.Config) {},
|
||||||
|
errMsg: "no notarization credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "s3 bucket without key",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c); c.S3Bucket = "b" },
|
||||||
|
errMsg: "s3-key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown notarization method",
|
||||||
|
mutate: func(c *config.Config) { fillAPIKey(c); c.NotarizationMethod = "smoke-signals" },
|
||||||
|
errMsg: "notarization-method",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c := base()
|
||||||
|
tc.mutate(c)
|
||||||
|
err := c.Validate()
|
||||||
|
if tc.errMsg == "" {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected ok, got %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got nil", tc.errMsg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.errMsg) {
|
||||||
|
t.Fatalf("error %q does not contain %q", err.Error(), tc.errMsg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envGetter(m map[string]string) func(string) string {
|
||||||
|
return func(k string) string { return m[k] }
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillAPIKey(c *config.Config) {
|
||||||
|
c.NotarizationAPIKeyBase64 = "k"
|
||||||
|
c.NotarizationAPIKeyID = "id"
|
||||||
|
c.NotarizationAPIIssuerID = "issuer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillAppleID(c *config.Config) {
|
||||||
|
c.NotarizationAppleID = "apple"
|
||||||
|
c.NotarizationApplePassword = "pw"
|
||||||
|
c.NotarizationTeamID = "team"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue