From a9e889e16154e1fe8826f3c2ee13fc8fa8b4bde6 Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Sun, 31 May 2026 09:37:40 -0700 Subject: [PATCH] Build release artifacts and SLSA provenance in CI Adds .github/workflows/release.yml, triggered on `release: created`, which: - checks out the release's target_commitish, - builds the source zip via CMake/CPack (`package_source`), - uploads the zip to the draft release, - calls slsa-framework/slsa-github-generator to produce a SLSA v1.0 *.intoto.jsonl provenance file and attach it to the same draft release. After CI completes, the draft has both the zip and the provenance attached, and the maintainer reviews and publishes as before. Updates support/release.py to stop building and uploading the zip locally; that work has moved to CI so the SLSA provenance attests to the actual build environment that produced the artifact, not to a hash observed after the fact. The script still builds docs locally because the subsequent mkdocs deploy step depends on them. --- .github/workflows/release.yml | 69 +++++++++++++++++++++++++++++++++++ support/release.py | 25 ++++--------- 2 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f10d8577 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +# Builds the release source package in CI when a draft release is created +# (typically via support/release.py), uploads the zip to that release, and +# attaches a SLSA v1.0 provenance attestation generated by the OpenSSF +# slsa-github-generator. The maintainer reviews the draft (which by then has +# both the zip and *.intoto.jsonl attached) and clicks Publish to finalize. +# +# This makes the provenance attest to the actual build that produced the +# artifact, rather than just attesting to a hash observed after the fact. + +name: release + +on: + release: + types: [created] + +permissions: read-all + +jobs: + build: + name: Build source package + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + package: ${{ steps.build.outputs.package }} + steps: + - name: Checkout the release ref + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + ref: ${{ github.event.release.target_commitish }} + persist-credentials: false + + - name: Build source zip via CPack + id: build + run: | + cmake -B build . + cmake --build build --target package_source + pkg=$(ls build/fmt-*.zip) + test -f "$pkg" + echo "package=$pkg" >> "$GITHUB_OUTPUT" + + - name: Compute base64-encoded SHA-256 subjects + id: hash + run: | + file="${{ steps.build.outputs.package }}" + subjects=$(cd "$(dirname "$file")" && sha256sum "$(basename "$file")") + echo "hashes=$(printf '%s' "$subjects" | base64 -w0)" >> "$GITHUB_OUTPUT" + + - name: Upload zip to the release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + "${{ steps.build.outputs.package }}" \ + --repo "${{ github.repository }}" --clobber + + provenance: + needs: [build] + permissions: + actions: read + id-token: write + contents: write + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + with: + base64-subjects: ${{ needs.build.outputs.hashes }} + provenance-name: "fmt-${{ github.event.release.tag_name }}.intoto.jsonl" + upload-assets: true + upload-tag-name: ${{ github.event.release.tag_name }} diff --git a/support/release.py b/support/release.py index 26de7f4f..caabf431 100755 --- a/support/release.py +++ b/support/release.py @@ -151,12 +151,17 @@ if __name__ == '__main__': fmt_repo.add(changelog) fmt_repo.commit('-m', 'Update version') - # Build the docs and package. + # Build the docs locally; the source zip is now built and attached to the + # release in CI by .github/workflows/release.yml, which also generates a + # SLSA provenance attestation for it. run = Runner(fmt_repo.dir) run('cmake', '.') - run('make', 'doc', 'package_source') + run('make', 'doc') - # Create a release on GitHub. + # Create a draft release on GitHub. The release workflow triggers on + # `release: created`, builds the source zip from `target_commitish`, and + # attaches the zip plus *.intoto.jsonl provenance to this draft. After + # reviewing the draft, the maintainer clicks Publish to finalize. fmt_repo.push('origin', 'release') auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')} req = urllib.request.Request( @@ -169,20 +174,6 @@ if __name__ == '__main__': if response.status != 201: raise Exception(f'Failed to create a release ' + '{response.status} {response.reason}') - response_data = json.loads(response.read().decode('utf-8')) - id = response_data['id'] - - # Upload the package. - uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases' - package = 'fmt-{}.zip'.format(version) - req = urllib.request.Request( - f'{uploads_url}/{id}/assets?name={package}', - headers={'Content-Type': 'application/zip'} | auth_headers, - data=open('build/fmt/' + package, 'rb').read(), method='POST') - with urllib.request.urlopen(req) as response: - if response.status != 201: - raise Exception(f'Failed to upload an asset ' - '{response.status} {response.reason}') short_version = '.'.join(version.split('.')[:-1]) check_call(['./mkdocs', 'deploy', short_version])