mirror of
https://github.com/borgbackup/borg.git
synced 2026-09-01 14:13:19 +02:00
a6970c7e7 did this for the sdist: its provenance attestation is only
stored in the GitHub attestations API, and fetching it from there needs
a recent gh and a GitHub token, so the sigstore bundle is attached to
the release as <asset>.sigstore.jsonl for offline verification.
Do the same for the standalone binaries: the jobs that build them attest
them, so they are also the ones that have the bundle. Each of them stores
it under the name of every asset it attested (one attestation covers both
the single-file binary and the .tgz) and uploads it in the artifact that
carries the binaries, so the release job attaches it to the release along
with them - it uploads whatever is in the artifacts.
vm_tests attested after uploading its artifact, so the attestation is now
made before the upload, like in the other two jobs.
Also document the offline verification in the binaries README (which is a
release asset itself) and in the release checklist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
226 lines
9.3 KiB
YAML
226 lines
9.3 KiB
YAML
# Release automation: build the source distribution, publish it to PyPI and
|
|
# create a GitHub release with the standalone binaries.
|
|
#
|
|
# This is called by ci.yml when a tag is pushed, and it deliberately runs as
|
|
# part of that same workflow run: that is where the binaries for the tag are
|
|
# built, and artifacts can only be downloaded within the run that created them.
|
|
#
|
|
# The GitHub release is created as a *draft* on purpose:
|
|
# - the release notes want a human,
|
|
# - the detached GPG signature of the sdist can only be made locally
|
|
# (scripts/sdist-sign), so it has to be added by hand,
|
|
# - and the binaries should be tried out before the release becomes visible.
|
|
# Publishing the draft is a single click in the GitHub UI.
|
|
#
|
|
# Every release asset has a build provenance attestation, and the sigstore bundle
|
|
# of each attestation is attached to the release as <asset>.sigstore.jsonl, for
|
|
# offline verification. The sdist is attested here, the binaries by the jobs in
|
|
# ci.yml that build them - those hand their bundles over in the artifact that
|
|
# carries the binaries.
|
|
#
|
|
# The upload to PyPI is *not* done here, but by the "pypi" job in ci.yml: PyPI
|
|
# trusted publishing can not be used from a reusable workflow, see
|
|
# https://docs.pypi.org/trusted-publishers/troubleshooting/ - so that job has to
|
|
# live in a workflow that is triggered by an event. This job hands the sdist
|
|
# over to it as a workflow artifact.
|
|
|
|
name: Release
|
|
|
|
on:
|
|
workflow_call:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
# The standalone binaries expected for a release: for every platform the
|
|
# single-file binary and, as a .tgz, the single-directory variant. See the
|
|
# "binary" entries of the native_tests matrix, the "artifact_prefix" entries
|
|
# of the vm_tests matrix and the windows_tests job in ci.yml - keep in sync.
|
|
EXPECTED_ASSETS: >-
|
|
borg-linux-glibc243-x86_64-gh
|
|
borg-linux-glibc243-x86_64-gh.tgz
|
|
borg-linux-glibc243-arm64-gh
|
|
borg-linux-glibc243-arm64-gh.tgz
|
|
borg-macos-15-arm64-gh
|
|
borg-macos-15-arm64-gh.tgz
|
|
borg-macos-15-x86_64-gh
|
|
borg-macos-15-x86_64-gh.tgz
|
|
borg-freebsd-15-x86_64-gh
|
|
borg-freebsd-15-x86_64-gh.tgz
|
|
borg-windows-x86_64-gh.exe
|
|
borg-windows-x86_64-gh.tgz
|
|
|
|
jobs:
|
|
github_release:
|
|
name: Draft the GitHub release
|
|
runs-on: ubuntu-26.04
|
|
timeout-minutes: 30
|
|
|
|
permissions:
|
|
contents: write # to create the release
|
|
id-token: write # to attest the sdist
|
|
attestations: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
# Just fetching one commit is not enough for setuptools-scm, so we fetch all.
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
with:
|
|
python-version: '3.13'
|
|
|
|
- name: Install Linux packages
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y pkg-config build-essential
|
|
sudo apt-get install -y libssl-dev libacl1-dev liblz4-dev
|
|
|
|
- name: Build the sdist
|
|
run: |
|
|
set -euxo pipefail
|
|
python -m pip install --upgrade pip build twine
|
|
# only a sdist: we do not publish wheels, they would be platform specific.
|
|
python -m build --sdist
|
|
twine check dist/*
|
|
ls -l dist/
|
|
|
|
- name: Check that the sdist has the version of the tag
|
|
# If the checkout is modified, setuptools-scm does not use the tag as the
|
|
# version, but silently guesses the next one and appends .devN, see
|
|
# #10199. Such a release must not be uploaded to PyPI.
|
|
env:
|
|
TAG: ${{ github.ref_name }}
|
|
run: |
|
|
set -euxo pipefail
|
|
python - "$TAG" dist/borgbackup-*.tar.gz <<'EOF'
|
|
import sys
|
|
from packaging.utils import parse_sdist_filename
|
|
from packaging.version import Version
|
|
tag, sdist = sys.argv[1], sys.argv[2]
|
|
_, version = parse_sdist_filename(sdist.rpartition("/")[2])
|
|
if version != Version(tag):
|
|
raise SystemExit(f"sdist version {version} is not the tag version {tag} - modified checkout?")
|
|
print(f"sdist version {version} is the tag version.")
|
|
EOF
|
|
|
|
- name: Check that the sdist is complete and installable
|
|
# A release that cannot be installed from PyPI is the worst kind of
|
|
# release, and nothing else in CI ever installs borg from a sdist or
|
|
# without the development requirements.
|
|
run: |
|
|
set -euxo pipefail
|
|
python -m venv "$RUNNER_TEMP/venv-sdist"
|
|
"$RUNNER_TEMP/venv-sdist/bin/pip" install --upgrade pip
|
|
"$RUNNER_TEMP/venv-sdist/bin/pip" install dist/borgbackup-*.tar.gz
|
|
"$RUNNER_TEMP/venv-sdist/bin/borg" --version
|
|
"$RUNNER_TEMP/venv-sdist/bin/borg" --help > /dev/null
|
|
|
|
- name: Attest the sdist provenance
|
|
id: attest-sdist
|
|
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
with:
|
|
subject-path: 'dist/*.tar.gz'
|
|
|
|
- name: Name the sigstore bundle after the sdist
|
|
# The attestation is also stored in the GitHub attestations API, but
|
|
# fetching it from there needs a recent gh and a GitHub token (see
|
|
# #10187) - so attach the sigstore bundle to the release as
|
|
# <sdist>.sigstore.jsonl, which allows offline/token-less verification
|
|
# with generic sigstore tooling (cosign, gh attestation verify --bundle).
|
|
env:
|
|
BUNDLE_PATH: ${{ steps.attest-sdist.outputs.bundle-path }}
|
|
run: |
|
|
set -euxo pipefail
|
|
sdist=$(basename dist/borgbackup-*.tar.gz)
|
|
cp "$BUNDLE_PATH" "dist/$sdist.sigstore.jsonl"
|
|
|
|
- name: Download the binaries built for this tag
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: assets
|
|
# the binary artifacts are all named like the binaries they contain
|
|
pattern: 'borg-*-gh'
|
|
merge-multiple: true
|
|
|
|
- name: Check that no binary is missing
|
|
run: |
|
|
set -uo pipefail
|
|
ls -l assets/ || true
|
|
missing=""
|
|
for asset in $EXPECTED_ASSETS; do
|
|
test -f "assets/$asset" || missing="$missing $asset"
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
# not an error: vm_tests is continue-on-error, so e.g. a flaky FreeBSD
|
|
# VM should not stop the release - but do not lose it silently either.
|
|
echo "::warning::binaries missing from this release:$missing"
|
|
fi
|
|
|
|
- name: Create the draft release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ github.ref_name }}
|
|
run: |
|
|
set -euxo pipefail
|
|
# 2.0.0b23 and friends are pre-releases, 2.1.0 is not.
|
|
prerelease=""
|
|
case "$TAG" in *a*|*b*|*rc*) prerelease="--prerelease" ;; esac
|
|
cat > release-notes.md <<EOF
|
|
See the [changelog](https://github.com/borgbackup/borg/blob/$TAG/CHANGES.rst) for what changed in this release.
|
|
|
|
### Installation
|
|
|
|
\`pip install borgbackup==$TAG\`, or use one of the standalone binaries below - they
|
|
contain everything they need and just have to be made executable.
|
|
|
|
For each platform there is a single-file binary (\`borg-*-gh\`, \`.exe\` on Windows)
|
|
and, as a \`.tgz\`, the same thing as a directory (\`borg-dir/borg.exe\`), which
|
|
starts up faster.
|
|
|
|
The macOS binaries are built **without** FUSE support, so \`borg mount\` does
|
|
not work with them; install via \`pip\` instead if you need FUSE support.
|
|
|
|
All release assets have a [build provenance attestation](https://github.com/borgbackup/borg/attestations),
|
|
verifiable with \`gh attestation verify --owner borgbackup <file>\`.
|
|
|
|
Every attestation is additionally attached as a [sigstore](https://www.sigstore.dev/)
|
|
bundle \`<asset>.sigstore.jsonl\`, so it can be verified offline and without a
|
|
GitHub token, e.g. with cosign:
|
|
|
|
\`\`\`
|
|
cosign verify-blob borgbackup-$TAG.tar.gz \\
|
|
--bundle borgbackup-$TAG.tar.gz.sigstore.jsonl \\
|
|
--certificate-identity https://github.com/borgbackup/borg/.github/workflows/release.yml@refs/tags/$TAG \\
|
|
--certificate-oidc-issuer https://token.actions.githubusercontent.com
|
|
\`\`\`
|
|
|
|
The binaries are built by \`ci.yml\`, not by \`release.yml\`, so their
|
|
\`--certificate-identity\` is
|
|
\`https://github.com/borgbackup/borg/.github/workflows/ci.yml@refs/tags/$TAG\`.
|
|
EOF
|
|
mkdir -p assets # there may not have been any artifact to download
|
|
if gh release view "$TAG" > /dev/null 2>&1; then
|
|
# a re-run of this job: keep the (possibly already edited) release and
|
|
# just replace its assets.
|
|
gh release upload "$TAG" --clobber dist/*.tar.gz dist/*.sigstore.jsonl $(find assets -type f | sort)
|
|
else
|
|
gh release create "$TAG" \
|
|
--draft $prerelease \
|
|
--title "borg $TAG" \
|
|
--notes-file release-notes.md \
|
|
dist/*.tar.gz dist/*.sigstore.jsonl $(find assets -type f | sort)
|
|
fi
|
|
gh release view "$TAG" --json isDraft,isPrerelease,assets
|
|
|
|
- name: Keep the sdist for the PyPI upload
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: sdist
|
|
path: dist/*.tar.gz
|
|
if-no-files-found: error
|