Skip to content

Add shared workflow for Heroku container deploys#42

Merged
baelter merged 7 commits intomainfrom
heroku-container-workflow
Mar 17, 2026
Merged

Add shared workflow for Heroku container deploys#42
baelter merged 7 commits intomainfrom
heroku-container-workflow

Conversation

@baelter
Copy link
Member

@baelter baelter commented Mar 11, 2026

Summary

Shared reusable workflow for repos using Heroku container stack. Builds Docker images in CI and pushes to Heroku's private container registry.

Companion to heroku.yml (git-push deploys) for repos that have been containerized.

Interface

Inputs:

  • heroku-app — Heroku app name (defaults to repo name)
  • branch — branch to deploy (defaults to default branch)
  • targets — space-separated Dockerfile build targets (required)

Secrets:

  • heroku-key — Heroku API key (required)
  • build-args — newline-separated KEY=VALUE pairs for docker build --build-arg (optional)

Usage

jobs:
  heroku:
    uses: 84codes/actions/.github/workflows/heroku-container.yml@main
    with:
      targets: "web worker release"
    secrets:
      heroku-key: ${{ secrets.HEROKU_API_KEY }}
      build-args: |
        BUNDLE_GITHUB__COM=x-access-token:${{ secrets.ORG_GITHUB_TOKEN_FOR_CI }}

