diff --git a/github/provider.go b/github/provider.go index 6f96245316..b363e82687 100644 --- a/github/provider.go +++ b/github/provider.go @@ -50,6 +50,7 @@ func Provider() terraform.ResourceProvider { "github_organization_block": resourceOrganizationBlock(), "github_organization_project": resourceGithubOrganizationProject(), "github_organization_webhook": resourceGithubOrganizationWebhook(), + "github_project_card": resourceGithubProjectCard(), "github_project_column": resourceGithubProjectColumn(), "github_repository_collaborator": resourceGithubRepositoryCollaborator(), "github_repository_deploy_key": resourceGithubRepositoryDeployKey(), diff --git a/github/resource_github_project_card.go b/github/resource_github_project_card.go new file mode 100644 index 0000000000..79cfbc95aa --- /dev/null +++ b/github/resource_github_project_card.go @@ -0,0 +1,160 @@ +package github + +import ( + "context" + "log" + "net/http" + "strconv" + "strings" + + "github.com/google/go-github/v32/github" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceGithubProjectCard() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubProjectCardCreate, + Read: resourceGithubProjectCardRead, + Update: resourceGithubProjectCardUpdate, + Delete: resourceGithubProjectCardDelete, + Importer: &schema.ResourceImporter{ + State: resourceGithubProjectCardImport, + }, + Schema: map[string]*schema.Schema{ + "column_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "note": { + Type: schema.TypeString, + Required: true, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + "card_id": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceGithubProjectCardCreate(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + columnIDStr := d.Get("column_id").(string) + columnID, err := strconv.ParseInt(columnIDStr, 10, 64) + if err != nil { + return unconvertibleIdErr(columnIDStr, err) + } + + log.Printf("[DEBUG] Creating project card note in column ID: %d", columnID) + client := meta.(*Owner).v3client + options := github.ProjectCardOptions{Note: d.Get("note").(string)} + ctx := context.Background() + card, _, err := client.Projects.CreateProjectCard(ctx, columnID, &options) + if err != nil { + return err + } + + d.Set("card_id", card.GetID()) + d.SetId(card.GetNodeID()) + + return resourceGithubProjectCardRead(d, meta) +} + +func resourceGithubProjectCardRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + nodeID := d.Id() + cardID := d.Get("card_id").(int) + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + if !d.IsNewResource() { + ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) + } + + log.Printf("[DEBUG] Reading project card: %s", nodeID) + card, _, err := client.Projects.GetProjectCard(ctx, int64(cardID)) + if err != nil { + if err, ok := err.(*github.ErrorResponse); ok { + if err.Response.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Removing project card %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + return nil + } + } + return err + } + + // FIXME: Remove URL parsing if a better option becomes available + columnURL := card.GetColumnURL() + columnIDStr := strings.TrimPrefix(columnURL, client.BaseURL.String()+`projects/columns/`) + if err != nil { + return unconvertibleIdErr(columnIDStr, err) + } + + d.Set("note", card.GetNote()) + d.Set("column_id", columnIDStr) + d.Set("card_id", card.GetID()) + + return nil +} + +func resourceGithubProjectCardUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + cardID := d.Get("card_id").(int) + + log.Printf("[DEBUG] Updating project Card: %s", d.Id()) + options := github.ProjectCardOptions{ + Note: d.Get("note").(string), + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + _, _, err := client.Projects.UpdateProjectCard(ctx, int64(cardID), &options) + if err != nil { + return err + } + + return resourceGithubProjectCardRead(d, meta) +} + +func resourceGithubProjectCardDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + log.Printf("[DEBUG] Deleting project Card: %s", d.Id()) + cardID := d.Get("card_id").(int) + _, err := client.Projects.DeleteProjectCard(ctx, int64(cardID)) + if err != nil { + return err + } + + return nil +} + +func resourceGithubProjectCardImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + + cardIDStr := d.Id() + cardID, err := strconv.ParseInt(cardIDStr, 10, 64) + if err != nil { + return []*schema.ResourceData{d}, unconvertibleIdErr(cardIDStr, err) + } + + log.Printf("[DEBUG] Importing project card with card ID: %d", cardID) + client := meta.(*Owner).v3client + ctx := context.Background() + card, _, err := client.Projects.GetProjectCard(ctx, cardID) + if card == nil || err != nil { + return []*schema.ResourceData{d}, err + } + + d.SetId(card.GetNodeID()) + d.Set("card_id", cardID) + + return []*schema.ResourceData{d}, nil + +} diff --git a/github/resource_github_project_card_test.go b/github/resource_github_project_card_test.go new file mode 100644 index 0000000000..08556575b6 --- /dev/null +++ b/github/resource_github_project_card_test.go @@ -0,0 +1,68 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccGithubProjectCard(t *testing.T) { + + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + t.Run("creates a project card", func(t *testing.T) { + + config := fmt.Sprintf(` + + resource "github_organization_project" "project" { + name = "tf-acc-%s" + body = "This is an organization project." + } + + resource "github_project_column" "column" { + project_id = github_organization_project.project.id + name = "Backlog" + } + + resource "github_project_card" "card" { + column_id = github_project_column.column.column_id + note = "## Unaccepted 👇" + } + + `, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_project_card.card", "note", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + }) +} diff --git a/github/resource_github_project_column.go b/github/resource_github_project_column.go index 69f0f90086..3581b70779 100644 --- a/github/resource_github_project_column.go +++ b/github/resource_github_project_column.go @@ -31,6 +31,10 @@ func resourceGithubProjectColumn() *schema.Resource { Type: schema.TypeString, Required: true, }, + "column_id": { + Type: schema.TypeInt, + Computed: true, + }, "etag": { Type: schema.TypeString, Computed: true, @@ -67,7 +71,9 @@ func resourceGithubProjectColumnCreate(d *schema.ResourceData, meta interface{}) if err != nil { return err } + d.SetId(strconv.FormatInt(column.GetID(), 10)) + d.Set("column_id", column.GetID()) return resourceGithubProjectColumnRead(d, meta) } @@ -102,6 +108,7 @@ func resourceGithubProjectColumnRead(d *schema.ResourceData, meta interface{}) e d.Set("name", column.GetName()) d.Set("project_id", projectID) + d.Set("column_id", column.GetID()) return nil } diff --git a/website/docs/r/project_card.html.markdown b/website/docs/r/project_card.html.markdown new file mode 100644 index 0000000000..378b1e1b90 --- /dev/null +++ b/website/docs/r/project_card.html.markdown @@ -0,0 +1,45 @@ +--- +layout: "github" +page_title: "GitHub: github_project_card" +description: |- + Creates and manages project cards for GitHub projects +--- + +# github_project_card + +This resource allows you to create and manage cards for GitHub projects. + +## Example Usage + +```hcl +resource "github_organization_project" "project" { + name = "An Organization Project" + body = "This is an organization project." +} + +resource "github_project_column" "column" { + project_id = github_organization_project.project.id + name = "Backlog" +} + +resource "github_project_card" "card" { + column_id = github_project_column.column.column_id + note = "## Unaccepted 👇" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `column_id` - (Required) The ID of the card. + +* `note` - (Required) The note contents of the card. Markdown supported. + +## Import + +A GitHub Project Card can be imported using its [Card ID](https://developer.github.com/v3/projects/cards/#get-a-project-card): + +``` +$ terraform import github_project_card.card 01234567 +``` diff --git a/website/github.erb b/website/github.erb index e6c09f5d08..235fb61191 100644 --- a/website/github.erb +++ b/website/github.erb @@ -85,6 +85,9 @@
  • github_organization_webhook
  • +
  • + github_project_card +
  • github_project_column