├── .github ├── .patch_files ├── .syncignore ├── CODEOWNERS ├── dependabot.yml ├── labels.yml └── workflows │ ├── approve-bot-pr.yml │ ├── codeql-analysis.yml │ ├── create-draft-release.yml │ ├── label-pr.yml │ ├── lint-yaml.yml │ ├── lint.yml │ ├── publish-releases.yml │ ├── push-buildpackage.yml │ ├── synchronize-labels.yml │ ├── test-pull-request.yml │ ├── update-dependencies-from-metadata.yml │ ├── update-github-config.yml │ └── update-go-mod-version.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── build.go ├── build_test.go ├── buildpack.toml ├── constants.go ├── dependency ├── Makefile └── retrieval │ ├── components │ ├── dependency.go │ ├── dependency_test.go │ ├── init_test.go │ ├── license.go │ ├── license_test.go │ ├── output.go │ ├── output_test.go │ ├── purl.go │ ├── purl_test.go │ ├── releases.go │ ├── releases_test.go │ ├── versions.go │ └── versions_test.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── detect.go ├── detect_test.go ├── fakes ├── dependency_manager.go ├── entry_resolver.go ├── sbom_generator.go └── version_parser.go ├── go.mod ├── go.sum ├── init_test.go ├── integration.json ├── integration ├── default_test.go ├── environment_variable_configuration_test.go ├── init_test.go ├── layer_reuse_test.go ├── offline_test.go └── testdata │ ├── buildpack_yaml_app │ ├── buildpack.yml │ ├── main.go │ └── plan.toml │ └── default_app │ ├── main.go │ └── plan.toml ├── run └── main.go └── scripts ├── .util ├── builders.sh ├── print.sh ├── tools.json └── tools.sh ├── build.sh ├── integration.sh ├── package.sh └── unit.sh /.github/.patch_files: -------------------------------------------------------------------------------- 1 | .github/.patch_files 2 | .github/.syncignore 3 | .github/CODEOWNERS 4 | .github/dependabot.yml 5 | .github/labels.yml 6 | .github/workflows/approve-bot-pr.yml 7 | .github/workflows/codeql-analysis.yml 8 | .github/workflows/go-get-update.yml 9 | .github/workflows/label-pr.yml 10 | .github/workflows/lint-yaml.yml 11 | .github/workflows/lint.yml 12 | .github/workflows/synchronize-labels.yml 13 | .github/workflows/test-pull-request.yml 14 | .github/workflows/update-dependencies.yml 15 | .github/workflows/update-dependencies-from-metadata.yml 16 | .github/workflows/update-github-config.yml 17 | .gitignore 18 | LICENSE 19 | NOTICE 20 | README.md 21 | go.mod 22 | go.sum 23 | scripts/.util/builders.sh 24 | scripts/.util/git.sh 25 | scripts/.util/print.sh 26 | scripts/.util/tools.json 27 | scripts/.util/tools.sh 28 | scripts/integration.sh 29 | scripts/unit.sh 30 | -------------------------------------------------------------------------------- /.github/.syncignore: -------------------------------------------------------------------------------- 1 | CODEOWNERS 2 | workflows/update-dependencies.yml 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @paketo-buildpacks/go-maintainers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | allow: 9 | # Allow both direct and indirect updates for all packages 10 | - dependency-type: "all" 11 | # group all minor and patch dependency updates together 12 | groups: 13 | go-modules: 14 | patterns: 15 | - "*" 16 | update-types: 17 | - "minor" 18 | - "patch" 19 | exclude-patterns: 20 | - "github.com/anchore/stereoscope" 21 | - "github.com/testcontainers/testcontainers-go" 22 | - "github.com/docker/docker" 23 | - "github.com/containerd/containerd" 24 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: status/possible-priority 2 | description: This issue is ready to work and should be considered as a potential priority 3 | color: F9D0C4 4 | - name: status/prioritized 5 | description: This issue has been triaged and resolving it is a priority 6 | color: BFD4F2 7 | - name: status/blocked 8 | description: This issue has been triaged and resolving it is blocked on some other issue 9 | color: 848978 10 | - name: bug 11 | description: Something isn't working 12 | color: d73a4a 13 | - name: enhancement 14 | description: A new feature or request 15 | color: a2eeef 16 | - name: documentation 17 | description: This issue relates to writing documentation 18 | color: D4C5F9 19 | - name: help wanted 20 | description: Extra attention is needed 21 | color: 008672 22 | - name: semver:major 23 | description: A change requiring a major version bump 24 | color: 6b230e 25 | - name: semver:minor 26 | description: A change requiring a minor version bump 27 | color: cc6749 28 | - name: semver:patch 29 | description: A change requiring a patch version bump 30 | color: f9d0c4 31 | - name: good first issue 32 | description: A good first issue to get started with 33 | color: d3fc03 34 | - name: "failure:release" 35 | description: An issue filed automatically when a release workflow run fails 36 | color: f00a0a 37 | - name: "failure:push" 38 | description: An issue filed automatically when a push buildpackage workflow run fails 39 | color: f00a0a 40 | - name: "failure:update-dependencies" 41 | description: An issue filed automatically when updating buildpack.toml dependencies fails in a workflow 42 | color: f00a0a 43 | - name: "failure:go-get-update" 44 | description: An issue filed automatically when a go get update workflow run fails 45 | color: f00a0a 46 | - name: "failure:update-github-config" 47 | description: An issue filed automatically when a github config update workflow run fails 48 | color: f00a0a 49 | - name: "failure:approve-bot-pr" 50 | description: An issue filed automatically when a PR auto-approve workflow run fails 51 | color: f00a0a 52 | -------------------------------------------------------------------------------- /.github/workflows/approve-bot-pr.yml: -------------------------------------------------------------------------------- 1 | name: Approve Bot PRs and Enable Auto-Merge 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Test Pull Request"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | download: 11 | name: Download PR Artifact 12 | if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} 13 | runs-on: ubuntu-22.04 14 | outputs: 15 | pr-author: ${{ steps.pr-data.outputs.author }} 16 | pr-number: ${{ steps.pr-data.outputs.number }} 17 | steps: 18 | - name: 'Download artifact' 19 | uses: paketo-buildpacks/github-config/actions/pull-request/download-artifact@main 20 | with: 21 | name: "event-payload" 22 | repo: ${{ github.repository }} 23 | run_id: ${{ github.event.workflow_run.id }} 24 | workspace: "/github/workspace" 25 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 26 | - id: pr-data 27 | run: | 28 | echo "author=$(cat event.json | jq -r '.pull_request.user.login')" >> "$GITHUB_OUTPUT" 29 | echo "number=$(cat event.json | jq -r '.pull_request.number')" >> "$GITHUB_OUTPUT" 30 | 31 | approve: 32 | name: Approve Bot PRs 33 | needs: download 34 | if: ${{ needs.download.outputs.pr-author == 'paketo-bot' || needs.download.outputs.pr-author == 'dependabot[bot]' }} 35 | runs-on: ubuntu-22.04 36 | steps: 37 | - name: Check Commit Verification 38 | id: unverified-commits 39 | uses: paketo-buildpacks/github-config/actions/pull-request/check-unverified-commits@main 40 | with: 41 | token: ${{ secrets.PAKETO_BOT_REVIEWER_GITHUB_TOKEN }} 42 | repo: ${{ github.repository }} 43 | number: ${{ needs.download.outputs.pr-number }} 44 | 45 | - name: Check for Human Commits 46 | id: human-commits 47 | uses: paketo-buildpacks/github-config/actions/pull-request/check-human-commits@main 48 | with: 49 | token: ${{ secrets.PAKETO_BOT_REVIEWER_GITHUB_TOKEN }} 50 | repo: ${{ github.repository }} 51 | number: ${{ needs.download.outputs.pr-number }} 52 | 53 | - name: Checkout 54 | if: steps.human-commits.outputs.human_commits == 'false' && steps.unverified-commits.outputs.unverified_commits == 'false' 55 | uses: actions/checkout@v3 56 | 57 | - name: Approve 58 | if: steps.human-commits.outputs.human_commits == 'false' && steps.unverified-commits.outputs.unverified_commits == 'false' 59 | uses: paketo-buildpacks/github-config/actions/pull-request/approve@main 60 | with: 61 | token: ${{ secrets.PAKETO_BOT_REVIEWER_GITHUB_TOKEN }} 62 | number: ${{ needs.download.outputs.pr-number }} 63 | 64 | - name: Enable Auto-Merge 65 | if: steps.human-commits.outputs.human_commits == 'false' && steps.unverified-commits.outputs.unverified_commits == 'false' 66 | run: | 67 | gh pr merge ${{ needs.download.outputs.pr-number }} --auto --rebase 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 70 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '34 5 * * *' # daily at 5:34am UTC 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-22.04 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: 20 | - 'go' 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v2 28 | with: 29 | languages: ${{ matrix.language }} 30 | 31 | - name: Autobuild 32 | uses: github/codeql-action/autobuild@v2 33 | 34 | - name: Perform CodeQL Analysis 35 | uses: github/codeql-action/analyze@v2 36 | -------------------------------------------------------------------------------- /.github/workflows/create-draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Create or Update Draft Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | repository_dispatch: 8 | types: [ version-bump ] 9 | workflow_dispatch: 10 | inputs: 11 | version: 12 | description: 'Version of the release to cut (e.g. 1.2.3)' 13 | required: false 14 | 15 | concurrency: release 16 | 17 | jobs: 18 | unit: 19 | name: Unit Tests 20 | runs-on: ubuntu-22.04 21 | outputs: 22 | builders: ${{ steps.builders.outputs.builders }} 23 | steps: 24 | - name: Setup Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: 'stable' 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Run Unit Tests 31 | run: ./scripts/unit.sh 32 | - name: Get builders from integration.json 33 | id: builders 34 | run: | 35 | source "${{ github.workspace }}/scripts/.util/builders.sh" 36 | builders="$(util::builders::list "${{ github.workspace }}/integration.json")" 37 | printf "Output: %s\n" "${builders}" 38 | printf "builders=%s\n" "${builders}" >> "$GITHUB_OUTPUT" 39 | 40 | integration: 41 | name: Integration Tests 42 | runs-on: ubuntu-22.04 43 | needs: unit 44 | strategy: 45 | matrix: 46 | builder: ${{ fromJSON(needs.unit.outputs.builders) }} 47 | fail-fast: false # don't cancel all test jobs when one fails 48 | steps: 49 | - name: Setup Go 50 | uses: actions/setup-go@v3 51 | with: 52 | go-version: 'stable' 53 | - name: Checkout 54 | uses: actions/checkout@v3 55 | - name: Run Integration Tests 56 | run: ./scripts/integration.sh --builder ${{ matrix.builder }} --token ${{ github.token }} 57 | env: 58 | TMPDIR: "${{ runner.temp }}" 59 | 60 | release: 61 | name: Release 62 | runs-on: ubuntu-22.04 63 | needs: integration 64 | steps: 65 | - name: Setup Go 66 | uses: actions/setup-go@v3 67 | with: 68 | go-version: 'stable' 69 | 70 | - name: Checkout 71 | uses: actions/checkout@v3 72 | with: 73 | fetch-tags: true 74 | 75 | - name: Reset Draft Release 76 | id: reset 77 | uses: paketo-buildpacks/github-config/actions/release/reset-draft@main 78 | with: 79 | repo: ${{ github.repository }} 80 | token: ${{ github.token }} 81 | 82 | - name: Calculate Semver Tag 83 | if: github.event.inputs.version == '' 84 | id: semver 85 | uses: paketo-buildpacks/github-config/actions/tag/calculate-semver@main 86 | with: 87 | repo: ${{ github.repository }} 88 | token: ${{ github.token }} 89 | ref-name: ${{ github.ref_name }} 90 | 91 | - name: Set Release Tag 92 | id: tag 93 | run: | 94 | tag="${{ github.event.inputs.version }}" 95 | if [ -z "${tag}" ]; then 96 | tag="${{ steps.semver.outputs.tag }}" 97 | fi 98 | echo "tag=${tag}" >> "$GITHUB_OUTPUT" 99 | 100 | - name: Package 101 | run: ./scripts/package.sh --version "${{ steps.tag.outputs.tag }}" 102 | 103 | - name: Get buildpack type 104 | id: get_buildpack_type 105 | run: | 106 | 107 | if [ -f "extension.toml" ]; then 108 | echo "buildpack_type=extension" >> "$GITHUB_OUTPUT" 109 | else 110 | echo "buildpack_type=buildpack" >> "$GITHUB_OUTPUT" 111 | fi 112 | 113 | - name: Create Release Notes 114 | id: create-release-notes 115 | uses: paketo-buildpacks/github-config/actions/release/notes@main 116 | with: 117 | repo: ${{ github.repository }} 118 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 119 | buildpack_type: ${{ steps.get_buildpack_type.outputs.buildpack_type }} 120 | 121 | - name: Create Release 122 | uses: paketo-buildpacks/github-config/actions/release/create@main 123 | with: 124 | repo: ${{ github.repository }} 125 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 126 | tag_name: v${{ steps.tag.outputs.tag }} 127 | target_commitish: ${{ github.sha }} 128 | name: v${{ steps.tag.outputs.tag }} 129 | body: ${{ steps.create-release-notes.outputs.release_body }} 130 | draft: true 131 | assets: | 132 | [ 133 | { 134 | "path": "build/buildpack.tgz", 135 | "name": "${{ github.event.repository.name }}-${{ steps.tag.outputs.tag }}.tgz", 136 | "content_type": "application/gzip" 137 | }, 138 | { 139 | "path": "build/buildpackage.cnb", 140 | "name": "${{ github.event.repository.name }}-${{ steps.tag.outputs.tag }}.cnb", 141 | "content_type": "application/gzip" 142 | } 143 | ] 144 | 145 | failure: 146 | name: Alert on Failure 147 | runs-on: ubuntu-22.04 148 | needs: [ unit, integration, release ] 149 | if: ${{ always() && needs.unit.result == 'failure' || needs.integration.result == 'failure' || needs.release.result == 'failure' }} 150 | steps: 151 | - name: File Failure Alert Issue 152 | uses: paketo-buildpacks/github-config/actions/issue/file@main 153 | with: 154 | token: ${{ secrets.GITHUB_TOKEN }} 155 | repo: ${{ github.repository }} 156 | label: "failure:release" 157 | comment_if_exists: true 158 | issue_title: "Failure: Create Draft Release workflow" 159 | issue_body: | 160 | Create Draft Release workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). 161 | comment_body: | 162 | Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} 163 | -------------------------------------------------------------------------------- /.github/workflows/label-pr.yml: -------------------------------------------------------------------------------- 1 | name: Set / Validate PR Labels 2 | on: 3 | pull_request_target: 4 | branches: 5 | - main 6 | types: 7 | - synchronize 8 | - opened 9 | - reopened 10 | - labeled 11 | - unlabeled 12 | 13 | concurrency: pr_labels_${{ github.event.number }} 14 | 15 | jobs: 16 | autolabel: 17 | name: Ensure Minimal Semver Labels 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - name: Check Minimal Semver Labels 21 | uses: mheap/github-action-required-labels@v3 22 | with: 23 | count: 1 24 | labels: semver:major, semver:minor, semver:patch 25 | mode: exactly 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Auto-label Semver 30 | if: ${{ failure() }} 31 | uses: paketo-buildpacks/github-config/actions/pull-request/auto-semver-label@main 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/lint-yaml.yml: -------------------------------------------------------------------------------- 1 | name: Lint Workflows 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/**.yml' 7 | - '.github/**.yaml' 8 | 9 | jobs: 10 | lintYaml: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Checkout github-config 16 | uses: actions/checkout@v3 17 | with: 18 | repository: paketo-buildpacks/github-config 19 | path: github-config 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: 3.8 25 | 26 | - name: Install yamllint 27 | run: pip install yamllint 28 | 29 | - name: Lint YAML files 30 | run: yamllint ./.github -c github-config/.github/.yamllint 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - name: Setup Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 'stable' 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: golangci-lint 25 | uses: golangci/golangci-lint-action@v3 26 | with: 27 | version: latest 28 | args: --timeout 3m0s 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-releases.yml: -------------------------------------------------------------------------------- 1 | name: Publish Draft Releases 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: '0 5 * * WED' # Weekly on Wednesday at 5:00 AM UTC 7 | 8 | concurrency: 9 | group: publish-release 10 | 11 | jobs: 12 | publish: 13 | name: Publish 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - name: Publish Draft Release With Highest Semantic Version 17 | id: drafts 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 20 | uses: paketo-buildpacks/github-config/actions/release/publish-drafts@main 21 | with: 22 | repo: ${{ github.repository }} 23 | 24 | failure: 25 | name: Alert on Failure 26 | runs-on: ubuntu-22.04 27 | needs: [ publish ] 28 | if: ${{ always() && needs.publish.result == 'failure' }} 29 | steps: 30 | - name: File Failure Alert Issue 31 | uses: paketo-buildpacks/github-config/actions/issue/file@main 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | repo: ${{ github.repository }} 35 | label: "failure:release" 36 | comment_if_exists: true 37 | issue_title: "Failure: Publish draft releases" 38 | issue_body: | 39 | Publish All Draft Releases workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). 40 | comment_body: | 41 | Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} 42 | -------------------------------------------------------------------------------- /.github/workflows/push-buildpackage.yml: -------------------------------------------------------------------------------- 1 | name: Push Buildpackage 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | env: 8 | REGISTRIES_FILENAME: "registries.json" 9 | 10 | jobs: 11 | push: 12 | name: Push 13 | runs-on: ubuntu-22.04 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Parse Event 20 | id: event 21 | run: | 22 | FULL_VERSION="$(jq -r '.release.tag_name' "${GITHUB_EVENT_PATH}" | sed s/^v//)" 23 | MINOR_VERSION="$(echo "${FULL_VERSION}" | awk -F '.' '{print $1 "." $2 }')" 24 | MAJOR_VERSION="$(echo "${FULL_VERSION}" | awk -F '.' '{print $1 }')" 25 | echo "tag_full=${FULL_VERSION}" >> "$GITHUB_OUTPUT" 26 | echo "tag_minor=${MINOR_VERSION}" >> "$GITHUB_OUTPUT" 27 | echo "tag_major=${MAJOR_VERSION}" >> "$GITHUB_OUTPUT" 28 | echo "download_url=$(jq -r '.release.assets[] | select(.name | endswith(".cnb")) | .url' "${GITHUB_EVENT_PATH}")" >> "$GITHUB_OUTPUT" 29 | 30 | - name: Download 31 | id: download 32 | uses: paketo-buildpacks/github-config/actions/release/download-asset@main 33 | with: 34 | url: ${{ steps.event.outputs.download_url }} 35 | output: "/github/workspace/buildpackage.cnb" 36 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 37 | 38 | - name: Parse Configs 39 | id: parse_configs 40 | run: | 41 | registries_filename="${{ env.REGISTRIES_FILENAME }}" 42 | 43 | push_to_dockerhub=true 44 | push_to_gcr=false 45 | 46 | if [[ -f $registries_filename ]]; then 47 | if jq 'has("dockerhub")' $registries_filename > /dev/null; then 48 | push_to_dockerhub=$(jq '.dockerhub' $registries_filename) 49 | fi 50 | if jq 'has("GCR")' $registries_filename > /dev/null; then 51 | push_to_gcr=$(jq '.GCR' $registries_filename) 52 | fi 53 | fi 54 | 55 | echo "push_to_dockerhub=${push_to_dockerhub}" >> "$GITHUB_OUTPUT" 56 | echo "push_to_gcr=${push_to_gcr}" >> "$GITHUB_OUTPUT" 57 | 58 | - name: Validate version 59 | run: | 60 | buidpackTomlVersion=$(sudo skopeo inspect "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" | jq -r '.Labels."io.buildpacks.buildpackage.metadata" | fromjson | .version') 61 | githubReleaseVersion="${{ steps.event.outputs.tag_full }}" 62 | if [[ "$buidpackTomlVersion" != "$githubReleaseVersion" ]]; then 63 | echo "Version in buildpack.toml ($buidpackTomlVersion) and github release ($githubReleaseVersion) are not identical" 64 | exit 1 65 | fi 66 | 67 | - name: Push to GCR 68 | if: ${{ steps.parse_configs.outputs.push_to_gcr == 'true' }} 69 | env: 70 | GCR_PUSH_BOT_JSON_KEY: ${{ secrets.GCR_PUSH_BOT_JSON_KEY }} 71 | run: | 72 | echo "${GCR_PUSH_BOT_JSON_KEY}" | sudo skopeo login --username _json_key --password-stdin gcr.io 73 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://gcr.io/${{ github.repository }}:${{ steps.event.outputs.tag_full }}" 74 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://gcr.io/${{ github.repository }}:${{ steps.event.outputs.tag_minor }}" 75 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://gcr.io/${{ github.repository }}:${{ steps.event.outputs.tag_major }}" 76 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://gcr.io/${{ github.repository }}:latest" 77 | 78 | - name: Push to DockerHub 79 | if: ${{ steps.parse_configs.outputs.push_to_dockerhub == 'true' }} 80 | id: push 81 | env: 82 | DOCKERHUB_USERNAME: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_USERNAME }} 83 | DOCKERHUB_PASSWORD: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_PASSWORD }} 84 | GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} 85 | run: | 86 | REPOSITORY="${GITHUB_REPOSITORY_OWNER/-/}/${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}" # translates 'paketo-buildpacks/bundle-install' to 'paketobuildpacks/bundle-install' 87 | IMAGE="index.docker.io/${REPOSITORY}" 88 | echo "${DOCKERHUB_PASSWORD}" | sudo skopeo login --username "${DOCKERHUB_USERNAME}" --password-stdin index.docker.io 89 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://${IMAGE}:${{ steps.event.outputs.tag_full }}" 90 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://${IMAGE}:${{ steps.event.outputs.tag_minor }}" 91 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://${IMAGE}:${{ steps.event.outputs.tag_major }}" 92 | sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" "docker://${IMAGE}:latest" 93 | echo "image=${IMAGE}" >> "$GITHUB_OUTPUT" 94 | echo "digest=$(sudo skopeo inspect "oci-archive:${GITHUB_WORKSPACE}/buildpackage.cnb" | jq -r .Digest)" >> "$GITHUB_OUTPUT" 95 | 96 | - name: Register with CNB Registry 97 | uses: docker://ghcr.io/buildpacks/actions/registry/request-add-entry:main 98 | with: 99 | id: ${{ github.repository }} 100 | version: ${{ steps.event.outputs.tag_full }} 101 | address: ${{ steps.push.outputs.image }}@${{ steps.push.outputs.digest }} 102 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 103 | 104 | failure: 105 | name: Alert on Failure 106 | runs-on: ubuntu-22.04 107 | needs: [push] 108 | if: ${{ always() && needs.push.result == 'failure' }} 109 | steps: 110 | - name: File Failure Alert Issue 111 | uses: paketo-buildpacks/github-config/actions/issue/file@main 112 | with: 113 | token: ${{ secrets.GITHUB_TOKEN }} 114 | repo: ${{ github.repository }} 115 | label: "failure:push" 116 | comment_if_exists: true 117 | issue_title: "Failure: Push Buildpackage workflow" 118 | issue_body: | 119 | Push Buildpackage workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). 120 | comment_body: | 121 | Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} 122 | -------------------------------------------------------------------------------- /.github/workflows/synchronize-labels.yml: -------------------------------------------------------------------------------- 1 | name: Synchronize Labels 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - .github/labels.yml 8 | workflow_dispatch: {} 9 | jobs: 10 | synchronize: 11 | name: Synchronize Labels 12 | runs-on: 13 | - ubuntu-22.04 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: micnncim/action-label-syncer@v1 17 | env: 18 | GITHUB_TOKEN: ${{ github.token }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Test Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | # only one instance of test suite per PR at one time 10 | group: pr-${{ github.event.number }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | unit: 15 | name: Unit Tests 16 | runs-on: ubuntu-22.04 17 | outputs: 18 | builders: ${{ steps.builders.outputs.builders }} 19 | steps: 20 | - name: Setup Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: 'stable' 24 | 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Run Unit Tests 29 | env: 30 | GO_TEST_PARAMS: -count=5 31 | run: ./scripts/unit.sh 32 | 33 | - name: Get builders from integration.json 34 | id: builders 35 | run: | 36 | source "${{ github.workspace }}/scripts/.util/builders.sh" 37 | 38 | builders="$(util::builders::list "${{ github.workspace }}/integration.json")" 39 | printf "Output: %s\n" "${builders}" 40 | printf "builders=%s\n" "${builders}" >> "$GITHUB_OUTPUT" 41 | 42 | integration: 43 | name: Integration Tests with Builders 44 | runs-on: ubuntu-22.04 45 | needs: unit 46 | strategy: 47 | matrix: 48 | builder: ${{ fromJSON(needs.unit.outputs.builders) }} 49 | fail-fast: false # don't cancel all test jobs when one fails 50 | steps: 51 | - name: Setup Go 52 | uses: actions/setup-go@v3 53 | with: 54 | go-version: 'stable' 55 | 56 | - name: Checkout 57 | uses: actions/checkout@v3 58 | 59 | - name: Run Integration Tests 60 | run: ./scripts/integration.sh --builder ${{ matrix.builder }} --token ${{ github.token }} 61 | env: 62 | TMPDIR: "${{ runner.temp }}" 63 | 64 | roundup: 65 | name: Integration Tests 66 | if: ${{ always() }} 67 | runs-on: ubuntu-22.04 68 | needs: integration 69 | steps: 70 | - run: | 71 | result="${{ needs.integration.result }}" 72 | if [[ $result == "success" ]]; then 73 | echo "Integration tests passed against all builders" 74 | exit 0 75 | else 76 | echo "Integration tests failed on one or more builders" 77 | exit 1 78 | fi 79 | 80 | upload: 81 | name: Upload Workflow Event Payload 82 | runs-on: ubuntu-22.04 83 | steps: 84 | - name: Upload Artifact 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: event-payload 88 | path: ${{ github.event_path }} 89 | -------------------------------------------------------------------------------- /.github/workflows/update-dependencies-from-metadata.yml: -------------------------------------------------------------------------------- 1 | name: Update Dependencies From Metadata (Retrieve, Metadata, Compile, Test, Create PR) 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '57 13 * * *' # daily at 13:57 UTC 7 | 8 | jobs: 9 | retrieve: 10 | name: Retrieve New Versions and Generate Metadata 11 | runs-on: ubuntu-latest 12 | outputs: 13 | metadata-filepath: ${{ steps.retrieve.outputs.metadata-filepath }} 14 | metadata-json: ${{ steps.retrieve.outputs.metadata-json }} 15 | # from-source-metadata-filepath is the path to a file containing a subset 16 | # of metadata-json entries for NON-compiled dependencies 17 | from-source-metadata-filepath: ${{ steps.retrieve.outputs.from-source-metadata-filepath }} 18 | # compilation-json is a subset of metadata-json entries which are missing 19 | # a `checksum` and `uri` 20 | compilation-json: ${{ steps.retrieve.outputs.compilation-json }} 21 | id: ${{ steps.retrieve.outputs.id }} 22 | length: ${{ steps.retrieve.outputs.length }} 23 | compilation-length: ${{ steps.retrieve.outputs.compilation-length }} 24 | steps: 25 | - name: Check out code 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: 'stable' 32 | 33 | - name: Run Retrieve 34 | id: retrieve 35 | working-directory: dependency 36 | run: | 37 | #!/usr/bin/env bash 38 | set -euo pipefail 39 | shopt -s inherit_errexit 40 | 41 | OUTPUT="/tmp/metadata.json" 42 | 43 | make retrieve \ 44 | buildpackTomlPath="${{ github.workspace }}/buildpack.toml" \ 45 | output="${OUTPUT}" 46 | 47 | id=$(jq -r .[0].id < "${OUTPUT}") 48 | content=$(jq -r < "${OUTPUT}") 49 | 50 | length=$(echo $content | jq -r '. | length') 51 | 52 | compilation=$(echo $content | jq -r 'map(select(.checksum == null and .uri == null))'?) 53 | complength=$(echo $compilation | jq -r '. | length') 54 | echo $content | jq -r 'map(select(.checksum != null and .uri != null))'? > "/tmp/from-source-metadata.json" 55 | echo "from-source-metadata-filepath=/tmp/from-source-metadata.json" >> "$GITHUB_OUTPUT" 56 | 57 | 58 | delimiter="$(uuidgen)" 59 | echo "metadata-filepath=${OUTPUT}" >> "$GITHUB_OUTPUT" 60 | printf "metadata-json<<%s\n%s\n%s\n" "${delimiter}" "${content}" "${delimiter}" >> "$GITHUB_OUTPUT" # see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings 61 | echo "id=$id" >> "$GITHUB_OUTPUT" 62 | echo "length=$length" >> "$GITHUB_OUTPUT" 63 | printf "compilation-json<<%s\n%s\n%s\n" "${delimiter}" "${compilation}" "${delimiter}" >> "$GITHUB_OUTPUT" # see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings 64 | echo "compilation-length=$complength" >> "$GITHUB_OUTPUT" 65 | 66 | 67 | - name: Upload `${{ steps.retrieve.outputs.metadata-filepath }}` 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: metadata.json 71 | path: ${{ steps.retrieve.outputs.metadata-filepath }} 72 | 73 | - name: Upload `${{ steps.retrieve.outputs.from-source-metadata-filepath }}` 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: from-source-metadata.json 77 | path: ${{ steps.retrieve.outputs.from-source-metadata-filepath }} 78 | 79 | # Check if there is buildpack-provided compilation code and testing code 80 | # Optional compilation code expected at: /dependency/actions/compile/ 81 | # Optional testing code expected at: /dependency/test/ 82 | get-compile-and-test: 83 | name: Get Compilation and Testing Code 84 | outputs: 85 | should-compile: ${{ steps.compile-check.outputs.should-compile }} 86 | should-test: ${{ steps.test-check.outputs.should-test }} 87 | runs-on: ubuntu-latest 88 | steps: 89 | - name: Check out code 90 | uses: actions/checkout@v4 91 | 92 | - name: Has Compilation Action? 93 | id: compile-check 94 | run: | 95 | if test -d "dependency/actions/compile"; then 96 | echo "Compilation action provided" 97 | echo "should-compile=true" >> "$GITHUB_OUTPUT" 98 | fi 99 | 100 | - name: Has Testing Action? 101 | id: test-check 102 | run: | 103 | if test -d "dependency/test"; then 104 | echo "Testing file provided" 105 | echo "should-test=true" >> "$GITHUB_OUTPUT" 106 | fi 107 | 108 | test: 109 | name: Test Non-Compiled Dependency 110 | needs: 111 | - retrieve 112 | - get-compile-and-test 113 | strategy: 114 | matrix: 115 | includes: ${{ fromJSON(needs.retrieve.outputs.metadata-json) }} 116 | # Run job step if BOTH: 117 | # (1) needs.get-compile-and-test.outputs.should-test = TRUE -> if there is a dependency/test directory in the buildpack 118 | # (2) needs.get-compile-and-test.outputs.should-compile = FALSE -> if there is NOT a dependency/actions/compile directory in the buildpack 119 | # AND: 120 | # (3) there is at least one new version to test 121 | if: ${{ needs.retrieve.outputs.length > 0 && needs.get-compile-and-test.outputs.should-test == 'true' && needs.get-compile-and-test.outputs.should-compile == 'false' }} 122 | runs-on: ubuntu-latest 123 | steps: 124 | - name: Check out code 125 | uses: actions/checkout@v4 126 | 127 | - name: Make Temporary Artifact Directory 128 | id: make-outputdir 129 | run: echo "outputdir=$(mktemp -d)" >> "$GITHUB_OUTPUT" 130 | 131 | # Download the tarball for testing if: 132 | # (1) dependency testing code is present in the buildpack directory 133 | # (2) URI in metadata.json is available 134 | - name: Download upstream tarball (if not compiled) 135 | if: ${{ matrix.includes.uri != '' && needs.get-compile-and-test.outputs.should-test == 'true' }} 136 | run: | 137 | #!/usr/bin/env bash 138 | set -euo pipefail 139 | shopt -s inherit_errexit 140 | 141 | curl ${{ matrix.includes.uri }} \ 142 | --fail-with-body \ 143 | --show-error \ 144 | --silent \ 145 | --location \ 146 | --output ${{ steps.make-outputdir.outputs.outputdir }}/dependency.tgz 147 | 148 | # Test the dependency tarball if: 149 | # (1) dependency testing code is present in the buildpack directory 150 | - name: Test Upstream Dependency 151 | working-directory: dependency 152 | if: ${{ needs.get-compile-and-test.outputs.should-test == 'true' }} 153 | run: | 154 | make test \ 155 | version="${{ matrix.includes.version }}" \ 156 | tarballPath="${{ steps.make-outputdir.outputs.outputdir }}/*.tgz" 157 | compile: 158 | name: Compile and Test Dependency 159 | needs: 160 | - retrieve 161 | - get-compile-and-test 162 | strategy: 163 | matrix: 164 | includes: ${{ fromJSON(needs.retrieve.outputs.compilation-json) }} 165 | # Run job step if: 166 | # (1) needs.get-compile-and-test.outputs.should-compile -> if there is a dependency/actions/compile directory in the buildpack 167 | # (2) OR needs.get-compile-and-test.outputs.should-test -> if there is a dependency/test directory in the buildpack 168 | # AND: 169 | # (3) there is at least one version to compile/test 170 | if: ${{ needs.retrieve.outputs.compilation-length > 0 && (needs.get-compile-and-test.outputs.should-compile == 'true' || needs.get-compile-and-test.outputs.should-test == 'true') }} 171 | runs-on: ubuntu-latest 172 | steps: 173 | - name: Check out code 174 | uses: actions/checkout@v4 175 | 176 | - name: Make Temporary Artifact Directory 177 | id: make-outputdir 178 | run: | 179 | echo "outputdir=$(mktemp -d)" >> "$GITHUB_OUTPUT" 180 | 181 | # Compile if all of the following conditions are met: 182 | # (1) compilation Github Action presetn in the buildpack directory 183 | # (2) checksum in metadata.json is empty 184 | # (3) URI in metadata.json is empty 185 | - name: Compile version ${{ matrix.includes.version }} on ${{ matrix.includes.target }} Dockerfile 186 | id: compile 187 | if: ${{ needs.get-compile-and-test.outputs.should-compile && matrix.includes.checksum == '' && matrix.includes.uri == '' }} 188 | uses: ./dependency/actions/compile 189 | with: 190 | version: "${{ matrix.includes.version }}" 191 | outputdir: "${{ steps.make-outputdir.outputs.outputdir }}" 192 | target: "${{ matrix.includes.target }}" 193 | 194 | # If compiled, upload the tarball and checksum file for usage in the Update metadata job 195 | - name: Upload workflow asset 196 | uses: actions/upload-artifact@v4 197 | if: ${{ needs.get-compile-and-test.outputs.should-compile && matrix.includes.checksum == '' && matrix.includes.uri == '' }} 198 | with: 199 | name: '${{ needs.retrieve.outputs.id }}-${{ matrix.includes.version }}-${{ matrix.includes.target }}' 200 | path: '${{ steps.make-outputdir.outputs.outputdir }}/*' 201 | 202 | # Test the dependency tarball if: 203 | # (1) dependency testing code is present in the buildpack directory 204 | - name: Test Dependency 205 | working-directory: dependency 206 | if: ${{ needs.get-compile-and-test.outputs.should-test == 'true' }} 207 | run: | 208 | #!/usr/bin/env bash 209 | set -euo pipefail 210 | shopt -s inherit_errexit 211 | 212 | make test \ 213 | version="${{ matrix.includes.version }}" \ 214 | tarballPath="${{ steps.make-outputdir.outputs.outputdir }}/*.tgz" 215 | 216 | # Add in the checksum and URI fields to the metadata if the dependency was compiled 217 | update-metadata: 218 | name: Update Metadata (if compiled) 219 | needs: 220 | - retrieve 221 | - get-compile-and-test 222 | - compile 223 | strategy: 224 | matrix: 225 | includes: ${{ fromJSON(needs.retrieve.outputs.compilation-json) }} 226 | if: ${{ needs.retrieve.outputs.compilation-length > 0 && needs.get-compile-and-test.outputs.should-compile == 'true' }} 227 | runs-on: ubuntu-latest 228 | steps: 229 | - name: Check out code 230 | uses: actions/checkout@v4 231 | 232 | - name: Download artifact files 233 | uses: actions/download-artifact@v4 234 | with: 235 | name: '${{ needs.retrieve.outputs.id }}-${{ matrix.includes.version }}-${{ matrix.includes.target }}' 236 | 237 | - name: Get artifact file name 238 | id: get-file-names 239 | run: | 240 | #!/usr/bin/env bash 241 | set -euo pipefail 242 | shopt -s inherit_errexit 243 | 244 | echo "artifact-file=$(basename ./*.tgz)" >> "$GITHUB_OUTPUT" 245 | echo "checksum-file=$(basename ./*.tgz.checksum)" >> "$GITHUB_OUTPUT" 246 | 247 | - name: Configure AWS Credentials 248 | uses: aws-actions/configure-aws-credentials@v2 249 | with: 250 | aws-access-key-id: ${{ secrets.AWS_S3_DEPENDENCIES_ACCESS_KEY_ID }} 251 | aws-secret-access-key: ${{ secrets.AWS_S3_DEPENDENCIES_SECRET_ACCESS_KEY }} 252 | aws-region: us-east-1 253 | 254 | - name: Upload to S3 255 | id: upload 256 | uses: paketo-buildpacks/github-config/actions/dependency/upload-to-s3@main 257 | with: 258 | bucket-name: "paketo-buildpacks" 259 | dependency-name: ${{ needs.retrieve.outputs.id }} 260 | artifact-path: ${{ steps.get-file-names.outputs.artifact-file }} 261 | 262 | - name: Get Checksum 263 | id: get-checksum 264 | run: echo "checksum=$(cat ${{ steps.get-file-names.outputs.checksum-file }})" >> "$GITHUB_OUTPUT" 265 | 266 | - name: Download metadata.json 267 | uses: actions/download-artifact@v4 268 | with: 269 | name: metadata.json 270 | 271 | # Create target/version specific metadata files 272 | # Due to limitations with the upload action, we can no longer modify/upload the same metadata file 273 | - name: Write dependency-specific metadata to new file 274 | id: dependency-metadata 275 | run: | 276 | #!/usr/bin/env bash 277 | set -euo pipefail 278 | shopt -s inherit_errexit 279 | 280 | metadata_file_name="${{ matrix.includes.target }}-${{ matrix.includes.version }}-metadata-file.json" 281 | cat metadata.json | jq -r ['.[] | select( .version == "${{ matrix.includes.version }}" and .target == "${{ matrix.includes.target }}")'] > $metadata_file_name 282 | echo "file=$(echo $metadata_file_name)" >> "$GITHUB_OUTPUT" 283 | 284 | - name: Update `checksum` and `uri` in metadata for ${{ matrix.includes.target }} ${{ matrix.includes.version }} 285 | if: ${{ matrix.includes.checksum == '' && matrix.includes.uri == '' }} 286 | uses: paketo-buildpacks/github-config/actions/dependency/update-metadata-json@main 287 | with: 288 | version: ${{ matrix.includes.version }} 289 | target: ${{ matrix.includes.target }} 290 | checksum: ${{ steps.get-checksum.outputs.checksum }} 291 | uri: ${{ steps.upload.outputs.dependency-uri }} 292 | file: ${{ steps.dependency-metadata.outputs.file }} 293 | 294 | - name: Upload modified metadata 295 | uses: actions/upload-artifact@v4 296 | with: 297 | name: ${{ steps.dependency-metadata.outputs.file }} 298 | path: ${{ steps.dependency-metadata.outputs.file }} 299 | 300 | assemble: 301 | name: Update buildpack.toml 302 | needs: 303 | - retrieve 304 | - test 305 | - compile 306 | - update-metadata 307 | # Update buildpack.toml only if ALL of the following conditions are met: 308 | # (1) Retrieval step has succeeded and has found at least 1 new version 309 | # (2) Testing step has succeeded OR been skipped 310 | # (3) Compilation/Testing step has succeeded OR been skipped 311 | # (4) Update metadata step has succeeded OR been skipped 312 | if: always() && needs.retrieve.result == 'success' && needs.retrieve.outputs.length > 0 && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.compile.result == 'success' || needs.compile.result == 'skipped') && (needs.update-metadata.result == 'success' || needs.update-metadata.result == 'skipped') 313 | runs-on: ubuntu-latest 314 | steps: 315 | - name: Check out code 316 | uses: actions/checkout@v4 317 | 318 | - name: Checkout Branch 319 | uses: paketo-buildpacks/github-config/actions/pull-request/checkout-branch@main 320 | with: 321 | branch: automation/dependencies/update-from-metadata 322 | 323 | - name: Make Temporary Artifact Directory 324 | id: make-outputdir 325 | run: echo "outputdir=$(mktemp -d)" >> "$GITHUB_OUTPUT" 326 | 327 | 328 | # Metadata file for the non-compiled dependencies, if there are any 329 | - name: Download metadata.json file 330 | uses: actions/download-artifact@v4 331 | with: 332 | path: "${{ steps.make-outputdir.outputs.outputdir }}/metadata-files" 333 | pattern: "from-source-metadata.json" 334 | merge-multiple: true 335 | 336 | # If we compiled the dependency, and updated the metadata: 337 | # Download each metadata file, and combine them into one 338 | - name: Download individual metadata-file.json file(s) 339 | if: ${{ needs.update-metadata.result == 'success' }} 340 | uses: actions/download-artifact@v4 341 | with: 342 | path: "${{ steps.make-outputdir.outputs.outputdir }}/metadata-files" 343 | pattern: "*metadata-file.json" 344 | merge-multiple: true 345 | - name: Display Metadata Files 346 | run: ls "${{ steps.make-outputdir.outputs.outputdir }}/metadata-files" 347 | - name: Combine Metadata Files 348 | run: | 349 | #!/usr/bin/env bash 350 | set -euo pipefail 351 | shopt -s inherit_errexit 352 | 353 | jq -s 'add' ${{ steps.make-outputdir.outputs.outputdir }}/metadata-files/* > "${{ steps.make-outputdir.outputs.outputdir }}/metadata.json" 354 | 355 | - name: Update dependencies from metadata.json 356 | id: update 357 | uses: paketo-buildpacks/github-config/actions/dependency/update-from-metadata@main 358 | with: 359 | buildpack_toml_path: "${{ github.workspace }}/buildpack.toml" 360 | metadata_file_path: "${{ steps.make-outputdir.outputs.outputdir }}/metadata.json" 361 | 362 | - name: Show git diff 363 | run: | 364 | git diff 365 | 366 | - name: Commit 367 | id: commit 368 | uses: paketo-buildpacks/github-config/actions/pull-request/create-commit@main 369 | with: 370 | message: "Updating buildpack.toml with new versions ${{ steps.update.outputs.new-versions }}" 371 | pathspec: "." 372 | keyid: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY_ID }} 373 | key: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY }} 374 | 375 | - name: Push Branch 'automation/dependencies/update-from-metadata' 376 | if: ${{ steps.commit.outputs.commit_sha != '' }} 377 | uses: paketo-buildpacks/github-config/actions/pull-request/push-branch@main 378 | with: 379 | branch: automation/dependencies/update-from-metadata 380 | 381 | - name: Open Pull Request 382 | if: ${{ steps.commit.outputs.commit_sha != '' }} 383 | uses: paketo-buildpacks/github-config/actions/pull-request/open@main 384 | with: 385 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 386 | title: "Updates buildpack.toml with ${{ steps.update.outputs.new-versions }}" 387 | branch: automation/buildpack.toml/update-from-metadata 388 | 389 | failure: 390 | name: Alert on Failure 391 | runs-on: ubuntu-22.04 392 | needs: [ retrieve, get-compile-and-test, test, compile, update-metadata, assemble ] 393 | if: ${{ always() && needs.retrieve.result == 'failure' || needs.get-compile-and-test.result == 'failure' || needs.test.result == 'failure' || needs.compile.result == 'failure' || needs.update-metadata.result == 'failure' || needs.assemble.result == 'failure' }} 394 | steps: 395 | - name: File Failure Alert Issue 396 | uses: paketo-buildpacks/github-config/actions/issue/file@main 397 | with: 398 | token: ${{ secrets.GITHUB_TOKEN }} 399 | repo: ${{ github.repository }} 400 | label: "failure:update-dependencies" 401 | comment_if_exists: true 402 | issue_title: "Failure: Update Dependencies workflow" 403 | issue_body: | 404 | Update Dependencies From Metadata workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). 405 | comment_body: | 406 | Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} 407 | -------------------------------------------------------------------------------- /.github/workflows/update-github-config.yml: -------------------------------------------------------------------------------- 1 | name: Update shared github-config 2 | 3 | on: 4 | schedule: 5 | - cron: '27 13 * * *' # daily at 13:27 UTC 6 | workflow_dispatch: {} 7 | 8 | concurrency: github_config_update 9 | 10 | jobs: 11 | build: 12 | name: Create PR to update shared files 13 | runs-on: ubuntu-22.04 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 20 | 21 | - name: Checkout github-config 22 | uses: actions/checkout@v3 23 | with: 24 | repository: paketo-buildpacks/github-config 25 | path: github-config 26 | 27 | - name: Checkout Branch 28 | uses: paketo-buildpacks/github-config/actions/pull-request/checkout-branch@main 29 | with: 30 | branch: automation/github-config/update 31 | 32 | - name: Run the sync action 33 | uses: paketo-buildpacks/github-config/actions/sync@main 34 | with: 35 | workspace: /github/workspace 36 | config: /github/workspace/github-config/implementation 37 | 38 | - name: Cleanup 39 | run: rm -rf github-config 40 | 41 | - name: Commit 42 | id: commit 43 | uses: paketo-buildpacks/github-config/actions/pull-request/create-commit@main 44 | with: 45 | message: "Updating github-config" 46 | pathspec: "." 47 | keyid: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY_ID }} 48 | key: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY }} 49 | 50 | - name: Push Branch 51 | if: ${{ steps.commit.outputs.commit_sha != '' }} 52 | uses: paketo-buildpacks/github-config/actions/pull-request/push-branch@main 53 | with: 54 | branch: automation/github-config/update 55 | 56 | - name: Open Pull Request 57 | if: ${{ steps.commit.outputs.commit_sha != '' }} 58 | uses: paketo-buildpacks/github-config/actions/pull-request/open@main 59 | with: 60 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 61 | title: "Updates github-config" 62 | branch: automation/github-config/update 63 | 64 | failure: 65 | name: Alert on Failure 66 | runs-on: ubuntu-22.04 67 | needs: [build] 68 | if: ${{ always() && needs.build.result == 'failure' }} 69 | steps: 70 | - name: File Failure Alert Issue 71 | uses: paketo-buildpacks/github-config/actions/issue/file@main 72 | with: 73 | token: ${{ secrets.GITHUB_TOKEN }} 74 | repo: ${{ github.repository }} 75 | label: "failure:update-github-config" 76 | comment_if_exists: true 77 | issue_title: "Failure: Update GitHub config workflow" 78 | issue_body: | 79 | Update GitHub config workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). 80 | comment_body: | 81 | Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} 82 | -------------------------------------------------------------------------------- /.github/workflows/update-go-mod-version.yml: -------------------------------------------------------------------------------- 1 | name: Update Go version 2 | 3 | on: 4 | schedule: 5 | - cron: '48 4 * * MON' # every monday at 4:48 UTC 6 | workflow_dispatch: 7 | 8 | concurrency: update-go 9 | 10 | jobs: 11 | update-go: 12 | name: Update go toolchain in go.mod 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Checkout PR Branch 18 | uses: paketo-buildpacks/github-config/actions/pull-request/checkout-branch@main 19 | with: 20 | branch: automation/go-mod-update/update-main 21 | - name: Setup Go 22 | id: setup-go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: 'stable' 26 | - name: Get current go toolchain version 27 | id: current-go-version 28 | uses: paketo-buildpacks/github-config/actions/update-go-mod-version@main 29 | with: 30 | go-version: ${{ steps.setup-go.outputs.go-version }} 31 | - name: Go mod tidy 32 | run: | 33 | #!/usr/bin/env bash 34 | set -euo pipefail 35 | shopt -s inherit_errexit 36 | 37 | echo "Before running go mod tidy" 38 | echo "head -n10 go.mod " 39 | head -n10 go.mod 40 | 41 | echo "git diff" 42 | git diff 43 | 44 | echo "Running go mod tidy" 45 | go mod tidy 46 | 47 | echo "After running go mod tidy" 48 | echo "head -n10 go.mod " 49 | head -n10 go.mod 50 | 51 | echo "git diff" 52 | git diff 53 | - name: Commit 54 | id: commit 55 | uses: paketo-buildpacks/github-config/actions/pull-request/create-commit@main 56 | with: 57 | message: "Updates go mod version to ${{ steps.setup-go.outputs.go-version }}" 58 | pathspec: "." 59 | keyid: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY_ID }} 60 | key: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY }} 61 | 62 | - name: Push Branch 63 | if: ${{ steps.commit.outputs.commit_sha != '' }} 64 | uses: paketo-buildpacks/github-config/actions/pull-request/push-branch@main 65 | with: 66 | branch: automation/go-mod-update/update-main 67 | 68 | - name: Open Pull Request 69 | if: ${{ steps.commit.outputs.commit_sha != '' }} 70 | uses: paketo-buildpacks/github-config/actions/pull-request/open@main 71 | with: 72 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 73 | title: "Updates go mod version to ${{ steps.setup-go.outputs.go-version }}" 74 | branch: automation/go-mod-update/update-main 75 | 76 | failure: 77 | name: Alert on Failure 78 | runs-on: ubuntu-22.04 79 | needs: [update-go] 80 | if: ${{ always() && needs.update-go.result == 'failure' }} 81 | steps: 82 | - name: File Failure Alert Issue 83 | uses: paketo-buildpacks/github-config/actions/issue/file@main 84 | with: 85 | token: ${{ secrets.GITHUB_TOKEN }} 86 | repo: ${{ github.repository }} 87 | label: "failure:update-go-version" 88 | comment_if_exists: true 89 | issue_title: "Failure: Update Go Mod Version workflow" 90 | issue_body: | 91 | Update Go Mod Version workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). 92 | comment_body: | 93 | Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bin 2 | /build 3 | /bin/ 4 | *.tgz 5 | go-cnb_* 6 | go-compiler-cnb_* 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 4 | You may not use this project except in compliance with the License. 5 | 6 | This project may include a number of subcomponents with separate copyright notices 7 | and license terms. Your use of these subcomponents is subject to the terms and 8 | conditions of the subcomponent's license, as noted in the LICENSE file. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Distribution Cloud Native Buildpack 2 | 3 | The Go Distribution CNB provides the Go binary distribution that can be used to 4 | execute [Go tooling](https://golang.org/cmd/go/). The buildpack installs the Go 5 | binary distribution onto the `$PATH` which makes it available for subsequent 6 | buildpacks. These buildpacks can then use that distribution to run Go tooling 7 | including building Go application binaries. Examples of buildpacks that perform 8 | this binary building process include the [Go Mod 9 | CNB](https://github.com/paketo-buildpacks/go-mod) and the [Dep 10 | CNB](https://github.com/paketo-buildpacks/dep). 11 | 12 | ## Integration 13 | 14 | The Go Distribution CNB provides Go as a dependency. Downstream buildpacks, like 15 | [Go Mod](https://github.com/paketo-buildpacks/go-mod) or 16 | [Dep](https://github.com/paketo-buildpacks/dep), can require the go dependency 17 | by generating a [Build Plan 18 | TOML](https://github.com/buildpacks/spec/blob/master/buildpack.md#build-plan-toml) 19 | file that looks like the following: 20 | 21 | ```toml 22 | [[requires]] 23 | 24 | # The name of the dependency is "go". This value is considered 25 | # part of the public API for the buildpack and will not change without a plan 26 | # for deprecation. 27 | name = "go" 28 | 29 | # The version of the Go dependency is not required. In the case it 30 | # is not specified, the buildpack will provide the default version, which can 31 | # be seen in the buildpack.toml file. 32 | # If you wish to request a specific version, the buildpack supports 33 | # specifying a semver constraint in the form of "1.*", "1.13.*", or even 34 | # "1.13.9". 35 | version = "1.13.9" 36 | 37 | # The Go buildpack supports some non-required metadata options. 38 | [requires.metadata] 39 | 40 | # Setting the build flag to true will ensure that the Go 41 | # depdendency is available on the $PATH for subsequent buildpacks during 42 | # their build phase. If you are writing a buildpack that needs to run Go 43 | # during its build process, this flag should be set to true. 44 | build = true 45 | 46 | # Setting the launch flag to true will ensure that the Go 47 | # dependency is available on the $PATH for the running application. If you are 48 | # writing an application that needs to run Go at runtime, this flag should 49 | # be set to true. 50 | launch = true 51 | ``` 52 | 53 | ## Usage 54 | 55 | To package this buildpack for consumption: 56 | 57 | ``` 58 | $ ./scripts/package.sh 59 | ``` 60 | 61 | This builds the buildpack's Go source using `GOOS=linux` by default. You can 62 | supply another value as the first argument to `package.sh`. 63 | 64 | ## Go Build Configuration 65 | 66 | To configure the Go version, please use the `BP_GO_VERSION` environment 67 | variable at build time either directly 68 | (ex. `pack build my-app --env BP_GO_VERSION=~1.14.1`) or through a 69 | [`project.toml` 70 | file](https://github.com/buildpacks/spec/blob/main/extensions/project-descriptor.md). 71 | -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | package godist 2 | 3 | import ( 4 | "path/filepath" 5 | "time" 6 | 7 | "github.com/paketo-buildpacks/packit/v2" 8 | "github.com/paketo-buildpacks/packit/v2/cargo" 9 | "github.com/paketo-buildpacks/packit/v2/chronos" 10 | "github.com/paketo-buildpacks/packit/v2/postal" 11 | "github.com/paketo-buildpacks/packit/v2/sbom" 12 | "github.com/paketo-buildpacks/packit/v2/scribe" 13 | ) 14 | 15 | //go:generate faux --interface EntryResolver --output fakes/entry_resolver.go 16 | type EntryResolver interface { 17 | Resolve(name string, entries []packit.BuildpackPlanEntry, priorites []interface{}) (packit.BuildpackPlanEntry, []packit.BuildpackPlanEntry) 18 | MergeLayerTypes(name string, entries []packit.BuildpackPlanEntry) (launch, build bool) 19 | } 20 | 21 | //go:generate faux --interface DependencyManager --output fakes/dependency_manager.go 22 | type DependencyManager interface { 23 | Resolve(path, id, version, stack string) (postal.Dependency, error) 24 | Deliver(dependency postal.Dependency, cnbPath, layerPath, platformPath string) error 25 | GenerateBillOfMaterials(dependencies ...postal.Dependency) []packit.BOMEntry 26 | } 27 | 28 | //go:generate faux --interface SBOMGenerator --output fakes/sbom_generator.go 29 | type SBOMGenerator interface { 30 | GenerateFromDependency(dependency postal.Dependency, dir string) (sbom.SBOM, error) 31 | } 32 | 33 | func Build(entryResolver EntryResolver, dependencyManager DependencyManager, sbomGenerator SBOMGenerator, clock chronos.Clock, logs scribe.Emitter) packit.BuildFunc { 34 | return func(context packit.BuildContext) (packit.BuildResult, error) { 35 | logs.Title("%s %s", context.BuildpackInfo.Name, context.BuildpackInfo.Version) 36 | 37 | logs.Process("Resolving Go version") 38 | entry, entries := entryResolver.Resolve(GoDependency, context.Plan.Entries, Priorities) 39 | logs.Candidates(entries) 40 | 41 | version, ok := entry.Metadata["version"].(string) 42 | if !ok { 43 | version = "default" 44 | } 45 | 46 | dependency, err := dependencyManager.Resolve(filepath.Join(context.CNBPath, "buildpack.toml"), entry.Name, version, context.Stack) 47 | if err != nil { 48 | return packit.BuildResult{}, err 49 | } 50 | 51 | logs.SelectedDependency(entry, dependency, clock.Now()) 52 | bom := dependencyManager.GenerateBillOfMaterials(dependency) 53 | 54 | goLayer, err := context.Layers.Get(GoLayerName) 55 | if err != nil { 56 | return packit.BuildResult{}, err 57 | } 58 | 59 | launch, build := entryResolver.MergeLayerTypes(GoDependency, context.Plan.Entries) 60 | 61 | var buildMetadata = packit.BuildMetadata{} 62 | var launchMetadata = packit.LaunchMetadata{} 63 | if build { 64 | buildMetadata = packit.BuildMetadata{BOM: bom} 65 | } 66 | 67 | if launch { 68 | launchMetadata = packit.LaunchMetadata{BOM: bom} 69 | } 70 | 71 | cachedChecksum, ok := goLayer.Metadata["dependency-checksum"].(string) 72 | if ok && cargo.Checksum(dependency.Checksum).MatchString(cachedChecksum) { 73 | logs.Process("Reusing cached layer %s", goLayer.Path) 74 | logs.Break() 75 | 76 | goLayer.Launch, goLayer.Build, goLayer.Cache = launch, build, build 77 | 78 | return packit.BuildResult{ 79 | Layers: []packit.Layer{goLayer}, 80 | Build: buildMetadata, 81 | Launch: launchMetadata, 82 | }, nil 83 | } 84 | 85 | logs.Process("Executing build process") 86 | 87 | goLayer, err = goLayer.Reset() 88 | if err != nil { 89 | return packit.BuildResult{}, err 90 | } 91 | 92 | goLayer.Launch, goLayer.Build, goLayer.Cache = launch, build, build 93 | 94 | logs.Subprocess("Installing Go %s", dependency.Version) 95 | duration, err := clock.Measure(func() error { 96 | return dependencyManager.Deliver(dependency, context.CNBPath, goLayer.Path, context.Platform.Path) 97 | }) 98 | if err != nil { 99 | return packit.BuildResult{}, err 100 | } 101 | logs.Action("Completed in %s", duration.Round(time.Millisecond)) 102 | logs.Break() 103 | 104 | logs.GeneratingSBOM(goLayer.Path) 105 | var sbomContent sbom.SBOM 106 | duration, err = clock.Measure(func() error { 107 | sbomContent, err = sbomGenerator.GenerateFromDependency(dependency, goLayer.Path) 108 | return err 109 | }) 110 | if err != nil { 111 | return packit.BuildResult{}, err 112 | } 113 | 114 | logs.Action("Completed in %s", duration.Round(time.Millisecond)) 115 | logs.Break() 116 | 117 | logs.FormattingSBOM(context.BuildpackInfo.SBOMFormats...) 118 | goLayer.SBOM, err = sbomContent.InFormats(context.BuildpackInfo.SBOMFormats...) 119 | if err != nil { 120 | return packit.BuildResult{}, err 121 | } 122 | 123 | goLayer.Metadata = map[string]interface{}{ 124 | "dependency-checksum": dependency.Checksum, 125 | } 126 | 127 | return packit.BuildResult{ 128 | Layers: []packit.Layer{goLayer}, 129 | Build: buildMetadata, 130 | Launch: launchMetadata, 131 | }, nil 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /build_test.go: -------------------------------------------------------------------------------- 1 | package godist_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | godist "github.com/paketo-buildpacks/go-dist" 12 | "github.com/paketo-buildpacks/go-dist/fakes" 13 | "github.com/paketo-buildpacks/packit/v2" 14 | "github.com/paketo-buildpacks/packit/v2/chronos" 15 | 16 | //nolint Ignore SA1019, informed usage of deprecated package 17 | "github.com/paketo-buildpacks/packit/v2/paketosbom" 18 | "github.com/paketo-buildpacks/packit/v2/postal" 19 | "github.com/paketo-buildpacks/packit/v2/sbom" 20 | "github.com/paketo-buildpacks/packit/v2/scribe" 21 | "github.com/sclevine/spec" 22 | 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func testBuild(t *testing.T, context spec.G, it spec.S) { 27 | var ( 28 | Expect = NewWithT(t).Expect 29 | 30 | layersDir string 31 | workingDir string 32 | cnbDir string 33 | entryResolver *fakes.EntryResolver 34 | dependencyManager *fakes.DependencyManager 35 | sbomGenerator *fakes.SBOMGenerator 36 | buffer *bytes.Buffer 37 | 38 | build packit.BuildFunc 39 | ) 40 | 41 | it.Before(func() { 42 | var err error 43 | layersDir, err = os.MkdirTemp("", "layers") 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | cnbDir, err = os.MkdirTemp("", "cnb") 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | workingDir, err = os.MkdirTemp("", "working-dir") 50 | Expect(err).NotTo(HaveOccurred()) 51 | 52 | entryResolver = &fakes.EntryResolver{} 53 | entryResolver.ResolveCall.Returns.BuildpackPlanEntry = packit.BuildpackPlanEntry{ 54 | Name: "go", 55 | } 56 | 57 | dependencyManager = &fakes.DependencyManager{} 58 | dependencyManager.ResolveCall.Returns.Dependency = postal.Dependency{ 59 | ID: "go", 60 | Name: "go-dependency-name", 61 | Checksum: "sha256:go-dependency-sha", 62 | Stacks: []string{"some-stack"}, 63 | URI: "go-dependency-uri", 64 | Version: "go-dependency-version", 65 | } 66 | 67 | dependencyManager.GenerateBillOfMaterialsCall.Returns.BOMEntrySlice = []packit.BOMEntry{ 68 | { 69 | Name: "go", 70 | Metadata: paketosbom.BOMMetadata{ 71 | Version: "go-dependency-version", 72 | Checksum: paketosbom.BOMChecksum{ 73 | Algorithm: paketosbom.SHA256, 74 | Hash: "go-dependency-sha", 75 | }, 76 | URI: "go-dependency-uri", 77 | }, 78 | }, 79 | } 80 | 81 | sbomGenerator = &fakes.SBOMGenerator{} 82 | sbomGenerator.GenerateFromDependencyCall.Returns.SBOM = sbom.SBOM{} 83 | 84 | buffer = bytes.NewBuffer(nil) 85 | 86 | build = godist.Build(entryResolver, dependencyManager, sbomGenerator, chronos.DefaultClock, scribe.NewEmitter(buffer)) 87 | }) 88 | 89 | it.After(func() { 90 | Expect(os.RemoveAll(layersDir)).To(Succeed()) 91 | Expect(os.RemoveAll(cnbDir)).To(Succeed()) 92 | Expect(os.RemoveAll(workingDir)).To(Succeed()) 93 | }) 94 | 95 | it("returns a result that installs go", func() { 96 | result, err := build(packit.BuildContext{ 97 | BuildpackInfo: packit.BuildpackInfo{ 98 | Name: "Some Buildpack", 99 | Version: "some-version", 100 | SBOMFormats: []string{sbom.CycloneDXFormat, sbom.SPDXFormat}, 101 | }, 102 | WorkingDir: workingDir, 103 | CNBPath: cnbDir, 104 | Plan: packit.BuildpackPlan{ 105 | Entries: []packit.BuildpackPlanEntry{ 106 | {Name: "go"}, 107 | }, 108 | }, 109 | Platform: packit.Platform{Path: "platform"}, 110 | Layers: packit.Layers{Path: layersDir}, 111 | Stack: "some-stack", 112 | }) 113 | Expect(err).NotTo(HaveOccurred()) 114 | 115 | Expect(result.Layers).To(HaveLen(1)) 116 | layer := result.Layers[0] 117 | 118 | Expect(layer.Name).To(Equal("go")) 119 | Expect(layer.Path).To(Equal(filepath.Join(layersDir, "go"))) 120 | Expect(layer.Metadata).To(Equal(map[string]interface{}{ 121 | "dependency-checksum": "sha256:go-dependency-sha", 122 | })) 123 | 124 | Expect(layer.SBOM.Formats()).To(HaveLen(2)) 125 | cdx := layer.SBOM.Formats()[0] 126 | spdx := layer.SBOM.Formats()[1] 127 | 128 | Expect(cdx.Extension).To(Equal("cdx.json")) 129 | content, err := io.ReadAll(cdx.Content) 130 | Expect(err).NotTo(HaveOccurred()) 131 | Expect(string(content)).To(MatchJSON(`{ 132 | "bomFormat": "CycloneDX", 133 | "components": [], 134 | "metadata": { 135 | "tools": [ 136 | { 137 | "name": "syft", 138 | "vendor": "anchore", 139 | "version": "[not provided]" 140 | } 141 | ] 142 | }, 143 | "specVersion": "1.3", 144 | "version": 1 145 | }`)) 146 | 147 | Expect(spdx.Extension).To(Equal("spdx.json")) 148 | content, err = io.ReadAll(spdx.Content) 149 | Expect(err).NotTo(HaveOccurred()) 150 | Expect(string(content)).To(MatchJSON(`{ 151 | "SPDXID": "SPDXRef-DOCUMENT", 152 | "creationInfo": { 153 | "created": "0001-01-01T00:00:00Z", 154 | "creators": [ 155 | "Organization: Anchore, Inc", 156 | "Tool: syft-" 157 | ], 158 | "licenseListVersion": "3.16" 159 | }, 160 | "dataLicense": "CC0-1.0", 161 | "documentNamespace": "https://paketo.io/packit/unknown-source-type/unknown-88cfa225-65e0-5755-895f-c1c8f10fde76", 162 | "name": "unknown", 163 | "relationships": [ 164 | { 165 | "relatedSpdxElement": "SPDXRef-DOCUMENT", 166 | "relationshipType": "DESCRIBES", 167 | "spdxElementId": "SPDXRef-DOCUMENT" 168 | } 169 | ], 170 | "spdxVersion": "SPDX-2.2" 171 | }`)) 172 | 173 | Expect(entryResolver.ResolveCall.Receives.Name).To(Equal("go")) 174 | Expect(entryResolver.ResolveCall.Receives.Entries).To(Equal([]packit.BuildpackPlanEntry{ 175 | {Name: "go"}, 176 | })) 177 | 178 | Expect(entryResolver.MergeLayerTypesCall.Receives.Name).To(Equal("go")) 179 | Expect(entryResolver.MergeLayerTypesCall.Receives.Entries).To(Equal([]packit.BuildpackPlanEntry{ 180 | {Name: "go"}, 181 | })) 182 | 183 | Expect(dependencyManager.ResolveCall.Receives.Path).To(Equal(filepath.Join(cnbDir, "buildpack.toml"))) 184 | Expect(dependencyManager.ResolveCall.Receives.Id).To(Equal("go")) 185 | Expect(dependencyManager.ResolveCall.Receives.Version).To(Equal("default")) 186 | Expect(dependencyManager.ResolveCall.Receives.Stack).To(Equal("some-stack")) 187 | 188 | Expect(dependencyManager.DeliverCall.Receives.Dependency).To(Equal(postal.Dependency{ 189 | ID: "go", 190 | Name: "go-dependency-name", 191 | Checksum: "sha256:go-dependency-sha", 192 | Stacks: []string{"some-stack"}, 193 | URI: "go-dependency-uri", 194 | Version: "go-dependency-version", 195 | })) 196 | Expect(dependencyManager.DeliverCall.Receives.CnbPath).To(Equal(cnbDir)) 197 | Expect(dependencyManager.DeliverCall.Receives.LayerPath).To(Equal(filepath.Join(layersDir, "go"))) 198 | Expect(dependencyManager.DeliverCall.Receives.PlatformPath).To(Equal("platform")) 199 | 200 | Expect(dependencyManager.GenerateBillOfMaterialsCall.Receives.Dependencies).To(Equal([]postal.Dependency{ 201 | { 202 | ID: "go", 203 | Name: "go-dependency-name", 204 | Checksum: "sha256:go-dependency-sha", 205 | Stacks: []string{"some-stack"}, 206 | URI: "go-dependency-uri", 207 | Version: "go-dependency-version", 208 | }, 209 | })) 210 | 211 | Expect(sbomGenerator.GenerateFromDependencyCall.Receives.Dependency).To(Equal(postal.Dependency{ 212 | ID: "go", 213 | Name: "go-dependency-name", 214 | Checksum: "sha256:go-dependency-sha", 215 | Stacks: []string{"some-stack"}, 216 | URI: "go-dependency-uri", 217 | Version: "go-dependency-version", 218 | })) 219 | Expect(sbomGenerator.GenerateFromDependencyCall.Receives.Dir).To(Equal(filepath.Join(layersDir, "go"))) 220 | 221 | Expect(buffer.String()).To(ContainSubstring("Some Buildpack some-version")) 222 | Expect(buffer.String()).To(ContainSubstring("Resolving Go version")) 223 | Expect(buffer.String()).To(ContainSubstring("Selected go-dependency-name version (using ): go-dependency-version")) 224 | Expect(buffer.String()).To(ContainSubstring("Executing build process")) 225 | Expect(buffer.String()).To(ContainSubstring("Installing Go go-dependency-version")) 226 | Expect(buffer.String()).To(ContainSubstring("Completed in")) 227 | }) 228 | 229 | context("when the plan entry requires the dependency during the build and launch phases", func() { 230 | it.Before(func() { 231 | entryResolver.MergeLayerTypesCall.Returns.Launch = true 232 | entryResolver.MergeLayerTypesCall.Returns.Build = true 233 | }) 234 | 235 | it("makes the layer available in those phases", func() { 236 | result, err := build(packit.BuildContext{ 237 | CNBPath: cnbDir, 238 | Plan: packit.BuildpackPlan{ 239 | Entries: []packit.BuildpackPlanEntry{ 240 | { 241 | Name: "go", 242 | Metadata: map[string]interface{}{ 243 | "build": true, 244 | "launch": true, 245 | }, 246 | }, 247 | }, 248 | }, 249 | Layers: packit.Layers{Path: layersDir}, 250 | Stack: "some-stack", 251 | }) 252 | Expect(err).NotTo(HaveOccurred()) 253 | 254 | Expect(result.Layers).To(HaveLen(1)) 255 | layer := result.Layers[0] 256 | 257 | Expect(layer.Name).To(Equal("go")) 258 | Expect(layer.Path).To(Equal(filepath.Join(layersDir, "go"))) 259 | Expect(layer.Metadata).To(Equal(map[string]interface{}{ 260 | "dependency-checksum": "sha256:go-dependency-sha", 261 | })) 262 | Expect(layer.Build).To(BeTrue()) 263 | Expect(layer.Launch).To(BeTrue()) 264 | Expect(layer.Cache).To(BeTrue()) 265 | 266 | Expect(result.Build.BOM).To(HaveLen(1)) 267 | buildBOMEntry := result.Build.BOM[0] 268 | Expect(buildBOMEntry.Name).To(Equal("go")) 269 | Expect(buildBOMEntry.Metadata).To(Equal(paketosbom.BOMMetadata{ 270 | Version: "go-dependency-version", 271 | Checksum: paketosbom.BOMChecksum{ 272 | Algorithm: paketosbom.SHA256, 273 | Hash: "go-dependency-sha", 274 | }, 275 | URI: "go-dependency-uri", 276 | })) 277 | 278 | Expect(result.Launch.BOM).To(HaveLen(1)) 279 | launchBOMEntry := result.Launch.BOM[0] 280 | Expect(launchBOMEntry.Name).To(Equal("go")) 281 | Expect(launchBOMEntry.Metadata).To(Equal(paketosbom.BOMMetadata{ 282 | Version: "go-dependency-version", 283 | Checksum: paketosbom.BOMChecksum{ 284 | Algorithm: paketosbom.SHA256, 285 | Hash: "go-dependency-sha", 286 | }, 287 | URI: "go-dependency-uri", 288 | })) 289 | }) 290 | }) 291 | 292 | context("failure cases", func() { 293 | context("when the dependency cannot be resolved", func() { 294 | it.Before(func() { 295 | dependencyManager.ResolveCall.Returns.Error = errors.New("failed to resolve dependency") 296 | }) 297 | 298 | it("returns an error", func() { 299 | _, err := build(packit.BuildContext{ 300 | CNBPath: cnbDir, 301 | Plan: packit.BuildpackPlan{ 302 | Entries: []packit.BuildpackPlanEntry{ 303 | {Name: "go"}, 304 | }, 305 | }, 306 | Layers: packit.Layers{Path: layersDir}, 307 | Stack: "some-stack", 308 | }) 309 | Expect(err).To(MatchError("failed to resolve dependency")) 310 | }) 311 | }) 312 | 313 | context("when the go layer cannot be retrieved", func() { 314 | it.Before(func() { 315 | err := os.WriteFile(filepath.Join(layersDir, "go.toml"), nil, 0000) 316 | Expect(err).NotTo(HaveOccurred()) 317 | }) 318 | 319 | it("returns an error", func() { 320 | _, err := build(packit.BuildContext{ 321 | CNBPath: cnbDir, 322 | Plan: packit.BuildpackPlan{ 323 | Entries: []packit.BuildpackPlanEntry{ 324 | {Name: "go"}, 325 | }, 326 | }, 327 | Layers: packit.Layers{Path: layersDir}, 328 | Stack: "some-stack", 329 | }) 330 | Expect(err).To(MatchError(ContainSubstring("failed to parse layer content metadata"))) 331 | }) 332 | }) 333 | 334 | context("when the go layer cannot be reset", func() { 335 | it.Before(func() { 336 | Expect(os.MkdirAll(filepath.Join(layersDir, "go", "something"), os.ModePerm)).To(Succeed()) 337 | Expect(os.Chmod(filepath.Join(layersDir, "go"), 0500)).To(Succeed()) 338 | }) 339 | 340 | it.After(func() { 341 | Expect(os.Chmod(filepath.Join(layersDir, "go"), os.ModePerm)).To(Succeed()) 342 | }) 343 | 344 | it("returns an error", func() { 345 | _, err := build(packit.BuildContext{ 346 | CNBPath: cnbDir, 347 | Plan: packit.BuildpackPlan{ 348 | Entries: []packit.BuildpackPlanEntry{ 349 | {Name: "go"}, 350 | }, 351 | }, 352 | Layers: packit.Layers{Path: layersDir}, 353 | Stack: "some-stack", 354 | }) 355 | Expect(err).To(MatchError(ContainSubstring("could not remove file"))) 356 | }) 357 | }) 358 | 359 | context("when the dependency cannot be installed", func() { 360 | it.Before(func() { 361 | dependencyManager.DeliverCall.Returns.Error = errors.New("failed to deliver dependency") 362 | }) 363 | 364 | it("returns an error", func() { 365 | _, err := build(packit.BuildContext{ 366 | CNBPath: cnbDir, 367 | Plan: packit.BuildpackPlan{ 368 | Entries: []packit.BuildpackPlanEntry{ 369 | {Name: "go"}, 370 | }, 371 | }, 372 | Layers: packit.Layers{Path: layersDir}, 373 | Stack: "some-stack", 374 | }) 375 | Expect(err).To(MatchError("failed to deliver dependency")) 376 | }) 377 | }) 378 | 379 | context("when generating the SBOM returns an error", func() { 380 | it.Before(func() { 381 | sbomGenerator.GenerateFromDependencyCall.Returns.Error = errors.New("failed to generate SBOM") 382 | }) 383 | 384 | it("returns an error", func() { 385 | _, err := build(packit.BuildContext{ 386 | CNBPath: cnbDir, 387 | Plan: packit.BuildpackPlan{ 388 | Entries: []packit.BuildpackPlanEntry{ 389 | {Name: "go"}, 390 | }, 391 | }, 392 | Layers: packit.Layers{Path: layersDir}, 393 | Stack: "some-stack", 394 | }) 395 | Expect(err).To(MatchError(ContainSubstring("failed to generate SBOM"))) 396 | }) 397 | }) 398 | 399 | context("when formatting the SBOM returns an error", func() { 400 | it("returns an error", func() { 401 | _, err := build(packit.BuildContext{ 402 | BuildpackInfo: packit.BuildpackInfo{SBOMFormats: []string{"random-format"}}, 403 | CNBPath: cnbDir, 404 | Plan: packit.BuildpackPlan{ 405 | Entries: []packit.BuildpackPlanEntry{ 406 | {Name: "go"}, 407 | }, 408 | }, 409 | Layers: packit.Layers{Path: layersDir}, 410 | Stack: "some-stack", 411 | }) 412 | Expect(err).To(MatchError("unsupported SBOM format: 'random-format'")) 413 | }) 414 | }) 415 | }) 416 | } 417 | -------------------------------------------------------------------------------- /buildpack.toml: -------------------------------------------------------------------------------- 1 | api = "0.7" 2 | 3 | [buildpack] 4 | description = "A buildpack for installing the appropriate Go compiler distribution version" 5 | homepage = "https://github.com/paketo-buildpacks/go-dist" 6 | id = "paketo-buildpacks/go-dist" 7 | keywords = ["go", "distribution", "compiler"] 8 | name = "Paketo Buildpack for Go Distribution" 9 | sbom-formats = ["application/vnd.cyclonedx+json", "application/spdx+json", "application/vnd.syft+json"] 10 | 11 | [[buildpack.licenses]] 12 | type = "Apache-2.0" 13 | uri = "https://github.com/paketo-buildpacks/go-dist/blob/main/LICENSE" 14 | 15 | [metadata] 16 | include-files = ["bin/run", "bin/build", "bin/detect", "buildpack.toml"] 17 | pre-package = "./scripts/build.sh" 18 | [metadata.default-versions] 19 | go = "1.23.*" 20 | 21 | [[metadata.dependencies]] 22 | checksum = "sha256:4741525e69841f2e22f9992af25df0c1112b07501f61f741c12c6389fcb119f3" 23 | cpe = "cpe:2.3:a:golang:go:1.23.7:*:*:*:*:*:*:*" 24 | id = "go" 25 | licenses = ["BSD-3-Clause"] 26 | name = "Go" 27 | purl = "pkg:generic/go@go1.23.7?checksum=4741525e69841f2e22f9992af25df0c1112b07501f61f741c12c6389fcb119f3&download_url=https://go.dev/dl/go1.23.7.linux-amd64.tar.gz" 28 | source = "https://go.dev/dl/go1.23.7.src.tar.gz" 29 | source-checksum = "sha256:7cfabd46b73eb4c26b19d69515dd043d7183a6559acccd5cfdb25eb6b266a458" 30 | stacks = ["*"] 31 | strip-components = 1 32 | uri = "https://go.dev/dl/go1.23.7.linux-amd64.tar.gz" 33 | version = "1.23.7" 34 | 35 | [[metadata.dependencies]] 36 | checksum = "sha256:45b87381172a58d62c977f27c4683c8681ef36580abecd14fd124d24ca306d3f" 37 | cpe = "cpe:2.3:a:golang:go:1.23.8:*:*:*:*:*:*:*" 38 | id = "go" 39 | licenses = ["BSD-3-Clause", "BSD-3-Clause-Clear"] 40 | name = "Go" 41 | purl = "pkg:generic/go@go1.23.8?checksum=45b87381172a58d62c977f27c4683c8681ef36580abecd14fd124d24ca306d3f&download_url=https://go.dev/dl/go1.23.8.linux-amd64.tar.gz" 42 | source = "https://go.dev/dl/go1.23.8.src.tar.gz" 43 | source-checksum = "sha256:0ca1f1e37ea255e3ce283af3f4e628502fb444587da987a5bb96d6c6f15930d4" 44 | stacks = ["*"] 45 | strip-components = 1 46 | uri = "https://go.dev/dl/go1.23.8.linux-amd64.tar.gz" 47 | version = "1.23.8" 48 | 49 | [[metadata.dependencies]] 50 | checksum = "sha256:cb2396bae64183cdccf81a9a6df0aea3bce9511fc21469fb89a0c00470088073" 51 | cpe = "cpe:2.3:a:golang:go:1.24.1:*:*:*:*:*:*:*" 52 | id = "go" 53 | licenses = ["BSD-2-Clause", "BSD-3-Clause", "BSD-Source-Code"] 54 | name = "Go" 55 | purl = "pkg:generic/go@go1.24.1?checksum=cb2396bae64183cdccf81a9a6df0aea3bce9511fc21469fb89a0c00470088073&download_url=https://go.dev/dl/go1.24.1.linux-amd64.tar.gz" 56 | source = "https://go.dev/dl/go1.24.1.src.tar.gz" 57 | source-checksum = "sha256:8244ebf46c65607db10222b5806aeb31c1fcf8979c1b6b12f60c677e9a3c0656" 58 | stacks = ["*"] 59 | strip-components = 1 60 | uri = "https://go.dev/dl/go1.24.1.linux-amd64.tar.gz" 61 | version = "1.24.1" 62 | 63 | [[metadata.dependencies]] 64 | checksum = "sha256:68097bd680839cbc9d464a0edce4f7c333975e27a90246890e9f1078c7e702ad" 65 | cpe = "cpe:2.3:a:golang:go:1.24.2:*:*:*:*:*:*:*" 66 | id = "go" 67 | licenses = ["BSD-2-Clause", "BSD-3-Clause", "BSD-Source-Code"] 68 | name = "Go" 69 | purl = "pkg:generic/go@go1.24.2?checksum=68097bd680839cbc9d464a0edce4f7c333975e27a90246890e9f1078c7e702ad&download_url=https://go.dev/dl/go1.24.2.linux-amd64.tar.gz" 70 | source = "https://go.dev/dl/go1.24.2.src.tar.gz" 71 | source-checksum = "sha256:9dc77ffadc16d837a1bf32d99c624cb4df0647cee7b119edd9e7b1bcc05f2e00" 72 | stacks = ["*"] 73 | strip-components = 1 74 | uri = "https://go.dev/dl/go1.24.2.linux-amd64.tar.gz" 75 | version = "1.24.2" 76 | 77 | [[metadata.dependency-constraints]] 78 | constraint = "1.23.*" 79 | id = "go" 80 | patches = 2 81 | 82 | [[metadata.dependency-constraints]] 83 | constraint = "1.24.*" 84 | id = "go" 85 | patches = 2 86 | 87 | [[stacks]] 88 | id = "*" 89 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package godist 2 | 3 | const ( 4 | GoDependency = "go" 5 | GoLayerName = "go" 6 | ) 7 | 8 | var Priorities = []interface{}{ 9 | "BP_GO_VERSION", 10 | "go.mod", 11 | } 12 | -------------------------------------------------------------------------------- /dependency/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: retrieve 2 | 3 | retrieve: 4 | @cd retrieval; \ 5 | go run main.go \ 6 | --buildpack-toml-path "${buildpackTomlPath}" \ 7 | --output "${output}" 8 | -------------------------------------------------------------------------------- /dependency/retrieval/components/dependency.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/paketo-buildpacks/packit/v2/cargo" 9 | ) 10 | 11 | func ConvertReleaseToDependency(release Release) (cargo.ConfigMetadataDependency, error) { 12 | var archive, source ReleaseFile 13 | for _, file := range release.Files { 14 | if file.OS == "linux" && file.Arch == "amd64" { 15 | archive = file 16 | } 17 | if file.Kind == "source" { 18 | source = file 19 | } 20 | } 21 | 22 | if (archive == ReleaseFile{} || source == ReleaseFile{}) { 23 | return cargo.ConfigMetadataDependency{}, fmt.Errorf("could not find release file for linux/amd64") 24 | } 25 | 26 | purl := GeneratePURL("go", release.Version, archive.SHA256, archive.URL) 27 | 28 | licenses, err := GenerateLicenseInformation(source.URL) 29 | if err != nil { 30 | return cargo.ConfigMetadataDependency{}, err 31 | } 32 | 33 | // Validate the artifact 34 | archiveResponse, err := http.Get(archive.URL) 35 | if err != nil { 36 | return cargo.ConfigMetadataDependency{}, err 37 | } 38 | defer archiveResponse.Body.Close() 39 | 40 | vr := cargo.NewValidatedReader(archiveResponse.Body, fmt.Sprintf("sha256:%s", archive.SHA256)) 41 | valid, err := vr.Valid() 42 | if err != nil { 43 | return cargo.ConfigMetadataDependency{}, err 44 | } 45 | 46 | if !valid { 47 | return cargo.ConfigMetadataDependency{}, fmt.Errorf("the given checksum of the artifact does not match with downloaded artifact") 48 | } 49 | 50 | // Validate the source 51 | sourceResponse, err := http.Get(source.URL) 52 | if err != nil { 53 | return cargo.ConfigMetadataDependency{}, err 54 | } 55 | defer sourceResponse.Body.Close() 56 | 57 | vr = cargo.NewValidatedReader(sourceResponse.Body, fmt.Sprintf("sha256:%s", source.SHA256)) 58 | valid, err = vr.Valid() 59 | if err != nil { 60 | return cargo.ConfigMetadataDependency{}, err 61 | } 62 | 63 | if !valid { 64 | return cargo.ConfigMetadataDependency{}, fmt.Errorf("the given checksum of the source does not match with downloaded source") 65 | } 66 | 67 | return cargo.ConfigMetadataDependency{ 68 | ID: "go", 69 | Name: "Go", 70 | Version: release.SemVer.String(), 71 | Stacks: []string{"*"}, 72 | URI: archive.URL, 73 | Checksum: fmt.Sprintf("sha256:%s", archive.SHA256), 74 | Source: source.URL, 75 | SourceChecksum: fmt.Sprintf("sha256:%s", source.SHA256), 76 | StripComponents: 1, 77 | CPE: fmt.Sprintf("cpe:2.3:a:golang:go:%s:*:*:*:*:*:*:*", strings.TrimPrefix(release.Version, "go")), 78 | PURL: purl, 79 | Licenses: licenses, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /dependency/retrieval/components/dependency_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/Masterminds/semver/v3" 13 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 14 | "github.com/paketo-buildpacks/packit/v2/cargo" 15 | "github.com/sclevine/spec" 16 | 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | const ( 21 | lFile = `The MIT License (MIT) 22 | 23 | Copyright (c) .NET Foundation and Contributors 24 | 25 | All rights reserved. 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. 44 | ` 45 | ) 46 | 47 | func testDependency(t *testing.T, context spec.G, it spec.S) { 48 | 49 | var ( 50 | Expect = NewWithT(t).Expect 51 | ) 52 | 53 | context("ConvertReleaseToDependeny", func() { 54 | var ( 55 | server *httptest.Server 56 | ) 57 | 58 | it.Before(func() { 59 | buffer := bytes.NewBuffer(nil) 60 | gw := gzip.NewWriter(buffer) 61 | tw := tar.NewWriter(gw) 62 | 63 | Expect(tw.WriteHeader(&tar.Header{Name: "some-dir", Mode: 0755, Typeflag: tar.TypeDir})).To(Succeed()) 64 | _, err := tw.Write(nil) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | licenseFile := "some-dir/LICENSE.txt" 68 | Expect(tw.WriteHeader(&tar.Header{Name: licenseFile, Mode: 0755, Size: int64(len(lFile))})).To(Succeed()) 69 | _, err = tw.Write([]byte(lFile)) 70 | Expect(err).NotTo(HaveOccurred()) 71 | 72 | Expect(tw.Close()).To(Succeed()) 73 | Expect(gw.Close()).To(Succeed()) 74 | 75 | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 76 | if req.Method == http.MethodHead { 77 | http.Error(w, "NotFound", http.StatusNotFound) 78 | return 79 | } 80 | 81 | switch req.URL.Path { 82 | case "/source": 83 | w.WriteHeader(http.StatusOK) 84 | _, err := w.Write(buffer.Bytes()) 85 | Expect(err).NotTo(HaveOccurred()) 86 | 87 | case "/archive": 88 | w.WriteHeader(http.StatusOK) 89 | _, err := w.Write(nil) 90 | Expect(err).NotTo(HaveOccurred()) 91 | 92 | case "/bad-archive": 93 | w.WriteHeader(http.StatusOK) 94 | _, err := w.Write([]byte("\x66\x4C\x61\x43\x00\x00\x00\x22")) 95 | Expect(err).NotTo(HaveOccurred()) 96 | 97 | default: 98 | t.Fatalf("unknown path: %s", req.URL.Path) 99 | } 100 | })) 101 | }) 102 | 103 | it("returns returns a cargo dependency generated from the given release", func() { 104 | dependency, err := components.ConvertReleaseToDependency(components.Release{ 105 | SemVer: semver.MustParse("1.19"), 106 | Version: "go1.19", 107 | Files: []components.ReleaseFile{ 108 | { 109 | URL: fmt.Sprintf("%s/source", server.URL), 110 | Filename: "go1.19.src.tar.gz", 111 | OS: "", 112 | Arch: "", 113 | Version: "go1.19", 114 | SHA256: "ad1b820bde8c32707f8bb8ce636750b1c1b7c83a82e43481910bef2f4f77dcb5", 115 | Size: 26521849, 116 | Kind: "source", 117 | }, 118 | { 119 | URL: fmt.Sprintf("%s/archive", server.URL), 120 | Filename: "go1.19.linux-amd64.tar.gz", 121 | OS: "linux", 122 | Arch: "amd64", 123 | Version: "go1.19", 124 | SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 125 | Size: 26521849, 126 | Kind: "archive", 127 | }, 128 | }, 129 | }, 130 | ) 131 | Expect(err).NotTo(HaveOccurred()) 132 | 133 | Expect(dependency).To(Equal(cargo.ConfigMetadataDependency{ 134 | Checksum: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 135 | CPE: "cpe:2.3:a:golang:go:1.19:*:*:*:*:*:*:*", 136 | PURL: fmt.Sprintf("pkg:generic/go@go1.19?checksum=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&download_url=%s", fmt.Sprintf("%s/archive", server.URL)), 137 | ID: "go", 138 | Licenses: []interface{}{"MIT", "MIT-0"}, 139 | Name: "Go", 140 | SHA256: "", 141 | Source: fmt.Sprintf("%s/source", server.URL), 142 | SourceChecksum: "sha256:ad1b820bde8c32707f8bb8ce636750b1c1b7c83a82e43481910bef2f4f77dcb5", 143 | SourceSHA256: "", 144 | Stacks: []string{"*"}, 145 | StripComponents: 1, 146 | URI: fmt.Sprintf("%s/archive", server.URL), 147 | Version: "1.19.0", 148 | })) 149 | }) 150 | 151 | context("failure cases", func() { 152 | context("when there is not a release files", func() { 153 | it("returns an error", func() { 154 | _, err := components.ConvertReleaseToDependency(components.Release{}) 155 | Expect(err).To(MatchError("could not find release file for linux/amd64")) 156 | }) 157 | }) 158 | 159 | context("when the source is not a supported archive type", func() { 160 | it("returns an error", func() { 161 | _, err := components.ConvertReleaseToDependency(components.Release{ 162 | SemVer: semver.MustParse("1.19"), 163 | Version: "go1.19", 164 | Files: []components.ReleaseFile{ 165 | { 166 | URL: fmt.Sprintf("%s/bad-archive", server.URL), 167 | Filename: "go1.19.src.tar.gz", 168 | OS: "", 169 | Arch: "", 170 | Version: "go1.19", 171 | SHA256: "5a95bcffa592dcc7689ef5b4d993da3ca805b3c58d1710da8effeedbda87d471", 172 | Size: 26521849, 173 | Kind: "source", 174 | }, 175 | { 176 | URL: fmt.Sprintf("%s/archive", server.URL), 177 | Filename: "go1.19.linux-amd64.tar.gz", 178 | OS: "linux", 179 | Arch: "amd64", 180 | Version: "go1.19", 181 | SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 182 | Size: 26521849, 183 | Kind: "archive", 184 | }, 185 | }, 186 | }, 187 | ) 188 | Expect(err).To(MatchError(ContainSubstring("unsupported archive type"))) 189 | }) 190 | }) 191 | 192 | context("when the checksum does not match", func() { 193 | context("when the artifact file has the wrong checksum", func() { 194 | it("returns an error", func() { 195 | _, err := components.ConvertReleaseToDependency(components.Release{ 196 | SemVer: semver.MustParse("1.19"), 197 | Version: "go1.19", 198 | Files: []components.ReleaseFile{ 199 | { 200 | URL: fmt.Sprintf("%s/source", server.URL), 201 | Filename: "go1.19.src.tar.gz", 202 | OS: "", 203 | Arch: "", 204 | Version: "go1.19", 205 | SHA256: "ad1b820bde8c32707f8bb8ce636750b1c1b7c83a82e43481910bef2f4f77dcb5", 206 | Size: 26521849, 207 | Kind: "source", 208 | }, 209 | { 210 | URL: fmt.Sprintf("%s/archive", server.URL), 211 | Filename: "go1.19.linux-amd64.tar.gz", 212 | OS: "linux", 213 | Arch: "amd64", 214 | Version: "go1.19", 215 | SHA256: "invalid checksum", 216 | Size: 26521849, 217 | Kind: "archive", 218 | }, 219 | }, 220 | }, 221 | ) 222 | Expect(err).To(MatchError("the given checksum of the artifact does not match with downloaded artifact")) 223 | }) 224 | }) 225 | 226 | context("when the source file has the wrong checksum", func() { 227 | it("returns an error", func() { 228 | _, err := components.ConvertReleaseToDependency(components.Release{ 229 | SemVer: semver.MustParse("1.19"), 230 | Version: "go1.19", 231 | Files: []components.ReleaseFile{ 232 | { 233 | URL: fmt.Sprintf("%s/source", server.URL), 234 | Filename: "go1.19.src.tar.gz", 235 | OS: "", 236 | Arch: "", 237 | Version: "go1.19", 238 | SHA256: "invalid checksum", 239 | Size: 26521849, 240 | Kind: "source", 241 | }, 242 | { 243 | URL: fmt.Sprintf("%s/archive", server.URL), 244 | Filename: "go1.19.linux-amd64.tar.gz", 245 | OS: "linux", 246 | Arch: "amd64", 247 | Version: "go1.19", 248 | SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 249 | Size: 26521849, 250 | Kind: "archive", 251 | }, 252 | }, 253 | }, 254 | ) 255 | Expect(err).To(MatchError("the given checksum of the source does not match with downloaded source")) 256 | }) 257 | }) 258 | }) 259 | }) 260 | }) 261 | } 262 | -------------------------------------------------------------------------------- /dependency/retrieval/components/init_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sclevine/spec" 7 | "github.com/sclevine/spec/report" 8 | ) 9 | 10 | func TestUnit(t *testing.T) { 11 | suite := spec.New("go", spec.Report(report.Terminal{}), spec.Parallel()) 12 | suite("Dependency", testDependency) 13 | suite("License", testLicense) 14 | suite("Output", testOutput) 15 | suite("Purl", testPurl) 16 | suite("Releases", testReleases) 17 | suite("Versions", testVersions) 18 | suite.Run(t) 19 | } 20 | -------------------------------------------------------------------------------- /dependency/retrieval/components/license.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "os" 7 | "sort" 8 | 9 | "github.com/go-enry/go-license-detector/v4/licensedb" 10 | "github.com/go-enry/go-license-detector/v4/licensedb/filer" 11 | "github.com/paketo-buildpacks/packit/v2/vacation" 12 | ) 13 | 14 | func GenerateLicenseInformation(url string) ([]interface{}, error) { 15 | dir, err := os.MkdirTemp("", "") 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer os.RemoveAll(dir) 20 | 21 | response, err := http.Get(url) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer response.Body.Close() 26 | 27 | err = vacation.NewArchive(response.Body).StripComponents(1).Decompress(dir) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | f, err := filer.FromDirectory(dir) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | ls, err := licensedb.Detect(f) 38 | if err != nil { 39 | if errors.Is(err, licensedb.ErrNoLicenseFound) { 40 | return nil, nil 41 | } else { 42 | return nil, err 43 | } 44 | } 45 | 46 | var licenseIDs []string 47 | for license := range ls { 48 | licenseIDs = append(licenseIDs, license) 49 | } 50 | 51 | sort.Strings(licenseIDs) 52 | 53 | var licenseIDsAsInterface []interface{} 54 | for _, licenseID := range licenseIDs { 55 | licenseIDsAsInterface = append(licenseIDsAsInterface, licenseID) 56 | } 57 | 58 | return licenseIDsAsInterface, nil 59 | } 60 | -------------------------------------------------------------------------------- /dependency/retrieval/components/license_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 13 | "github.com/sclevine/spec" 14 | 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | func testLicense(t *testing.T, context spec.G, it spec.S) { 19 | 20 | var ( 21 | Expect = NewWithT(t).Expect 22 | ) 23 | 24 | context("GenerateLicenseInformation", func() { 25 | var ( 26 | server *httptest.Server 27 | ) 28 | 29 | it.Before(func() { 30 | buffer := bytes.NewBuffer(nil) 31 | gw := gzip.NewWriter(buffer) 32 | tw := tar.NewWriter(gw) 33 | 34 | Expect(tw.WriteHeader(&tar.Header{Name: "some-dir", Mode: 0755, Typeflag: tar.TypeDir})).To(Succeed()) 35 | _, err := tw.Write(nil) 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | licenseFile := "some-dir/LICENSE.txt" 39 | Expect(tw.WriteHeader(&tar.Header{Name: licenseFile, Mode: 0755, Size: int64(len(lFile))})).To(Succeed()) 40 | _, err = tw.Write([]byte(lFile)) 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | Expect(tw.Close()).To(Succeed()) 44 | Expect(gw.Close()).To(Succeed()) 45 | 46 | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 47 | if req.Method == http.MethodHead { 48 | http.Error(w, "NotFound", http.StatusNotFound) 49 | return 50 | } 51 | 52 | switch req.URL.Path { 53 | case "/": 54 | w.WriteHeader(http.StatusOK) 55 | _, err := w.Write(buffer.Bytes()) 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | case "/bad-archive": 59 | w.WriteHeader(http.StatusOK) 60 | _, err := w.Write([]byte("\x66\x4C\x61\x43\x00\x00\x00\x22")) 61 | Expect(err).NotTo(HaveOccurred()) 62 | 63 | default: 64 | t.Fatalf("unknown path: %s", req.URL.Path) 65 | } 66 | })) 67 | 68 | }) 69 | 70 | it("returns returns a cargo dependency generated from the given release", func() { 71 | licenses, err := components.GenerateLicenseInformation(server.URL) 72 | Expect(err).NotTo(HaveOccurred()) 73 | 74 | Expect(licenses).To(Equal([]interface{}{"MIT", "MIT-0"})) 75 | }) 76 | 77 | context("failure cases", func() { 78 | context("when the url is invalid", func() { 79 | it("returns an error", func() { 80 | _, err := components.GenerateLicenseInformation("invalid url") 81 | Expect(err).To(MatchError(ContainSubstring("unsupported protocol scheme"))) 82 | }) 83 | }) 84 | 85 | context("when the artifact is not a supported archive type", func() { 86 | it("returns an error", func() { 87 | _, err := components.GenerateLicenseInformation(fmt.Sprintf("%s/bad-archive", server.URL)) 88 | Expect(err).To(MatchError(ContainSubstring("unsupported archive type"))) 89 | }) 90 | }) 91 | }) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /dependency/retrieval/components/output.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/paketo-buildpacks/packit/v2/cargo" 8 | ) 9 | 10 | type OutputDependency struct { 11 | cargo.ConfigMetadataDependency 12 | Target string `json:"target"` 13 | } 14 | 15 | func WriteOutput(path string, dependencies []cargo.ConfigMetadataDependency, target string) error { 16 | var output []OutputDependency 17 | for _, dependency := range dependencies { 18 | output = append(output, OutputDependency{ 19 | ConfigMetadataDependency: dependency, 20 | Target: target, 21 | }) 22 | } 23 | 24 | file, err := os.Create(path) 25 | if err != nil { 26 | return err 27 | } 28 | defer file.Close() 29 | 30 | err = json.NewEncoder(file).Encode(output) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /dependency/retrieval/components/output_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 10 | "github.com/paketo-buildpacks/packit/v2/cargo" 11 | "github.com/sclevine/spec" 12 | 13 | . "github.com/onsi/gomega" 14 | . "github.com/paketo-buildpacks/occam/matchers" 15 | ) 16 | 17 | func testOutput(t *testing.T, context spec.G, it spec.S) { 18 | var ( 19 | Expect = NewWithT(t).Expect 20 | outputDir string 21 | depDate time.Time 22 | ) 23 | 24 | it.Before(func() { 25 | var err error 26 | outputDir, err = os.MkdirTemp("", "") 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | depDate, err = time.ParseInLocation("2006-01-02", "2024-11-12", time.UTC) 30 | Expect(err).NotTo(HaveOccurred()) 31 | }) 32 | 33 | it.After(func() { 34 | Expect(os.RemoveAll(outputDir)).To(Succeed()) 35 | }) 36 | 37 | context("WriteOutput", func() { 38 | it("will write an output file", func() { 39 | err := components.WriteOutput(filepath.Join(outputDir, "output.json"), []cargo.ConfigMetadataDependency{ 40 | { 41 | DeprecationDate: &depDate, 42 | Licenses: []interface{}{"MIT", "MIT-0"}, 43 | Name: ".NET Core SDK", 44 | SHA256: "", 45 | }, 46 | }, "target") 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | Expect(filepath.Join(outputDir, "output.json")).To(BeAFileMatching("[{\"deprecation_date\":\"2024-11-12T00:00:00Z\",\"licenses\":[\"MIT\",\"MIT-0\"],\"name\":\".NET Core SDK\",\"target\":\"target\"}]\n")) 50 | }) 51 | 52 | context("failure cases", func() { 53 | context("the output file cannot be created", func() { 54 | it.Before(func() { 55 | Expect(os.Chmod(outputDir, 0000)).To(Succeed()) 56 | }) 57 | 58 | it.After(func() { 59 | Expect(os.Chmod(outputDir, os.ModePerm)).To(Succeed()) 60 | }) 61 | 62 | it("returns an error", func() { 63 | err := components.WriteOutput(filepath.Join(outputDir, "output.json"), []cargo.ConfigMetadataDependency{ 64 | { 65 | DeprecationDate: &depDate, 66 | Licenses: []interface{}{"MIT", "MIT-0"}, 67 | Name: ".NET Core SDK", 68 | SHA256: "", 69 | }, 70 | }, "target") 71 | Expect(err).To(MatchError(ContainSubstring("permission denied"))) 72 | }) 73 | }) 74 | }) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /dependency/retrieval/components/purl.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/package-url/packageurl-go" 7 | ) 8 | 9 | // GeneratePURL can be used to populate the `purl` field of dependency metadata 10 | // PURL stands for package URL. 11 | // https://github.com/package-url/purl-spec 12 | func GeneratePURL(id, version, checksum, source string) string { 13 | purl := packageurl.NewPackageURL( 14 | packageurl.TypeGeneric, 15 | "", 16 | id, 17 | version, 18 | packageurl.QualifiersFromMap(map[string]string{ 19 | "checksum": checksum, 20 | "download_url": source, 21 | }), 22 | "", 23 | ) 24 | 25 | // Unescape the path to remove the added `%2F` and other encodings added to 26 | // the URL by packageurl-go 27 | // If the unescaping fails, we should still return the path URL with the 28 | // encodings, packageurl-go has examples with both the encodings and without, 29 | // we prefer to avoid the encodings when we can for convenience. 30 | purlString, err := url.PathUnescape(purl.ToString()) 31 | if err != nil { 32 | return purl.ToString() 33 | } 34 | 35 | return purlString 36 | } 37 | -------------------------------------------------------------------------------- /dependency/retrieval/components/purl_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 7 | "github.com/sclevine/spec" 8 | 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func testPurl(t *testing.T, context spec.G, it spec.S) { 13 | Expect := NewWithT(t).Expect 14 | 15 | context("GeneratePURL", func() { 16 | it("will generate a purl", func() { 17 | purl := components.GeneratePURL("NAME", "VERSION", "CHECKSUM", "source") 18 | 19 | Expect(purl).To(Equal("pkg:generic/NAME@VERSION?checksum=CHECKSUM&download_url=source")) 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /dependency/retrieval/components/releases.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/Masterminds/semver/v3" 10 | ) 11 | 12 | type Release struct { 13 | SemVer *semver.Version 14 | 15 | Version string `json:"version"` 16 | Stable bool `json:"stable"` 17 | Files []ReleaseFile `json:"files"` 18 | } 19 | 20 | type ReleaseFile struct { 21 | URL string 22 | 23 | Filename string `json:"filename"` 24 | OS string `json:"os"` 25 | Arch string `json:"arch"` 26 | Version string `json:"version"` 27 | SHA256 string `json:"sha256"` 28 | Size int `json:"size"` 29 | Kind string `json:"kind"` 30 | } 31 | 32 | type Fetcher struct { 33 | releasePage string 34 | } 35 | 36 | func NewFetcher() Fetcher { 37 | return Fetcher{ 38 | releasePage: "https://go.dev/dl/?mode=json&include=all", 39 | } 40 | } 41 | 42 | func (f Fetcher) WithReleasePage(uri string) Fetcher { 43 | f.releasePage = uri 44 | return f 45 | } 46 | 47 | func (f Fetcher) Get() ([]Release, error) { 48 | response, err := http.Get(f.releasePage) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer response.Body.Close() 53 | 54 | if !(response.StatusCode >= 200 && response.StatusCode < 300) { 55 | return nil, fmt.Errorf("received a non 200 status code from %s: status code %d received", f.releasePage, response.StatusCode) 56 | } 57 | 58 | var releasesPage []Release 59 | err = json.NewDecoder(response.Body).Decode(&releasesPage) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | var releases []Release 65 | for _, release := range releasesPage { 66 | if !release.Stable { 67 | continue 68 | } 69 | 70 | release.SemVer, err = semver.NewVersion(strings.TrimPrefix(release.Version, "go")) 71 | if err != nil { 72 | return nil, fmt.Errorf("%w: the following version string could not be parsed %q", err, release.Version) 73 | } 74 | 75 | for i := range release.Files { 76 | release.Files[i].URL = fmt.Sprintf("https://go.dev/dl/%s", release.Files[i].Filename) 77 | } 78 | 79 | releases = append(releases, release) 80 | } 81 | 82 | return releases, nil 83 | } 84 | -------------------------------------------------------------------------------- /dependency/retrieval/components/releases_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/Masterminds/semver/v3" 10 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 11 | "github.com/sclevine/spec" 12 | 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | func testReleases(t *testing.T, context spec.G, it spec.S) { 17 | 18 | var ( 19 | Expect = NewWithT(t).Expect 20 | ) 21 | 22 | context("Fetcher", func() { 23 | var ( 24 | fetcher components.Fetcher 25 | 26 | server *httptest.Server 27 | ) 28 | 29 | it.Before(func() { 30 | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 31 | if req.Method == http.MethodHead { 32 | http.Error(w, "NotFound", http.StatusNotFound) 33 | return 34 | } 35 | 36 | switch req.URL.Path { 37 | case "/": 38 | w.WriteHeader(http.StatusOK) 39 | fmt.Fprintf(w, `[ 40 | { 41 | "version": "go1.19", 42 | "stable": true, 43 | "files": [ 44 | { 45 | "filename": "go1.19.src.tar.gz", 46 | "os": "", 47 | "arch": "", 48 | "version": "go1.19", 49 | "sha256": "9419cc70dc5a2523f29a77053cafff658ed21ef3561d9b6b020280ebceab28b9", 50 | "size": 26521849, 51 | "kind": "source" 52 | } 53 | ] 54 | }, 55 | { 56 | "version": "go1.19rc2", 57 | "stable": false, 58 | "files": [ 59 | { 60 | "filename": "go1.19rc2.src.tar.gz", 61 | "os": "", 62 | "arch": "", 63 | "version": "go1.19rc2", 64 | "sha256": "c68d7019a6a0b9852ae0e96d7e7deb772492a23272fd6d13afe05b40c912e51b", 65 | "size": 26593323, 66 | "kind": "source" 67 | } 68 | ] 69 | } 70 | ] 71 | `) 72 | 73 | case "/non-200": 74 | w.WriteHeader(http.StatusTeapot) 75 | 76 | case "/no-parse": 77 | w.WriteHeader(http.StatusOK) 78 | fmt.Fprintln(w, `???`) 79 | 80 | case "/no-version-parse": 81 | w.WriteHeader(http.StatusOK) 82 | fmt.Fprintf(w, `[ 83 | { 84 | "version": "invalid semver", 85 | "stable": true 86 | }] 87 | `) 88 | 89 | default: 90 | t.Fatalf("unknown path: %s", req.URL.Path) 91 | } 92 | })) 93 | 94 | fetcher = components.NewFetcher().WithReleasePage(server.URL) 95 | }) 96 | 97 | it("fetches a list of relevant releases", func() { 98 | releases, err := fetcher.Get() 99 | Expect(err).NotTo(HaveOccurred()) 100 | 101 | Expect(releases).To(Equal([]components.Release{ 102 | { 103 | SemVer: semver.MustParse("1.19"), 104 | Version: "go1.19", 105 | Stable: true, 106 | Files: []components.ReleaseFile{ 107 | { 108 | URL: "https://go.dev/dl/go1.19.src.tar.gz", 109 | Filename: "go1.19.src.tar.gz", 110 | OS: "", 111 | Arch: "", 112 | Version: "go1.19", 113 | SHA256: "9419cc70dc5a2523f29a77053cafff658ed21ef3561d9b6b020280ebceab28b9", 114 | Size: 26521849, 115 | Kind: "source", 116 | }, 117 | }, 118 | }, 119 | })) 120 | }) 121 | 122 | context("failure cases", func() { 123 | context("when the release page get fails", func() { 124 | it.Before(func() { 125 | fetcher = fetcher.WithReleasePage("not a valid URL") 126 | }) 127 | 128 | it("returns an error", func() { 129 | _, err := fetcher.Get() 130 | Expect(err).To(MatchError(ContainSubstring("unsupported protocol scheme"))) 131 | }) 132 | }) 133 | 134 | context("when the release page returns non 200 code", func() { 135 | it.Before(func() { 136 | fetcher = fetcher.WithReleasePage(fmt.Sprintf("%s/non-200", server.URL)) 137 | }) 138 | 139 | it("returns an error", func() { 140 | _, err := fetcher.Get() 141 | Expect(err).To(MatchError(fmt.Sprintf("received a non 200 status code from %s: status code 418 received", fmt.Sprintf("%s/non-200", server.URL)))) 142 | }) 143 | }) 144 | 145 | context("when the release page cannot be parsed", func() { 146 | it.Before(func() { 147 | fetcher = fetcher.WithReleasePage(fmt.Sprintf("%s/no-parse", server.URL)) 148 | }) 149 | 150 | it("returns an error", func() { 151 | _, err := fetcher.Get() 152 | Expect(err).To(MatchError(ContainSubstring("invalid character '?' looking for beginning of value"))) 153 | }) 154 | }) 155 | 156 | context("when the release page has unparsable version", func() { 157 | it.Before(func() { 158 | fetcher = fetcher.WithReleasePage(fmt.Sprintf("%s/no-version-parse", server.URL)) 159 | }) 160 | 161 | it("returns an error", func() { 162 | _, err := fetcher.Get() 163 | Expect(err).To(MatchError(ContainSubstring("Invalid Semantic Version"))) 164 | }) 165 | }) 166 | }) 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /dependency/retrieval/components/versions.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/Masterminds/semver" 7 | "github.com/paketo-buildpacks/packit/v2/cargo" 8 | ) 9 | 10 | func FindNewVersions(path string, versions []string) ([]string, error) { 11 | config, err := cargo.NewBuildpackParser().Parse(path) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | var newVersions []string 17 | for _, constraint := range config.Metadata.DependencyConstraints { 18 | svConstraint, err := semver.NewConstraint(constraint.Constraint) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | latestVersion := semver.MustParse("0.0.0") 24 | for _, dependency := range config.Metadata.Dependencies { 25 | svVersion := semver.MustParse(dependency.Version) 26 | if svConstraint.Check(svVersion) && svVersion.GreaterThan(latestVersion) { 27 | latestVersion = svVersion 28 | } 29 | } 30 | 31 | var compatibleVersions []string 32 | for _, version := range versions { 33 | svVersion := semver.MustParse(version) 34 | if svConstraint.Check(svVersion) && svVersion.GreaterThan(latestVersion) { 35 | compatibleVersions = append(compatibleVersions, version) 36 | } 37 | } 38 | 39 | sort.Slice(compatibleVersions, func(i, j int) bool { 40 | jVersion := semver.MustParse(compatibleVersions[j]) 41 | iVersion := semver.MustParse(compatibleVersions[i]) 42 | return iVersion.GreaterThan(jVersion) 43 | }) 44 | 45 | if constraint.Patches > len(compatibleVersions) { 46 | newVersions = append(newVersions, compatibleVersions...) 47 | } else { 48 | newVersions = append(newVersions, compatibleVersions[:constraint.Patches]...) 49 | } 50 | } 51 | 52 | return newVersions, nil 53 | } 54 | -------------------------------------------------------------------------------- /dependency/retrieval/components/versions_test.go: -------------------------------------------------------------------------------- 1 | package components_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 8 | "github.com/sclevine/spec" 9 | 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | func testVersions(t *testing.T, context spec.G, it spec.S) { 14 | 15 | var ( 16 | Expect = NewWithT(t).Expect 17 | 18 | buildpackTOMLPath string 19 | ) 20 | 21 | it.Before(func() { 22 | file, err := os.CreateTemp("", "") 23 | Expect(err).NotTo(HaveOccurred()) 24 | defer file.Close() 25 | 26 | _, err = file.Write([]byte(` 27 | [metadata] 28 | 29 | [[metadata.dependencies]] 30 | version = "1.2.0" 31 | 32 | [[metadata.dependencies]] 33 | version = "1.2.1" 34 | 35 | [[metadata.dependencies]] 36 | version = "2.3.4" 37 | 38 | [[metadata.dependencies]] 39 | version = "2.3.5" 40 | 41 | [[metadata.dependency-constraints]] 42 | constraint = "1.2.*" 43 | id = "test" 44 | patches = 2 45 | 46 | [[metadata.dependency-constraints]] 47 | constraint = "2.3.*" 48 | id = "test" 49 | patches = 2 50 | `)) 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | buildpackTOMLPath = file.Name() 54 | 55 | }) 56 | 57 | it.After(func() { 58 | Expect(os.RemoveAll(buildpackTOMLPath)).To(Succeed()) 59 | }) 60 | 61 | context("FindNewVersions", func() { 62 | it("returns a list of all new versions", func() { 63 | newVersions, err := components.FindNewVersions(buildpackTOMLPath, []string{"1.2.2", "1.2.3", "1.2.4", "2.3.4", "2.3.5", "2.3.6", "9.9.9"}) 64 | Expect(err).NotTo(HaveOccurred()) 65 | 66 | Expect(newVersions).To(Equal([]string{"1.2.4", "1.2.3", "2.3.6"})) 67 | }) 68 | }) 69 | 70 | context("failure cases", func() { 71 | context("the buildpack cannot be parsed", func() { 72 | it.Before(func() { 73 | Expect(os.RemoveAll(buildpackTOMLPath)).To(Succeed()) 74 | }) 75 | 76 | it("returns an error", func() { 77 | _, err := components.FindNewVersions(buildpackTOMLPath, []string{"1.2.2", "1.2.3", "1.2.4", "2.3.4", "2.3.5", "2.3.6", "9.9.9"}) 78 | Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) 79 | }) 80 | }) 81 | 82 | context("the constraints cannot be parsed", func() { 83 | it.Before(func() { 84 | Expect(os.WriteFile(buildpackTOMLPath, []byte(` 85 | [metadata] 86 | 87 | [[metadata.dependency-constraints]] 88 | constraint = "not-valid-constraint" 89 | id = "test" 90 | patches = 2 91 | `), os.ModePerm)) 92 | }) 93 | 94 | it("returns an error", func() { 95 | _, err := components.FindNewVersions(buildpackTOMLPath, []string{"1.2.2", "1.2.3", "1.2.4", "2.3.4", "2.3.5", "2.3.6", "9.9.9"}) 96 | Expect(err).To(MatchError(ContainSubstring("improper constraint: not-valid-constraint"))) 97 | }) 98 | }) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /dependency/retrieval/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paketo-buildpacks/go-dist/dependency/retrieval 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/Masterminds/semver v1.5.0 7 | github.com/Masterminds/semver/v3 v3.3.1 8 | github.com/go-enry/go-license-detector/v4 v4.3.1 9 | github.com/onsi/gomega v1.36.3 10 | github.com/package-url/packageurl-go v0.1.3 11 | github.com/paketo-buildpacks/occam v0.24.0 12 | github.com/paketo-buildpacks/packit/v2 v2.16.0 13 | github.com/sclevine/spec v1.4.0 14 | ) 15 | 16 | require ( 17 | dario.cat/mergo v1.0.1 // indirect 18 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 19 | github.com/BurntSushi/toml v1.5.0 // indirect 20 | github.com/ForestEckhardt/freezer v0.1.1 // indirect 21 | github.com/Microsoft/go-winio v0.6.2 // indirect 22 | github.com/Microsoft/hcsshim v0.11.7 // indirect 23 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 24 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 25 | github.com/cloudflare/circl v1.6.0 // indirect 26 | github.com/containerd/containerd v1.7.27 // indirect 27 | github.com/containerd/log v0.1.0 // indirect 28 | github.com/containerd/platforms v0.2.1 // indirect 29 | github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect 30 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 31 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 32 | github.com/dgryski/go-minhash v0.0.0-20190315135803-ad340ca03076 // indirect 33 | github.com/distribution/reference v0.6.0 // indirect 34 | github.com/docker/docker v26.1.5+incompatible // indirect 35 | github.com/docker/go-connections v0.5.0 // indirect 36 | github.com/docker/go-units v0.5.0 // indirect 37 | github.com/ekzhu/minhash-lsh v0.0.0-20190924033628-faac2c6342f8 // indirect 38 | github.com/emirpasic/gods v1.18.1 // indirect 39 | github.com/felixge/httpsnoop v1.0.4 // indirect 40 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 41 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 42 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 43 | github.com/go-git/go-git/v5 v5.14.0 // indirect 44 | github.com/go-logr/logr v1.4.2 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-ole/go-ole v1.2.6 // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 49 | github.com/google/go-cmp v0.7.0 // indirect 50 | github.com/google/go-containerregistry v0.20.2 // indirect 51 | github.com/google/uuid v1.6.0 // indirect 52 | github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b // indirect 53 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 54 | github.com/jdkato/prose v1.2.1 // indirect 55 | github.com/kevinburke/ssh_config v1.2.0 // indirect 56 | github.com/klauspost/compress v1.16.7 // indirect 57 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 58 | github.com/magiconair/properties v1.8.7 // indirect 59 | github.com/moby/docker-image-spec v1.3.1 // indirect 60 | github.com/moby/patternmatcher v0.6.0 // indirect 61 | github.com/moby/sys/sequential v0.5.0 // indirect 62 | github.com/moby/sys/user v0.3.0 // indirect 63 | github.com/moby/sys/userns v0.1.0 // indirect 64 | github.com/moby/term v0.5.0 // indirect 65 | github.com/montanaflynn/stats v0.7.1 // indirect 66 | github.com/morikuni/aec v1.0.0 // indirect 67 | github.com/oklog/ulid v1.3.1 // indirect 68 | github.com/opencontainers/go-digest v1.0.0 // indirect 69 | github.com/opencontainers/image-spec v1.1.0 // indirect 70 | github.com/pjbgf/sha1cd v0.3.2 // indirect 71 | github.com/pkg/errors v0.9.1 // indirect 72 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 73 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 74 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 75 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 76 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 77 | github.com/shogo82148/go-shuffle v1.1.1 // indirect 78 | github.com/sirupsen/logrus v1.9.3 // indirect 79 | github.com/skeema/knownhosts v1.3.1 // indirect 80 | github.com/testcontainers/testcontainers-go v0.31.0 // indirect 81 | github.com/tklauser/go-sysconf v0.3.12 // indirect 82 | github.com/tklauser/numcpus v0.6.1 // indirect 83 | github.com/ulikunitz/xz v0.5.12 // indirect 84 | github.com/vbatts/tar-split v0.11.3 // indirect 85 | github.com/xanzy/ssh-agent v0.3.3 // indirect 86 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 87 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 88 | go.opentelemetry.io/otel v1.28.0 // indirect 89 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 90 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 91 | golang.org/x/crypto v0.36.0 // indirect 92 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect 93 | golang.org/x/net v0.38.0 // indirect 94 | golang.org/x/sync v0.12.0 // indirect 95 | golang.org/x/sys v0.31.0 // indirect 96 | golang.org/x/text v0.23.0 // indirect 97 | gonum.org/v1/gonum v0.16.0 // indirect 98 | gopkg.in/neurosnap/sentences.v1 v1.0.7 // indirect 99 | gopkg.in/warnings.v0 v0.1.2 // indirect 100 | gopkg.in/yaml.v3 v3.0.1 // indirect 101 | ) 102 | -------------------------------------------------------------------------------- /dependency/retrieval/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | "github.com/paketo-buildpacks/go-dist/dependency/retrieval/components" 9 | "github.com/paketo-buildpacks/packit/v2/cargo" 10 | ) 11 | 12 | func main() { 13 | var buildpackTOMLPath, outputPath string 14 | set := flag.NewFlagSet("", flag.ContinueOnError) 15 | set.StringVar(&buildpackTOMLPath, "buildpack-toml-path", "", "path to the buildpack.toml file") 16 | set.StringVar(&outputPath, "output", "", "path to the output file") 17 | err := set.Parse(os.Args[1:]) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | fetcher := components.NewFetcher() 23 | releases, err := fetcher.Get() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | var versions []string 29 | for _, release := range releases { 30 | versions = append(versions, release.SemVer.String()) 31 | } 32 | 33 | newVersions, err := components.FindNewVersions(buildpackTOMLPath, versions) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | var dependencies []cargo.ConfigMetadataDependency 39 | for _, version := range newVersions { 40 | for _, r := range releases { 41 | if r.SemVer.String() == version { 42 | dependency, err := components.ConvertReleaseToDependency(r) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | dependencies = append(dependencies, dependency) 47 | } 48 | } 49 | } 50 | 51 | err = components.WriteOutput(outputPath, dependencies, "") 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /detect.go: -------------------------------------------------------------------------------- 1 | package godist 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/paketo-buildpacks/packit/v2" 9 | "github.com/paketo-buildpacks/packit/v2/fs" 10 | ) 11 | 12 | type BuildPlanMetadata struct { 13 | VersionSource string `toml:"version-source"` 14 | Build bool `toml:"build"` 15 | Version string `toml:"version"` 16 | } 17 | 18 | func Detect() packit.DetectFunc { 19 | return func(context packit.DetectContext) (packit.DetectResult, error) { 20 | bpYML, err := fs.Exists(filepath.Join(context.WorkingDir, "buildpack.yml")) 21 | if err != nil { 22 | return packit.DetectResult{}, fmt.Errorf("failed to check for buildpack.yml: %w", err) 23 | } 24 | if bpYML { 25 | return packit.DetectResult{}, fmt.Errorf("working directory contains deprecated 'buildpack.yml'; use environment variables for configuration") 26 | } 27 | 28 | var requirements []packit.BuildPlanRequirement 29 | 30 | if version, ok := os.LookupEnv("BP_GO_VERSION"); ok { 31 | requirements = append(requirements, packit.BuildPlanRequirement{ 32 | Name: GoDependency, 33 | Metadata: BuildPlanMetadata{ 34 | VersionSource: "BP_GO_VERSION", 35 | Version: version, 36 | }, 37 | }) 38 | } 39 | 40 | return packit.DetectResult{ 41 | Plan: packit.BuildPlan{ 42 | Provides: []packit.BuildPlanProvision{ 43 | {Name: GoDependency}, 44 | }, 45 | Requires: requirements, 46 | }, 47 | }, nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /detect_test.go: -------------------------------------------------------------------------------- 1 | package godist_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | godist "github.com/paketo-buildpacks/go-dist" 9 | "github.com/paketo-buildpacks/packit/v2" 10 | "github.com/sclevine/spec" 11 | 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | func testDetect(t *testing.T, context spec.G, it spec.S) { 16 | var ( 17 | Expect = NewWithT(t).Expect 18 | 19 | detect packit.DetectFunc 20 | ) 21 | 22 | it.Before(func() { 23 | detect = godist.Detect() 24 | }) 25 | 26 | it("returns a plan that provides go", func() { 27 | result, err := detect(packit.DetectContext{ 28 | WorkingDir: "/working-dir", 29 | }) 30 | Expect(err).NotTo(HaveOccurred()) 31 | Expect(result).To(Equal(packit.DetectResult{ 32 | Plan: packit.BuildPlan{ 33 | Provides: []packit.BuildPlanProvision{ 34 | {Name: "go"}, 35 | }, 36 | }, 37 | })) 38 | 39 | }) 40 | 41 | context("when the BP_GO_VERSION is set", func() { 42 | it.Before(func() { 43 | Expect(os.Setenv("BP_GO_VERSION", "some-version")).To(Succeed()) 44 | }) 45 | 46 | it.After(func() { 47 | Expect(os.Unsetenv("BP_GO_VERSION")).To(Succeed()) 48 | }) 49 | 50 | it("returns a plan that requires that version of go", func() { 51 | result, err := detect(packit.DetectContext{ 52 | WorkingDir: "/working-dir", 53 | }) 54 | Expect(err).NotTo(HaveOccurred()) 55 | Expect(result).To(Equal(packit.DetectResult{ 56 | Plan: packit.BuildPlan{ 57 | Provides: []packit.BuildPlanProvision{ 58 | {Name: "go"}, 59 | }, 60 | Requires: []packit.BuildPlanRequirement{ 61 | { 62 | Name: "go", 63 | Metadata: godist.BuildPlanMetadata{ 64 | VersionSource: "BP_GO_VERSION", 65 | Version: "some-version", 66 | }, 67 | }, 68 | }, 69 | }, 70 | })) 71 | 72 | }) 73 | }) 74 | 75 | context("when there is a buildpack.yml", func() { 76 | var workingDir string 77 | it.Before(func() { 78 | var err error 79 | workingDir, err = os.MkdirTemp("", "working-dir") 80 | Expect(err).NotTo(HaveOccurred()) 81 | Expect(os.WriteFile(filepath.Join(workingDir, "buildpack.yml"), nil, os.ModePerm)) 82 | }) 83 | 84 | it.After(func() { 85 | Expect(os.RemoveAll(workingDir)).To(Succeed()) 86 | }) 87 | 88 | it("fails the build with a deprecation notice", func() { 89 | _, err := detect(packit.DetectContext{ 90 | WorkingDir: workingDir, 91 | }) 92 | Expect(err).To(MatchError("working directory contains deprecated 'buildpack.yml'; use environment variables for configuration")) 93 | }) 94 | 95 | context("and its contents cannot be read", func() { 96 | it.Before(func() { 97 | Expect(os.Chmod(workingDir, 0000)).To(Succeed()) 98 | }) 99 | it.After(func() { 100 | Expect(os.Chmod(workingDir, os.ModePerm)).To(Succeed()) 101 | }) 102 | it("returns an error", func() { 103 | _, err := detect(packit.DetectContext{ 104 | WorkingDir: workingDir, 105 | }) 106 | Expect(err).To(MatchError(ContainSubstring("permission denied"))) 107 | }) 108 | }) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /fakes/dependency_manager.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import ( 4 | "sync" 5 | 6 | packit "github.com/paketo-buildpacks/packit/v2" 7 | "github.com/paketo-buildpacks/packit/v2/postal" 8 | ) 9 | 10 | type DependencyManager struct { 11 | DeliverCall struct { 12 | mutex sync.Mutex 13 | CallCount int 14 | Receives struct { 15 | Dependency postal.Dependency 16 | CnbPath string 17 | LayerPath string 18 | PlatformPath string 19 | } 20 | Returns struct { 21 | Error error 22 | } 23 | Stub func(postal.Dependency, string, string, string) error 24 | } 25 | GenerateBillOfMaterialsCall struct { 26 | mutex sync.Mutex 27 | CallCount int 28 | Receives struct { 29 | Dependencies []postal.Dependency 30 | } 31 | Returns struct { 32 | BOMEntrySlice []packit.BOMEntry 33 | } 34 | Stub func(...postal.Dependency) []packit.BOMEntry 35 | } 36 | ResolveCall struct { 37 | mutex sync.Mutex 38 | CallCount int 39 | Receives struct { 40 | Path string 41 | Id string 42 | Version string 43 | Stack string 44 | } 45 | Returns struct { 46 | Dependency postal.Dependency 47 | Error error 48 | } 49 | Stub func(string, string, string, string) (postal.Dependency, error) 50 | } 51 | } 52 | 53 | func (f *DependencyManager) Deliver(param1 postal.Dependency, param2 string, param3 string, param4 string) error { 54 | f.DeliverCall.mutex.Lock() 55 | defer f.DeliverCall.mutex.Unlock() 56 | f.DeliverCall.CallCount++ 57 | f.DeliverCall.Receives.Dependency = param1 58 | f.DeliverCall.Receives.CnbPath = param2 59 | f.DeliverCall.Receives.LayerPath = param3 60 | f.DeliverCall.Receives.PlatformPath = param4 61 | if f.DeliverCall.Stub != nil { 62 | return f.DeliverCall.Stub(param1, param2, param3, param4) 63 | } 64 | return f.DeliverCall.Returns.Error 65 | } 66 | func (f *DependencyManager) GenerateBillOfMaterials(param1 ...postal.Dependency) []packit.BOMEntry { 67 | f.GenerateBillOfMaterialsCall.mutex.Lock() 68 | defer f.GenerateBillOfMaterialsCall.mutex.Unlock() 69 | f.GenerateBillOfMaterialsCall.CallCount++ 70 | f.GenerateBillOfMaterialsCall.Receives.Dependencies = param1 71 | if f.GenerateBillOfMaterialsCall.Stub != nil { 72 | return f.GenerateBillOfMaterialsCall.Stub(param1...) 73 | } 74 | return f.GenerateBillOfMaterialsCall.Returns.BOMEntrySlice 75 | } 76 | func (f *DependencyManager) Resolve(param1 string, param2 string, param3 string, param4 string) (postal.Dependency, error) { 77 | f.ResolveCall.mutex.Lock() 78 | defer f.ResolveCall.mutex.Unlock() 79 | f.ResolveCall.CallCount++ 80 | f.ResolveCall.Receives.Path = param1 81 | f.ResolveCall.Receives.Id = param2 82 | f.ResolveCall.Receives.Version = param3 83 | f.ResolveCall.Receives.Stack = param4 84 | if f.ResolveCall.Stub != nil { 85 | return f.ResolveCall.Stub(param1, param2, param3, param4) 86 | } 87 | return f.ResolveCall.Returns.Dependency, f.ResolveCall.Returns.Error 88 | } 89 | -------------------------------------------------------------------------------- /fakes/entry_resolver.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import ( 4 | "sync" 5 | 6 | packit "github.com/paketo-buildpacks/packit/v2" 7 | ) 8 | 9 | type EntryResolver struct { 10 | MergeLayerTypesCall struct { 11 | mutex sync.Mutex 12 | CallCount int 13 | Receives struct { 14 | Name string 15 | Entries []packit.BuildpackPlanEntry 16 | } 17 | Returns struct { 18 | Launch bool 19 | Build bool 20 | } 21 | Stub func(string, []packit.BuildpackPlanEntry) (bool, bool) 22 | } 23 | ResolveCall struct { 24 | mutex sync.Mutex 25 | CallCount int 26 | Receives struct { 27 | Name string 28 | Entries []packit.BuildpackPlanEntry 29 | Priorites []interface { 30 | } 31 | } 32 | Returns struct { 33 | BuildpackPlanEntry packit.BuildpackPlanEntry 34 | BuildpackPlanEntrySlice []packit.BuildpackPlanEntry 35 | } 36 | Stub func(string, []packit.BuildpackPlanEntry, []interface { 37 | }) (packit.BuildpackPlanEntry, []packit.BuildpackPlanEntry) 38 | } 39 | } 40 | 41 | func (f *EntryResolver) MergeLayerTypes(param1 string, param2 []packit.BuildpackPlanEntry) (bool, bool) { 42 | f.MergeLayerTypesCall.mutex.Lock() 43 | defer f.MergeLayerTypesCall.mutex.Unlock() 44 | f.MergeLayerTypesCall.CallCount++ 45 | f.MergeLayerTypesCall.Receives.Name = param1 46 | f.MergeLayerTypesCall.Receives.Entries = param2 47 | if f.MergeLayerTypesCall.Stub != nil { 48 | return f.MergeLayerTypesCall.Stub(param1, param2) 49 | } 50 | return f.MergeLayerTypesCall.Returns.Launch, f.MergeLayerTypesCall.Returns.Build 51 | } 52 | func (f *EntryResolver) Resolve(param1 string, param2 []packit.BuildpackPlanEntry, param3 []interface { 53 | }) (packit.BuildpackPlanEntry, []packit.BuildpackPlanEntry) { 54 | f.ResolveCall.mutex.Lock() 55 | defer f.ResolveCall.mutex.Unlock() 56 | f.ResolveCall.CallCount++ 57 | f.ResolveCall.Receives.Name = param1 58 | f.ResolveCall.Receives.Entries = param2 59 | f.ResolveCall.Receives.Priorites = param3 60 | if f.ResolveCall.Stub != nil { 61 | return f.ResolveCall.Stub(param1, param2, param3) 62 | } 63 | return f.ResolveCall.Returns.BuildpackPlanEntry, f.ResolveCall.Returns.BuildpackPlanEntrySlice 64 | } 65 | -------------------------------------------------------------------------------- /fakes/sbom_generator.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/paketo-buildpacks/packit/v2/postal" 7 | "github.com/paketo-buildpacks/packit/v2/sbom" 8 | ) 9 | 10 | type SBOMGenerator struct { 11 | GenerateFromDependencyCall struct { 12 | mutex sync.Mutex 13 | CallCount int 14 | Receives struct { 15 | Dependency postal.Dependency 16 | Dir string 17 | } 18 | Returns struct { 19 | SBOM sbom.SBOM 20 | Error error 21 | } 22 | Stub func(postal.Dependency, string) (sbom.SBOM, error) 23 | } 24 | } 25 | 26 | func (f *SBOMGenerator) GenerateFromDependency(param1 postal.Dependency, param2 string) (sbom.SBOM, error) { 27 | f.GenerateFromDependencyCall.mutex.Lock() 28 | defer f.GenerateFromDependencyCall.mutex.Unlock() 29 | f.GenerateFromDependencyCall.CallCount++ 30 | f.GenerateFromDependencyCall.Receives.Dependency = param1 31 | f.GenerateFromDependencyCall.Receives.Dir = param2 32 | if f.GenerateFromDependencyCall.Stub != nil { 33 | return f.GenerateFromDependencyCall.Stub(param1, param2) 34 | } 35 | return f.GenerateFromDependencyCall.Returns.SBOM, f.GenerateFromDependencyCall.Returns.Error 36 | } 37 | -------------------------------------------------------------------------------- /fakes/version_parser.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import "sync" 4 | 5 | type VersionParser struct { 6 | ParseVersionCall struct { 7 | mutex sync.Mutex 8 | CallCount int 9 | Receives struct { 10 | Path string 11 | } 12 | Returns struct { 13 | Version string 14 | Err error 15 | } 16 | Stub func(string) (string, error) 17 | } 18 | } 19 | 20 | func (f *VersionParser) ParseVersion(param1 string) (string, error) { 21 | f.ParseVersionCall.mutex.Lock() 22 | defer f.ParseVersionCall.mutex.Unlock() 23 | f.ParseVersionCall.CallCount++ 24 | f.ParseVersionCall.Receives.Path = param1 25 | if f.ParseVersionCall.Stub != nil { 26 | return f.ParseVersionCall.Stub(param1) 27 | } 28 | return f.ParseVersionCall.Returns.Version, f.ParseVersionCall.Returns.Err 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paketo-buildpacks/go-dist 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.4.0 7 | github.com/onsi/gomega v1.34.1 8 | github.com/paketo-buildpacks/occam v0.18.7 9 | github.com/paketo-buildpacks/packit/v2 v2.14.0 10 | github.com/sclevine/spec v1.4.0 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 16 | github.com/CycloneDX/cyclonedx-go v0.7.2 // indirect 17 | github.com/DataDog/zstd v1.5.5 // indirect 18 | github.com/ForestEckhardt/freezer v0.1.0 // indirect 19 | github.com/Masterminds/goutils v1.1.1 // indirect 20 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 21 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 22 | github.com/Microsoft/go-winio v0.6.2 // indirect 23 | github.com/Microsoft/hcsshim v0.12.4 // indirect 24 | github.com/ProtonMail/go-crypto v1.1.3 // indirect 25 | github.com/acobaugh/osrelease v0.1.0 // indirect 26 | github.com/anchore/go-logger v0.0.0-20230120230012-47be9bb822a2 // indirect 27 | github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect 28 | github.com/anchore/go-struct-converter v0.0.0-20221221214134-65614c61201e // indirect 29 | github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b // indirect 30 | github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 // indirect 31 | github.com/anchore/stereoscope v0.0.0-20230412183729-8602f1afc574 // indirect 32 | github.com/anchore/syft v0.80.0 // indirect 33 | github.com/andybalholm/brotli v1.1.0 // indirect 34 | github.com/apex/log v1.9.0 // indirect 35 | github.com/becheran/wildmatch-go v1.0.0 // indirect 36 | github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect 37 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 38 | github.com/cloudflare/circl v1.3.9 // indirect 39 | github.com/containerd/containerd v1.7.27 // indirect 40 | github.com/containerd/errdefs v0.3.0 // indirect 41 | github.com/containerd/log v0.1.0 // indirect 42 | github.com/containerd/platforms v0.2.1 // indirect 43 | github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect 44 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 45 | github.com/cyphar/filepath-securejoin v0.2.5 // indirect 46 | github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect 47 | github.com/distribution/reference v0.6.0 // indirect 48 | github.com/docker/cli v24.0.7+incompatible // indirect 49 | github.com/docker/distribution v2.8.3+incompatible // indirect 50 | github.com/docker/docker v26.1.4+incompatible // indirect 51 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 52 | github.com/docker/go-connections v0.5.0 // indirect 53 | github.com/docker/go-units v0.5.0 // indirect 54 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect 55 | github.com/dustin/go-humanize v1.0.1 // indirect 56 | github.com/emirpasic/gods v1.18.1 // indirect 57 | github.com/facebookincubator/nvdtools v0.1.5 // indirect 58 | github.com/felixge/httpsnoop v1.0.4 // indirect 59 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect 60 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 61 | github.com/go-git/go-billy/v5 v5.6.0 // indirect 62 | github.com/go-git/go-git/v5 v5.13.0 // indirect 63 | github.com/go-logr/logr v1.4.2 // indirect 64 | github.com/go-logr/stdr v1.2.2 // indirect 65 | github.com/go-ole/go-ole v1.3.0 // indirect 66 | github.com/go-restruct/restruct v1.2.0-alpha // indirect 67 | github.com/gogo/protobuf v1.3.2 // indirect 68 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 69 | github.com/golang/protobuf v1.5.4 // indirect 70 | github.com/golang/snappy v0.0.4 // indirect 71 | github.com/google/go-cmp v0.6.0 // indirect 72 | github.com/google/go-containerregistry v0.19.2 // indirect 73 | github.com/google/licensecheck v0.3.1 // indirect 74 | github.com/google/uuid v1.6.0 // indirect 75 | github.com/hashicorp/errwrap v1.1.0 // indirect 76 | github.com/hashicorp/go-multierror v1.1.1 // indirect 77 | github.com/huandu/xstrings v1.5.0 // indirect 78 | github.com/imdario/mergo v0.3.15 // indirect 79 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 80 | github.com/jinzhu/copier v0.4.0 // indirect 81 | github.com/kevinburke/ssh_config v1.2.0 // indirect 82 | github.com/klauspost/compress v1.17.9 // indirect 83 | github.com/klauspost/pgzip v1.2.6 // indirect 84 | github.com/knqyf263/go-rpmdb v0.1.1 // indirect 85 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 86 | github.com/magiconair/properties v1.8.7 // indirect 87 | github.com/mattn/go-runewidth v0.0.15 // indirect 88 | github.com/mholt/archiver/v3 v3.5.1 // indirect 89 | github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 // indirect 90 | github.com/mitchellh/copystructure v1.2.0 // indirect 91 | github.com/mitchellh/go-homedir v1.1.0 // indirect 92 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 93 | github.com/mitchellh/mapstructure v1.5.0 // indirect 94 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 95 | github.com/moby/docker-image-spec v1.3.1 // indirect 96 | github.com/moby/patternmatcher v0.6.0 // indirect 97 | github.com/moby/sys/sequential v0.5.0 // indirect 98 | github.com/moby/sys/user v0.3.0 // indirect 99 | github.com/moby/sys/userns v0.1.0 // indirect 100 | github.com/moby/term v0.5.0 // indirect 101 | github.com/morikuni/aec v1.0.0 // indirect 102 | github.com/nwaples/rardecode v1.1.3 // indirect 103 | github.com/oklog/ulid v1.3.1 // indirect 104 | github.com/olekukonko/tablewriter v0.0.5 // indirect 105 | github.com/opencontainers/go-digest v1.0.0 // indirect 106 | github.com/opencontainers/image-spec v1.1.0 // indirect 107 | github.com/pelletier/go-toml v1.9.5 // indirect 108 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 109 | github.com/pjbgf/sha1cd v0.3.0 // indirect 110 | github.com/pkg/errors v0.9.1 // indirect 111 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 112 | github.com/rivo/uniseg v0.4.7 // indirect 113 | github.com/sassoftware/go-rpmutils v0.4.0 // indirect 114 | github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect 115 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 116 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 117 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 118 | github.com/shopspring/decimal v1.4.0 // indirect 119 | github.com/sirupsen/logrus v1.9.3 // indirect 120 | github.com/skeema/knownhosts v1.3.0 // indirect 121 | github.com/spdx/tools-golang v0.5.4 // indirect 122 | github.com/spf13/afero v1.11.0 // indirect 123 | github.com/spf13/cast v1.6.0 // indirect 124 | github.com/sylabs/sif/v2 v2.17.0 // indirect 125 | github.com/sylabs/squashfs v0.6.1 // indirect 126 | github.com/testcontainers/testcontainers-go v0.31.0 // indirect 127 | github.com/therootcompany/xz v1.0.1 // indirect 128 | github.com/tklauser/go-sysconf v0.3.14 // indirect 129 | github.com/tklauser/numcpus v0.8.0 // indirect 130 | github.com/ulikunitz/xz v0.5.12 // indirect 131 | github.com/vbatts/go-mtree v0.5.4 // indirect 132 | github.com/vbatts/tar-split v0.11.5 // indirect 133 | github.com/vifraa/gopom v0.2.1 // indirect 134 | github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 // indirect 135 | github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 // indirect 136 | github.com/xanzy/ssh-agent v0.3.3 // indirect 137 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 138 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 139 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 140 | go.opentelemetry.io/otel v1.24.0 // indirect 141 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 142 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 143 | golang.org/x/crypto v0.36.0 // indirect 144 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 145 | golang.org/x/mod v0.19.0 // indirect 146 | golang.org/x/net v0.38.0 // indirect 147 | golang.org/x/sync v0.12.0 // indirect 148 | golang.org/x/sys v0.31.0 // indirect 149 | golang.org/x/text v0.23.0 // indirect 150 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 151 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect 152 | google.golang.org/grpc v1.62.0 // indirect 153 | google.golang.org/protobuf v1.35.2 // indirect 154 | gopkg.in/warnings.v0 v0.1.2 // indirect 155 | gopkg.in/yaml.v2 v2.4.0 // indirect 156 | gopkg.in/yaml.v3 v3.0.1 // indirect 157 | ) 158 | 159 | replace github.com/CycloneDX/cyclonedx-go => github.com/CycloneDX/cyclonedx-go v0.7.2 160 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package godist_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sclevine/spec" 7 | "github.com/sclevine/spec/report" 8 | ) 9 | 10 | func TestUnit(t *testing.T) { 11 | suite := spec.New("go-dist", spec.Report(report.Terminal{}), spec.Parallel()) 12 | suite("Build", testBuild) 13 | suite("Detect", testDetect, spec.Sequential()) 14 | suite.Run(t) 15 | } 16 | -------------------------------------------------------------------------------- /integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": [ 3 | "paketobuildpacks/builder:buildpackless-base", 4 | "paketobuildpacks/builder-jammy-buildpackless-base" 5 | ], 6 | "buildplan": "github.com/paketo-community/build-plan" 7 | } 8 | -------------------------------------------------------------------------------- /integration/default_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/paketo-buildpacks/occam" 11 | "github.com/sclevine/spec" 12 | 13 | . "github.com/onsi/gomega" 14 | . "github.com/paketo-buildpacks/occam/matchers" 15 | ) 16 | 17 | func testDefault(t *testing.T, context spec.G, it spec.S) { 18 | var ( 19 | Expect = NewWithT(t).Expect 20 | Eventually = NewWithT(t).Eventually 21 | 22 | pack occam.Pack 23 | docker occam.Docker 24 | ) 25 | 26 | it.Before(func() { 27 | pack = occam.NewPack().WithVerbose() 28 | docker = occam.NewDocker() 29 | }) 30 | 31 | context("when the buildpack is run with pack build", func() { 32 | var ( 33 | image occam.Image 34 | container occam.Container 35 | 36 | name string 37 | source string 38 | sbomDir string 39 | ) 40 | 41 | it.Before(func() { 42 | var err error 43 | name, err = occam.RandomName() 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | source, err = occam.Source(filepath.Join("testdata", "default_app")) 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | sbomDir, err = os.MkdirTemp("", "sbom") 50 | Expect(err).NotTo(HaveOccurred()) 51 | Expect(os.Chmod(sbomDir, os.ModePerm)).To(Succeed()) 52 | }) 53 | 54 | it.After(func() { 55 | Expect(docker.Container.Remove.Execute(container.ID)).To(Succeed()) 56 | Expect(docker.Image.Remove.Execute(image.ID)).To(Succeed()) 57 | Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) 58 | 59 | Expect(os.RemoveAll(source)).To(Succeed()) 60 | Expect(os.RemoveAll(sbomDir)).To(Succeed()) 61 | }) 62 | 63 | it("builds with the defaults", func() { 64 | var ( 65 | logs fmt.Stringer 66 | err error 67 | ) 68 | 69 | image, logs, err = pack.WithNoColor().Build. 70 | WithPullPolicy("never"). 71 | WithBuildpacks(buildpack, buildPlanBuildpack). 72 | WithEnv(map[string]string{ 73 | "BP_LOG_LEVEL": "DEBUG", 74 | }). 75 | WithSBOMOutputDir(sbomDir). 76 | Execute(name, source) 77 | Expect(err).ToNot(HaveOccurred(), logs.String) 78 | 79 | // Ensure go is installed correctly 80 | container, err = docker.Container.Run. 81 | WithCommand("go run main.go"). 82 | WithEnv(map[string]string{"PORT": "8080"}). 83 | WithPublish("8080"). 84 | WithPublishAll(). 85 | Execute(image.ID) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | Eventually(container).Should(Serve(ContainSubstring("go1.23")).OnPort(8080)) 89 | 90 | Expect(logs).To(ContainLines( 91 | MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)), 92 | " Resolving Go version", 93 | " Candidate version sources (in priority order):", 94 | " -> \"\"", 95 | "", 96 | MatchRegexp(` Selected Go version \(using \): 1\.23\.\d+`), 97 | "", 98 | " Executing build process", 99 | MatchRegexp(` Installing Go 1\.23\.\d+`), 100 | MatchRegexp(` Completed in ([0-9]*(\.[0-9]*)?[a-z]+)+`), 101 | "", 102 | fmt.Sprintf(" Generating SBOM for /layers/%s/go", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_")), 103 | MatchRegexp(` Completed in ([0-9]*(\.[0-9]*)?[a-z]+)+`), 104 | "", 105 | " Writing SBOM in the following format(s):", 106 | " application/vnd.cyclonedx+json", 107 | " application/spdx+json", 108 | " application/vnd.syft+json", 109 | "", 110 | )) 111 | 112 | contents, err := os.ReadFile(filepath.Join(sbomDir, "sbom", "launch", "sbom.legacy.json")) 113 | Expect(err).NotTo(HaveOccurred()) 114 | Expect(string(contents)).To(ContainSubstring(`"name":"Go"`)) 115 | 116 | // check that all required SBOM files are present 117 | Expect(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "go", "sbom.cdx.json")).To(BeARegularFile()) 118 | Expect(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "go", "sbom.spdx.json")).To(BeARegularFile()) 119 | Expect(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "go", "sbom.syft.json")).To(BeARegularFile()) 120 | 121 | // check an SBOM file to make sure it has an entry for go 122 | contents, err = os.ReadFile(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "go", "sbom.cdx.json")) 123 | Expect(err).NotTo(HaveOccurred()) 124 | Expect(string(contents)).To(ContainSubstring(`"name": "Go"`)) 125 | }) 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /integration/environment_variable_configuration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/paketo-buildpacks/occam" 10 | "github.com/sclevine/spec" 11 | 12 | . "github.com/onsi/gomega" 13 | . "github.com/paketo-buildpacks/occam/matchers" 14 | ) 15 | 16 | func testEnvironmentVariableConfiguration(t *testing.T, context spec.G, it spec.S) { 17 | var ( 18 | Expect = NewWithT(t).Expect 19 | Eventually = NewWithT(t).Eventually 20 | 21 | pack occam.Pack 22 | docker occam.Docker 23 | ) 24 | 25 | it.Before(func() { 26 | pack = occam.NewPack().WithVerbose() 27 | docker = occam.NewDocker() 28 | }) 29 | 30 | context("when the buildpack is run with pack build", func() { 31 | var ( 32 | image occam.Image 33 | container occam.Container 34 | 35 | name string 36 | source string 37 | ) 38 | 39 | it.Before(func() { 40 | var err error 41 | name, err = occam.RandomName() 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | source, err = occam.Source(filepath.Join("testdata", "default_app")) 45 | Expect(err).NotTo(HaveOccurred()) 46 | }) 47 | 48 | it.After(func() { 49 | Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) 50 | Expect(os.RemoveAll(source)).To(Succeed()) 51 | }) 52 | 53 | context("env vars are used for configuration", func() { 54 | it.After(func() { 55 | Expect(docker.Container.Remove.Execute(container.ID)).To(Succeed()) 56 | Expect(docker.Image.Remove.Execute(image.ID)).To(Succeed()) 57 | }) 58 | 59 | it("builds with the version from BP_GO_VERSION", func() { 60 | var ( 61 | logs fmt.Stringer 62 | err error 63 | ) 64 | 65 | image, logs, err = pack.WithNoColor().Build. 66 | WithPullPolicy("never"). 67 | WithBuildpacks(buildpack, buildPlanBuildpack). 68 | WithEnv(map[string]string{"BP_GO_VERSION": "1.23.*"}). 69 | Execute(name, source) 70 | Expect(err).ToNot(HaveOccurred(), logs.String) 71 | 72 | container, err = docker.Container.Run. 73 | WithCommand("go run main.go"). 74 | WithEnv(map[string]string{"PORT": "8080"}). 75 | WithPublish("8080"). 76 | WithPublishAll(). 77 | Execute(image.ID) 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | Eventually(container).Should(Serve(ContainSubstring("go1.23")).OnPort(8080)) 81 | 82 | Expect(logs).To(ContainLines( 83 | MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)), 84 | " Resolving Go version", 85 | " Candidate version sources (in priority order):", 86 | " BP_GO_VERSION -> \"1.23.*\"", 87 | " -> \"\"", 88 | "", 89 | MatchRegexp(` Selected Go version \(using BP_GO_VERSION\): 1\.23\.\d+`), 90 | "", 91 | " Executing build process", 92 | MatchRegexp(` Installing Go 1\.23\.\d+`), 93 | MatchRegexp(` Completed in ([0-9]*(\.[0-9]*)?[a-z]+)+`), 94 | )) 95 | }) 96 | }) 97 | 98 | context("the app contains a buildpack.yml", func() { 99 | it.Before(func() { 100 | Expect(os.WriteFile(filepath.Join(source, "buildpack.yml"), nil, os.ModePerm)).To(Succeed()) 101 | }) 102 | it("fails the build", func() { 103 | var err error 104 | 105 | _, logs, err := pack.Build. 106 | WithPullPolicy("never"). 107 | WithBuildpacks(buildpack, buildPlanBuildpack). 108 | Execute(name, source) 109 | Expect(err).To(HaveOccurred(), logs.String) 110 | 111 | Expect(logs).To(ContainSubstring("working directory contains deprecated 'buildpack.yml'; use environment variables for configuration")) 112 | Expect(logs).NotTo(ContainLines( 113 | MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)), 114 | " Executing build process", 115 | )) 116 | }) 117 | 118 | }) 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /integration/init_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/BurntSushi/toml" 11 | "github.com/onsi/gomega/format" 12 | "github.com/paketo-buildpacks/occam" 13 | "github.com/sclevine/spec" 14 | "github.com/sclevine/spec/report" 15 | 16 | . "github.com/onsi/gomega" 17 | ) 18 | 19 | var ( 20 | buildpack string 21 | buildPlanBuildpack string 22 | offlineBuildpack string 23 | root string 24 | 25 | buildpackInfo struct { 26 | Buildpack struct { 27 | ID string 28 | Name string 29 | } 30 | } 31 | ) 32 | 33 | func TestIntegration(t *testing.T) { 34 | Expect := NewWithT(t).Expect 35 | 36 | format.MaxLength = 0 37 | 38 | var config struct { 39 | BuildPlan string `json:"buildplan"` 40 | } 41 | 42 | file, err := os.Open("../integration.json") 43 | Expect(err).NotTo(HaveOccurred()) 44 | defer file.Close() 45 | 46 | Expect(json.NewDecoder(file).Decode(&config)).To(Succeed()) 47 | 48 | file, err = os.Open("../buildpack.toml") 49 | Expect(err).NotTo(HaveOccurred()) 50 | 51 | _, err = toml.NewDecoder(file).Decode(&buildpackInfo) 52 | Expect(err).NotTo(HaveOccurred()) 53 | 54 | root, err = filepath.Abs("./..") 55 | Expect(err).ToNot(HaveOccurred()) 56 | 57 | buildpackStore := occam.NewBuildpackStore() 58 | 59 | buildpack, err = buildpackStore.Get. 60 | WithVersion("1.2.3"). 61 | Execute(root) 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | offlineBuildpack, err = buildpackStore.Get. 65 | WithOfflineDependencies(). 66 | WithVersion("1.2.3"). 67 | Execute(root) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | buildPlanBuildpack, err = buildpackStore.Get. 71 | Execute(config.BuildPlan) 72 | Expect(err).NotTo(HaveOccurred()) 73 | 74 | SetDefaultEventuallyTimeout(120 * time.Second) 75 | 76 | suite := spec.New("Integration", spec.Report(report.Terminal{}), spec.Parallel()) 77 | suite("Default", testDefault) 78 | suite("EnvironmentVariableConfiguration", testEnvironmentVariableConfiguration) 79 | suite("LayerReuse", testLayerReuse) 80 | suite("Offline", testOffline) 81 | suite.Run(t) 82 | } 83 | -------------------------------------------------------------------------------- /integration/layer_reuse_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/paketo-buildpacks/occam" 11 | "github.com/sclevine/spec" 12 | 13 | . "github.com/onsi/gomega" 14 | . "github.com/paketo-buildpacks/occam/matchers" 15 | ) 16 | 17 | func testLayerReuse(t *testing.T, context spec.G, it spec.S) { 18 | var ( 19 | Expect = NewWithT(t).Expect 20 | Eventually = NewWithT(t).Eventually 21 | 22 | docker occam.Docker 23 | pack occam.Pack 24 | 25 | imageIDs map[string]struct{} 26 | containerIDs map[string]struct{} 27 | ) 28 | 29 | it.Before(func() { 30 | docker = occam.NewDocker() 31 | pack = occam.NewPack() 32 | imageIDs = map[string]struct{}{} 33 | containerIDs = map[string]struct{}{} 34 | }) 35 | 36 | it.After(func() { 37 | for id := range containerIDs { 38 | Expect(docker.Container.Remove.Execute(id)).To(Succeed()) 39 | } 40 | 41 | for id := range imageIDs { 42 | Expect(docker.Image.Remove.Execute(id)).To(Succeed()) 43 | } 44 | 45 | }) 46 | 47 | context("when an app is rebuilt and does not change", func() { 48 | var ( 49 | name string 50 | source string 51 | ) 52 | 53 | it.Before(func() { 54 | var err error 55 | name, err = occam.RandomName() 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | source, err = occam.Source(filepath.Join("testdata", "default_app")) 59 | Expect(err).NotTo(HaveOccurred()) 60 | }) 61 | 62 | it.After(func() { 63 | Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) 64 | Expect(os.RemoveAll(source)).To(Succeed()) 65 | }) 66 | 67 | it("reuses a layer from a previous build", func() { 68 | firstImage, logs, err := pack.WithNoColor().Build. 69 | WithPullPolicy("never"). 70 | WithBuildpacks(buildpack, buildPlanBuildpack). 71 | Execute(name, source) 72 | Expect(err).NotTo(HaveOccurred()) 73 | 74 | imageIDs[firstImage.ID] = struct{}{} 75 | 76 | Expect(firstImage.Buildpacks).To(HaveLen(2)) 77 | Expect(firstImage.Buildpacks[0].Key).To(Equal(buildpackInfo.Buildpack.ID)) 78 | Expect(firstImage.Buildpacks[0].Layers).To(HaveKey("go")) 79 | 80 | Expect(logs).To(ContainLines( 81 | MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)), 82 | " Resolving Go version", 83 | " Candidate version sources (in priority order):", 84 | " -> \"\"", 85 | "", 86 | MatchRegexp(` Selected Go version \(using \): 1\.23\.\d+`), 87 | "", 88 | " Executing build process", 89 | MatchRegexp(` Installing Go 1\.23\.\d+`), 90 | MatchRegexp(` Completed in \d+(\.?\d+)*`), 91 | )) 92 | 93 | firstContainer, err := docker.Container.Run. 94 | WithCommand("go run main.go"). 95 | WithEnv(map[string]string{"PORT": "8080"}). 96 | WithPublish("8080"). 97 | WithPublishAll(). 98 | Execute(firstImage.ID) 99 | Expect(err).NotTo(HaveOccurred()) 100 | 101 | containerIDs[firstContainer.ID] = struct{}{} 102 | 103 | Eventually(firstContainer).Should(BeAvailable()) 104 | 105 | // Second pack build 106 | secondImage, logs, err := pack.WithNoColor().Build. 107 | WithPullPolicy("never"). 108 | WithBuildpacks(buildpack, buildPlanBuildpack). 109 | Execute(name, source) 110 | Expect(err).NotTo(HaveOccurred()) 111 | 112 | imageIDs[secondImage.ID] = struct{}{} 113 | 114 | Expect(secondImage.Buildpacks).To(HaveLen(2)) 115 | Expect(secondImage.Buildpacks[0].Key).To(Equal(buildpackInfo.Buildpack.ID)) 116 | Expect(secondImage.Buildpacks[0].Layers).To(HaveKey("go")) 117 | 118 | Expect(logs).To(ContainLines( 119 | MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)), 120 | " Resolving Go version", 121 | " Candidate version sources (in priority order):", 122 | " -> \"\"", 123 | "", 124 | MatchRegexp(` Selected Go version \(using \): 1\.23\.\d+`), 125 | "", 126 | fmt.Sprintf(" Reusing cached layer /layers/%s/go", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_")), 127 | )) 128 | 129 | secondContainer, err := docker.Container.Run. 130 | WithCommand("go run main.go"). 131 | WithEnv(map[string]string{"PORT": "8080"}). 132 | WithPublish("8080"). 133 | WithPublishAll(). 134 | Execute(secondImage.ID) 135 | Expect(err).NotTo(HaveOccurred()) 136 | 137 | containerIDs[secondContainer.ID] = struct{}{} 138 | 139 | Eventually(secondContainer).Should(Serve(ContainSubstring("go1.23")).OnPort(8080)) 140 | 141 | Expect(secondImage.Buildpacks[0].Layers["go"].SHA).To(Equal(firstImage.Buildpacks[0].Layers["go"].SHA)) 142 | }) 143 | }) 144 | 145 | context("when an app is rebuilt and there is a change", func() { 146 | var ( 147 | name string 148 | source string 149 | ) 150 | 151 | it.Before(func() { 152 | var err error 153 | name, err = occam.RandomName() 154 | Expect(err).NotTo(HaveOccurred()) 155 | 156 | source, err = occam.Source(filepath.Join("testdata", "default_app")) 157 | Expect(err).NotTo(HaveOccurred()) 158 | }) 159 | 160 | it.After(func() { 161 | Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) 162 | Expect(os.RemoveAll(source)).To(Succeed()) 163 | }) 164 | 165 | it("rebuilds the layer", func() { 166 | firstImage, _, err := pack.WithNoColor().Build. 167 | WithPullPolicy("never"). 168 | WithBuildpacks(buildpack, buildPlanBuildpack). 169 | WithEnv(map[string]string{"BP_GO_VERSION": "1.23.*"}). 170 | Execute(name, source) 171 | Expect(err).NotTo(HaveOccurred()) 172 | 173 | imageIDs[firstImage.ID] = struct{}{} 174 | 175 | Expect(firstImage.Buildpacks).To(HaveLen(2)) 176 | Expect(firstImage.Buildpacks[0].Key).To(Equal(buildpackInfo.Buildpack.ID)) 177 | Expect(firstImage.Buildpacks[0].Layers).To(HaveKey("go")) 178 | 179 | firstContainer, err := docker.Container.Run. 180 | WithCommand("go run main.go"). 181 | WithEnv(map[string]string{"PORT": "8080"}). 182 | WithPublish("8080"). 183 | WithPublishAll(). 184 | Execute(firstImage.ID) 185 | Expect(err).NotTo(HaveOccurred()) 186 | 187 | containerIDs[firstContainer.ID] = struct{}{} 188 | 189 | Eventually(firstContainer).Should(BeAvailable()) 190 | 191 | // Second pack build 192 | secondImage, _, err := pack.WithNoColor().Build. 193 | WithPullPolicy("never"). 194 | WithBuildpacks(buildpack, buildPlanBuildpack). 195 | WithEnv(map[string]string{"BP_GO_VERSION": "1.24.*"}). 196 | Execute(name, source) 197 | Expect(err).NotTo(HaveOccurred()) 198 | 199 | imageIDs[secondImage.ID] = struct{}{} 200 | 201 | Expect(secondImage.Buildpacks).To(HaveLen(2)) 202 | Expect(secondImage.Buildpacks[0].Key).To(Equal(buildpackInfo.Buildpack.ID)) 203 | Expect(secondImage.Buildpacks[0].Layers).To(HaveKey("go")) 204 | 205 | secondContainer, err := docker.Container.Run. 206 | WithCommand("go run main.go"). 207 | WithEnv(map[string]string{"PORT": "8080"}). 208 | WithPublish("8080"). 209 | WithPublishAll(). 210 | Execute(secondImage.ID) 211 | Expect(err).NotTo(HaveOccurred()) 212 | 213 | containerIDs[secondContainer.ID] = struct{}{} 214 | 215 | Eventually(secondContainer).Should(Serve(ContainSubstring("go1.24")).OnPort(8080)) 216 | 217 | Expect(secondImage.Buildpacks[0].Layers["go"].SHA).NotTo(Equal(firstImage.Buildpacks[0].Layers["go"].SHA)) 218 | }) 219 | }) 220 | } 221 | -------------------------------------------------------------------------------- /integration/offline_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/paketo-buildpacks/occam" 10 | "github.com/sclevine/spec" 11 | 12 | . "github.com/onsi/gomega" 13 | . "github.com/paketo-buildpacks/occam/matchers" 14 | ) 15 | 16 | func testOffline(t *testing.T, context spec.G, it spec.S) { 17 | var ( 18 | Expect = NewWithT(t).Expect 19 | Eventually = NewWithT(t).Eventually 20 | 21 | pack occam.Pack 22 | docker occam.Docker 23 | ) 24 | 25 | it.Before(func() { 26 | pack = occam.NewPack() 27 | docker = occam.NewDocker() 28 | }) 29 | 30 | context("when offline", func() { 31 | var ( 32 | image occam.Image 33 | container occam.Container 34 | 35 | name string 36 | source string 37 | ) 38 | 39 | it.Before(func() { 40 | var err error 41 | name, err = occam.RandomName() 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | source, err = occam.Source(filepath.Join("testdata", "default_app")) 45 | Expect(err).NotTo(HaveOccurred()) 46 | }) 47 | 48 | it.After(func() { 49 | Expect(docker.Container.Remove.Execute(container.ID)).To(Succeed()) 50 | Expect(docker.Image.Remove.Execute(image.ID)).To(Succeed()) 51 | Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed()) 52 | Expect(os.RemoveAll(source)).NotTo(HaveOccurred()) 53 | }) 54 | 55 | it("installs golang", func() { 56 | var ( 57 | logs fmt.Stringer 58 | err error 59 | ) 60 | 61 | image, logs, err = pack.WithNoColor().Build. 62 | WithPullPolicy("never"). 63 | WithBuildpacks(offlineBuildpack, buildPlanBuildpack). 64 | WithNetwork("none"). 65 | Execute(name, source) 66 | 67 | Expect(err).NotTo(HaveOccurred(), logs.String()) 68 | 69 | container, err = docker.Container.Run. 70 | WithCommand("go run main.go"). 71 | WithEnv(map[string]string{"PORT": "8080"}). 72 | WithPublish("8080"). 73 | WithPublishAll(). 74 | Execute(image.ID) 75 | Expect(err).NotTo(HaveOccurred()) 76 | 77 | Eventually(container).Should(Serve(ContainSubstring("go1.23")).OnPort(8080)) 78 | }) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /integration/testdata/buildpack_yaml_app/buildpack.yml: -------------------------------------------------------------------------------- 1 | --- 2 | go: 3 | version: 1.16.* 4 | -------------------------------------------------------------------------------- /integration/testdata/buildpack_yaml_app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 13 | fmt.Fprintln(w, runtime.Version()) 14 | }) 15 | 16 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil)) 17 | } 18 | -------------------------------------------------------------------------------- /integration/testdata/buildpack_yaml_app/plan.toml: -------------------------------------------------------------------------------- 1 | [[requires]] 2 | name = "go" 3 | 4 | [requires.metadata] 5 | launch = true 6 | -------------------------------------------------------------------------------- /integration/testdata/default_app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 13 | fmt.Fprintln(w, runtime.Version()) 14 | }) 15 | 16 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil)) 17 | } 18 | -------------------------------------------------------------------------------- /integration/testdata/default_app/plan.toml: -------------------------------------------------------------------------------- 1 | [[requires]] 2 | name = "go" 3 | 4 | [requires.metadata] 5 | launch = true 6 | -------------------------------------------------------------------------------- /run/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | godist "github.com/paketo-buildpacks/go-dist" 7 | "github.com/paketo-buildpacks/packit/v2" 8 | "github.com/paketo-buildpacks/packit/v2/cargo" 9 | "github.com/paketo-buildpacks/packit/v2/chronos" 10 | "github.com/paketo-buildpacks/packit/v2/draft" 11 | "github.com/paketo-buildpacks/packit/v2/postal" 12 | "github.com/paketo-buildpacks/packit/v2/sbom" 13 | "github.com/paketo-buildpacks/packit/v2/scribe" 14 | ) 15 | 16 | type Generator struct{} 17 | 18 | func (f Generator) GenerateFromDependency(dependency postal.Dependency, path string) (sbom.SBOM, error) { 19 | return sbom.GenerateFromDependency(dependency, path) 20 | } 21 | 22 | func main() { 23 | logEmitter := scribe.NewEmitter(os.Stdout).WithLevel(os.Getenv("BP_LOG_LEVEL")) 24 | entryResolver := draft.NewPlanner() 25 | dependencyManager := postal.NewService(cargo.NewTransport()) 26 | 27 | packit.Run( 28 | godist.Detect(), 29 | godist.Build(entryResolver, dependencyManager, Generator{}, chronos.DefaultClock, logEmitter), 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/.util/builders.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | # shellcheck source=SCRIPTDIR/print.sh 7 | source "$(dirname "${BASH_SOURCE[0]}")/print.sh" 8 | 9 | function util::builders::list() { 10 | local integrationJSON="${1}" 11 | local builders="" 12 | if [[ -f "${integrationJSON}" ]]; then 13 | builders="$(jq --compact-output 'select(.builder != null) | [.builder]' "${integrationJSON}")" 14 | 15 | if [[ -z "${builders}" ]]; then 16 | builders="$(jq --compact-output 'select(.builders != null) | .builders' "${integrationJSON}")" 17 | fi 18 | fi 19 | 20 | if [[ -z "${builders}" ]]; then 21 | util::print::info "No builders specified. Falling back to default builder..." 22 | builders="$(jq --compact-output --null-input '["index.docker.io/paketobuildpacks/builder-jammy-buildpackless-base:latest"]')" 23 | fi 24 | 25 | echo "${builders}" 26 | } 27 | -------------------------------------------------------------------------------- /scripts/.util/print.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | function util::print::title() { 7 | local blue reset message 8 | blue="\033[0;34m" 9 | reset="\033[0;39m" 10 | message="${1}" 11 | 12 | echo -e "\n${blue}${message}${reset}" >&2 13 | } 14 | 15 | function util::print::info() { 16 | local message 17 | message="${1}" 18 | 19 | echo -e "${message}" >&2 20 | } 21 | 22 | function util::print::error() { 23 | local message red reset 24 | message="${1}" 25 | red="\033[0;31m" 26 | reset="\033[0;39m" 27 | 28 | echo -e "${red}${message}${reset}" >&2 29 | exit 1 30 | } 31 | 32 | function util::print::success() { 33 | local message green reset 34 | message="${1}" 35 | green="\033[0;32m" 36 | reset="\033[0;39m" 37 | 38 | echo -e "${green}${message}${reset}" >&2 39 | exitcode="${2:-0}" 40 | exit "${exitcode}" 41 | } 42 | 43 | function util::print::warn() { 44 | local message yellow reset 45 | message="${1}" 46 | yellow="\033[0;33m" 47 | reset="\033[0;39m" 48 | 49 | echo -e "${yellow}${message}${reset}" >&2 50 | exit 0 51 | } 52 | -------------------------------------------------------------------------------- /scripts/.util/tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "createpackage": "v1.73.0", 3 | "jam": "v2.11.5", 4 | "pack": "v0.37.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/.util/tools.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | # shellcheck source=SCRIPTDIR/print.sh 7 | source "$(dirname "${BASH_SOURCE[0]}")/print.sh" 8 | 9 | function util::tools::os() { 10 | case "$(uname)" in 11 | "Darwin") 12 | echo "${1:-darwin}" 13 | ;; 14 | 15 | "Linux") 16 | echo "linux" 17 | ;; 18 | 19 | *) 20 | util::print::error "Unknown OS \"$(uname)\"" 21 | exit 1 22 | esac 23 | } 24 | 25 | function util::tools::arch() { 26 | case "$(uname -m)" in 27 | arm64|aarch64) 28 | echo "arm64" 29 | ;; 30 | 31 | amd64|x86_64) 32 | if [[ "${1:-}" == "--blank-amd64" ]]; then 33 | echo "" 34 | else 35 | echo "amd64" 36 | fi 37 | ;; 38 | 39 | *) 40 | util::print::error "Unknown Architecture \"$(uname -m)\"" 41 | exit 1 42 | esac 43 | } 44 | 45 | function util::tools::path::export() { 46 | local dir 47 | dir="${1}" 48 | 49 | if ! echo "${PATH}" | grep -q "${dir}"; then 50 | PATH="${dir}:$PATH" 51 | export PATH 52 | fi 53 | } 54 | 55 | function util::tools::jam::install() { 56 | local dir token 57 | token="" 58 | 59 | while [[ "${#}" != 0 ]]; do 60 | case "${1}" in 61 | --directory) 62 | dir="${2}" 63 | shift 2 64 | ;; 65 | 66 | --token) 67 | token="${2}" 68 | shift 2 69 | ;; 70 | 71 | *) 72 | util::print::error "unknown argument \"${1}\"" 73 | esac 74 | done 75 | 76 | mkdir -p "${dir}" 77 | util::tools::path::export "${dir}" 78 | 79 | if [[ ! -f "${dir}/jam" ]]; then 80 | local version curl_args os arch 81 | 82 | version="$(jq -r .jam "$(dirname "${BASH_SOURCE[0]}")/tools.json")" 83 | 84 | curl_args=( 85 | "--fail" 86 | "--silent" 87 | "--location" 88 | "--output" "${dir}/jam" 89 | ) 90 | 91 | if [[ "${token}" != "" ]]; then 92 | curl_args+=("--header" "Authorization: Token ${token}") 93 | fi 94 | 95 | util::print::title "Installing jam ${version}" 96 | 97 | os=$(util::tools::os) 98 | arch=$(util::tools::arch) 99 | 100 | curl "https://github.com/paketo-buildpacks/jam/releases/download/${version}/jam-${os}-${arch}" \ 101 | "${curl_args[@]}" 102 | 103 | chmod +x "${dir}/jam" 104 | else 105 | util::print::info "Using $("${dir}"/jam version)" 106 | fi 107 | } 108 | 109 | function util::tools::pack::install() { 110 | local dir token 111 | token="" 112 | 113 | while [[ "${#}" != 0 ]]; do 114 | case "${1}" in 115 | --directory) 116 | dir="${2}" 117 | shift 2 118 | ;; 119 | 120 | --token) 121 | token="${2}" 122 | shift 2 123 | ;; 124 | 125 | *) 126 | util::print::error "unknown argument \"${1}\"" 127 | esac 128 | done 129 | 130 | mkdir -p "${dir}" 131 | util::tools::path::export "${dir}" 132 | 133 | if [[ ! -f "${dir}/pack" ]]; then 134 | local version curl_args os arch 135 | 136 | version="$(jq -r .pack "$(dirname "${BASH_SOURCE[0]}")/tools.json")" 137 | 138 | local pack_config_enable_experimental 139 | if [ -f "$(dirname "${BASH_SOURCE[0]}")/../options.json" ]; then 140 | pack_config_enable_experimental="$(jq -r .pack_config_enable_experimental "$(dirname "${BASH_SOURCE[0]}")/../options.json")" 141 | else 142 | pack_config_enable_experimental="false" 143 | fi 144 | 145 | curl_args=( 146 | "--fail" 147 | "--silent" 148 | "--location" 149 | ) 150 | 151 | if [[ "${token}" != "" ]]; then 152 | curl_args+=("--header" "Authorization: Token ${token}") 153 | fi 154 | 155 | util::print::title "Installing pack ${version}" 156 | 157 | os=$(util::tools::os macos) 158 | arch=$(util::tools::arch --blank-amd64) 159 | 160 | curl "https://github.com/buildpacks/pack/releases/download/${version}/pack-${version}-${os}${arch:+-$arch}.tgz" \ 161 | "${curl_args[@]}" | \ 162 | tar xzf - -C "${dir}" 163 | chmod +x "${dir}/pack" 164 | 165 | if [[ "${pack_config_enable_experimental}" == "true" ]]; then 166 | "${dir}"/pack config experimental true 167 | fi 168 | 169 | else 170 | util::print::info "Using pack $("${dir}"/pack version)" 171 | fi 172 | } 173 | 174 | function util::tools::packager::install () { 175 | local dir 176 | while [[ "${#}" != 0 ]]; do 177 | case "${1}" in 178 | --directory) 179 | dir="${2}" 180 | shift 2 181 | ;; 182 | 183 | *) 184 | util::print::error "unknown argument \"${1}\"" 185 | ;; 186 | 187 | esac 188 | done 189 | 190 | mkdir -p "${dir}" 191 | util::tools::path::export "${dir}" 192 | 193 | if [[ ! -f "${dir}/packager" ]]; then 194 | util::print::title "Installing packager" 195 | GOBIN="${dir}" go install github.com/cloudfoundry/libcfbuildpack/packager@latest 196 | fi 197 | } 198 | 199 | function util::tools::create-package::install () { 200 | local dir version 201 | while [[ "${#}" != 0 ]]; do 202 | case "${1}" in 203 | --directory) 204 | dir="${2}" 205 | shift 2 206 | ;; 207 | 208 | *) 209 | util::print::error "unknown argument \"${1}\"" 210 | ;; 211 | 212 | esac 213 | done 214 | 215 | version="$(jq -r .createpackage "$(dirname "${BASH_SOURCE[0]}")/tools.json")" 216 | 217 | mkdir -p "${dir}" 218 | util::tools::path::export "${dir}" 219 | 220 | if [[ ! -f "${dir}/create-package" ]]; then 221 | util::print::title "Installing create-package" 222 | GOBIN="${dir}" go install -ldflags="-s -w" "github.com/paketo-buildpacks/libpak/cmd/create-package@${version}" 223 | fi 224 | } 225 | 226 | function util::tools::tests::checkfocus() { 227 | testout="${1}" 228 | if grep -q 'Focused: [1-9]' "${testout}"; then 229 | echo "Detected Focused Test(s) - setting exit code to 197" 230 | rm "${testout}" 231 | util::print::success "** GO Test Succeeded **" 197 232 | fi 233 | rm "${testout}" 234 | } 235 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | readonly PROGDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | readonly BUILDPACKDIR="$(cd "${PROGDIR}/.." && pwd)" 8 | 9 | function main() { 10 | while [[ "${#}" != 0 ]]; do 11 | case "${1}" in 12 | --help|-h) 13 | shift 1 14 | usage 15 | exit 0 16 | ;; 17 | 18 | "") 19 | # skip if the argument is empty 20 | shift 1 21 | ;; 22 | 23 | *) 24 | util::print::error "unknown argument \"${1}\"" 25 | esac 26 | done 27 | 28 | mkdir -p "${BUILDPACKDIR}/bin" 29 | 30 | run::build 31 | cmd::build 32 | } 33 | 34 | function usage() { 35 | cat <<-USAGE 36 | build.sh [OPTIONS] 37 | 38 | Builds the buildpack executables. 39 | 40 | OPTIONS 41 | --help -h prints the command usage 42 | USAGE 43 | } 44 | 45 | function run::build() { 46 | if [[ -f "${BUILDPACKDIR}/run/main.go" ]]; then 47 | pushd "${BUILDPACKDIR}/bin" > /dev/null || return 48 | printf "%s" "Building run... " 49 | 50 | GOOS=linux \ 51 | CGO_ENABLED=0 \ 52 | go build \ 53 | -ldflags="-s -w" \ 54 | -o "run" \ 55 | "${BUILDPACKDIR}/run" 56 | 57 | echo "Success!" 58 | 59 | names=("detect") 60 | 61 | if [ -f "${BUILDPACKDIR}/extension.toml" ]; then 62 | names+=("generate") 63 | else 64 | names+=("build") 65 | fi 66 | 67 | for name in "${names[@]}"; do 68 | printf "%s" "Linking ${name}... " 69 | 70 | ln -sf "run" "${name}" 71 | 72 | echo "Success!" 73 | done 74 | popd > /dev/null || return 75 | fi 76 | } 77 | 78 | function cmd::build() { 79 | if [[ -d "${BUILDPACKDIR}/cmd" ]]; then 80 | local name 81 | for src in "${BUILDPACKDIR}"/cmd/*; do 82 | name="$(basename "${src}")" 83 | 84 | if [[ -f "${src}/main.go" ]]; then 85 | printf "%s" "Building ${name}... " 86 | 87 | GOOS="linux" \ 88 | CGO_ENABLED=0 \ 89 | go build \ 90 | -ldflags="-s -w" \ 91 | -o "${BUILDPACKDIR}/bin/${name}" \ 92 | "${src}/main.go" 93 | 94 | echo "Success!" 95 | else 96 | printf "%s" "Skipping ${name}... " 97 | fi 98 | done 99 | fi 100 | } 101 | 102 | main "${@:-}" 103 | -------------------------------------------------------------------------------- /scripts/integration.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | readonly PROGDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | readonly BUILDPACKDIR="$(cd "${PROGDIR}/.." && pwd)" 8 | 9 | # shellcheck source=SCRIPTDIR/.util/tools.sh 10 | source "${PROGDIR}/.util/tools.sh" 11 | 12 | # shellcheck source=SCRIPTDIR/.util/print.sh 13 | source "${PROGDIR}/.util/print.sh" 14 | 15 | # shellcheck source=SCRIPTDIR/.util/builders.sh 16 | source "${PROGDIR}/.util/builders.sh" 17 | 18 | function main() { 19 | local builderArray token 20 | builderArray=() 21 | token="" 22 | 23 | while [[ "${#}" != 0 ]]; do 24 | case "${1}" in 25 | --help|-h) 26 | shift 1 27 | usage 28 | exit 0 29 | ;; 30 | 31 | --builder|-b) 32 | builderArray+=("${2}") 33 | shift 2 34 | ;; 35 | 36 | --token|-t) 37 | token="${2}" 38 | shift 2 39 | ;; 40 | 41 | "") 42 | # skip if the argument is empty 43 | shift 1 44 | ;; 45 | 46 | *) 47 | util::print::error "unknown argument \"${1}\"" 48 | esac 49 | done 50 | 51 | if [[ ! -d "${BUILDPACKDIR}/integration" ]]; then 52 | util::print::warn "** WARNING No Integration tests **" 53 | fi 54 | 55 | tools::install "${token}" 56 | 57 | if [ ${#builderArray[@]} -eq 0 ]; then 58 | util::print::title "No builders provided. Finding builders in integration.json..." 59 | 60 | local builders 61 | builders="$(util::builders::list "${BUILDPACKDIR}/integration.json" | jq -r '.[]' )" 62 | 63 | util::print::info "Found the following builders:" 64 | util::print::info "${builders}" 65 | 66 | # shellcheck disable=SC2206 67 | IFS=$'\n' builderArray=(${builders}) 68 | unset IFS 69 | fi 70 | 71 | local testout 72 | testout=$(mktemp) 73 | for builder in "${builderArray[@]}"; do 74 | util::print::title "Getting images for builder: '${builder}'" 75 | builder_images::pull "${builder}" 76 | 77 | util::print::title "Setting default pack builder image..." 78 | pack config default-builder "${builder}" 79 | 80 | tests::run "${builder}" "${testout}" 81 | done 82 | 83 | util::tools::tests::checkfocus "${testout}" 84 | util::print::success "** GO Test Succeeded with all builders**" 85 | } 86 | 87 | function usage() { 88 | cat <<-USAGE 89 | integration.sh [OPTIONS] 90 | 91 | Runs the integration test suite. 92 | 93 | OPTIONS 94 | --help -h prints the command usage 95 | --builder -b sets the name of the builder(s) that are pulled / used for testing. 96 | Defaults to "builders" array in integration.json, if present. 97 | --token Token used to download assets from GitHub (e.g. jam, pack, etc) (optional) 98 | USAGE 99 | } 100 | 101 | function tools::install() { 102 | local token 103 | token="${1}" 104 | 105 | util::tools::pack::install \ 106 | --directory "${BUILDPACKDIR}/.bin" \ 107 | --token "${token}" 108 | 109 | util::tools::jam::install \ 110 | --directory "${BUILDPACKDIR}/.bin" \ 111 | --token "${token}" 112 | 113 | util::tools::create-package::install \ 114 | --directory "${BUILDPACKDIR}/.bin" 115 | 116 | if [[ -f "${BUILDPACKDIR}/.libbuildpack" ]]; then 117 | util::tools::packager::install \ 118 | --directory "${BUILDPACKDIR}/.bin" 119 | fi 120 | } 121 | 122 | function builder_images::pull() { 123 | local builder 124 | builder="${1}" 125 | 126 | util::print::title "Pulling builder image ${builder}..." 127 | docker pull "${builder}" 128 | 129 | local run_image lifecycle_image 130 | run_image="$( 131 | pack inspect-builder "${builder}" --output json \ 132 | | jq -r '.remote_info.run_images[0].name' 133 | )" 134 | lifecycle_image="index.docker.io/buildpacksio/lifecycle:$( 135 | pack inspect-builder "${builder}" --output json \ 136 | | jq -r '.remote_info.lifecycle.version' 137 | )" 138 | 139 | util::print::title "Pulling run image..." 140 | docker pull "${run_image}" 141 | 142 | util::print::title "Pulling lifecycle image..." 143 | docker pull "${lifecycle_image}" 144 | } 145 | 146 | function tests::run() { 147 | util::print::title "Run Buildpack Runtime Integration Tests" 148 | util::print::info "Using ${1} as builder..." 149 | 150 | export CGO_ENABLED=0 151 | pushd "${BUILDPACKDIR}" > /dev/null 152 | if GOMAXPROCS="${GOMAXPROCS:-4}" go test -count=1 -timeout 0 ./integration/... -v -run Integration | tee "${2}"; then 153 | util::print::info "** GO Test Succeeded with ${1}**" 154 | else 155 | util::print::error "** GO Test Failed with ${1}**" 156 | fi 157 | popd > /dev/null 158 | } 159 | 160 | main "${@:-}" 161 | -------------------------------------------------------------------------------- /scripts/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | readonly ROOT_DIR="$(cd "$(dirname "${0}")/.." && pwd)" 7 | readonly BIN_DIR="${ROOT_DIR}/.bin" 8 | readonly BUILD_DIR="${ROOT_DIR}/build" 9 | 10 | # shellcheck source=SCRIPTDIR/.util/tools.sh 11 | source "${ROOT_DIR}/scripts/.util/tools.sh" 12 | 13 | # shellcheck source=SCRIPTDIR/.util/print.sh 14 | source "${ROOT_DIR}/scripts/.util/print.sh" 15 | 16 | function main { 17 | local version output token 18 | token="" 19 | 20 | while [[ "${#}" != 0 ]]; do 21 | case "${1}" in 22 | --version|-v) 23 | version="${2}" 24 | shift 2 25 | ;; 26 | 27 | --output|-o) 28 | output="${2}" 29 | shift 2 30 | ;; 31 | 32 | --token|-t) 33 | token="${2}" 34 | shift 2 35 | ;; 36 | 37 | --help|-h) 38 | shift 1 39 | usage 40 | exit 0 41 | ;; 42 | 43 | "") 44 | # skip if the argument is empty 45 | shift 1 46 | ;; 47 | 48 | *) 49 | util::print::error "unknown argument \"${1}\"" 50 | esac 51 | done 52 | 53 | if [[ -z "${version:-}" ]]; then 54 | usage 55 | echo 56 | util::print::error "--version is required" 57 | fi 58 | 59 | if [[ -z "${output:-}" ]]; then 60 | output="${BUILD_DIR}/buildpackage.cnb" 61 | fi 62 | 63 | repo::prepare 64 | 65 | tools::install "${token}" 66 | 67 | buildpack_type=buildpack 68 | if [ -f "${ROOT_DIR}/extension.toml" ]; then 69 | buildpack_type=extension 70 | fi 71 | 72 | buildpack::archive "${version}" "${buildpack_type}" 73 | buildpackage::create "${output}" "${buildpack_type}" 74 | } 75 | 76 | function usage() { 77 | cat <<-USAGE 78 | package.sh --version [OPTIONS] 79 | 80 | Packages a buildpack or an extension into a buildpackage .cnb file. 81 | 82 | OPTIONS 83 | --help -h prints the command usage 84 | --version -v specifies the version number to use when packaging a buildpack or an extension 85 | --output -o location to output the packaged buildpackage or extension artifact (default: ${ROOT_DIR}/build/buildpackage.cnb) 86 | --token Token used to download assets from GitHub (e.g. jam, pack, etc) (optional) 87 | USAGE 88 | } 89 | 90 | function repo::prepare() { 91 | util::print::title "Preparing repo..." 92 | 93 | rm -rf "${BUILD_DIR}" 94 | 95 | mkdir -p "${BIN_DIR}" 96 | mkdir -p "${BUILD_DIR}" 97 | 98 | export PATH="${BIN_DIR}:${PATH}" 99 | } 100 | 101 | function tools::install() { 102 | local token 103 | token="${1}" 104 | 105 | util::tools::pack::install \ 106 | --directory "${BIN_DIR}" \ 107 | --token "${token}" 108 | 109 | if [[ -f "${ROOT_DIR}/.libbuildpack" ]]; then 110 | util::tools::packager::install \ 111 | --directory "${BIN_DIR}" 112 | else 113 | util::tools::jam::install \ 114 | --directory "${BIN_DIR}" \ 115 | --token "${token}" 116 | fi 117 | } 118 | 119 | function buildpack::archive() { 120 | local version 121 | version="${1}" 122 | buildpack_type="${2}" 123 | 124 | util::print::title "Packaging ${buildpack_type} into ${BUILD_DIR}/buildpack.tgz..." 125 | 126 | if [[ -f "${ROOT_DIR}/.libbuildpack" ]]; then 127 | packager \ 128 | --uncached \ 129 | --archive \ 130 | --version "${version}" \ 131 | "${BUILD_DIR}/buildpack" 132 | else 133 | jam pack \ 134 | "--${buildpack_type}" "${ROOT_DIR}/${buildpack_type}.toml"\ 135 | --version "${version}" \ 136 | --output "${BUILD_DIR}/buildpack.tgz" 137 | fi 138 | } 139 | 140 | function buildpackage::create() { 141 | local output 142 | output="${1}" 143 | buildpack_type="${2}" 144 | 145 | util::print::title "Packaging ${buildpack_type}... ${output}" 146 | 147 | if [ "$buildpack_type" == "extension" ]; then 148 | cwd=$(pwd) 149 | cd ${BUILD_DIR} 150 | mkdir cnbdir 151 | cd cnbdir 152 | cp ../buildpack.tgz . 153 | tar -xvf buildpack.tgz 154 | rm buildpack.tgz 155 | 156 | pack \ 157 | extension package "${output}" \ 158 | --format file 159 | 160 | cd $cwd 161 | else 162 | pack \ 163 | buildpack package "${output}" \ 164 | --path "${BUILD_DIR}/buildpack.tgz" \ 165 | --format file 166 | fi 167 | } 168 | 169 | main "${@:-}" -------------------------------------------------------------------------------- /scripts/unit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | readonly PROGDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | readonly BUILDPACKDIR="$(cd "${PROGDIR}/.." && pwd)" 8 | 9 | GO_TEST_PARAMS="${GO_TEST_PARAMS:-}" 10 | 11 | # shellcheck source=SCRIPTDIR/.util/tools.sh 12 | source "${PROGDIR}/.util/tools.sh" 13 | 14 | # shellcheck source=SCRIPTDIR/.util/print.sh 15 | source "${PROGDIR}/.util/print.sh" 16 | 17 | function main() { 18 | while [[ "${#}" != 0 ]]; do 19 | case "${1}" in 20 | --help|-h) 21 | shift 1 22 | usage 23 | exit 0 24 | ;; 25 | 26 | "") 27 | # skip if the argument is empty 28 | shift 1 29 | ;; 30 | 31 | *) 32 | util::print::error "unknown argument \"${1}\"" 33 | esac 34 | done 35 | 36 | unit::run 37 | } 38 | 39 | function usage() { 40 | cat <<-USAGE 41 | unit.sh [OPTIONS] 42 | 43 | Runs the unit test suite. 44 | 45 | OPTIONS 46 | --help -h prints the command usage 47 | USAGE 48 | } 49 | 50 | function unit::run() { 51 | util::print::title "Run Buildpack Unit Tests" 52 | 53 | testout=$(mktemp) 54 | pushd "${BUILDPACKDIR}" > /dev/null 55 | if go test ./... -v ${GO_TEST_PARAMS} -run Unit | tee "${testout}"; then 56 | util::tools::tests::checkfocus "${testout}" 57 | util::print::success "** GO Test Succeeded **" 58 | else 59 | util::print::error "** GO Test Failed **" 60 | fi 61 | popd > /dev/null 62 | } 63 | 64 | main "${@:-}" 65 | --------------------------------------------------------------------------------