diff --git a/client/project_get.go b/client/project_get.go index 8cae3cf5..9bc43002 100644 --- a/client/project_get.go +++ b/client/project_get.go @@ -11,8 +11,9 @@ import ( // Repository defines the information about a projects git connection. type Repository struct { - Type string - Repo string + Type string + Repo string + ProductionBranch *string } // getRepoNameFromURL is a helper method to extract the repo name from a GitLab URL. @@ -34,18 +35,21 @@ func (r *ProjectResponse) Repository() *Repository { switch r.Link.Type { case "github": return &Repository{ - Type: "github", - Repo: fmt.Sprintf("%s/%s", r.Link.Org, r.Link.Repo), + Type: "github", + Repo: fmt.Sprintf("%s/%s", r.Link.Org, r.Link.Repo), + ProductionBranch: r.Link.ProductionBranch, } case "gitlab": return &Repository{ - Type: "gitlab", - Repo: fmt.Sprintf("%s/%s", r.Link.ProjectNamespace, getRepoNameFromURL(r.Link.ProjectURL)), + Type: "gitlab", + Repo: fmt.Sprintf("%s/%s", r.Link.ProjectNamespace, getRepoNameFromURL(r.Link.ProjectURL)), + ProductionBranch: r.Link.ProductionBranch, } case "bitbucket": return &Repository{ - Type: "bitbucket", - Repo: fmt.Sprintf("%s/%s", r.Link.Owner, r.Link.Slug), + Type: "bitbucket", + Repo: fmt.Sprintf("%s/%s", r.Link.Owner, r.Link.Slug), + ProductionBranch: r.Link.ProductionBranch, } } return nil @@ -73,6 +77,8 @@ type ProjectResponse struct { ProjectNamespace string `json:"projectNamespace"` ProjectURL string `json:"projectUrl"` ProjectID int64 `json:"projectId,string"` + // production branch + ProductionBranch *string `json:"productionBranch"` } `json:"link"` Name string `json:"name"` OutputDirectory *string `json:"outputDirectory"` diff --git a/client/project_update_production_branch.go b/client/project_update_production_branch.go new file mode 100644 index 00000000..db063234 --- /dev/null +++ b/client/project_update_production_branch.go @@ -0,0 +1,48 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type UpdateProductionBranchRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + Branch string `json:"branch"` +} + +func (c *Client) UpdateProductionBranch(ctx context.Context, request UpdateProductionBranchRequest) (r ProjectResponse, err error) { + url := fmt.Sprintf("%s/v9/projects/%s/branch", c.baseURL, request.ProjectID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + req, err := http.NewRequestWithContext( + ctx, + "PATCH", + url, + strings.NewReader(string(mustMarshal(request))), + ) + if err != nil { + return r, err + } + + tflog.Trace(ctx, "updating project production branch", map[string]interface{}{ + "url": url, + "payload": string(mustMarshal(request)), + }) + err = c.doRequest(req, &r) + if err != nil { + return r, err + } + env, err := c.getEnvironmentVariables(ctx, r.ID, request.TeamID) + if err != nil { + return r, fmt.Errorf("error getting environment variables: %w", err) + } + r.EnvironmentVariables = env + r.TeamID = c.teamID(c.teamID(request.TeamID)) + return r, err +} diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 1de63d6f..a0c2b7d0 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -147,9 +147,8 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ }, }, "git_repository": { - Description: "The Git Repository that will be connected to the project. When this is defined, any pushes to the specified connected Git Repository will be automatically deployed. This requires the corresponding Vercel for [Github](https://vercel.com/docs/concepts/git/vercel-for-github), [Gitlab](https://vercel.com/docs/concepts/git/vercel-for-gitlab) or [Bitbucket](https://vercel.com/docs/concepts/git/vercel-for-bitbucket) plugins to be installed.", - Optional: true, - PlanModifiers: tfsdk.AttributePlanModifiers{resource.RequiresReplace()}, + Description: "The Git Repository that will be connected to the project. When this is defined, any pushes to the specified connected Git Repository will be automatically deployed. This requires the corresponding Vercel for [Github](https://vercel.com/docs/concepts/git/vercel-for-github), [Gitlab](https://vercel.com/docs/concepts/git/vercel-for-gitlab) or [Bitbucket](https://vercel.com/docs/concepts/git/vercel-for-bitbucket) plugins to be installed.", + Optional: true, Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ "type": { Description: "The git provider of the repository. Must be either `github`, `gitlab`, or `bitbucket`.", @@ -166,6 +165,12 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Required: true, PlanModifiers: tfsdk.AttributePlanModifiers{resource.RequiresReplace()}, }, + "production_branch": { + Description: "By default, every commit pushed to the main branch will trigger a Production Deployment instead of the usual Preview Deployment. You can switch to a different branch here.", + Type: types.StringType, + Optional: true, + Computed: true, + }, }), }, "id": { @@ -230,6 +235,34 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), }) + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if plan.GitRepository == nil || plan.GitRepository.ProductionBranch.IsNull() || plan.GitRepository.ProductionBranch.IsUnknown() { + return + } + + out, err = r.client.UpdateProductionBranch(ctx, client.UpdateProductionBranchRequest{ + ProjectID: out.ID, + Branch: plan.GitRepository.ProductionBranch.ValueString(), + TeamID: plan.TeamID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project", + "Failed to create project, an error occurred setting the production branch: "+err.Error(), + ) + return + } + + result = convertResponseToProject(out, plan.coercedFields(), plan.Environment) + tflog.Trace(ctx, "updated project production branch", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ID.ValueString(), + }) diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) @@ -414,6 +447,26 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest return } + if plan.GitRepository != nil && !plan.GitRepository.ProductionBranch.IsNull() && (state.GitRepository == nil || state.GitRepository.ProductionBranch.ValueString() != plan.GitRepository.ProductionBranch.ValueString()) { + out, err = r.client.UpdateProductionBranch(ctx, client.UpdateProductionBranchRequest{ + ProjectID: plan.ID.ValueString(), + TeamID: plan.TeamID.ValueString(), + Branch: plan.GitRepository.ProductionBranch.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project", + fmt.Sprintf( + "Could not update project %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + } + result := convertResponseToProject(out, plan.coercedFields(), plan.Environment) tflog.Trace(ctx, "updated project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), diff --git a/vercel/resource_project_model.go b/vercel/resource_project_model.go index 22498599..699d549c 100644 --- a/vercel/resource_project_model.go +++ b/vercel/resource_project_model.go @@ -124,8 +124,9 @@ func (e *EnvironmentItem) toCreateEnvironmentVariableRequest(projectID, teamID s // GitRepository reflects the state terraform stores internally for a nested git_repository block on a project resource. type GitRepository struct { - Type types.String `tfsdk:"type"` - Repo types.String `tfsdk:"repo"` + Type types.String `tfsdk:"type"` + Repo types.String `tfsdk:"repo"` + ProductionBranch types.String `tfsdk:"production_branch"` } func (g *GitRepository) toCreateProjectRequest() *client.GitRepository { @@ -200,8 +201,12 @@ func convertResponseToProject(response client.ProjectResponse, fields projectCoe var gr *GitRepository if repo := response.Repository(); repo != nil { gr = &GitRepository{ - Type: types.StringValue(repo.Type), - Repo: types.StringValue(repo.Repo), + Type: types.StringValue(repo.Type), + Repo: types.StringValue(repo.Repo), + ProductionBranch: types.StringNull(), + } + if repo.ProductionBranch != nil { + gr.ProductionBranch = types.StringValue(*repo.ProductionBranch) } } diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index b7dc607b..54a28f0f 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -130,9 +130,8 @@ func TestAcc_ProjectWithGitRepository(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccProjectExists("vercel_project.test_git", testTeam()), resource.TestCheckTypeSetElemNestedAttrs("vercel_project.test_git", "environment.*", map[string]string{ - "key": "foo", - "value": "bar2", - "git_branch": "staging", + "key": "foo", + "value": "bar2", }), ), }, @@ -300,13 +299,13 @@ resource "vercel_project" "test_git" { git_repository = { type = "github" repo = "%s" + production_branch = "staging" } environment = [ { key = "foo" value = "bar2" target = ["preview"] - git_branch = "staging" } ] }