mirror of
https://github.com/borgbackup/borg.git
synced 2026-09-02 14:43:21 +02:00
The sdist provenance attestation is only stored in the GitHub attestations API, and fetching it from there requires a recent gh version and a GitHub token - both inconvenient for CI/CD jobs that verify the sdist before building borg from source (see the discussion in #10187). Attach the sigstore bundle produced by actions/attest-build-provenance to the release as borgbackup-<version>.tar.gz.sigstore.jsonl, so the sdist can be verified offline and without a GitHub token using generic sigstore tooling (cosign verify-blob / verify-blob-attestation, or gh attestation verify --bundle), and document that in the generated release notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
216 lines
8.7 KiB
YAML
216 lines
8.7 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.
|
|
#
|
|
# 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>\`.
|
|
|
|
The sdist's attestation is additionally attached as the [sigstore](https://www.sigstore.dev/)
|
|
bundle \`borgbackup-$TAG.tar.gz.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
|
|
\`\`\`
|
|
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
|