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
61 changes: 0 additions & 61 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,66 +218,6 @@ jobs:
cat apppack.toml
test "$(python -c 'import tomllib; print(tomllib.load(open("apppack.toml", "rb"))["services"]["web"]["command"])')" = "bash -c 'gunicorn --access-logfile - --bind 0.0.0.0:\$PORT --forwarded-allow-ips '\"'\"'*'\"'\"' app:app'"

integration-heroku20:
runs-on: ubuntu-latest
needs: [test, build-image]
permissions:
id-token: write
contents: read
packages: read
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull image
run: docker pull ${{ needs.build-image.outputs.image }}
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::891426818781:role/github-actions-integration-tests
aws-region: us-east-1
- name: Checkout sample repo
run: git clone --branch buildpacks-20 https://github.com/apppackio/apppack-demo-python.git
- name: Run integration tests
working-directory: ./apppack-demo-python
run: |
cat <<EOF > .envfile
APPNAME=gh-integration
CODEBUILD_BUILD_ID=demo-python:${{ github.run_id }}
CODEBUILD_SOURCE_VERSION=${{ github.sha }}
DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_ACCESS_TOKEN=${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
DOCKER_REPO=891426818781.dkr.ecr.us-east-1.amazonaws.com/github-integration-test
ARTIFACT_BUCKET=integration-test-buildartifacts
ALLOW_EOL_SHIMMED_BUILDER=1
AWS_REGION
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
EOF

docker run \
--rm \
--privileged \
--env-file .envfile \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd):/app" \
--workdir /app \
${{ needs.build-image.outputs.image }} \
/bin/sh -c "set -x; git config --global --add safe.directory /app && apppack-builder prebuild; apppack-builder build; apppack-builder postbuild"
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Verify apppack.toml
working-directory: ./apppack-demo-python
run: |
set -ex
cat apppack.toml
test "$(python -c 'import tomllib; print(tomllib.load(open("apppack.toml", "rb"))["services"]["web"]["command"])')" = 'gunicorn --access-logfile - --bind 0.0.0.0:$PORT --forwarded-allow-ips '"'"'*'"' app:app"

integration-docker:
runs-on: ubuntu-latest
needs: [test, build-image]
Expand Down Expand Up @@ -346,7 +286,6 @@ jobs:
- integration
- integration-docker
- integration-appjson
- integration-heroku20
- integration-heroku24
permissions:
id-token: write
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

* Removed support for EOL `heroku-20` builds

## [2.5.0] - 2025-10-21

### Added
Expand Down
11 changes: 3 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache curl
# Install current `pack` and v0.31.0, the last pack version that supports heroku/buildpacks:20 builder
ENV PACK_VER=0.38.0
RUN set -ex && \
mkdir -p /tmp/legacy-pack /tmp/current-pack && \
cd /tmp/legacy-pack && \
curl -sLO "https://github.com/buildpacks/pack/releases/download/v0.31.0/pack-v0.31.0-linux.tgz" && \
tar xvzf "pack-v0.31.0-linux.tgz" && \
cd /tmp/current-pack && \
mkdir -p /tmp/pack && \
cd /tmp/pack && \
curl -sLO "https://github.com/buildpacks/pack/releases/download/v$PACK_VER/pack-v$PACK_VER-linux.tgz" && \
tar xvzf "pack-v$PACK_VER-linux.tgz"

Expand All @@ -16,7 +12,6 @@ COPY ./builder .
RUN go build -o /go/bin/apppack-builder main.go

FROM docker:27-dind
COPY --from=builder /tmp/legacy-pack/pack /usr/local/bin/pack-legacy
COPY --from=builder /tmp/current-pack/pack /usr/local/bin/pack
COPY --from=builder /tmp/pack/pack /usr/local/bin/pack
RUN apk add --no-cache git
COPY --from=builder /go/bin/apppack-builder /usr/local/bin/apppack-builder
34 changes: 7 additions & 27 deletions builder/build/appjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package build
import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -31,27 +32,6 @@ const DefaultStack = "heroku-22"

