diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0bd7f96..a3c559e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,14 +4,19 @@ on: push: tags: - 'v*.*.*' + # A change to this file runs the build and SBOM 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: 'Version to release (e.g., v0.1.0)' + description: 'Existing tag to release (e.g., v0.1.0). The build checks out this tag.' required: true permissions: - contents: write - id-token: write + contents: read jobs: build: name: Build Package @@ -21,15 +26,32 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] julia-version: ['1.10'] + defaults: + run: + shell: bash 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + with: + ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }} - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 with: version: ${{ matrix.julia-version }} - uses: julia-actions/cache@e32267b41e0fa7df82a3d53e8c7273620d52c586 # v3.4.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 + # AcceleratorGate is 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.build(); Pkg.test()' + run: julia --project -e 'using Pkg; Pkg.build(); Pkg.test()' - name: Package artifacts run: | mkdir -p dist @@ -46,15 +68,32 @@ jobs: needs: build steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + with: + ref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }} - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 with: version: '1.10' + # Pkg.dependencies() needs a resolved environment. 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: | + rm -f Manifest.toml + julia -e 'using Pkg; Pkg.Registry.add("General"); Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/hyperpolymath/julia-professional-registry"))' julia --project -e ' - using Pkg, JSON + using Pkg + Pkg.instantiate() pkg_info = Pkg.project() - deps = Pkg.dependencies() + 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", @@ -66,13 +105,7 @@ jobs: "version" => string(pkg_info.version) ) ), - "components" => [ - Dict( - "type" => "library", - "name" => string(uuid), - "version" => string(info.version) - ) for (uuid, info) in deps if info.is_direct_dep - ] + "components" => components ) open("sbom.json", "w") do f JSON.print(f, sbom, 2) @@ -85,6 +118,7 @@ jobs: path: sbom.json release: name: Create Release + if: github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 15 needs: [build, sbom] @@ -93,7 +127,28 @@ jobs: id-token: write attestations: write steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + # Refuse anything but an existing vX.Y.Z 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#refs/tags/}" + fi + if ! printf '%s\n' "$version" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::the version to release is not of the form vX.Y.Z" + 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 + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -103,19 +158,11 @@ jobs: mkdir -p release find release-artifacts -type f -exec cp {} release/ \; cd release - sha256sum * > checksums.txt + sha256sum -- * > checksums.txt - name: Attest build provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: 'release/*' - - name: Extract version - id: version - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - else - echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - fi - name: Create GitHub Release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v2 with: @@ -133,6 +180,7 @@ jobs: ```bash sha256sum -c checksums.txt + gh attestation verify -R hyperpolymath/SMTLib.jl ``` files: release/* draft: false