From b8b9a8b959c702a10405747245ea6126e179e044 Mon Sep 17 00:00:00 2001 From: Nathan Houle Date: Tue, 3 Feb 2026 15:23:51 -0800 Subject: [PATCH] feat: accept deploy-specific environment variable data to DeploySite, DoDeploy --- go/models/deploy_environment_variable.go | 132 +++++++++++++++++++++++ go/models/deploy_files.go | 48 +++++++++ go/porcelain/deploy.go | 33 +++--- swagger.yml | 43 ++++++++ 4 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 go/models/deploy_environment_variable.go diff --git a/go/models/deploy_environment_variable.go b/go/models/deploy_environment_variable.go new file mode 100644 index 00000000..37f4fc7f --- /dev/null +++ b/go/models/deploy_environment_variable.go @@ -0,0 +1,132 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// DeployEnvironmentVariable deploy environment variable +// +// swagger:model deployEnvironmentVariable +type DeployEnvironmentVariable struct { + + // is secret + IsSecret bool `json:"is_secret,omitempty"` + + // key + // Required: true + Key *string `json:"key"` + + // scopes + // Required: true + Scopes []string `json:"scopes"` + + // value + // Required: true + Value *string `json:"value"` +} + +// Validate validates this deploy environment variable +func (m *DeployEnvironmentVariable) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateKey(formats); err != nil { + res = append(res, err) + } + + if err := m.validateScopes(formats); err != nil { + res = append(res, err) + } + + if err := m.validateValue(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *DeployEnvironmentVariable) validateKey(formats strfmt.Registry) error { + + if err := validate.Required("key", "body", m.Key); err != nil { + return err + } + + return nil +} + +var deployEnvironmentVariableScopesItemsEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["builds","functions","runtime","post-processing"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + deployEnvironmentVariableScopesItemsEnum = append(deployEnvironmentVariableScopesItemsEnum, v) + } +} + +func (m *DeployEnvironmentVariable) validateScopesItemsEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, deployEnvironmentVariableScopesItemsEnum, true); err != nil { + return err + } + return nil +} + +func (m *DeployEnvironmentVariable) validateScopes(formats strfmt.Registry) error { + + if err := validate.Required("scopes", "body", m.Scopes); err != nil { + return err + } + + for i := 0; i < len(m.Scopes); i++ { + + // value enum + if err := m.validateScopesItemsEnum("scopes"+"."+strconv.Itoa(i), "body", m.Scopes[i]); err != nil { + return err + } + + } + + return nil +} + +func (m *DeployEnvironmentVariable) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *DeployEnvironmentVariable) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *DeployEnvironmentVariable) UnmarshalBinary(b []byte) error { + var res DeployEnvironmentVariable + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/go/models/deploy_files.go b/go/models/deploy_files.go index 1aaba0dd..9afba55f 100644 --- a/go/models/deploy_files.go +++ b/go/models/deploy_files.go @@ -33,6 +33,25 @@ type DeployFiles struct { // draft Draft bool `json:"draft,omitempty"` + // A list of deploy-specific environment variable data. Data specified this way applies only + // to this specific deploy and is merged into any existing environment variables set on the + // account and site. + // + // Deploy-specific environment variable data takes precedence over account and site + // environment variable data: For example, a deploy-specific variable with the key `NODE_ENV` + // will take priority over any existing site- and account-level environment variable data + // with the key `NODE_ENV`. + // + // Environment variable data may be provided at one of two times: + // + // - When creating a new Deploy with deploy files (most common) + // - When finalizing an existing Deploy with deploy files + // + // Once set, environment variables for a specific deploy cannot be modified. Subsequent + // attempts to modify environment variable data for a deploy will be ignored. + // + Environment []*DeployEnvironmentVariable `json:"environment"` + // A hash mapping file paths to SHA1 digests of the file contents. Files interface{} `json:"files,omitempty"` @@ -63,6 +82,10 @@ type DeployFiles struct { func (m *DeployFiles) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateEnvironment(formats); err != nil { + res = append(res, err) + } + if err := m.validateFunctionSchedules(formats); err != nil { res = append(res, err) } @@ -77,6 +100,31 @@ func (m *DeployFiles) Validate(formats strfmt.Registry) error { return nil } +func (m *DeployFiles) validateEnvironment(formats strfmt.Registry) error { + + if swag.IsZero(m.Environment) { // not required + return nil + } + + for i := 0; i < len(m.Environment); i++ { + if swag.IsZero(m.Environment[i]) { // not required + continue + } + + if m.Environment[i] != nil { + if err := m.Environment[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("environment" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + func (m *DeployFiles) validateFunctionSchedules(formats strfmt.Registry) error { if swag.IsZero(m.FunctionSchedules) { // not required diff --git a/go/porcelain/deploy.go b/go/porcelain/deploy.go index a577a636..b1017894 100644 --- a/go/porcelain/deploy.go +++ b/go/porcelain/deploy.go @@ -49,11 +49,13 @@ const ( var installDirs = []string{"node_modules/", "bower_components/"} -type uploadType int -type pointerData struct { - SHA string - Size int64 -} +type ( + uploadType int + pointerData struct { + SHA string + Size int64 + } +) type DeployObserver interface { OnSetupWalk() error @@ -74,6 +76,12 @@ type DeployWarner interface { OnWalkWarning(path, msg string) } +type DeployEnvironmentVariable struct { + Key string + Value string + IsSecret bool +} + // DeployOptions holds the option for creating a new deploy type DeployOptions struct { SiteID string @@ -83,6 +91,7 @@ type DeployOptions struct { EdgeRedirectsDir string BuildDir string LargeMediaEnabled bool + Environment []*models.DeployEnvironmentVariable IsDraft bool SkipRetry bool @@ -270,6 +279,10 @@ func (n *Netlify) DoDeploy(ctx context.Context, options *DeployOptions, deploy * deployFiles.Functions = options.functions.Sums } + if len(options.Environment) > 0 { + deployFiles.Environment = options.Environment + } + if options.Observer != nil { if err := options.Observer.OnSuccessfulWalk(deployFiles); err != nil { return nil, err @@ -763,7 +776,6 @@ func bundle(ctx context.Context, functionDir string, observer DeployObserver) (* func bundleFromManifest(ctx context.Context, manifestFile *os.File, observer DeployObserver) (*deployFiles, []*models.FunctionSchedule, map[string]models.FunctionConfig, error) { manifestBytes, err := ioutil.ReadAll(manifestFile) - if err != nil { return nil, nil, nil, err } @@ -774,7 +786,6 @@ func bundleFromManifest(ctx context.Context, manifestFile *os.File, observer Dep var manifest functionsManifest err = json.Unmarshal(manifestBytes, &manifest) - if err != nil { return nil, nil, nil, fmt.Errorf("malformed functions manifest file: %w", err) } @@ -785,7 +796,6 @@ func bundleFromManifest(ctx context.Context, manifestFile *os.File, observer Dep for _, function := range manifest.Functions { fileInfo, err := os.Stat(function.Path) - if err != nil { return nil, nil, nil, fmt.Errorf("manifest file specifies a function path that cannot be found: %s", function.Path) } @@ -802,7 +812,6 @@ func bundleFromManifest(ctx context.Context, manifestFile *os.File, observer Dep Timeout: function.Timeout, } file, err := newFunctionFile(function.Path, fileInfo, runtime, &meta, observer) - if err != nil { return nil, nil, nil, err } @@ -974,7 +983,7 @@ func jsFile(i os.FileInfo) bool { func goFile(filePath string, i os.FileInfo, observer DeployObserver) bool { warner, hasWarner := observer.(DeployWarner) - if m := i.Mode(); m&0111 == 0 && runtime.GOOS != "windows" { // check if it's an executable file. skip on windows, since it doesn't have that mode + if m := i.Mode(); m&0o111 == 0 && runtime.GOOS != "windows" { // check if it's an executable file. skip on windows, since it doesn't have that mode if hasWarner { warner.OnWalkWarning(filePath, "Go binary does not have executable permissions") } @@ -1016,8 +1025,8 @@ func ignoreFile(rel string, ignoreInstallDirs bool) bool { func createHeader(archive *zip.Writer, i os.FileInfo, runtime string) (io.Writer, error) { if runtime == goRuntime || runtime == amazonLinux2 { return archive.CreateHeader(&zip.FileHeader{ - CreatorVersion: 3 << 8, // indicates Unix - ExternalAttrs: 0777 << 16, // -rwxrwxrwx file permissions + CreatorVersion: 3 << 8, // indicates Unix + ExternalAttrs: 0o777 << 16, // -rwxrwxrwx file permissions // we need to make sure we don't have two ZIP files with the exact same contents - otherwise, our upload deduplication mechanism will do weird things. // adding in the function name as a comment ensures that every function ZIP is unique diff --git a/swagger.yml b/swagger.yml index 8b381bf3..d33c002f 100644 --- a/swagger.yml +++ b/swagger.yml @@ -4146,6 +4146,28 @@ definitions: type: array items: $ref: '#/definitions/functionSchedule' + deployEnvironmentVariable: + type: object + required: + - key + - value + - scopes + properties: + key: + type: string + value: + type: string + is_secret: + type: boolean + scopes: + type: array + items: + type: string + enum: + - builds + - functions + - runtime + - post-processing deployFiles: type: object description: | @@ -4186,6 +4208,27 @@ definitions: type: string framework_version: type: string + environment: + description: | + A list of deploy-specific environment variable data. Data specified this way applies only + to this specific deploy and is merged into any existing environment variables set on the + account and site. + + Deploy-specific environment variable data takes precedence over account and site + environment variable data: For example, a deploy-specific variable with the key `NODE_ENV` + will take priority over any existing site- and account-level environment variable data + with the key `NODE_ENV`. + + Environment variable data may be provided at one of two times: + + - When creating a new Deploy with deploy files (most common) + - When finalizing an existing Deploy with deploy files + + Once set, environment variables for a specific deploy cannot be modified. Subsequent + attempts to modify environment variable data for a deploy will be ignored. + type: array + items: + $ref: '#/definitions/deployEnvironmentVariable' pluginParams: type: object properties: