From cc337d0d3444f8edd54367060c9c9dac69cd0b26 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:24:19 +0100 Subject: [PATCH 1/2] fix(ci): replace the scaffold release workflow with a working one The release workflow was the unfilled template: the build step echoed "Build your artifacts here", the SLSA provenance job attested an empty subject list, and git-cliff was fetched from releases/latest by a URL that no longer resolves. So tag v0.1.1 has no GitHub Release. The build job now tests the package on Julia 1.10 against General and the hyperpolymath registry (where Causals, ZeroProb, Cliodynamics and AcceleratorGate are registered), resolving fresh from [compat] with any committed manifest removed. It packages the source and a CycloneDX SBOM. The changelog uses orhun/git-cliff-action v4.9.1 with git-cliff pinned to v2.14.2. Provenance comes from actions/attest-build-provenance v4.2.2 on the release files, replacing the SLSA generator, whose nested actions cannot be pinned by SHA. A manual run takes an existing tag and checks it out, so the artefacts match the tagged tree. The version is passed through env, must match vX.Y.Z or vX.Y.Z-pre, and must already exist as a tag. A pull_request trigger on this file runs the build and changelog jobs at review; the release job is skipped there. The unused full-CHANGELOG.md artifact is dropped: nothing consumed it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016XJoh8YA2sCWQmq93CF8RX --- .github/workflows/release.yml | 221 ++++++++++++++++++++++------------ 1 file changed, 142 insertions(+), 79 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e83dabf..cce8342 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,166 @@ 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. On other events + # inputs.version is empty and checkout uses the triggering ref. - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ 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 }} 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/* From 238d922e711577f97b67ee613ee91525432a4351 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:57:48 +0100 Subject: [PATCH 2/2] fix(ci): check out refs/tags/ on a manual release run actions/checkout resolves an unqualified ref to a branch of that name before a tag (src/ref-helper.ts:52-57 at the pinned SHA). If a branch named like the tag existed, a manual run would package that branch, while the release job only confirms that the tag exists. Both checkouts now pass refs/tags/ on a manual run, and an empty ref (the triggering ref) on push and pull_request runs, as before. Raised by CodeRabbit on hyperpolymath/Causals.jl#69. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016XJoh8YA2sCWQmq93CF8RX --- .github/workflows/release.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cce8342..ebae880 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,11 +35,13 @@ jobs: 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. On other events - # inputs.version is empty and checkout uses the triggering ref. + # 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 }} + ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }} - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 with: @@ -117,7 +119,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ inputs.version }} + ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }} fetch-depth: 0 - name: Generate changelog for this release