Skip to content

Create Release

Create Release #50513

name: Create Release
on:
schedule:
- cron: '*/30 * * * *' # every 30 minutes
push:
branches:
- main
workflow_dispatch:
inputs:
version:
description: 'Version of the release to cut (e.g. 1.2.3). No leading v'
required: false
force:
description: 'Release stack even if change validator does not detect changes, or a package is removed'
required: true
type: choice
default: 'false'
options:
- 'true'
- 'false'
concurrency: release
env:
BUILD_RECEIPT_FILENAME: "build-receipt.cyclonedx.json"
RUN_RECEIPT_FILENAME: "run-receipt.cyclonedx.json"
PATCHED_USNS_FILENAME: "patched-usns.json"
jobs:
get_architectures:
name: Get Architectures
runs-on: ubuntu-22.04
outputs:
architectures: ${{ steps.lookup.outputs.platforms }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Lookup Supported Architectures
id: lookup
run: |
#! /usr/bin/env bash
set -euo pipefail
shopt -s inherit_errexit
# install yj to parse TOML
curl -L $(curl -sL https://api.github.com/repos/sclevine/yj/releases/latest | jq -r '.assets[] | select(.name=="yj-linux-amd64").browser_download_url') -o yj
chmod +x yj
# parse stack.toml for platforms
platforms="$(cat stack/stack.toml | ./yj -tj | jq -c '[.platforms[] | sub("linux/"; "")]')"
printf "Platforms: %s\n" "${platforms}"
printf "platforms=%s\n" "${platforms}" >> "$GITHUB_OUTPUT"
poll_usns:
name: Poll USNs
runs-on: ubuntu-22.04
needs: [get_architectures]
strategy:
matrix:
arch: ${{ fromJSON(needs.get_architectures.outputs.architectures) }}
outputs:
usns: ${{ steps.usns.outputs.usns }}
steps:
- name: Find and Download Previous Build Receipt
id: previous_build
uses: paketo-buildpacks/github-config/actions/release/find-and-download-asset@main
with:
asset_pattern: "${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
search_depth: 1
repo: ${{ github.repository }}
output_path: "/github/workspace/${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }}
- name: Find and Download Previous Run Receipt
id: previous_run
uses: paketo-buildpacks/github-config/actions/release/find-and-download-asset@main
with:
asset_pattern: "${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
search_depth: 1
repo: ${{ github.repository }}
output_path: "/github/workspace/${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }}
- name: Get Package List
id: packages
if: ${{ steps.previous_build.outputs.output_path != '' && steps.previous_run.outputs.output_path != '' }}
uses: paketo-buildpacks/github-config/actions/stack/generate-package-list@main
with:
build_receipt: "${{ github.workspace }}/${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
run_receipt: "${{ github.workspace }}/${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
- name: Find and Download Previous Patched USNs
id: download_patched
uses: paketo-buildpacks/github-config/actions/release/find-and-download-asset@main
with:
asset_pattern: "${{ matrix.arch }}-${{ env.PATCHED_USNS_FILENAME }}"
search_depth: "-1" # Search all releases
repo: ${{ github.repository }}
output_path: "/github/workspace/${{ matrix.arch }}-${{ env.PATCHED_USNS_FILENAME }}-previous"
token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }}
- name: Output Patched USNs as JSON String
id: patched
if: ${{ steps.download_patched.outputs.output_path != '' }}
run: |
patched=$(jq --compact-output . < "${GITHUB_WORKSPACE}/${{ matrix.arch }}-${{ env.PATCHED_USNS_FILENAME }}-previous")
printf "patched=%s\n" "${patched}" >> "$GITHUB_OUTPUT"
- name: Get Stack Distribution Name
id: distro
run: |
# Extract distro from repo name:
# paketo-buildpacks/jammy-tiny-stack --> jammy
distro="$(echo "${{ github.repository }}" | sed 's/^.*\///' | sed 's/\-.*$//')"
echo "Ubuntu distribution: ${distro}"
printf "distro=%s\n" "${distro}" >> "$GITHUB_OUTPUT"
- name: Get New USNs
id: usns
uses: paketo-buildpacks/github-config/actions/stack/get-usns@main
with:
distribution: ${{ steps.distro.outputs.distro }}
packages: ${{ steps.packages.outputs.packages }}
last_usns: ${{ steps.patched.outputs.patched }}
- name: Write USNs File
id: write_usns
run: |
jq . <<< "${USNS}" > "${USNS_PATH}"
echo "usns=${USNS_PATH}" >> "$GITHUB_OUTPUT"
env:
USNS_PATH: "${{ matrix.arch }}-${{ env.PATCHED_USNS_FILENAME }}"
USNS: ${{ steps.usns.outputs.usns }}
- name: Upload USNs file
uses: actions/upload-artifact@v4
with:
name: "${{ matrix.arch }}-${{ env.PATCHED_USNS_FILENAME }}"
path: "${{ matrix.arch }}-${{ env.PATCHED_USNS_FILENAME }}"
stack_files_changed:
name: Determine If Stack Files Changed
runs-on: ubuntu-22.04
needs: poll_usns
if: ${{ ! ( needs.poll_usns.outputs.usns == '[]' && github.event_name == 'schedule' ) }}
outputs:
stack_files_changed: ${{ steps.compare.outputs.stack_files_changed }}
steps:
- name: Checkout With History
uses: actions/checkout@v4
with:
fetch-depth: 0 # gets full history
- name: Compare With Previous Release
id: compare
run: |
# shellcheck disable=SC2046
changed="$(git diff --name-only $(git describe --tags --abbrev=0) -- stack)"
if [ -z "${changed}" ]
then
echo "No relevant files changed since previous release."
echo "stack_files_changed=false" >> "$GITHUB_OUTPUT"
else
echo "Relevant files have changed since previous release."
echo "${changed}"
echo "stack_files_changed=true" >> "$GITHUB_OUTPUT"
fi
run_if_stack_files_changed:
name: Run If Stack Files Changed
runs-on: ubuntu-22.04
needs: [stack_files_changed]
if: ${{ needs.stack_files_changed.outputs.stack_files_changed == 'true' }}
steps:
- name: Run if stack files changed
run: |
echo "stack files have changed"
create_stack:
name: Create Stack
needs: poll_usns
if: ${{ ! ( needs.poll_usns.outputs.usns == '[]' && github.event_name == 'schedule' ) }}
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Create stack
id: create-stack
run: |
./scripts/create.sh
- name: Upload run image
uses: actions/upload-artifact@v3
with:
name: current-run-image
path: build/run.oci
- name: Upload build image
uses: actions/upload-artifact@v3
with:
name: current-build-image
path: build/build.oci
- name: Generate Package Receipts
id: receipts
run: |
scripts/receipts.sh --build-image "${GITHUB_WORKSPACE}/build/build.oci" \
--run-image "${GITHUB_WORKSPACE}/build/run.oci" \
--build-receipt ${{ env.BUILD_RECEIPT_FILENAME }} \
--run-receipt ${{ env.RUN_RECEIPT_FILENAME }}
- name: Upload ${{ matrix.arch }} Build receipt(s)
uses: actions/upload-artifact@v3
with:
name: current-build-receipt
path: "*${{ env.BUILD_RECEIPT_FILENAME }}"
- name: Upload ${{ matrix.arch }} Run receipt(s)
uses: actions/upload-artifact@v3
with:
name: current-run-receipt
path: "*${{ env.RUN_RECEIPT_FILENAME }}"
diff:
name: Diff Packages (${{ matrix.arch }})
outputs:
build_added: ${{ steps.build_diff.outputs.added }}
build_modified: ${{ steps.build_diff.outputs.modified }}
build_removed_with_force: ${{ steps.build_diff.outputs.removed }}
run_added: ${{ steps.run_diff.outputs.added }}
run_modified: ${{ steps.run_diff.outputs.modified }}
run_removed_with_force: ${{ steps.run_diff.outputs.removed }}
removed_with_force: ${{ steps.removed_with_force.outputs.packages_removed }}
packages_changed: ${{ steps.compare.outputs.packages_changed }}
needs: [ create_stack, get_architectures ]
runs-on: ubuntu-22.04
strategy:
matrix:
arch: ${{ fromJSON(needs.get_architectures.outputs.architectures) }}
steps:
- name: Download Build Receipt
uses: actions/download-artifact@v3
with:
name: current-build-receipt
- name: Download Run Receipt
uses: actions/download-artifact@v3
with:
name: current-run-receipt
- name: Display structure of downloaded files
run: ls -R
- name: Check for Previous Releases
id: check_previous
run: |
gh auth status
# shellcheck disable=SC2046
if [ $(gh api "/repos/${{ github.repository }}/releases" | jq -r 'length') -eq 0 ]; then
echo "exists=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "exists=true" >> "$GITHUB_OUTPUT"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Write Empty Previous Receipts
if: ${{ steps.check_previous.outputs.exists == 'false' }}
run: |
echo '{"components":[]}' > "${{ github.workspace }}/previous-${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
echo '{"components":[]}' > "${{ github.workspace }}/previous-${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
- name: Find and Download Previous Build Receipt
if: ${{ steps.check_previous.outputs.exists == 'true' }}
uses: paketo-buildpacks/github-config/actions/release/find-and-download-asset@main
with:
asset_pattern: "${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
search_depth: 1
repo: ${{ github.repository }}
output_path: "/github/workspace/previous-${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }}
- name: Find and Download Previous Run Receipt
if: ${{ steps.check_previous.outputs.exists == 'true' }}
uses: paketo-buildpacks/github-config/actions/release/find-and-download-asset@main
with:
asset_pattern: "${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
search_depth: 1
repo: ${{ github.repository }}
output_path: "/github/workspace/previous-${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }}
- name: Compare Build Packages
id: build_diff
uses: paketo-buildpacks/github-config/actions/stack/diff-package-receipts@main
with:
previous: "/github/workspace/previous-${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
current: "/github/workspace/${{ matrix.arch }}-${{ env.BUILD_RECEIPT_FILENAME }}"
- name: Compare Run Packages
id: run_diff
uses: paketo-buildpacks/github-config/actions/stack/diff-package-receipts@main
with:
previous: "/github/workspace/previous-${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
current: "/github/workspace/${{ matrix.arch }}-${{ env.RUN_RECEIPT_FILENAME }}"
- name: Fail If Packages Removed
id: removed_with_force
run: |
build=$(jq '. | length' <<< "${BUILD_REMOVED}")
echo "Build (${{ matrix.arch }}) packages removed: ${build}"
run=$(jq '. | length' <<< "${RUN_REMOVED}")
echo "Run (${{ matrix.arch }}) packages removed: ${run}"
# only fail if packages are removed AND the release has not been forced
if [ "${build}" -gt 0 ] || [ "${run}" -gt 0 ]; then
if [ "${{ github.event.inputs.force }}" != 'true' ]; then
echo "Packages removed without authorization. Stack cannot be released."
exit 1
else
echo "packages removed with user-provided force"
echo "packages_removed=true" >> "$GITHUB_OUTPUT"
fi
else
echo "packages_removed=false" >> "$GITHUB_OUTPUT"
fi
env:
BUILD_REMOVED: ${{ steps.build_diff.outputs.removed }}
RUN_REMOVED: ${{ steps.run_diff.outputs.removed }}
- name: Compare With Previous Release
id: compare
run: |
# shellcheck disable=SC2153
build_added=$(jq '. | length' <<< "${BUILD_ADDED}")
echo "Build packages added: ${build_added}"
# shellcheck disable=SC2153
build_modified=$(jq '. | length' <<< "${BUILD_MODIFIED}")
echo "Build packages modified: ${build_modified}"
# shellcheck disable=SC2153
run_added=$(jq '. | length' <<< "${RUN_ADDED}")
echo "Run packages added: ${run_added}"
# shellcheck disable=SC2153
run_modified=$(jq '. | length' <<< "${RUN_MODIFIED}")
echo "Run packages modified: ${run_modified}"
if [ "${build_added}" -eq 0 ] && [ "${build_modified}" -eq 0 ] && [ "${run_added}" -eq 0 ] && [ "${run_modified}" -eq 0 ]; then
echo "No packages changed."
echo "packages_changed=false" >> "$GITHUB_OUTPUT"
else
echo "Packages changed."
echo "packages_changed=true" >> "$GITHUB_OUTPUT"
fi
env:
BUILD_ADDED: ${{ steps.build_diff.outputs.added }}
BUILD_MODIFIED: ${{ steps.build_diff.outputs.modified }}
RUN_ADDED: ${{ steps.run_diff.outputs.added }}
RUN_MODIFIED: ${{ steps.run_diff.outputs.modified }}
- name: Get Repository Name
id: repo_name
run: |
full=${{ github.repository }}
# Strip off the org and slash from repo name
# paketo-buildpacks/repo-name --> repo-name
repo=$(echo "${full}" | sed 's/^.*\///')
echo "github_repo_name=${repo}" >> "$GITHUB_OUTPUT"
# Strip off 'stack' suffix from repo name
# some-name-stack --> some-name
registry_repo="${repo//-stack/}"
echo "registry_repo_name=${registry_repo}" >> "$GITHUB_OUTPUT"
- name: Create Release Notes
id: notes
uses: paketo-buildpacks/github-config/actions/stack/release-notes@main
with:
build_image: "paketobuildpacks/build-${{ steps.repo_name.outputs.registry_repo_name }}"
run_image: "paketobuildpacks/run-${{ steps.repo_name.outputs.registry_repo_name }}"
build_packages_added: ${{ steps.build_diff.outputs.added }}
build_packages_modified: ${{ steps.build_diff.outputs.modified }}
build_packages_removed_with_force: ${{ steps.build_diff.outputs.removed }}
run_packages_added: ${{ steps.run_diff.outputs.added }}
run_packages_modified: ${{ steps.run_diff.outputs.modified }}
run_packages_removed_with_force: ${{ steps.run_diff.outputs.removed }}
- name: Release Notes File
id: release-notes-file
run: |
printf '%s\n' '${{ steps.notes.outputs.release_body }}' > "${{ matrix.arch }}-release-notes.md"
- name: Upload ${{ matrix.arch }} release notes file
uses: actions/upload-artifact@v4
with:
name: "${{ matrix.arch }}-release-notes.md"
path: "${{ matrix.arch }}-release-notes.md"
test:
name: Acceptance Test
needs: [ create_stack ]
runs-on: ubuntu-22.04
steps:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.20.x
- name: Checkout
uses: actions/checkout@v4
- name: Create OCI artifacts destination directory
run: |
mkdir -p build
- name: Download Build Image
uses: actions/download-artifact@v3
with:
name: current-build-image
path: build
- name: Download Run Image
uses: actions/download-artifact@v3
with:
name: current-run-image
path: build
- name: Run Acceptance Tests
run: ./scripts/test.sh
force_release_creation:
name: Force Release Creation
runs-on: ubuntu-22.04
if: ${{github.event.inputs.force == 'true'}}
steps:
- name: Signal force release creation
run: |
echo "Force release creation input set to true"
release:
name: Release
runs-on: ubuntu-22.04
needs: [poll_usns, create_stack, diff, test, force_release_creation ]
if: ${{ always() && needs.diff.result == 'success' && needs.test.result == 'success' && (needs.diff.outputs.packages_changed == 'true' || needs.run_if_stack_files_changed.result == 'success' || needs.force_release_creation.result == 'success' ) }}
steps:
- name: Print Release Reasoning
run: |
printf "Diff Packages: %s\n" "${{ needs.diff.result }}"
printf "Acceptance Tests: %s\n" "${{ needs.test.result }}"
printf "Run If Packages Changed: %s\n" "${{ needs.diff.outputs.packages_changed }}"
printf "Run If Packages Removed With Force: %s\n" "${{ needs.diff.outputs.removed_with_force }}"
printf "Run If Stack Files Changed: %s\n" "${{ needs.run_if_stack_files_changed.result }}"
printf "Force Release: %s\n" "${{ github.event.inputs.force }}"
- name: Checkout With History
uses: actions/checkout@v4
with:
fetch-depth: 0 # gets full history
- name: Download current build image
uses: actions/download-artifact@v3
with:
name: current-build-image
- name: Download current run image
uses: actions/download-artifact@v3
with:
name: current-run-image
- name: Download Build Receipt
uses: actions/download-artifact@v3
with:
name: current-build-receipt
- name: Download Run Receipt
uses: actions/download-artifact@v3
with:
name: current-run-receipt
- name: Increment Tag
if: github.event.inputs.version == ''
id: semver
uses: paketo-buildpacks/github-config/actions/tag/increment-tag@main
with:
allow_head_tagged: true
- name: Set Release Tag
id: tag
run: |
tag="${{ github.event.inputs.version }}"
if [ -z "${tag}" ]; then
tag="${{ steps.semver.outputs.tag }}"
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
- name: Download USN File(s)
uses: actions/download-artifact@v4
with:
path: usn-files
pattern: "*${{ env.PATCHED_USNS_FILENAME }}"
merge-multiple: true
- name: Display USN Files
run: ls usn-files
- name: Get Repository Name
id: repo_name
run: |
full=${{ github.repository }}
# Strip off the org and slash from repo name
# paketo-buildpacks/repo-name --> repo-name
repo=$(echo "${full}" | sed 's/^.*\///')
echo "github_repo_name=${repo}" >> "$GITHUB_OUTPUT"
# Strip off 'stack' suffix from repo name
# some-name-stack --> some-name
registry_repo="${repo//-stack/}"
echo "registry_repo_name=${registry_repo}" >> "$GITHUB_OUTPUT"
- name: Download Release Note File(s)
uses: actions/download-artifact@v4
with:
path: release-notes
pattern: "*release-notes.md"
merge-multiple: true
- name: Display Release Note Files
run: ls release-notes
- name: Setup Release Assets
id: assets
run: |
assets="$(jq --null-input --compact-output \
--arg tag "${{ steps.tag.outputs.tag }}" \
--arg repo "${{ steps.repo_name.outputs.github_repo_name }}" \
'[
{
"path": "build.oci",
"name": ($repo + "-" + $tag + "-" + "build.oci"),
"content_type": "application/gzip"
},
{
"path": "run.oci",
"name": ($repo + "-" + $tag + "-" + "run.oci"),
"content_type": "application/gzip"
}]')"
for buildReceipt in $(ls -R *${{ env.BUILD_RECEIPT_FILENAME }}); do
assets="$(jq --compact-output \
--arg tag "${{ steps.tag.outputs.tag }}" \
--arg repo "${{ steps.repo_name.outputs.github_repo_name }}" \
--arg build_receipt "${buildReceipt}" \
'. += [
{
"path": $build_receipt,
"name": ($repo + "-" + $tag + "-" + $build_receipt),
"content_type": "text/plain"
}
]' <<< "${assets}")"
done
for runReceipt in $(ls -R *${{ env.RUN_RECEIPT_FILENAME }}); do
assets="$(jq --compact-output \
--arg tag "${{ steps.tag.outputs.tag }}" \
--arg repo "${{ steps.repo_name.outputs.github_repo_name }}" \
--arg run_receipt "${runReceipt}" \
'. += [
{
"path": $run_receipt,
"name": ($repo + "-" + $tag + "-" + $run_receipt),
"content_type": "text/plain"
}
]' <<< "${assets}")"
done
for usnFile in $(ls usn-files); do
assets="$(jq --compact-output \
--arg tag "${{ steps.tag.outputs.tag }}" \
--arg repo "${{ steps.repo_name.outputs.github_repo_name }}" \
--arg usn_path "usn-files/${usnFile}" \
--arg usn_name "${usnFile}" \
'. += [
{
"path": $usn_path,
"name": ($repo + "-" + $tag + "-" + $usn_name),
"content_type": "text/plain"
}
]' <<< "${assets}")"
done
for releaseNoteFile in $(ls release-notes); do
assets="$(jq --compact-output \
--arg tag "${{ steps.tag.outputs.tag }}" \
--arg repo "${{ steps.repo_name.outputs.github_repo_name }}" \
--arg note_path "release-notes/${releaseNoteFile}" \
--arg note_name "${releaseNoteFile}" \
'. += [
{
"path": $note_path,
"name": ($repo + "-" + $tag + "-" + $note_name),
"content_type": "text/plain"
}
]' <<< "${assets}")"
done
printf "assets=%s\n" "${assets}" >> "$GITHUB_OUTPUT"
- name: Create Release
uses: paketo-buildpacks/github-config/actions/release/create@main
with:
repo: ${{ github.repository }}
token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }}
tag_name: v${{ steps.tag.outputs.tag }}
target_commitish: ${{ github.sha }}
name: v${{ steps.tag.outputs.tag }}
body: "## Images\nBuild: `paketobuildpacks/build-${{ steps.repo_name.outputs.registry_repo_name }}:${{ steps.tag.outputs.tag }}`\nRun: `paketobuildpacks/run-${{ steps.repo_name.outputs.registry_repo_name }}:${{ steps.tag.outputs.tag }}`\n\nSee `release-note.md` and `patched-usns.json` attachments for changes"
draft: false
assets: ${{ steps.assets.outputs.assets }}
failure:
name: Alert on Failure
runs-on: ubuntu-22.04
needs: [poll_usns, create_stack, diff, test, release, stack_files_changed]
if: ${{ always() && needs.poll_usns.result == 'failure' || needs.create_stack.result == 'failure' || needs.diff.result == 'failure' || needs.test.result == 'failure' || needs.release.result == 'failure' || needs.packages_changed.result == 'failure' || needs.stack_files_changed.result == 'failure' }}
steps:
- name: File Failure Alert Issue
uses: paketo-buildpacks/github-config/actions/issue/file@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
label: "failure:release"
comment_if_exists: true
issue_title: "Failure: Create Release workflow"
issue_body: |
Create Release workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}).
Please take a look to ensure CVE patches can be released. (cc @paketo-buildpacks/stacks-maintainers).
comment_body: |
Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}