// buildpacks included in builder
var IncludedBuildpacks = map[string][]string{
"heroku-20": {
// $ pack builder inspect heroku/buildpacks:20 -o json | jq '.remote_info.buildpacks[].id'
"heroku/builder-eol-warning",
"heroku/go",
"heroku/gradle",
"heroku/java",
"heroku/jvm",
"heroku/maven",
"heroku/nodejs",
"heroku/nodejs-corepack",
"heroku/nodejs-engine",
"heroku/nodejs-npm-engine",
"heroku/nodejs-npm-install",
"heroku/nodejs-pnpm-install",
"heroku/nodejs-yarn",
"heroku/php",
"heroku/procfile",
"heroku/python",
"heroku/ruby",
"heroku/scala",
},
"heroku-22": {
// $ pack builder inspect heroku/builder:22 -o json | jq '.remote_info.buildpacks[].id'
"heroku/deb-packages",
Expand Down Expand Up @@ -101,6 +81,9 @@ func patchBuildpack(buildpack string, stack string) string {
return buildpack
}

// EOLStacks contains stacks that have reached end-of-life
var EOLStacks = []string{"heroku-18", "heroku-20"}

func (a *AppJSON) Unmarshal() error {
content, err := a.reader()
if err != nil {
Expand All @@ -115,6 +98,9 @@ func (a *AppJSON) Unmarshal() error {
log.Ctx(a.ctx).Error().Err(err).Msg("failed to parse app.json")
return err
}
if contains(EOLStacks, a.Stack) {
return fmt.Errorf("stack %q is end-of-life and no longer supported; upgrade to heroku-24", a.Stack)
}
return nil
}

Expand All @@ -136,12 +122,6 @@ func ParseAppJson(ctx context.Context) (*AppJSON, error) {
// the first item in the list is the builder, followed by the stack image
// the stack image is only used for prefetching, so non-heroku stacks should still work
func (a *AppJSON) GetBuilders() []string {
if a.Stack == "heroku-18" {
return []string{"heroku/buildpacks:18", "heroku/heroku:18-cnb"}
}
if a.Stack == "heroku-20" {
return []string{"heroku/buildpacks:20", "heroku/heroku:20-cnb"}
}
if a.Stack == "heroku-22" {
return []string{"heroku/builder:22", "heroku/heroku:22-cnb"}
}
Expand Down
55 changes: 31 additions & 24 deletions builder/build/appjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"context"
"os"
"reflect"
"strings"
"testing"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func stringSliceEqual(a, b []string) bool {
Expand All @@ -24,21 +24,6 @@ func stringSliceEqual(a, b []string) bool {

var testContext = zerolog.New(os.Stdout).With().Timestamp().Logger().WithContext(context.Background())

func TestAppJsonBuildpackPatch(t *testing.T) {
a := AppJSON{
Buildpacks: []Buildpack{
{URL: "heroku/nodejs"},
{URL: "heroku/python"},
},
Stack: "heroku-20",
ctx: log.With().Logger().WithContext(context.Background()),
}
expected := []string{"urn:cnb:builder:heroku/nodejs", "urn:cnb:builder:heroku/python"}
if !stringSliceEqual(a.GetBuildpacks(), expected) {
t.Errorf("expected %s, got %s", expected, a.GetBuildpacks())
}
}

func TestAppJsonMissing(t *testing.T) {
a := AppJSON{
reader: func() ([]byte, error) {
Expand All @@ -58,27 +43,49 @@ func TestAppJsonMissing(t *testing.T) {
func TestAppJsonStack(t *testing.T) {
a := AppJSON{
reader: func() ([]byte, error) {
return []byte(`{"stack": "heroku-18"}`), nil
return []byte(`{"stack": "heroku-22"}`), nil
},
ctx: testContext,
}
err := a.Unmarshal()
if err != nil {
t.Errorf("expected no error, got %s", err)
}
if a.Stack != "heroku-18" {
if a.Stack != "heroku-22" {
t.Errorf("expected heroku-22, got %s", a.Stack)
}
}

func TestAppJsonBuilders(t *testing.T) {
a := AppJSON{
Stack: "heroku-22",
ctx: testContext,
tests := []struct {
stack string
expected []string
}{
{"heroku-22", []string{"heroku/builder:22", "heroku/heroku:22-cnb"}},
{"custom/builder:latest", []string{"custom/builder:latest"}},
}
for _, tt := range tests {
a := AppJSON{Stack: tt.stack, ctx: testContext}
if !stringSliceEqual(a.GetBuilders(), tt.expected) {
t.Errorf("stack %s: expected %s, got %s", tt.stack, tt.expected, a.GetBuilders())
}
}
expected := []string{"heroku/builder:22", "heroku/heroku:22-cnb"}
if !stringSliceEqual(a.GetBuilders(), expected) {
t.Errorf("expected %s, got %s", expected, a.GetBuilders())
}

func TestAppJsonEOLStackError(t *testing.T) {
for _, stack := range EOLStacks {
a := AppJSON{
reader: func() ([]byte, error) {
return []byte(`{"stack": "` + stack + `"}`), nil
},
ctx: testContext,
}
err := a.Unmarshal()
if err == nil {
t.Errorf("stack %s: expected error for EOL stack, got nil", stack)
} else if !strings.Contains(err.Error(), "end-of-life") {
t.Errorf("stack %s: expected end-of-life error, got: %s", stack, err)
}
}
}

Expand Down
10 changes: 2 additions & 8 deletions builder/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,7 @@ func (b *Build) buildWithDocker(config *containers.BuildConfig) error {
func (b *Build) buildWithPack(config *containers.BuildConfig) error {
b.Log().Debug().Msg("pack config registry-mirrors")
builder := b.BuildpackBuilders()[0]
packBinary := "pack"
if builder == "heroku/buildpacks:20" {
// use legacy pack for heroku/buildpacks:20
packBinary = "pack-legacy"
b.Log().Debug().Msg(fmt.Sprintf("using legacy pack version for %s", builder))
}
cmd := exec.Command(packBinary, "config", "registry-mirrors", "add", "index.docker.io", "--mirror", DockerHubMirror)
cmd := exec.Command("pack", "config", "registry-mirrors", "add", "index.docker.io", "--mirror", DockerHubMirror)
if err := cmd.Run(); err != nil {
return err
}
Expand All @@ -170,7 +164,7 @@ func (b *Build) buildWithPack(config *containers.BuildConfig) error {
}
packArgs = append(packArgs, config.Image)
b.Log().Debug().Str("builder", builder).Str("buildpacks", buildpacks).Msg("building image")
cmd = exec.Command(packBinary, packArgs...)
cmd = exec.Command("pack", packArgs...)
out := io.MultiWriter(os.Stdout, config.LogFile)
cmd.Stdout = out
cmd.Stderr = out
Expand Down
Loading