diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e83dabf..ebae880 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,27 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# Release workflow — triggered by version tags (v*). -# Builds artifacts, generates changelog via git-cliff, creates a GitHub Release, -# and produces SLSA provenance attestations. +# Release workflow — triggered by version tags (v*), or run by hand for an +# existing tag. Tests the package, packages its source with a CycloneDX SBOM, +# generates the changelog via git-cliff, attests build provenance and creates +# a GitHub Release. name: Release on: push: tags: - 'v*' + # A change to this file runs the build and changelog jobs on its PR, so a + # broken release path shows up at review rather than at the next tag. The + # release job is skipped on PRs. + pull_request: + paths: + - '.github/workflows/release.yml' + workflow_dispatch: + inputs: + version: + description: 'Existing tag to release (e.g., v0.1.1). The build checks out this tag.' + required: true permissions: contents: read @@ -18,115 +30,168 @@ jobs: build: name: Build Artifacts runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read + # A cold install of the dependency closure takes more than 15 minutes on + # the hosted runners; 45 matches ci.yml. + timeout-minutes: 45 steps: + # On a manual run, build the tag being released, not the default branch, + # so the artefacts match the tagged and registered tree. The ref is + # qualified as refs/tags/: checkout resolves a bare name to a branch of + # that name before a tag (src/ref-helper.ts). On other events + # inputs.version is empty, ref is empty, and checkout uses the triggering ref. - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }} + + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 + with: + version: '1.10' + + - uses: julia-actions/cache@9353db09a0f2bbbdec6f41791918c75d37391b36 # v3.5.0 + + # A library release is tested the way users install it: resolved fresh + # from Project.toml [compat]. + - name: Ignore any committed manifest + run: rm -f Manifest.toml + + # Causals, ZeroProb, Cliodynamics and their dependency AcceleratorGate + # are registered in the hyperpolymath registry, not in General. + - name: Add registries + run: julia -e 'using Pkg; Pkg.Registry.add("General"); Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/hyperpolymath/julia-professional-registry"))' + + - name: Build and test + run: julia --project -e 'using Pkg; Pkg.instantiate(); Pkg.build(); Pkg.test()' - - name: Build + # Pkg.dependencies() needs the resolved environment from the step above. + # JSON is not a dependency of this package, so it is loaded from a + # throwaway environment after the component list has been read. + - name: Generate SBOM run: | - echo "Build your artifacts here" - # TODO: Replace with your build commands - # Examples: - # cargo build --release - # zig build -Doptimize=ReleaseFast - # gleam build - # mix release - - # TODO: Upload build artifacts if needed - # - uses: actions/upload-artifact@ea165f8d65b6db9a8b71b5c2d1a090c0daf9c8bb # v4 - # with: - # name: release-artifacts - # path: target/release/ + julia --project -e ' + using Pkg + pkg_info = Pkg.project() + components = [ + Dict( + "type" => "library", + "name" => info.name, + "version" => info.version === nothing ? "stdlib" : string(info.version) + ) for (uuid, info) in Pkg.dependencies() if info.is_direct_dep + ] + Pkg.activate(temp=true) + Pkg.add("JSON") + using JSON + sbom = Dict( + "bomFormat" => "CycloneDX", + "specVersion" => "1.4", + "version" => 1, + "metadata" => Dict( + "component" => Dict( + "type" => "library", + "name" => pkg_info.name, + "version" => string(pkg_info.version) + ) + ), + "components" => components + ) + open("sbom.json", "w") do f + JSON.print(f, sbom, 2) + end + ' + + - name: Package artifacts + run: | + mkdir -p dist + tar -czf dist/InvestigativeJournalism.jl-src.tar.gz src/ Project.toml README.adoc LICENSE + mv sbom.json dist/ + + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-artifacts + path: dist/ changelog: name: Generate Changelog runs-on: ubuntu-latest timeout-minutes: 15 - permissions: - contents: read outputs: changelog: ${{ steps.cliff.outputs.content }} - version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: + ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }} fetch-depth: 0 - - name: Extract version from tag - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - - - name: Install git-cliff - run: | - curl -sSfL https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-$(uname -m)-unknown-linux-gnu.tar.gz \ - | tar -xz --strip-components=1 -C /usr/local/bin/ git-cliff-*/git-cliff - - name: Generate changelog for this release id: cliff - run: | - # Generate changelog for the current tag only - CHANGELOG=$(git cliff --latest --strip header) - # Write to output using delimiter to handle multiline - { - echo "content<> "$GITHUB_OUTPUT" - - - name: Update full CHANGELOG.md - run: | - git cliff --output CHANGELOG.md - - - name: Upload updated CHANGELOG.md - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1 with: - name: changelog - path: CHANGELOG.md - retention-days: 5 + version: v2.14.2 + config: cliff.toml + args: --latest --strip header release: name: Create GitHub Release + if: github.event_name != 'pull_request' needs: [build, changelog] runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write + id-token: write + attestations: write steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + # Refuse anything but an existing vX.Y.Z[-pre] tag, so a mistyped manual + # run cannot make action-gh-release create a new tag on the default branch. + - name: Extract version + id: version + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + version="$INPUT_VERSION" + else + version="$GITHUB_REF_NAME" + fi + if ! printf '%s\n' "$version" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then + echo "::error::the version to release is not of the form vX.Y.Z or vX.Y.Z-pre" + exit 1 + fi + if ! git ls-remote --exit-code --tags "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" "refs/tags/$version" > /dev/null; then + echo "::error::tag $version does not exist; push the tag first" + exit 1 + fi + case "$version" in + *-*) prerelease=true ;; + *) prerelease=false ;; + esac + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-artifacts + path: release/ - # TODO: Download build artifacts if uploading to the release - # - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - # with: - # name: release-artifacts - # path: artifacts/ + - name: Checksums + run: | + cd release + sha256sum -- * > checksums.txt + + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: 'release/*' - name: Create GitHub Release uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: + tag_name: ${{ steps.version.outputs.version }} + name: InvestigativeJournalism.jl ${{ steps.version.outputs.version }} body: ${{ needs.changelog.outputs.changelog }} draft: false - prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} + prerelease: ${{ steps.version.outputs.prerelease }} generate_release_notes: false - # TODO: Add artifact files to the release - # files: | - # artifacts/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - provenance: - name: SLSA Provenance - needs: [build] - permissions: - actions: read - id-token: write - contents: write - # SLSA generator must run in a separate, isolated workflow - # See: https://slsa.dev/spec/v1.0/requirements#build-l3 - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 - with: - base64-subjects: "" - # TODO: Replace with actual artifact hashes - # Generate with: sha256sum artifact | base64 -w0 - # base64-subjects: "${{ needs.build.outputs.hashes }}" + files: release/*