diff --git a/.github/workflows/release-acceptance.yml b/.github/workflows/release-acceptance.yml new file mode 100644 index 000000000..049bb2866 --- /dev/null +++ b/.github/workflows/release-acceptance.yml @@ -0,0 +1,91 @@ +name: Release acceptance + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +jobs: + build: + name: Build tagged release evidence + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Verify exact checked-out revision + env: + EXPECTED_SHA: ${{ github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + + - name: Use preinstalled Temurin JDK 21 + run: | + echo "JAVA_HOME=$JAVA_HOME_21_X64" >> "$GITHUB_ENV" + echo "$JAVA_HOME_21_X64/bin" >> "$GITHUB_PATH" + + - name: Bind tag to Maven version with pinned help:evaluate -Dexpression=project.version + shell: bash + run: | + set -euo pipefail + PROJECT_VERSION="$(mvn -q --no-transfer-progress org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate -Dexpression=project.version -DforceStdout)" + test -n "$PROJECT_VERSION" + test "$GITHUB_REF_NAME" = "v$PROJECT_VERSION" + printf '%s\n' "$PROJECT_VERSION" > "$RUNNER_TEMP/clearfolio-project-version" + + - name: Run authoritative Maven acceptance + run: mvn -B --no-transfer-progress verify + + - name: Generate CycloneDX SBOM + run: mvn -B --no-transfer-progress org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom + + - name: Enforce buyer-release license policy + run: | + python3 scripts/check_sbom_license_policy.py \ + --sbom target/bom.json \ + --policy docs/security/2026-07-02-license-policy.json \ + --summary target/license-policy.json \ + --require-no-review + + - name: Build immutable release evidence bundle + shell: bash + env: + SOURCE_REVISION: ${{ github.sha }} + run: | + set -euo pipefail + PROJECT_VERSION="$(cat "$RUNNER_TEMP/clearfolio-project-version")" + JAR="target/clearfolio-viewer-${PROJECT_VERSION}.jar" + test -f "$JAR" + test -f target/bom.json + test -f target/license-policy.json + + mkdir -p target/release-evidence + cp "$JAR" target/release-evidence/ + cp target/bom.json target/release-evidence/sbom-cyclonedx.json + cp target/license-policy.json target/release-evidence/license-policy.json + cp pom.xml target/release-evidence/pom.xml + printf '%s\n' "$SOURCE_REVISION" > target/release-evidence/source-revision.txt + printf '%s\n' "$GITHUB_REF_NAME" > target/release-evidence/source-tag.txt + ( + cd target/release-evidence + sha256sum \ + "clearfolio-viewer-${PROJECT_VERSION}.jar" \ + sbom-cyclonedx.json \ + license-policy.json \ + pom.xml \ + source-revision.txt \ + source-tag.txt \ + > release-manifest.sha256 + ) + + - name: Upload tagged release acceptance evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: clearfolio-release-acceptance-${{ github.ref_name }} + path: target/release-evidence/ + if-no-files-found: error + retention-days: 90 + compression-level: 0 diff --git a/scripts/test_release_acceptance_workflow.py b/scripts/test_release_acceptance_workflow.py new file mode 100644 index 000000000..b87622b66 --- /dev/null +++ b/scripts/test_release_acceptance_workflow.py @@ -0,0 +1,34 @@ +from pathlib import Path + + +WORKFLOW = Path(".github/workflows/release-acceptance.yml") + + +def test_release_acceptance_is_tag_scoped_and_exact_revision_bound() -> None: + text = WORKFLOW.read_text(encoding="utf-8") + assert "tags: [ 'v*' ]" in text or "tags: ['v*']" in text + assert "permissions:\n contents: read" in text + assert "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" in text + assert "persist-credentials: false" in text + assert "ref: ${{ github.sha }}" in text + assert 'test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"' in text + + +def test_release_acceptance_verifies_build_sbom_license_and_tag_version() -> None: + text = WORKFLOW.read_text(encoding="utf-8") + assert "mvn -B --no-transfer-progress verify" in text + assert "org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom" in text + assert "scripts/check_sbom_license_policy.py" in text + assert "--require-no-review" in text + assert "help:evaluate -Dexpression=project.version" in text + assert "GITHUB_REF_NAME" in text + + +def test_release_acceptance_persists_byte_digests_without_publish_authority() -> None: + text = WORKFLOW.read_text(encoding="utf-8") + assert "sha256sum" in text + assert "release-manifest.sha256" in text + assert "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in text + assert "if-no-files-found: error" in text + assert "contents: write" not in text + assert "gh release" not in text