Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
144 changes: 129 additions & 15 deletions docs/resources/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ If the build fails, the build log will be output in the error message.

To start the app from a successful build, use a [Formation resource](formation.html) to specify the process, dyno size, and dyno quantity.

## Generation Compatibility

Build configuration varies between Cedar (traditional) and Fir (next-generation) apps:

- **Cedar apps**: Support traditional `buildpacks` configuration for specifying buildpack URLs or names
- **Fir apps**: Use [Cloud Native Buildpacks](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app) configured via `project.toml` in the source code. The `buildpacks` argument cannot be used with Fir generation apps.

Apps inherit their generation from their space (Fir) or default to Cedar when not in a space.

## Source code layout

The code contained in the source directory or tarball must follow the layout required by the [buildpack](https://devcenter.heroku.com/articles/buildpacks)
Expand All @@ -46,6 +55,25 @@ A [`heroku.yml` manifest](https://devcenter.heroku.com/articles/build-docker-ima
file is required to declare which `Dockerfile` to build for each process. Be careful not to create conflicting configuration
between `heroku.yml` and Terraform, such as addons or config vars.

### Building with Cloud Native Buildpacks (Fir Generation)

Fir generation apps use [Cloud Native Buildpacks](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app) instead of traditional buildpacks. Buildpack configuration must be specified in a `project.toml` file in the source code rather than the Terraform configuration.

The `buildpacks` argument cannot be used with Fir generation apps. Attempting to do so will result in an error during `terraform apply`.

Example `project.toml` for a Node.js app:

```toml
[build]
buildpacks = ["heroku/nodejs"]

[[build.env]]
name = "NODE_ENV"
value = "production"
```

For more information, see [Using Multiple Buildpacks for an App](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app).

## Source URLs
A `source.url` may point to any `https://` URL that responds to a `GET` with a tarball source code. When running `terraform apply`,
the source code will only be fetched once for a successful build. Change the URL to force a new resource.
Expand All @@ -68,14 +96,16 @@ of the Terraform configuration.

### Example Usage with Source URL

#### Cedar Generation (Traditional)

```hcl-terraform
resource "heroku_app" "foobar" {
name = "foobar"
resource "heroku_app" "cedar_app" {
name = "my-cedar-app"
region = "us"
}

resource "heroku_build" "foobar" {
app_id = heroku_app.foobar.id
resource "heroku_build" "cedar_build" {
app_id = heroku_app.cedar_app.id
buildpacks = ["https://github.com/mars/create-react-app-buildpack"]

source {
Expand All @@ -85,12 +115,53 @@ resource "heroku_build" "foobar" {
}
}

resource "heroku_formation" "foobar" {
app_id = heroku_app.foobar.id
resource "heroku_formation" "cedar_formation" {
app_id = heroku_app.cedar_app.id
type = "web"
quantity = 1
size = "Standard-1x"
depends_on = ["heroku_build.cedar_build"]
}
```

#### Fir Generation (Cloud Native Buildpacks)

```hcl-terraform
resource "heroku_space" "fir_space" {
name = "my-fir-space"
organization = "my-organization"
region = "virginia"
generation = "fir"
}

resource "heroku_app" "fir_app" {
name = "my-fir-app"
region = heroku_space.fir_space.region
space = heroku_space.fir_space.id

organization {
name = "my-organization"
}
}

resource "heroku_build" "fir_build" {
app_id = heroku_app.fir_app.id
# Note: Do not specify buildpacks for Fir apps
# Buildpacks are configured via project.toml in the source code

source {
# Source must include project.toml for CNB configuration
url = "https://github.com/username/my-cnb-app/archive/v1.0.0.tar.gz"
version = "v1.0.0"
}
}

resource "heroku_formation" "fir_formation" {
app_id = heroku_app.fir_app.id
type = "web"
quantity = 1
size = "Standard-1x"
depends_on = ["heroku_build.foobar"]
depends_on = ["heroku_build.fir_build"]
}
```

Expand All @@ -113,28 +184,71 @@ When running `terraform apply`, if the contents (SHA256) of the source path chan

### Example Usage with Local Source Directory

#### Cedar Generation (Traditional)

```hcl-terraform
resource "heroku_app" "foobar" {
name = "foobar"
resource "heroku_app" "cedar_app" {
name = "my-cedar-app"
region = "us"
}

resource "heroku_build" "foobar" {
app_id = heroku_app.foobar.id
resource "heroku_build" "cedar_build" {
app_id = heroku_app.cedar_app.id
buildpacks = ["heroku/nodejs"]

source {
# A local directory, changing its contents will
# force a new build during `terraform apply`
path = "src/example-app"
path = "src/my-cedar-app"
}
}

resource "heroku_formation" "cedar_formation" {
app_id = heroku_app.cedar_app.id
type = "web"
quantity = 1
size = "Standard-1x"
depends_on = ["heroku_build.cedar_build"]
}
```

#### Fir Generation (Cloud Native Buildpacks)

```hcl-terraform
resource "heroku_space" "fir_space" {
name = "my-fir-space"
organization = "my-organization"
region = "virginia"
generation = "fir"
}

resource "heroku_app" "fir_app" {
name = "my-fir-app"
region = heroku_space.fir_space.region
space = heroku_space.fir_space.id

organization {
name = "my-organization"
}
}

resource "heroku_build" "fir_build" {
app_id = heroku_app.fir_app.id
# Note: Do not specify buildpacks for Fir apps

source {
# Local directory must contain project.toml
# for Cloud Native Buildpack configuration
path = "src/my-cnb-app"
}
}

resource "heroku_formation" "foobar" {
app_id = heroku_app.foobar.id
resource "heroku_formation" "fir_formation" {
app_id = heroku_app.fir_app.id
type = "web"
quantity = 1
size = "Standard-1x"
depends_on = ["heroku_build.foobar"]
depends_on = ["heroku_build.fir_build"]
}
```

Expand Down
71 changes: 71 additions & 0 deletions heroku/resource_heroku_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ func resourceHerokuBuildCreate(d *schema.ResourceData, meta interface{}) error {

appID := getAppId(d)

// Apply-time validation: ensure buildpacks are not specified for Fir apps
if err := validateBuildpacksForApp(client, appID, d); err != nil {
return err
}

// Build up our creation options
opts := heroku.BuildCreateOpts{}

Expand Down Expand Up @@ -333,6 +338,11 @@ func resourceHerokuBuildDelete(d *schema.ResourceData, meta interface{}) error {
}

func resourceHerokuBuildCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
// Validate buildpack compatibility with app generation during plan phase
if err := validateBuildpacksForAppGeneration(ctx, diff, v); err != nil {
return err
}

// Detect changes to the content of local source archive.
if v, ok := diff.GetOk("source"); ok {
vL := v.([]interface{})
Expand Down Expand Up @@ -376,6 +386,67 @@ func resourceHerokuBuildCustomizeDiff(ctx context.Context, diff *schema.Resource
return nil
}

// validateBuildpacksForAppGeneration validates buildpack configuration against the target app's generation
func validateBuildpacksForAppGeneration(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
// Only validate if buildpacks are specified
if _, ok := diff.GetOk("buildpacks"); !ok {
return nil // No buildpacks specified, nothing to validate
}

// Get the app ID to check its generation
appID := diff.Get("app_id").(string)
if appID == "" {
// During plan phase, the app_id might not be available yet if the app is being created
// in the same configuration. Skip validation for now - it will be caught at apply time.
return nil
}

config := v.(*Config)
client := config.Api

// Fetch app info to determine its generation
app, err := client.AppInfo(ctx, appID)
if err != nil {
// If we can't fetch the app (it might not exist yet during plan), skip validation
// It will be caught at apply time
return nil
}

// Validate buildpacks against app generation
return validateBuildpacksForGeneration(app.Generation.Name)
}

// validateBuildpacksForApp validates buildpack configuration at apply-time
func validateBuildpacksForApp(client *heroku.Service, appID string, d *schema.ResourceData) error {
// Only validate if buildpacks are specified
if _, ok := d.GetOk("buildpacks"); !ok {
return nil // No buildpacks specified, nothing to validate
}

// Fetch app info to determine its generation
app, err := client.AppInfo(context.TODO(), appID)
if err != nil {
return fmt.Errorf("failed to get app info for build validation: %w", err)
}

// Validate buildpacks against app generation
return validateBuildpacksForGeneration(app.Generation.Name)
}

// validateBuildpacksForGeneration validates buildpacks against a given generation
func validateBuildpacksForGeneration(generationName string) error {
appGeneration := generationName
if appGeneration == "" {
appGeneration = "cedar" // Default to cedar if generation is not specified
}

if !IsFeatureSupported(appGeneration, "app", "buildpacks") {
return fmt.Errorf("buildpacks cannot be specified for %s generation apps. Use project.toml to configure Cloud Native Buildpacks instead. See: https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app", appGeneration)
}

return nil
}

func uploadSource(filePath, httpMethod, httpUrl string) error {
method := strings.ToUpper(httpMethod)
log.Printf("[DEBUG] Uploading source '%s' to %s %s", filePath, method, httpUrl)
Expand Down
Loading