Skip to content

chore(release): prepare v2.0.1 (#624) #19

chore(release): prepare v2.0.1 (#624)

chore(release): prepare v2.0.1 (#624) #19

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version without the leading v"
required: true
default: "2.0.1"
permissions:
contents: write
packages: write
id-token: write
attestations: write
artifact-metadata: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
GOFLAGS: -mod=readonly
jobs:
prepare:
name: Prepare release metadata
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
version: ${{ steps.meta.outputs.version }}
numeric_version: ${{ steps.meta.outputs.numeric_version }}
build_date: ${{ steps.meta.outputs.build_date }}
source_date_epoch: ${{ steps.meta.outputs.source_date_epoch }}
channel: ${{ steps.meta.outputs.channel }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Validate version and immutable release source
id: meta
shell: bash
env:
DISPATCH_VERSION: ${{ inputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
version="${GITHUB_REF_NAME#v}"
else
version="$DISPATCH_VERSION"
fi
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::invalid release version: $version"
exit 1
fi
git fetch --no-tags origin main
main_sha="$(git rev-parse FETCH_HEAD)"
if [ "$GITHUB_SHA" != "$main_sha" ]; then
echo "::error::release source $GITHUB_SHA is not the current main commit $main_sha"
exit 1
fi
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
echo "::error::release tag and version disagree"
exit 1
fi
if [ "$(git cat-file -t "$GITHUB_REF")" != "tag" ]; then
echo "::error::release tags must be annotated"
exit 1
fi
if gh api "repos/$GITHUB_REPOSITORY/releases/tags/$GITHUB_REF_NAME" \
>/dev/null 2>&1; then
echo "::error::GitHub Release $GITHUB_REF_NAME already exists"
exit 1
fi
else
if [ "$GITHUB_REF_TYPE" != "branch" ] || [ "$GITHUB_REF_NAME" != "main" ]; then
echo "::error::manual release preflight must run from main"
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v$version" \
>/dev/null 2>&1; then
echo "::error::release tag v$version already exists"
exit 1
fi
fi
numeric_version="${version%%-*}"
if [[ "$version" == *-* ]]; then
channel="beta"
else
channel="latest"
fi
source_date_epoch="$(git show -s --format=%ct "$GITHUB_SHA")"
build_date="$(date -u -d "@$source_date_epoch" +'%Y-%m-%dT%H:%M:%SZ')"
{
echo "version=$version"
echo "numeric_version=$numeric_version"
echo "build_date=$build_date"
echo "source_date_epoch=$source_date_epoch"
echo "channel=$channel"
} >> "$GITHUB_OUTPUT"
supply-chain:
name: Supply-chain policy, SBOM, and vulnerability gates
needs: prepare
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache-dependency-path: |
go.sum
clients/desktop/go.sum
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"
cache: npm
cache-dependency-path: |
clients/web/package-lock.json
clients/desktop/frontend/package-lock.json
website/package-lock.json
- name: Verify immutable production inputs
run: |
go mod verify
go run ./cmd/trustdb release verify-policy \
--root . \
--policy supply-chain/production-inputs.json
- name: Generate pinned source SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
with:
path: .
format: spdx-json
output-file: trustdb-release.spdx.json
artifact-name: trustdb-release.spdx.json
syft-version: v1.49.0
upload-artifact: false
upload-release-assets: false
- name: Audit production dependency graphs
shell: bash
run: |
set -euo pipefail
mkdir -p security-reports
for project in clients/web clients/desktop/frontend website; do
report_name="$(printf '%s' "$project" | tr '/' '-')"
npm --prefix "$project" ci --ignore-scripts
npm --prefix "$project" audit --audit-level=high --json \
> "security-reports/$report_name.json"
done
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 -json ./... \
> security-reports/go-vulncheck.json
jq -n \
--arg schema "trustdb.vulnerability-report.v1" \
--arg source_commit "$GITHUB_SHA" \
--slurpfile admin security-reports/clients-web.json \
--slurpfile desktop security-reports/clients-desktop-frontend.json \
--slurpfile website security-reports/website.json \
--rawfile go security-reports/go-vulncheck.json \
'{schema: $schema, source_commit: $source_commit, policy: "fail-on-high-or-critical", npm: {admin_web: $admin[0], desktop: $desktop[0], website: $website[0]}, govulncheck_jsonl: $go}' \
> TRUSTDB_VULNERABILITY_REPORT.json
- name: Stage retained release metadata
shell: bash
run: |
cp supply-chain/production-inputs.json TRUSTDB_PRODUCTION_INPUTS.json
- name: Upload retained supply-chain metadata
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: supply-chain-evidence
path: |
TRUSTDB_PRODUCTION_INPUTS.json
TRUSTDB_VULNERABILITY_REPORT.json
trustdb-release.spdx.json
if-no-files-found: error
admin-web:
name: Build Admin Web
needs:
- prepare
- supply-chain
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"
cache: npm
cache-dependency-path: clients/web/package-lock.json
- name: Build Admin Web
working-directory: clients/web
run: |
npm ci
npm run build
- name: Upload Admin Web
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: admin-web-assets
path: clients/web/dist/
if-no-files-found: error
reproducibility:
name: Reproduce Server/CLI package
needs:
- prepare
- admin-web
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Download Admin Web
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: admin-web-assets
path: release-admin
- name: Build twice from the same commit timestamp
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
BUILD_DATE: ${{ needs.prepare.outputs.build_date }}
SOURCE_DATE_EPOCH: ${{ needs.prepare.outputs.source_date_epoch }}
TARGET_OS: linux
TARGET_ARCH: amd64
CGO_ENABLED: "1"
ARCHIVE_KIND: tar.gz
run: |
set -euo pipefail
bash packaging/release/build-server.sh
first="$(sha256sum "release-output/trustdb-$VERSION-linux-amd64.tar.gz" | cut -d' ' -f1)"
rm -rf release-bin release-stage release-output
bash packaging/release/build-server.sh
second="$(sha256sum "release-output/trustdb-$VERSION-linux-amd64.tar.gz" | cut -d' ' -f1)"
test "$first" = "$second"
printf 'reproducible sha256=%s\n' "$second"
server:
name: Server/CLI · ${{ matrix.os }} · ${{ matrix.arch }}
needs:
- prepare
- admin-web
runs-on: ${{ matrix.runner }}
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
os: linux
arch: amd64
cgo: "1"
exe: ""
archive: tar.gz
- runner: ubuntu-24.04-arm
os: linux
arch: arm64
cgo: "1"
exe: ""
archive: tar.gz
- runner: macos-15-intel
os: darwin
arch: amd64
cgo: "1"
exe: ""
archive: tar.gz
- runner: macos-15
os: darwin
arch: arm64
cgo: "1"
exe: ""
archive: tar.gz
- runner: windows-2025
os: windows
arch: amd64
cgo: "0"
exe: ".exe"
archive: zip
- runner: windows-11-arm
os: windows
arch: arm64
cgo: "0"
exe: ".exe"
archive: zip
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Download Admin Web
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: admin-web-assets
path: release-admin
- name: Build and package Server/CLI
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
BUILD_DATE: ${{ needs.prepare.outputs.build_date }}
TARGET_OS: ${{ matrix.os }}
TARGET_ARCH: ${{ matrix.arch }}
CGO_ENABLED: ${{ matrix.cgo }}
EXE_SUFFIX: ${{ matrix.exe }}
ARCHIVE_KIND: ${{ matrix.archive }}
run: bash packaging/release/build-server.sh
- name: Upload Server/CLI package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: server-${{ matrix.os }}-${{ matrix.arch }}
path: release-output/*
if-no-files-found: error
desktop-macos:
name: Desktop · macOS · ${{ matrix.arch }}
needs: prepare
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
arch: amd64
- runner: macos-15
arch: arm64
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: clients/desktop/go.mod
cache-dependency-path: |
go.sum
clients/desktop/go.sum
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"
cache: npm
cache-dependency-path: clients/desktop/frontend/package-lock.json
- name: Build, self-sign, and package
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
BUILD_DATE: ${{ needs.prepare.outputs.build_date }}
TARGET_ARCH: ${{ matrix.arch }}
run: bash packaging/release/build-desktop-macos.sh
- name: Upload macOS desktop packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-darwin-${{ matrix.arch }}
path: clients/desktop/release-output/*
if-no-files-found: error
desktop-windows:
name: Desktop · Windows · ${{ matrix.arch }}
needs: prepare
runs-on: ${{ matrix.runner }}
timeout-minutes: 55
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2025
arch: amd64
wix_arch: x64
signtool_arch: x64
- runner: windows-11-arm
arch: arm64
wix_arch: arm64
signtool_arch: arm64
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: clients/desktop/go.mod
cache-dependency-path: |
go.sum
clients/desktop/go.sum
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"
cache: npm
cache-dependency-path: clients/desktop/frontend/package-lock.json
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: "8.0.x"
- name: Install installer toolchains
shell: pwsh
run: |
choco install nsis -y --no-progress
dotnet tool install --global wix --version 5.0.2
- name: Build, self-sign, and package
shell: pwsh
env:
VERSION: ${{ needs.prepare.outputs.version }}
NUMERIC_VERSION: ${{ needs.prepare.outputs.numeric_version }}
BUILD_DATE: ${{ needs.prepare.outputs.build_date }}
TARGET_ARCH: ${{ matrix.arch }}
WIX_ARCH: ${{ matrix.wix_arch }}
SIGNTOOL_ARCH: ${{ matrix.signtool_arch }}
CGO_ENABLED: "0"
run: ./packaging/release/build-desktop-windows.ps1
- name: Upload Windows desktop packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-windows-${{ matrix.arch }}
path: clients/desktop/release-output/*
if-no-files-found: error
docker:
name: Container registries · linux/amd64 + linux/arm64
needs:
- prepare
- supply-chain
if: github.ref_type == 'tag'
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.ref }}
- name: Login to Docker Hub
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Reject an existing immutable version tag
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/trustdb
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/trustdb
run: |
set -euo pipefail
for image in "$DOCKERHUB_IMAGE" "$GHCR_IMAGE"; do
if docker buildx imagetools inspect "$image:$VERSION" \
>/dev/null 2>&1; then
echo "::error::immutable image tag already exists: $image:$VERSION"
exit 1
fi
done
- name: Build and push multi-architecture image
id: image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ vars.DOCKERHUB_USERNAME }}/trustdb:${{ needs.prepare.outputs.version }}
${{ vars.DOCKERHUB_USERNAME }}/trustdb:${{ needs.prepare.outputs.channel }}
ghcr.io/${{ github.repository_owner }}/trustdb:${{ needs.prepare.outputs.version }}
ghcr.io/${{ github.repository_owner }}/trustdb:${{ needs.prepare.outputs.channel }}
labels: |
org.opencontainers.image.version=${{ needs.prepare.outputs.version }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ needs.prepare.outputs.build_date }}
build-args: |
VERSION=${{ needs.prepare.outputs.version }}
VCS_REF=${{ github.sha }}
BUILD_DATE=${{ needs.prepare.outputs.build_date }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true
- name: Attest immutable container digest
id: container-attestation
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4
with:
subject-name: ghcr.io/${{ github.repository_owner }}/trustdb
subject-digest: ${{ steps.image.outputs.digest }}
push-to-registry: true
- name: Retain container digest and provenance
shell: bash
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
ATTESTATION_BUNDLE: ${{ steps.container-attestation.outputs.bundle-path }}
run: |
jq -n \
--arg schema "trustdb.container-digests.v1" \
--arg digest "$IMAGE_DIGEST" \
--arg docker "${{ vars.DOCKERHUB_USERNAME }}/trustdb" \
--arg ghcr "ghcr.io/${{ github.repository_owner }}/trustdb" \
--arg version "${{ needs.prepare.outputs.version }}" \
'{schema: $schema, digest: $digest, platforms: ["linux/amd64", "linux/arm64"], references: [($docker + ":" + $version), ($ghcr + ":" + $version)]}' \
> TRUSTDB_CONTAINER_DIGESTS.json
cp "$ATTESTATION_BUNDLE" trustdb-container-attestation.sigstore.json
- name: Upload container evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: container-evidence
path: |
TRUSTDB_CONTAINER_DIGESTS.json
trustdb-container-attestation.sigstore.json
if-no-files-found: error
release:
name: Publish GitHub Release
needs:
- prepare
- server
- desktop-macos
- desktop-windows
- docker
- reproducibility
- supply-chain
if: github.ref_type == 'tag'
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Download Server/CLI packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: server-*
path: release-assets
merge-multiple: true
- name: Download desktop packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: desktop-*
path: release-assets
merge-multiple: true
- name: Download retained supply-chain metadata
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: supply-chain-evidence
path: release-assets
- name: Download container evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: container-evidence
path: release-assets
- name: Generate release manifest and dual checksums
shell: bash
run: |
go run ./cmd/trustdb release manifest \
--dir release-assets \
--version "${{ needs.prepare.outputs.version }}" \
--commit "$GITHUB_SHA" \
--build-date "${{ needs.prepare.outputs.build_date }}" \
--policy supply-chain/production-inputs.json
- name: Sign release manifest provenance
id: release-attestation
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4
with:
subject-path: release-assets/TRUSTDB_RELEASE_MANIFEST.json
- name: Retain and self-check offline verification bundle
shell: bash
env:
ATTESTATION_BUNDLE: ${{ steps.release-attestation.outputs.bundle-path }}
run: |
cp "$ATTESTATION_BUNDLE" \
release-assets/trustdb-release-attestation.sigstore.json
gh attestation verify \
release-assets/TRUSTDB_RELEASE_MANIFEST.json \
--repo "$GITHUB_REPOSITORY" \
--signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" \
--source-digest "$GITHUB_SHA" \
--deny-self-hosted-runners \
--bundle release-assets/trustdb-release-attestation.sigstore.json
go run ./cmd/trustdb release verify --dir release-assets
- name: Publish GitHub release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
name: TrustDB v${{ needs.prepare.outputs.version }}
prerelease: ${{ contains(needs.prepare.outputs.version, '-') }}
generate_release_notes: true
fail_on_unmatched_files: true
files: release-assets/*