Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions docs/resources/pipeline_promotion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
layout: "heroku"
page_title: "Heroku: heroku_pipeline_promotion"
sidebar_current: "docs-heroku-resource-pipeline-promotion"
description: |-
Provides a Heroku Pipeline Promotion resource.
---

# heroku\_pipeline\_promotion

Provides a [Heroku Pipeline Promotion](https://devcenter.heroku.com/articles/pipelines)
resource.

A pipeline promotion allows you to deploy releases from one app to other apps within the same
pipeline. This enables infrastructure-as-code workflow for promoting code between pipeline stages
such as staging to production.

Currently promotes the latest release from the source app. Support for promoting specific releases
(`release_id` parameter) requires additional API support from the Heroku platform team.

## Example Usage

```hcl
# Basic promotion from staging to production
resource "heroku_pipeline_promotion" "staging_to_prod" {
pipeline = heroku_pipeline.my_app.id
source_app_id = heroku_app.staging.id
targets = [heroku_app.production.id]
}

# Promotion to multiple target apps
resource "heroku_pipeline_promotion" "staging_to_multiple" {
pipeline = heroku_pipeline.my_app.id
source_app_id = heroku_app.staging.id
targets = [
heroku_app.production.id,
heroku_app.demo.id
]
}
```

## Argument Reference

The following arguments are supported:

* `pipeline` - (Required) The UUID of the pipeline containing the apps.
* `source_app_id` - (Required) The UUID of the source app to promote from.
* `targets` - (Required) Set of UUIDs of target apps to promote to.
* `release_id` - (Optional) **Not yet supported**. The UUID of a specific release to promote.
Currently returns an error as this requires additional Heroku platform API support.

## Attributes Reference

The following attributes are exported:

* `id` - The UUID of this pipeline promotion.
* `status` - The status of the promotion (`pending`, `completed`).
* `created_at` - When the promotion was created.
* `promoted_release_id` - The UUID of the release that was actually promoted.

## Notes

* Pipeline promotions are immutable - they cannot be updated or modified after creation.
* All apps (source and targets) must be in the same pipeline.
* All apps must have the same generation (Cedar or Fir). See [`heroku_pipeline`](./pipeline.html) for generation compatibility requirements.
* The source app must have at least one release to promote.
* Promotions copy the latest release from the source app to all target apps.

## Future Enhancement

The `release_id` parameter will be supported once the Heroku platform team adds the necessary API
functionality. This will enable promoting specific releases rather than just the latest release.
1 change: 1 addition & 0 deletions heroku/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func Provider() *schema.Provider {
"heroku_pipeline": resourceHerokuPipeline(),
"heroku_pipeline_config_var": resourceHerokuPipelineConfigVar(),
"heroku_pipeline_coupling": resourceHerokuPipelineCoupling(),
"heroku_pipeline_promotion": resourceHerokuPipelinePromotion(),
"heroku_review_app_config": resourceHerokuReviewAppConfig(),
"heroku_slug": resourceHerokuSlug(),
"heroku_space": resourceHerokuSpace(),
Expand Down
162 changes: 162 additions & 0 deletions heroku/resource_heroku_pipeline_promotion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Pipeline Promotion Resource
//
// This resource allows promoting releases between apps in a Heroku Pipeline.
// Currently promotes the latest release from the source app to target apps.
//
// DEPENDENCY: The 'release_id' field requires Flow team to add Promotion#release_id
// API support. Until then, only latest release promotion is supported.
package heroku

import (
"context"
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
heroku "github.com/heroku/heroku-go/v6"
)

func resourceHerokuPipelinePromotion() *schema.Resource {
return &schema.Resource{
Create: resourceHerokuPipelinePromotionCreate,
Read: resourceHerokuPipelinePromotionRead,
Delete: resourceHerokuPipelinePromotionDelete,

Schema: map[string]*schema.Schema{
"pipeline": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsUUID,
Description: "Pipeline ID for the promotion",
},

"source_app_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsUUID,
Description: "Source app ID to promote from",
},

"release_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.IsUUID,
Description: "Specific release ID to promote (requires Flow team API update)",
},

"targets": {
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Description: "Set of target app IDs to promote to",
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.IsUUID,
},
},

// Computed fields
"status": {
Type: schema.TypeString,
Computed: true,
Description: "Status of the promotion (pending, completed)",
},

"created_at": {
Type: schema.TypeString,
Computed: true,
Description: "When the promotion was created",
},

"promoted_release_id": {
Type: schema.TypeString,
Computed: true,
Description: "ID of the release that was actually promoted",
},
},
}
}

func resourceHerokuPipelinePromotionCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).Api

log.Printf("[DEBUG] Creating pipeline promotion")

// Check if release_id is specified - this requires Flow team API support
if releaseID, ok := d.GetOk("release_id"); ok {
return fmt.Errorf("release_id parameter (%s) is not yet supported - waiting for Flow team to add Promotion#release_id API support", releaseID.(string))
}

