chore(release): make a release verifiable, and pin what builds it (epic #701, phase 5) (#759)

* chore(release): build once, and publish sha256sums and provenance (epic #701, phase 5)

* chore(ci): pin every third-party action by commit sha (epic #701, phase 5)

* test(ci): anchor the dependabot guard on the ecosystem it must cover
This commit is contained in:
Sergey
2026-08-31 14:32:20 -07:00
committed by GitHub
parent 9816fe2d16
commit 0ba029b015
12 changed files with 480 additions and 90 deletions
+14
View File
@@ -0,0 +1,14 @@
version: 2
updates:
# Actions are pinned by commit SHA (tests/test_workflow_action_pins.py), which
# is only safe if something moves the pins: an action nobody bumps stops
# receiving its own security fixes, and the pin hides that it has gone stale.
# Grouped so this arrives as one pull request a week rather than fifteen.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
github-actions:
patterns:
- '*'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'VocaHQ/vocalinux'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Install basic dependencies
run: |
+2 -2
View File
@@ -51,10 +51,10 @@ jobs:
options: --privileged
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Build and bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6
with:
bundle: vocalinux-${{ matrix.arch }}.flatpak
manifest-path: packaging/flatpak/com.vocalinux.Vocalinux.yml
+4
View File
@@ -13,4 +13,8 @@ permissions:
jobs:
welcome:
# Deliberately unpinned: this is our own org's shared workflow, inside the
# same trust boundary as this repository, and the point of it is that a
# change to the welcome text reaches every VocaHQ repo without 30 bumps.
# Everything third-party is pinned by SHA (tests/test_workflow_action_pins.py).
uses: VocaHQ/.github/.github/workflows/issue-welcome.yml@main
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Label PR
uses: actions/labeler@v6
uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true
+7 -7
View File
@@ -18,13 +18,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: main
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
cache: 'pip'
@@ -67,7 +67,7 @@ jobs:
# glslc is built from the pinned shaderc commit inside the container -
# Ubuntu 22.04 packages none - and depends on nothing but that commit.
- name: Cache AppImage build tools
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/vocalinux-appimage
key: appimage-tools-${{ runner.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }}
@@ -232,7 +232,7 @@ jobs:
echo "tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
- name: Upload artifacts (backup)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: vocalinux-nightly-${{ steps.version.outputs.version }}
path: dist/
@@ -246,12 +246,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: main
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
cache: 'pip'
@@ -278,7 +278,7 @@ jobs:
# glslc is built from the pinned shaderc commit inside the container -
# Ubuntu 22.04 packages none - and depends on nothing but that commit.
- name: Cache AppImage build tools
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/vocalinux-appimage
key: appimage-tools-${{ runner.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }}
+140 -50
View File
@@ -5,10 +5,11 @@ on:
tags:
- 'v*'
# Least privilege, granted per job. `id-token: write` at the top level handed an
# OIDC token to every job in the workflow, including publish-aur, which runs a
# third-party action with our AUR signing key.
permissions:
contents: write
packages: write
id-token: write # Required for PyPI trusted publishing
contents: read
env:
# Build against the supported floor (requires-python in pyproject.toml).
@@ -18,14 +19,16 @@ jobs:
build-and-release:
name: Build and Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -33,7 +36,7 @@ jobs:
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine wheel setuptools
pip install build wheel setuptools
- name: Extract version from tag
id: get_version
@@ -54,16 +57,37 @@ jobs:
echo "Warning: Version mismatch between tag ($TAG_VERSION) and package ($PACKAGE_VERSION)"
fi
# Archive timestamps are what made two builds of one tag differ. Pinned to
# the tagged commit, re-running this workflow reproduces the bytes we
# published, so a checksum stays true for the life of the release.
- name: Pin the build timestamp to the tagged commit
run: |
echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV"
# The only `python -m build` in this workflow. PyPI, the aarch64 AppImage
# and the checksums all consume the artifact below, so the wheel on PyPI
# is the wheel on the release. It used to be built three times, on three
# runners, and nothing guaranteed the three agreed.
- name: Build package
run: |
python -m build
echo "Built packages:"
ls -la dist/
- name: Upload the built distributions
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: python-dist
path: |
dist/*.whl
dist/*.tar.gz
if-no-files-found: error
retention-days: 7
# glslc is built from the pinned shaderc commit inside the container -
# Ubuntu 22.04 packages none - and depends on nothing but that commit.
- name: Cache AppImage build tools
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/vocalinux-appimage
key: appimage-tools-${{ runner.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }}
@@ -75,9 +99,17 @@ jobs:
bash packaging/appimage/docker-build.sh dist/*.whl "${{ steps.get_version.outputs.VERSION }}" dist
ls -la dist/*.AppImage
- name: Upload the x86_64 AppImage
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: appimage-x86_64
path: dist/*.AppImage
if-no-files-found: error
retention-days: 7
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
files: |
dist/*.whl
@@ -129,6 +161,19 @@ jobs:
```
Still requires `xdotool`/`wtype`/`ydotool` on the host for text injection, same as the PyPI path.
### Verifying what you downloaded
`SHA256SUMS` covers every file here and is attached once the aarch64 build finishes,
a few minutes after this release appears. The wheel and sdist below are the same bytes
published to PyPI.
```bash
sha256sum -c --ignore-missing SHA256SUMS
```
Each artifact also carries GitHub build provenance, which ties it to this workflow run:
```bash
gh attestation verify Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.AppImage \
--repo ${{ github.repository }}
```
---
See [CHANGELOG](https://github.com/VocaHQ/vocalinux/releases) for detailed changes.
@@ -136,20 +181,11 @@ jobs:
name: Build and Attach AppImage (aarch64)
needs: build-and-release
runs-on: ubuntu-24.04-arm
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build wheel setuptools
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Extract version from tag
id: get_version
@@ -157,15 +193,19 @@ jobs:
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
- name: Build package
run: |
python -m build
ls -la dist/
# The wheel is py3-none-any, so this is the wheel the release and PyPI
# carry. Building it again here made a third copy of the same version that
# no published checksum could describe.
- name: Download the built distributions
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-dist
path: dist
# glslc is built from the pinned shaderc commit inside the container -
# Ubuntu 22.04 packages none - and depends on nothing but that commit.
- name: Cache AppImage build tools
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/vocalinux-appimage
key: appimage-tools-${{ runner.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }}
@@ -177,6 +217,14 @@ jobs:
bash packaging/appimage/docker-build.sh dist/*.whl "${{ steps.get_version.outputs.VERSION }}" dist
ls -la dist/*.AppImage
- name: Upload the aarch64 AppImage
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: appimage-aarch64
path: dist/*.AppImage
if-no-files-found: error
retention-days: 7
- name: Attach to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -186,49 +234,89 @@ jobs:
--repo ${{ github.repository }} \
--clobber
# Runs last because the aarch64 AppImage lands after the release is created:
# a checksum manifest that covers three of the four files is worse than none,
# since the missing one looks like the tampered one.
publish-checksums:
name: Checksums and Provenance
needs: [build-and-release, build-appimage-arm64]
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Collect every artifact this run published
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
merge-multiple: true
path: dist
- name: Generate SHA256SUMS
working-directory: dist
run: |
set -euo pipefail
ls -la
# Bare names, so `sha256sum -c` works in whatever directory the user
# downloaded the release into.
sha256sum -- *.whl *.tar.gz *.AppImage | tee SHA256SUMS
# Attesting from the manifest rather than a second glob: the file we
# publish and the set we attest cannot drift apart.
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
with:
subject-checksums: dist/SHA256SUMS
- name: Attach SHA256SUMS to the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ github.ref_name }}" \
dist/SHA256SUMS \
--repo ${{ github.repository }} \
--clobber
publish-pypi:
name: Publish to PyPI
needs: build-and-release
runs-on: ubuntu-latest
permissions:
id-token: write # PyPI trusted publishing; there is no API token to leak
environment:
name: pypi
url: https://pypi.org/project/vocalinux/
steps:
- name: Checkout code
uses: actions/checkout@v5
# Not a rebuild: the artifact attached to the GitHub release, so one
# checksum describes both.
- name: Download the built distributions
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-dist
path: dist
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: |
python -m build
echo "Built packages:"
ls -la dist/
- name: Verify package
run: |
python -m pip install --upgrade pip
pip install twine
twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/* --skip-existing
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
skip-existing: true
publish-aur:
name: Publish to AUR
needs: build-and-release
runs-on: ubuntu-latest
permissions:
contents: read
# Secrets: AUR_SSH_PRIVATE_KEY, AUR_USERNAME, AUR_EMAIL — see docs/AUR.md
steps:
- name: Check for AUR credentials
@@ -245,7 +333,7 @@ jobs:
- name: Checkout code
if: steps.aur.outputs.configured == 'true'
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Bump PKGBUILD version
if: steps.aur.outputs.configured == 'true'
@@ -258,7 +346,7 @@ jobs:
- name: Publish AUR package
if: steps.aur.outputs.configured == 'true'
uses: KSXGitHub/github-actions-deploy-aur@v4.2.0
uses: KSXGitHub/github-actions-deploy-aur@084b0d9b15415bf9cdb65d44dad1efe37a354050 # v4.2.0
with:
pkgname: vocalinux
pkgbuild: packaging/aur/vocalinux/PKGBUILD
@@ -272,12 +360,14 @@ jobs:
name: Deploy Website
runs-on: ubuntu-latest
needs: [build-and-release, publish-pypi]
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
cache: 'npm'
@@ -319,7 +409,7 @@ jobs:
npm run deploy
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./web/out
+21 -21
View File
@@ -25,8 +25,8 @@ jobs:
python: ${{ steps.filter.outputs.python }}
deploy_website: ${{ steps.deploy_check.outputs.deploy }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: dorny/paths-filter@0e4a8c6effa4802afeda77dc8d303f8176d7dfad # v3
id: filter
with:
filters: |
@@ -79,10 +79,10 @@ jobs:
needs: changes
if: needs.changes.outputs.python == 'true'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Set up uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
enable-cache: true
@@ -116,10 +116,10 @@ jobs:
python-version: ['3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Set up uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@@ -162,14 +162,14 @@ jobs:
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-${{ matrix.python-version }}
path: junit-${{ matrix.python-version }}.xml
- name: Upload coverage to Codecov
if: matrix.python-version == '3.13'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -182,10 +182,10 @@ jobs:
needs: [changes, python-lint]
if: needs.changes.outputs.python == 'true'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Set up uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
# Caches uv's download cache, not .venv/, so the checkout below stays
# the fresh clone this job exists to exercise.
@@ -231,10 +231,10 @@ jobs:
- arch: aarch64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
cache: 'pip'
@@ -248,7 +248,7 @@ jobs:
# glslc is built from the pinned shaderc commit inside the container -
# Ubuntu 22.04 packages none - and depends on nothing but that commit.
- name: Cache AppImage build tools
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/vocalinux-appimage
key: appimage-tools-${{ matrix.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }}
@@ -263,7 +263,7 @@ jobs:
ls -la dist/*.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: vocalinux-appimage-${{ matrix.arch }}
path: dist/*.AppImage
@@ -289,10 +289,10 @@ jobs:
- archlinux:latest
- opensuse/tumbleweed
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Download the AppImage built above
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: vocalinux-appimage-x86_64
path: dist
@@ -313,10 +313,10 @@ jobs:
needs: changes
if: needs.changes.outputs.deploy_website == 'true'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
cache: 'npm'
@@ -371,10 +371,10 @@ jobs:
needs.changes.outputs.deploy_website == 'true' &&
needs.web-build.result == 'success'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '24'
cache: 'npm'
@@ -387,7 +387,7 @@ jobs:
npm run deploy
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./web/out