Test plan

  • Test with billing-and-compliance (84codes/billing-and-compliance#220)

@baelter baelter marked this pull request as ready for review March 11, 2026 15:52
@baelter baelter requested a review from dentarg March 11, 2026 15:52
@dentarg
Copy link
Contributor

dentarg commented Mar 11, 2026

Here's some feedback in form of a PR: #43

@dentarg
Copy link
Contributor

dentarg commented Mar 11, 2026

Do we need to support multiple targets?

@dentarg
Copy link
Contributor

dentarg commented Mar 11, 2026

Ah, it is the Heroku targets (web, worker, release) but they should all be the same image, so we don't need to docker build multiple times? 🤔

@dentarg
Copy link
Contributor

dentarg commented Mar 11, 2026

Question is, is it important for Heroku to receive the push for each process type, or is one just enough, and heroku container:release without arguments, will release all?

@dentarg
Copy link
Contributor

dentarg commented Mar 11, 2026

I renamed targets to process-types in my PR, but I think we should go either all the way and support individual images, or, if heroku container:release without arguments releases all, omit process-types from the deploy workflow.

@dentarg
Copy link
Contributor

dentarg commented Mar 11, 2026

Now it worked 🎉 https://github.com/84codes/dev-playground84/actions/runs/22967795091

@baelter
Copy link
Member Author

baelter commented Mar 11, 2026

Do we need to support multiple targets?

When you push to Heroku's container registry, the CMD in the image is what runs. heroku.yml's is ignored for container registry pushes. So if you build one image with CMD puma and push it as both web and worker, both processes run Puma. That's the bug you hit.

Question is, is it important for Heroku to receive the push for each process type, or is one just enough, and heroku container:release without arguments, will release all?

We need to build each target separately docker buildx build --target web, --target worker, etc. Each Dockerfile stage has its own CMD, so each process type gets the right command. The shared stages (build, app) are cached by buildx/GHA, so it's not actually rebuilding everything N times. Heroku expects one image per process type.

I renamed targets to process-types in my PR, but I think we should go either all the way and support individual images, or, if heroku container:release without arguments releases all, omit process-types from the deploy workflow.

I renamed process-types back to targets since that's what you're passing: Dockerfile build targets. And moved the RUBY_VERSION logic out of the shared workflow since it should be language-agnostic. Repos that need it pass it via build-args.

@dentarg
Copy link
Contributor

dentarg commented Mar 12, 2026

What was the reason for not using docker/build-push-action@v7? Changed in 2dd3cca

@dentarg
Copy link
Contributor

dentarg commented Mar 12, 2026

Should we have a established way build and push outside of Github Actions, in the case GitHub Actions is unavailable? Today it is as simple as github push heroku

@dentarg
Copy link
Contributor

dentarg commented Mar 12, 2026

Should we have a established way build and push outside of Github Actions, in the case GitHub Actions is unavailable?

That would be an argument for not using any actions in the workflow? Outsource everything to a shell script that is possible to run elsewhere (developer machine?)

@baelter
Copy link
Member Author

baelter commented Mar 12, 2026

What was the reason for not using docker/build-push-action@v7? Changed in 2dd3cca

docker/build-push-action can only build one target per step. Since targets is a dynamic input we need to loop. You can't loop over an action step. It's the same engine underneath, caching works the same way.

@baelter
Copy link
Member Author

baelter commented Mar 12, 2026

Should we have a established way build and push outside of Github Actions, in the case GitHub Actions is unavailable?

That would be an argument for not using any actions in the workflow? Outsource everything to a shell script that is possible to run elsewhere (developer machine?)

Good idea. The GHA-specific bits (layer caching, login-action, setup-buildx) wouldn't transfer anyway, so a local fallback would be simpler: just docker login, docker build --target $t -t registry.heroku.com/$APP/$t ., docker push, and heroku container:release. Might be worth having a script for that, easy to add when we need it.

@dentarg
Copy link
Contributor

dentarg commented Mar 12, 2026

docker/build-push-action can only build one target per step. Since targets is a dynamic input we need to loop. You can't loop over an action step. It's the same engine underneath, caching works the same way.

Right, I got that same above answer from my 🤖 too but then I made it use tags:

tags: ${{ steps.prep.outputs.tags }}

EDIT:

I guess what is lost is in that change is --target

              --target "$target" \
              --tag "registry.heroku.com/$APP/$target" \

@baelter
Copy link
Member Author

baelter commented Mar 12, 2026

docker/build-push-action can only build one target per step. Since targets is a dynamic input we need to loop. You can't loop over an action step. It's the same engine underneath, caching works the same way.

Right, I got that same above answer from my 🤖 too but then I made it use tags:

tags: ${{ steps.prep.outputs.tags }}

Right, but that's exactly what caused the worker-starts-Puma bug? Multiple tags on one build = one image with one CMD pushed to all process types. We need separate builds with --target so each process type gets its own CMD from its Dockerfile stage.

@dentarg
Copy link
Contributor

dentarg commented Mar 12, 2026

Might be worth having a script for that, easy to add when we need it.

I think we should construct it now?

@dentarg
Copy link
Contributor

dentarg commented Mar 12, 2026

Right, but that's exactly what caused the worker-starts-Puma bug? Multiple tags on one build = one image with one CMD pushed to all process types. We need separate builds with --target so each process type gets its own CMD from its Dockerfile stage.

Yes updated my comment #42 (comment)

baelter and others added 6 commits March 17, 2026 16:19
Builds Docker images in CI and pushes to Heroku's container registry.
Companion to heroku.yml (git-push) for repos using container stack.

Inputs: heroku-app, branch, targets (Dockerfile build targets)
Secrets: heroku-key, build-args (for private dependency auth)
* Use docker actions for buildx caching and secure login

Replace raw docker login with docker/login-action for secure secret
handling, add docker/setup-buildx-action for BuildKit with GHA layer
caching, and switch to docker buildx build --push with cache flags.
Removes the standalone --target build warmup step since the GHA cache
handles shared layer reuse across targets.

* Build once and push to all process types

Single docker buildx build with multiple -t flags instead of building
per target. Rename targets input to process-types to reflect what they
actually are (Heroku process types, not Dockerfile targets).

* Pass RUBY_VERSION build arg from .ruby-version file if present

* Bump actions so Node.js 24 is used

https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

* Use docker/build-push-action instead of shell docker buildx build

* Fix build-args passing and add context to build-push-action

Construct build-args as a multiline step output instead of mixing
expressions with YAML block scalars. Add context: . to use the local
checkout instead of git context.

* Disable provenance attestation for Heroku registry compatibility

Heroku's container registry doesn't support OCI manifest lists that
buildx creates when provenance attestations are enabled.
Single-build-multiple-tag pushed the same image (same CMD) to all
process types, causing e.g. worker to start Puma instead of its own
command. Build with --target per process type so each image gets the
CMD from its Dockerfile stage.
The shared workflow should be language-agnostic. Repos that need
RUBY_VERSION can pass it via build-args in their own deploy workflow.
Same build logic as the workflow but without GHA-specific bits (layer
caching, login-action). Run from a developer machine with docker,
heroku CLI, and HEROKU_API_KEY.
Scans for .*-version files (e.g. .ruby-version, .node-version) after
checkout and passes them as --build-arg flags. This lets Dockerfiles
use ARG RUBY_VERSION (or similar) without callers having to explicitly
pass the version.

This comment was marked as resolved.

@baelter baelter force-pushed the heroku-container-workflow branch from 642b712 to 20af4ac Compare March 17, 2026 15:53
- Pin actions to SHA hashes (checkout, login-action, setup-buildx)
- Fix template injection: move inputs/context to env vars
- Use jq --arg for safe JSON construction
- Convert build_arg_flags from string to bash array
- Map cancelled job status to error for deployment API
- Guard deployment status step on DEPLOYMENT_ID existence
- Add persist-credentials: false to checkout
@baelter baelter force-pushed the heroku-container-workflow branch from 20af4ac to 526c07f Compare March 17, 2026 16:09
@baelter baelter merged commit c25aa57 into main Mar 17, 2026
7 checks passed
@baelter baelter deleted the heroku-container-workflow branch March 17, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants