diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fdab66..e313fb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 < .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] @@ -346,7 +286,6 @@ jobs: - integration - integration-docker - integration-appjson - - integration-heroku20 - integration-heroku24 permissions: id-token: write diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ccbacc..f3343d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile index 79823bd..dcf0f85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" @@ -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 diff --git a/builder/build/appjson.go b/builder/build/appjson.go index 7574002..ab2209a 100644 --- a/builder/build/appjson.go +++ b/builder/build/appjson.go @@ -3,6 +3,7 @@ package build import ( "context" "encoding/json" + "fmt" "os" "github.com/rs/zerolog/log" @@ -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", @@ -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 { @@ -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 } @@ -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"} } diff --git a/builder/build/appjson_test.go b/builder/build/appjson_test.go index 416c6f6..a0ecda7 100644 --- a/builder/build/appjson_test.go +++ b/builder/build/appjson_test.go @@ -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 { @@ -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) { @@ -58,7 +43,7 @@ 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, } @@ -66,19 +51,41 @@ func TestAppJsonStack(t *testing.T) { 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) + } } } diff --git a/builder/build/build.go b/builder/build/build.go index 3a7605d..a03268a 100644 --- a/builder/build/build.go +++ b/builder/build/build.go @@ -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 } @@ -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