diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8045a66..1fb4039 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ permissions: jobs: build: - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0') }} + if: ${{ github.event_name == 'push' }} strategy: fail-fast: false matrix: @@ -38,7 +38,6 @@ jobs: with: fetch-depth: 0 persist-credentials: false - ref: ${{ inputs.tag || github.ref }} - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # upstream commit with: toolchain: 1.97.1 @@ -51,7 +50,7 @@ jobs: Copy-Item "target/${{ matrix.target }}/release/dev.exe" "${{ matrix.asset }}" Copy-Item "target/${{ matrix.target }}/release/dev.exe" "release-assets/dev.exe" Copy-Item "powershell/DevNav.psm1" "release-assets/DevNav.psm1" - $version = "${{ inputs.tag || github.ref_name }}".TrimStart('v') + $version = "${env:GITHUB_REF_NAME}".TrimStart('v') git fetch origin main --quiet git merge-base --is-ancestor $env:GITHUB_SHA origin/main if ($LASTEXITCODE -ne 0) { throw 'Release tags must point to a commit contained in main.' } @@ -147,7 +146,7 @@ jobs: DevNav-scoop-${{ matrix.architecture }}.zip.sigstore.json publish: - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0') }} + if: ${{ github.event_name == 'push' }} needs: build runs-on: ubuntu-latest permissions: @@ -156,7 +155,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.tag || github.ref }} persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: @@ -167,8 +165,7 @@ jobs: working-directory: artifacts shell: bash run: | - version="${{ inputs.tag || github.ref_name }}" - version="${version#v}" + version="${GITHUB_REF_NAME#v}" mkdir -p manifests/j/JacobOptimiza/DevNav/"$version" x64_sha=$(sha256sum DevNavSetup-x64.exe | cut -d' ' -f1) arm64_sha=$(sha256sum DevNavSetup-arm64.exe | cut -d' ' -f1) @@ -214,8 +211,7 @@ jobs: - name: Generate the Scoop manifest for this release working-directory: artifacts run: | - version="${{ inputs.tag || github.ref_name }}" - version="${version#v}" + version="${GITHUB_REF_NAME#v}" hash() { sha256sum "$1" | cut -d' ' -f1; } base="https://github.com/JacobOptimiza/dev-nav/releases/download/v$version" jq --arg version "$version" \ @@ -230,8 +226,7 @@ jobs: - name: Generate the release manifest working-directory: artifacts run: | - version="${{ inputs.tag || github.ref_name }}" - version="${version#v}" + version="${GITHUB_REF_NAME#v}" hash() { sha256sum "$1" | cut -d' ' -f1; } jq -n --arg version "$version" \ --arg winget_file "winget-manifests-$version.zip" \ @@ -346,7 +341,7 @@ jobs: GH_TOKEN: ${{ github.token }} # Upload files only; the unpacked manifests directory is not a release asset. run: | - gh release create "${{ inputs.tag || github.ref_name }}" \ + gh release create "${GITHUB_REF_NAME}" \ artifacts/*.exe \ artifacts/*.zip \ artifacts/*.psm1 \ @@ -366,7 +361,7 @@ jobs: --repo "${GITHUB_REPOSITORY}" --generate-notes --verify-tag npm-package: - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0') }} + if: ${{ github.event_name == 'push' }} needs: publish runs-on: windows-latest permissions: @@ -374,7 +369,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.tag || github.ref }} persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -392,7 +386,7 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' - $version = "${{ inputs.tag || github.ref_name }}".TrimStart('v') + $version = "${env:GITHUB_REF_NAME}".TrimStart('v') if ([version]$version -le [version]'0.9.7') { throw "npm must not publish v${version}; the first multichannel release must be newer than 0.9.7." } @@ -444,7 +438,7 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' - $version = "${{ inputs.tag || github.ref_name }}".TrimStart('v') + $version = "${env:GITHUB_REF_NAME}".TrimStart('v') $tarball = (Resolve-Path "npm-package\jacoboptimiza-devnav-$version.tgz").Path # Same forms the README documents, resolved against the local tarball # instead of the registry. @@ -480,12 +474,12 @@ jobs: } - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: npm-package-${{ inputs.tag || github.ref_name }} + name: npm-package-${{ github.ref_name }} path: npm-package/*.tgz if-no-files-found: error npm-publish: - if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0')) && vars.NPM_TRUSTED_PUBLISHING_ENABLED == 'true' }} + if: ${{ github.event_name == 'push' && vars.NPM_TRUSTED_PUBLISHING_ENABLED == 'true' }} needs: npm-package runs-on: ubuntu-latest environment: npm-production @@ -495,7 +489,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.tag || github.ref }} persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -507,7 +500,7 @@ jobs: uses: ./.github/actions/install-npm-cli - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: npm-package-${{ inputs.tag || github.ref_name }} + name: npm-package-${{ github.ref_name }} path: npm-package - name: Stage the exact tested tarball via OIDC trusted publishing working-directory: npm-package @@ -519,7 +512,7 @@ jobs: npm stage publish "./${tarball[0]}" npm-recovery-package: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.tag != 'v0.14.0' }} + if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: windows-latest permissions: contents: read @@ -634,7 +627,7 @@ jobs: if-no-files-found: error npm-recovery-publish: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.tag != 'v0.14.0' && vars.NPM_TRUSTED_PUBLISHING_ENABLED == 'true' }} + if: ${{ github.event_name == 'workflow_dispatch' && vars.NPM_TRUSTED_PUBLISHING_ENABLED == 'true' }} needs: npm-recovery-package runs-on: ubuntu-latest environment: npm-production diff --git a/SIGNING.md b/SIGNING.md index 8d5703e..0fb2179 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -11,6 +11,11 @@ and recorded in the Rekor transparency log. - **v0.13.0 (retroactive):** the 12 distributed artifacts of the release (installers, binaries, PowerShell module files, Scoop/WinGet packages and metadata). Bundles live in [`signatures/v0.13.0/`](signatures/v0.13.0/). +- **v0.14.0:** the assets were built from the immutable `v0.14.0` tag. The + final recovery run was triggered with `workflow_dispatch` after the initial + ARM64 signing failure, so its Sigstore certificates identify + `release.yml@refs/heads/main`, not the tag ref. The recovery workflow commit + was `d79cfbccdc8705006faeeb61d8368fd6e534bc4c`. - **Future releases:** every artifact is signed by `.github/workflows/release.yml` before the GitHub Release is created, and the `.sigstore.json` bundles are published alongside the assets. The workflow @@ -51,6 +56,14 @@ bundle file. - OIDC issuer: `https://token.actions.githubusercontent.com` - Certificate identity for the retroactive v0.13.0 signatures: `https://github.com/JacobOptimiza/dev-nav/.github/workflows/sign-release.yml@refs/heads/main` +- Certificate identity for v0.14.0 recovery signatures: + `https://github.com/JacobOptimiza/dev-nav/.github/workflows/release.yml@refs/heads/main` + with repository `JacobOptimiza/dev-nav`, workflow ref `refs/heads/main`, + workflow SHA + `d79cfbccdc8705006faeeb61d8368fd6e534bc4c`, and trigger + `workflow_dispatch`. Verify these claims with the corresponding Cosign + `--certificate-github-workflow-*` constraints; the issuer remains + `https://token.actions.githubusercontent.com`. - Certificate identity pattern for future releases: `https://github.com/JacobOptimiza/dev-nav/.github/workflows/release.yml@refs/tags/v` diff --git a/tests/powershell/ReleaseWorkflowModeContract.Tests.ps1 b/tests/powershell/ReleaseWorkflowModeContract.Tests.ps1 new file mode 100644 index 0000000..526c0c7 --- /dev/null +++ b/tests/powershell/ReleaseWorkflowModeContract.Tests.ps1 @@ -0,0 +1,25 @@ +BeforeAll { + $script:workflowPath = Join-Path $PSScriptRoot '..\..\.github\workflows\release.yml' + $script:workflow = Get-Content -LiteralPath $script:workflowPath -Raw +} + +Describe 'release workflow mode contract' { + It 'runs normal build, publish and npm packaging only for push events' { + $script:workflow | Should -Match '(?ms)build:\s+if:\s+\$\{\{ github\.event_name == ''push'' \}\}' + $script:workflow | Should -Match '(?ms)publish:\s+if:\s+\$\{\{ github\.event_name == ''push'' \}\}' + $script:workflow | Should -Match '(?ms)npm-package:\s+if:\s+\$\{\{ github\.event_name == ''push'' \}\}' + $script:workflow | Should -Match '(?ms)npm-publish:\s+if:\s+\$\{\{ github\.event_name == ''push'' && vars\.NPM_TRUSTED_PUBLISHING_ENABLED == ''true'' \}\}' + } + + It 'keeps workflow dispatch limited to npm recovery' { + $script:workflow | Should -Match '(?ms)npm-recovery-package:\s+if:\s+\$\{\{ github\.event_name == ''workflow_dispatch'' \}\}' + $script:workflow | Should -Match '(?ms)npm-recovery-publish:\s+if:\s+\$\{\{ github\.event_name == ''workflow_dispatch'' && vars\.NPM_TRUSTED_PUBLISHING_ENABLED == ''true'' \}\}' + $script:workflow | Should -Not -Match 'inputs\.tag == ''v0\.14\.0''' + } + + It 'cannot reach release creation from workflow dispatch or a hardcoded release version' { + $script:workflow | Should -Not -Match 'v0\.14\.0' + $script:workflow | Should -Match 'gh release create "\$\{GITHUB_REF_NAME\}"' + $script:workflow | Should -Match '--verify-tag' + } +}