name: Release
on:
push:
tags:
- "v*"
env:
BINARY_NAME: tact
CARGO_TERM_COLOR: always
REGISTRY: ghcr.io
REGISTRY_IMAGE: ghcr.io/clabby/tact
jobs:
validate:
name: Validate Release
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout sources
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with:
fetch-depth: 0
persist-credentials: false
- name: Verify tag matches crate version on main
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Tagged commit $GITHUB_SHA is not on main" >&2
exit 1
fi
crate_version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "tact") | .version')
tag_version="${GITHUB_REF_NAME#v}"
if [[ "$tag_version" != "$crate_version" ]]; then
echo "Tag version $tag_version does not match crate version $crate_version" >&2
exit 1
fi
build:
name: Build ${{ matrix.target }}
needs: validate
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
archive_ext: tar.gz
- os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
archive_ext: tar.gz
- os: macos-latest
target: aarch64-apple-darwin
archive_ext: tar.gz
- os: macos-latest
target: x86_64-apple-darwin
archive_ext: tar.gz
steps:
- name: Checkout sources
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with:
persist-credentials: false
- name: Install Rust stable toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 with:
targets: ${{ matrix.target }}
- name: Build with Cargo
run: cargo build --locked --release --target ${{ matrix.target }}
env:
TACT_RELEASE_BUILD: "1"
- name: Package binary
shell: bash
run: |
set -euo pipefail
package_name="${BINARY_NAME}-${{ matrix.target }}-${GITHUB_REF_NAME}"
mkdir "$package_name"
cp "target/${{ matrix.target }}/release/${BINARY_NAME}" "$package_name/"
cp README.md LICENSE.md "$package_name/"
tar -czf "$package_name.${{ matrix.archive_ext }}" "$package_name"
shasum -a 256 "$package_name.${{ matrix.archive_ext }}" > "$package_name.${{ matrix.archive_ext }}.sha256"
- name: Upload packaged binary
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: ${{ matrix.target }}
path: |
${{ env.BINARY_NAME }}-${{ matrix.target }}-${{ github.ref_name }}.${{ matrix.archive_ext }}
${{ env.BINARY_NAME }}-${{ matrix.target }}-${{ github.ref_name }}.${{ matrix.archive_ext }}.sha256
if-no-files-found: error
sign:
name: Sign Release Assets
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout sources
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with:
persist-credentials: false
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with:
path: dist
merge-multiple: true
- name: Install rsign2
uses: taiki-e/install-action@c44f6b046f1c29ae5918b1e0bfdbb2f1813836fd with:
tool: rsign2
- name: Sign binary artifacts
shell: bash
run: |
set -euo pipefail
trap 'rm -f minisign.key minisign.pub' EXIT
rsign generate -f -W -p minisign.pub -s minisign.key
echo "::add-mask::$(tail -n 1 minisign.key)"
{
printf '\n[package.metadata.binstall.signing]\n'
printf 'algorithm = "minisign"\n'
printf 'pubkey = "%s"\n' "$(tail -n 1 minisign.pub)"
} >> Cargo.toml
cp minisign.pub dist/minisign.pub
for archive in dist/*.tar.gz; do
rsign sign -W -s minisign.key -x "${archive}.sig" "${archive}"
done
- name: Verify signed release assets
shell: bash
run: |
set -euo pipefail
cd dist
for archive in *.tar.gz; do
test -s "$archive"
test -s "${archive}.sha256"
test -s "${archive}.sig"
shasum -a 256 --check "${archive}.sha256"
rsign verify "$archive" -P "$(tail -n 1 minisign.pub)" -x "${archive}.sig"
done
test "$(find . -maxdepth 1 -name '*.tar.gz' | wc -l | tr -d ' ')" = "4"
- name: Upload signed release bundle
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: signed-release
path: |
Cargo.toml
dist
if-no-files-found: error
publish_crate:
name: Publish Crate
needs: sign
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout sources
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with:
persist-credentials: false
- name: Download signed release bundle
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with:
name: signed-release
path: ${{ runner.temp }}/signed-release
- name: Install signed crate manifest
run: cp "${RUNNER_TEMP}/signed-release/Cargo.toml" Cargo.toml
- name: Install Rust stable toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 - name: Verify crate package
run: cargo package --locked --allow-dirty
- name: Publish crate
shell: bash
run: |
set -euo pipefail
version=$(cargo metadata --locked --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "tact") | .version')
crate="target/package/tact-${version}.crate"
test -f "$crate"
local_checksum=$(sha256sum "$crate" | cut -d ' ' -f 1)
for attempt in {1..5}; do
if cargo publish --locked --allow-dirty --no-verify; then
exit 0
fi
if [[ "$attempt" -lt 5 ]]; then
sleep $((attempt * 15))
fi
done
published_checksum=$(curl --fail --silent --show-error \
--retry 6 --retry-delay 5 --retry-all-errors \
--user-agent "tact-release-workflow/${version} (https://github.com/clabby/tact)" \
"https://crates.io/api/v1/crates/tact/${version}" \
| jq -er '.version.checksum')
if [[ "$published_checksum" != "$local_checksum" ]]; then
echo "crates.io has different contents for tact ${version}" >&2
exit 1
fi
echo "cargo publish reported failure, but crates.io has the exact packaged crate"
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
release:
name: Publish GitHub Release
needs: publish_crate
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout sources
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with:
fetch-depth: 0
persist-credentials: false
- name: Download signed release bundle
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with:
name: signed-release
path: .
- name: Install git-cliff
uses: taiki-e/install-action@c44f6b046f1c29ae5918b1e0bfdbb2f1813836fd with:
tool: git-cliff@2.13.1
fallback: none
- name: Generate release notes
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
cp .github/RELEASE_TEMPLATE.md release-notes.md
printf '\n\n' >> release-notes.md
git cliff --current --strip all >> release-notes.md
- name: Publish release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 with:
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
body_path: release-notes.md
files: |
dist/*.tar.gz
dist/*.sha256
dist/*.sig
dist/minisign.pub
fail_on_unmatched_files: true
container_build:
name: Package Image (${{ matrix.platform }})
needs: release
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
platform: linux/amd64
platform_pair: linux-amd64
target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04-arm
platform: linux/arm64
platform_pair: linux-arm64
target: aarch64-unknown-linux-gnu
steps:
- name: Checkout sources
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with:
persist-credentials: false
- name: Download signed release bundle
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with:
name: signed-release
path: release-bundle
- name: Install rsign2
uses: taiki-e/install-action@c44f6b046f1c29ae5918b1e0bfdbb2f1813836fd with:
tool: rsign2
- name: Extract verified release binary
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
package_name="${BINARY_NAME}-${TARGET}-${GITHUB_REF_NAME}"
archive="${package_name}.tar.gz"
mkdir -p "${GITHUB_WORKSPACE}/target/image-context" "${RUNNER_TEMP}/release-package"
cd release-bundle/dist
shasum -a 256 --check "${archive}.sha256"
rsign verify "$archive" -P "$(tail -n 1 minisign.pub)" -x "${archive}.sig"
tar -xzf "$archive" -C "${RUNNER_TEMP}/release-package" "${package_name}/${BINARY_NAME}"
install -m 0755 \
"${RUNNER_TEMP}/release-package/${package_name}/${BINARY_NAME}" \
"${GITHUB_WORKSPACE}/target/image-context/${BINARY_NAME}"
- name: Authenticate with container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 - name: Package and push image by digest
id: build
uses: docker/bake-action@3acf805d94d93a86cce4ca44798a76464a75b88c env:
RELEASE_BINARY_CONTEXT: target/image-context
RELEASE_REVISION: ${{ github.sha }}
RELEASE_VERSION: ${{ github.ref_name }}
with:
source: .
files: docker/docker-bake.hcl
targets: release
set: |
release.platform=${{ matrix.platform }}
release.output=type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Verify image binary matches release binary
shell: bash
env:
DIGEST: ${{ fromJSON(steps.build.outputs.metadata)['release']['containerimage.digest'] }}
PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
container_id=$(docker create --platform "$PLATFORM" "${REGISTRY_IMAGE}@${DIGEST}" /tact)
trap 'docker rm -f "$container_id" >/dev/null' EXIT
docker cp "${container_id}:/tact" "${RUNNER_TEMP}/image-tact"
cmp "${GITHUB_WORKSPACE}/target/image-context/tact" "${RUNNER_TEMP}/image-tact"
- name: Export image digest
shell: bash
env:
DIGEST: ${{ fromJSON(steps.build.outputs.metadata)['release']['containerimage.digest'] }}
run: |
mkdir -p "${RUNNER_TEMP}/digests"
touch "${RUNNER_TEMP}/digests/${DIGEST#sha256:}"
- name: Upload image digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: release-image-digest-${{ matrix.platform_pair }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
container_publish:
name: Publish Release Image
needs: container_build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Download image digests
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with:
path: ${{ runner.temp }}/digests
pattern: release-image-digest-*
merge-multiple: true
- name: Authenticate with container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 - name: Create and inspect release manifest
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
images=()
for digest in "${RUNNER_TEMP}"/digests/*; do
images+=("${REGISTRY_IMAGE}@sha256:${digest##*/}")
done
test "${#images[@]}" = 2
docker buildx imagetools create \
--tag "${REGISTRY_IMAGE}:${version}" \
--tag "${REGISTRY_IMAGE}:latest" \
"${images[@]}"
docker buildx imagetools inspect "${REGISTRY_IMAGE}:${version}"