// Build promotion options using current API
pipelineID := d.Get("pipeline").(string)
sourceAppID := d.Get("source_app_id").(string)
targets := d.Get("targets").(*schema.Set)

opts := heroku.PipelinePromotionCreateOpts{}
opts.Pipeline.ID = pipelineID
opts.Source.App = &struct {
ID *string `json:"id,omitempty" url:"id,omitempty,key"`
}{ID: &sourceAppID}

// Convert targets set to slice
for _, target := range targets.List() {
targetAppID := target.(string)
targetApp := &struct {
ID *string `json:"id,omitempty" url:"id,omitempty,key"`
}{ID: &targetAppID}

opts.Targets = append(opts.Targets, struct {
App *struct {
ID *string `json:"id,omitempty" url:"id,omitempty,key"`
} `json:"app,omitempty" url:"app,omitempty,key"`
}{App: targetApp})
}

log.Printf("[DEBUG] Pipeline promotion create configuration: %#v", opts)

promotion, err := client.PipelinePromotionCreate(context.TODO(), opts)
if err != nil {
return fmt.Errorf("error creating pipeline promotion: %s", err)
}

log.Printf("[INFO] Created pipeline promotion ID: %s", promotion.ID)
d.SetId(promotion.ID)

return resourceHerokuPipelinePromotionRead(d, meta)
}

func resourceHerokuPipelinePromotionRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).Api

log.Printf("[DEBUG] Reading pipeline promotion: %s", d.Id())

promotion, err := client.PipelinePromotionInfo(context.TODO(), d.Id())
if err != nil {
return fmt.Errorf("error retrieving pipeline promotion: %s", err)
}

// Set computed fields
d.Set("status", promotion.Status)
d.Set("created_at", promotion.CreatedAt.String())

// Set the release that was actually promoted
if promotion.Source.Release.ID != "" {
d.Set("promoted_release_id", promotion.Source.Release.ID)
}

// Set configuration from API response
d.Set("pipeline", promotion.Pipeline.ID)
d.Set("source_app_id", promotion.Source.App.ID)

log.Printf("[DEBUG] Pipeline promotion read completed for: %s", d.Id())
return nil
}

func resourceHerokuPipelinePromotionDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[INFO] There is no DELETE for pipeline promotion resource so this is a no-op. Promotion will be removed from state.")
return nil
}
80 changes: 80 additions & 0 deletions heroku/resource_heroku_pipeline_promotion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package heroku

import (
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func TestResourceHerokuPipelinePromotion_Schema(t *testing.T) {
resource := resourceHerokuPipelinePromotion()

// Test required fields
requiredFields := []string{"pipeline", "source_app_id", "targets"}
for _, field := range requiredFields {
if _, ok := resource.Schema[field]; !ok {
t.Errorf("Required field %s not found in schema", field)
}
if !resource.Schema[field].Required {
t.Errorf("Field %s should be required", field)
}
if !resource.Schema[field].ForceNew {
t.Errorf("Field %s should be ForceNew", field)
}
}

// Test optional fields
optionalFields := []string{"release_id"}
for _, field := range optionalFields {
if _, ok := resource.Schema[field]; !ok {
t.Errorf("Optional field %s not found in schema", field)
}
if !resource.Schema[field].Optional {
t.Errorf("Field %s should be optional", field)
}
if !resource.Schema[field].ForceNew {
t.Errorf("Field %s should be ForceNew", field)
}
}

// Test computed fields
computedFields := []string{"status", "created_at", "promoted_release_id"}
for _, field := range computedFields {
if _, ok := resource.Schema[field]; !ok {
t.Errorf("Computed field %s not found in schema", field)
}
if !resource.Schema[field].Computed {
t.Errorf("Field %s should be computed", field)
}
}

// Test targets field is a Set
if resource.Schema["targets"].Type != schema.TypeSet {
t.Errorf("targets field should be TypeSet")
}
}

func TestResourceHerokuPipelinePromotion_ReleaseIdValidation(t *testing.T) {
// This test validates that release_id parameter currently returns an error
// Once Flow team adds API support, this test should be updated

d := schema.TestResourceDataRaw(t, resourceHerokuPipelinePromotion().Schema, map[string]interface{}{
"pipeline": "01234567-89ab-cdef-0123-456789abcdef",
"source_app_id": "01234567-89ab-cdef-0123-456789abcdef",
"release_id": "01234567-89ab-cdef-0123-456789abcdef",
"targets": []interface{}{"01234567-89ab-cdef-0123-456789abcdef"},
})

meta := &Config{} // Mock config

err := resourceHerokuPipelinePromotionCreate(d, meta)
if err == nil {
t.Error("Expected error when release_id is provided, but got none")
}

expectedError := "release_id parameter"
if err != nil && !strings.Contains(err.Error(), expectedError) {
t.Errorf("Expected error to contain '%s', got: %s", expectedError, err.Error())
}
}