Release #28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: ['v*'] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Existing release tag to recover for npm staging (for example v0.11.0)' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0') }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| asset: dev-windows-x86_64.exe | |
| architecture: x64 | |
| - runner: windows-11-arm | |
| target: aarch64-pc-windows-msvc | |
| asset: dev-windows-aarch64.exe | |
| architecture: arm64 | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ inputs.tag || github.ref }} | |
| - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # upstream commit | |
| with: | |
| toolchain: 1.97.1 | |
| targets: ${{ matrix.target }} | |
| - run: cargo build --release --target ${{ matrix.target }} | |
| - run: cargo test --target ${{ matrix.target }} | |
| - shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Path release-assets -Force | Out-Null | |
| 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') | |
| 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.' } | |
| $cargoVersion = (Get-Content Cargo.toml | Select-String '^version = "([0-9.]+)"$').Matches.Groups[1].Value | |
| if ($cargoVersion -ne $version) { throw "Tag version $version does not match Cargo.toml version $cargoVersion." } | |
| $moduleManifestVersion = (Test-ModuleManifest -Path powershell/DevNav.psd1).Version.ToString() | |
| if ($moduleManifestVersion -ne $version) { throw "Tag version $version does not match DevNav.psd1 version $moduleManifestVersion." } | |
| $npmPackageVersion = (Get-Content packaging/npm/package.json -Raw | ConvertFrom-Json).version | |
| if ($npmPackageVersion -ne $version) { throw "Tag version $version does not match packaging/npm/package.json version $npmPackageVersion." } | |
| $scoopTemplateVersion = (Get-Content packaging/scoop/devnav.template.json -Raw | ConvertFrom-Json).version | |
| if ($scoopTemplateVersion -ne $version) { throw "Tag version $version does not match packaging/scoop/devnav.template.json version $scoopTemplateVersion." } | |
| & ./scripts/invoke-inno-compiler.ps1 -Architecture '${{ matrix.architecture }}' -Version $version | |
| Copy-Item "release-assets/DevNavSetup-${{ matrix.architecture }}.exe" "DevNavSetup-${{ matrix.architecture }}.exe" | |
| if ([version]$version -le [version]'0.9.7') { | |
| throw "npm/Scoop channels must not publish v${version}; the first multichannel release must be newer than 0.9.7." | |
| } | |
| # Scoop owns the files it installs, so it gets a portable ZIP built | |
| # from the same compiled bytes instead of the Inno installer. The | |
| # marker file tells DevNav to defer updates to Scoop. | |
| $scoopStage = Join-Path $env:RUNNER_TEMP "DevNav-scoop-${{ matrix.architecture }}" | |
| New-Item -ItemType Directory -Path $scoopStage -Force | Out-Null | |
| Copy-Item "release-assets/dev.exe" (Join-Path $scoopStage 'dev.exe') | |
| Copy-Item "powershell/DevNav.psm1" (Join-Path $scoopStage 'DevNav.psm1') | |
| Copy-Item "powershell/DevNav.psd1" (Join-Path $scoopStage 'DevNav.psd1') | |
| New-Item -ItemType File -Path (Join-Path $scoopStage '.devnav-managed-by-scoop') -Force | Out-Null | |
| Compress-Archive -Path (Join-Path $scoopStage '*') -DestinationPath "DevNav-scoop-${{ matrix.architecture }}.zip" -CompressionLevel Optimal -Force | |
| - name: Attest built release artifacts | |
| id: attest-release-artifacts | |
| uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 | |
| with: | |
| subject-path: | | |
| ${{ matrix.asset }} | |
| DevNavSetup-${{ matrix.architecture }}.exe | |
| DevNav-scoop-${{ matrix.architecture }}.zip | |
| - name: Preserve build provenance bundle | |
| shell: pwsh | |
| run: | | |
| Copy-Item "${{ steps.attest-release-artifacts.outputs.bundle-path }}" ` | |
| "DevNav-build-provenance-${{ matrix.architecture }}.sigstore.json" ` | |
| -ErrorAction Stop | |
| # Extract the DSSE envelope from the Sigstore bundle into a | |
| # standards-compliant .intoto.jsonl (one envelope per line). The | |
| # script validates the envelope, the SLSA predicate and the | |
| # expected subjects before the file can be published; the original | |
| # Sigstore bundle with its verification material is kept above. | |
| ./scripts/convert-attestation-to-intoto.ps1 ` | |
| -BundlePath "${{ steps.attest-release-artifacts.outputs.bundle-path }}" ` | |
| -OutputPath "DevNav-build-provenance-${{ matrix.architecture }}.intoto.jsonl" ` | |
| -ExpectedSubject '${{ matrix.asset }}', "DevNavSetup-${{ matrix.architecture }}.exe", "DevNav-scoop-${{ matrix.architecture }}.zip" | |
| - name: Install cosign | |
| if: ${{ matrix.architecture != 'arm64' }} | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| with: | |
| cosign-release: 'v3.1.3' | |
| - name: Install cosign (ARM64 runner compatibility) | |
| if: ${{ matrix.architecture == 'arm64' }} | |
| shell: pwsh | |
| run: | | |
| $cosignDir = Join-Path $env:RUNNER_TEMP 'cosign' | |
| New-Item -ItemType Directory -Path $cosignDir -Force | Out-Null | |
| $cosignPath = Join-Path $cosignDir 'cosign.exe' | |
| Invoke-WebRequest ` | |
| -Uri 'https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-windows-amd64.exe' ` | |
| -OutFile $cosignPath | |
| $actual = (Get-FileHash -LiteralPath $cosignPath -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($actual -ne '9fe59be0eca1271873ce019061335eb1ac419b7059202e797828467ddabe33be') { | |
| throw "Unexpected cosign digest: $actual" | |
| } | |
| Add-Content -LiteralPath $env:GITHUB_PATH -Value $cosignDir | |
| - name: Sign release artifacts (Sigstore keyless) | |
| shell: pwsh | |
| run: | | |
| $files = @( | |
| '${{ matrix.asset }}', | |
| "DevNavSetup-${{ matrix.architecture }}.exe", | |
| "DevNav-scoop-${{ matrix.architecture }}.zip" | |
| ) | |
| foreach ($f in $files) { | |
| cosign sign-blob --yes --bundle "$f.sigstore.json" "$f" | |
| if ($LASTEXITCODE -ne 0) { throw "cosign sign-blob failed for $f" } | |
| } | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: ${{ matrix.asset }} | |
| path: | | |
| ${{ matrix.asset }} | |
| DevNavSetup-${{ matrix.architecture }}.exe | |
| DevNav-scoop-${{ matrix.architecture }}.zip | |
| DevNav-build-provenance-${{ matrix.architecture }}.sigstore.json | |
| DevNav-build-provenance-${{ matrix.architecture }}.intoto.jsonl | |
| ${{ matrix.asset }}.sigstore.json | |
| DevNavSetup-${{ matrix.architecture }}.exe.sigstore.json | |
| DevNav-scoop-${{ matrix.architecture }}.zip.sigstore.json | |
| publish: | |
| if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0') }} | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.tag || github.ref }} | |
| persist-credentials: false | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - run: cp powershell/DevNav.psm1 powershell/DevNav.psd1 artifacts/ | |
| - name: Generate immutable WinGet manifests | |
| working-directory: artifacts | |
| shell: bash | |
| run: | | |
| version="${{ inputs.tag || github.ref_name }}" | |
| version="${version#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) | |
| cat > manifests/j/JacobOptimiza/DevNav/"$version"/JacobOptimiza.DevNav.installer.yaml <<EOF | |
| # yaml-language-server: \$schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json | |
| PackageIdentifier: JacobOptimiza.DevNav | |
| PackageVersion: $version | |
| InstallerType: inno | |
| Scope: user | |
| Installers: | |
| - Architecture: x64 | |
| InstallerUrl: https://github.com/JacobOptimiza/dev-nav/releases/download/v$version/DevNavSetup-x64.exe | |
| InstallerSha256: $x64_sha | |
| - Architecture: arm64 | |
| InstallerUrl: https://github.com/JacobOptimiza/dev-nav/releases/download/v$version/DevNavSetup-arm64.exe | |
| InstallerSha256: $arm64_sha | |
| ManifestType: installer | |
| ManifestVersion: 1.12.0 | |
| EOF | |
| cat > manifests/j/JacobOptimiza/DevNav/"$version"/JacobOptimiza.DevNav.locale.en-US.yaml <<EOF | |
| # yaml-language-server: \$schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json | |
| PackageIdentifier: JacobOptimiza.DevNav | |
| PackageVersion: $version | |
| PackageLocale: en-US | |
| Publisher: Jacob Optimiza | |
| PackageName: DevNav | |
| License: MIT | |
| ShortDescription: Native high-performance workspace navigator for PowerShell 7 on Windows. | |
| PackageUrl: https://github.com/JacobOptimiza/dev-nav | |
| LicenseUrl: https://github.com/JacobOptimiza/dev-nav/blob/main/LICENSE | |
| ManifestType: defaultLocale | |
| ManifestVersion: 1.12.0 | |
| EOF | |
| cat > manifests/j/JacobOptimiza/DevNav/"$version"/JacobOptimiza.DevNav.yaml <<EOF | |
| # yaml-language-server: \$schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json | |
| PackageIdentifier: JacobOptimiza.DevNav | |
| PackageVersion: $version | |
| DefaultLocale: en-US | |
| ManifestType: version | |
| ManifestVersion: 1.12.0 | |
| EOF | |
| tar -a -c -f "winget-manifests-$version.zip" manifests | |
| - name: Generate the Scoop manifest for this release | |
| working-directory: artifacts | |
| run: | | |
| version="${{ inputs.tag || github.ref_name }}" | |
| version="${version#v}" | |
| hash() { sha256sum "$1" | cut -d' ' -f1; } | |
| base="https://github.com/JacobOptimiza/dev-nav/releases/download/v$version" | |
| jq --arg version "$version" \ | |
| --arg url_x64 "$base/DevNav-scoop-x64.zip" --arg sha_x64 "$(hash DevNav-scoop-x64.zip)" \ | |
| --arg url_arm64 "$base/DevNav-scoop-arm64.zip" --arg sha_arm64 "$(hash DevNav-scoop-arm64.zip)" \ | |
| '.version = $version | .architecture = { | |
| "64bit": { url: $url_x64, hash: $sha_x64 }, | |
| "arm64": { url: $url_arm64, hash: $sha_arm64 } | |
| }' "$GITHUB_WORKSPACE/packaging/scoop/devnav.template.json" > devnav.scoop.json | |
| # Release payload artifacts are inventoried here; npm, Scoop and | |
| # WinGet all derive from this single source of truth. | |
| - name: Generate the release manifest | |
| working-directory: artifacts | |
| run: | | |
| version="${{ inputs.tag || github.ref_name }}" | |
| version="${version#v}" | |
| hash() { sha256sum "$1" | cut -d' ' -f1; } | |
| jq -n --arg version "$version" \ | |
| --arg winget_file "winget-manifests-$version.zip" \ | |
| --arg installer_x64_sha "$(hash DevNavSetup-x64.exe)" \ | |
| --arg installer_arm64_sha "$(hash DevNavSetup-arm64.exe)" \ | |
| --arg scoop_x64_sha "$(hash DevNav-scoop-x64.zip)" \ | |
| --arg scoop_arm64_sha "$(hash DevNav-scoop-arm64.zip)" \ | |
| --arg winget_sha "$(hash "winget-manifests-$version.zip")" \ | |
| --arg binary_x64_sha "$(hash dev-windows-x86_64.exe)" \ | |
| --arg binary_arm64_sha "$(hash dev-windows-aarch64.exe)" \ | |
| --arg module_sha "$(hash DevNav.psm1)" \ | |
| --arg module_manifest_sha "$(hash DevNav.psd1)" \ | |
| --arg scoop_manifest_sha "$(hash devnav.scoop.json)" \ | |
| '{ | |
| schemaVersion: 1, | |
| version: $version, | |
| artifacts: { | |
| "installer-x64": { file: "DevNavSetup-x64.exe", sha256: $installer_x64_sha }, | |
| "installer-arm64": { file: "DevNavSetup-arm64.exe", sha256: $installer_arm64_sha }, | |
| "scoop-x64": { file: "DevNav-scoop-x64.zip", sha256: $scoop_x64_sha }, | |
| "scoop-arm64": { file: "DevNav-scoop-arm64.zip", sha256: $scoop_arm64_sha }, | |
| "winget-manifests": { file: $winget_file, sha256: $winget_sha }, | |
| "binary-x64": { file: "dev-windows-x86_64.exe", sha256: $binary_x64_sha }, | |
| "binary-arm64": { file: "dev-windows-aarch64.exe", sha256: $binary_arm64_sha }, | |
| "module": { file: "DevNav.psm1", sha256: $module_sha }, | |
| "module-manifest": { file: "DevNav.psd1", sha256: $module_manifest_sha }, | |
| "scoop-manifest": { file: "devnav.scoop.json", sha256: $scoop_manifest_sha } | |
| } | |
| }' > release-manifest.json | |
| - working-directory: artifacts | |
| run: sha256sum *.exe *.zip DevNav-build-provenance-x64.sigstore.json DevNav-build-provenance-arm64.sigstore.json DevNav-build-provenance-x64.intoto.jsonl DevNav-build-provenance-arm64.intoto.jsonl DevNav.psm1 DevNav.psd1 devnav.scoop.json release-manifest.json > SHA256SUMS.txt | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| with: | |
| cosign-release: 'v3.1.3' | |
| # The build job already signed the compiled artifacts; sign here the | |
| # files that only exist in this job, before the release is created. | |
| - name: Sign publish-job artifacts (Sigstore keyless) | |
| working-directory: artifacts | |
| run: | | |
| set -euo pipefail | |
| for f in DevNav.psm1 DevNav.psd1 devnav.scoop.json release-manifest.json winget-manifests-*.zip SHA256SUMS.txt; do | |
| cosign sign-blob --yes --bundle "$f.sigstore.json" "$f" | |
| done | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: release-manifest | |
| path: artifacts/release-manifest.json | |
| - name: Verify release provenance assets | |
| working-directory: artifacts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| verify_provenance() { | |
| local arch="$1" | |
| local binary="$2" | |
| local installer="$3" | |
| local scoop="$4" | |
| local file="DevNav-build-provenance-${arch}.intoto.jsonl" | |
| test -s "$file" | |
| test "$(grep -cve '^$' "$file")" -eq 1 | |
| jq -e ' | |
| has("payloadType") and | |
| has("payload") and | |
| has("signatures") and | |
| (has("dsseEnvelope") | not) and | |
| (has("verificationMaterial") | not) | |
| ' "$file" >/dev/null | |
| statement="$(jq -r '.payload' "$file" | base64 --decode)" | |
| jq -e ' | |
| ._type == "https://in-toto.io/Statement/v1" and | |
| ( | |
| .predicateType == "https://slsa.dev/provenance/v1" or | |
| .predicateType == "https://slsa.dev/provenance/v0.2" | |
| ) | |
| ' <<<"$statement" >/dev/null | |
| for artifact in "$binary" "$installer" "$scoop"; do | |
| expected="$( | |
| jq -r --arg name "$artifact" ' | |
| .subject[] | | |
| select(.name == $name) | | |
| .digest.sha256 | |
| ' <<<"$statement" | |
| )" | |
| test -n "$expected" | |
| test "$expected" != "null" | |
| actual="$(sha256sum "$artifact" | cut -d' ' -f1)" | |
| test "$expected" = "$actual" | |
| done | |
| } | |
| verify_provenance \ | |
| x64 \ | |
| dev-windows-x86_64.exe \ | |
| DevNavSetup-x64.exe \ | |
| DevNav-scoop-x64.zip | |
| verify_provenance \ | |
| arm64 \ | |
| dev-windows-aarch64.exe \ | |
| DevNavSetup-arm64.exe \ | |
| DevNav-scoop-arm64.zip | |
| - env: | |
| 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 }}" \ | |
| artifacts/*.exe \ | |
| artifacts/*.zip \ | |
| artifacts/*.psm1 \ | |
| artifacts/*.psd1 \ | |
| artifacts/SHA256SUMS.txt \ | |
| artifacts/DevNav-build-provenance-x64.sigstore.json \ | |
| artifacts/DevNav-build-provenance-arm64.sigstore.json \ | |
| artifacts/*.intoto.jsonl \ | |
| artifacts/*.exe.sigstore.json \ | |
| artifacts/*.zip.sigstore.json \ | |
| artifacts/*.psm1.sigstore.json \ | |
| artifacts/*.psd1.sigstore.json \ | |
| artifacts/*.json.sigstore.json \ | |
| artifacts/*.txt.sigstore.json \ | |
| artifacts/release-manifest.json \ | |
| artifacts/devnav.scoop.json \ | |
| --repo "${GITHUB_REPOSITORY}" --generate-notes --verify-tag | |
| npm-package: | |
| if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.tag == 'v0.14.0') }} | |
| needs: publish | |
| runs-on: windows-latest | |
| permissions: | |
| contents: read | |
| 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: | |
| # Node 24 is the canonical LTS baseline for release and npm | |
| # publishing; Node 22 stays supported and Node 26 is exercised in CI | |
| # for forward compatibility, but release ships on 24. | |
| node-version: '24' | |
| - name: Install verified npm CLI | |
| uses: ./.github/actions/install-npm-cli | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Assemble and verify the npm package | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $version = "${{ inputs.tag || 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." | |
| } | |
| $stage = 'npm-package' | |
| New-Item -ItemType Directory -Path "$stage\payload" -Force | Out-Null | |
| Copy-Item packaging\npm\package.json, packaging\npm\README.md $stage | |
| Copy-Item packaging\npm\bin $stage -Recurse | |
| Copy-Item LICENSE $stage | |
| Copy-Item artifacts\DevNavSetup-x64.exe, artifacts\DevNavSetup-arm64.exe "$stage\payload" | |
| Copy-Item artifacts\release-manifest.json $stage | |
| $package = Get-Content "$stage\package.json" -Raw | ConvertFrom-Json | |
| if ($package.version -ne $version) { throw "package.json version $($package.version) does not match tag version $version." } | |
| if ($package.PSObject.Properties['dependencies'] -or $package.PSObject.Properties['devDependencies']) { | |
| throw 'The bootstrap package must not declare dependencies.' | |
| } | |
| if ($package.PSObject.Properties['scripts'] -and $package.scripts.PSObject.Properties['postinstall']) { | |
| throw 'postinstall scripts are forbidden in the bootstrap package.' | |
| } | |
| $manifest = Get-Content "$stage\release-manifest.json" -Raw | ConvertFrom-Json | |
| if ($manifest.version -ne $version) { throw "release-manifest.json version $($manifest.version) does not match tag version $version." } | |
| foreach ($name in @('installer-x64', 'installer-arm64')) { | |
| $entry = $manifest.artifacts.$name | |
| $file = Join-Path $stage "payload\$($entry.file)" | |
| if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "Missing payload file $($entry.file)." } | |
| $actualHash = (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash | |
| if ($actualHash -ne $entry.sha256.ToUpperInvariant()) { throw "SHA-256 mismatch for $($entry.file)." } | |
| } | |
| - name: Inspect the tarball before packing | |
| working-directory: npm-package | |
| run: npm pack --dry-run | |
| - name: Pack the tarball | |
| working-directory: npm-package | |
| run: npm pack | |
| - name: Run the bootstrap unit tests | |
| run: node --test "tests/npm/**/*.test.mjs" | |
| - name: End-to-end install on x64 | |
| working-directory: npm-package | |
| shell: pwsh | |
| run: | | |
| node bin/devnav.mjs install | |
| if ($LASTEXITCODE -ne 0) { throw 'Bootstrap install failed.' } | |
| - name: Install pinned package managers | |
| shell: pwsh | |
| run: | | |
| npm install --global --allow-scripts=bun bun@1.3.14 pnpm@11.21.0 | |
| corepack enable | |
| corepack prepare yarn@4.18.0 --activate | |
| - name: Smoke test the documented ephemeral invocations | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $version = "${{ inputs.tag || 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. | |
| $commands = [ordered]@{ | |
| 'npm' = { & npx.cmd --yes --package $tarball devnav --version } | |
| 'bun' = { & bunx.cmd --bun "@jacoboptimiza/devnav@file:$tarball" --version } | |
| 'pnpm' = { & pnx.cmd $tarball --version } | |
| 'yarn' = { | |
| & corepack.cmd yarn@4.18.0 dlx ` | |
| -p "@jacoboptimiza/devnav@file:$tarball" ` | |
| devnav --version | |
| } | |
| } | |
| foreach ($manager in $commands.GetEnumerator()) { | |
| $sandbox = Join-Path $env:RUNNER_TEMP "devnav-dlx-$($manager.Key)" | |
| New-Item -ItemType Directory -Path $sandbox -Force | Out-Null | |
| Push-Location $sandbox | |
| try { | |
| $output = & $manager.Value | |
| if ($LASTEXITCODE -ne 0) { throw "$($manager.Key) ephemeral run failed." } | |
| $versionLineFound = $false | |
| foreach ($line in $output) { | |
| if (([string]$line).Trim() -eq $version) { | |
| $versionLineFound = $true | |
| break | |
| } | |
| } | |
| if (-not $versionLineFound) { throw "$($manager.Key) did not emit an exact version line: $output" } | |
| } | |
| finally { | |
| Pop-Location | |
| } | |
| } | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: npm-package-${{ inputs.tag || 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' }} | |
| needs: npm-package | |
| runs-on: ubuntu-latest | |
| environment: npm-production | |
| permissions: | |
| contents: read | |
| id-token: write | |
| 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: | |
| node-version: '24' | |
| # Configure the npm registry so the tarball staging below publishes | |
| # to npmjs through OIDC trusted publishing (no token is stored). | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install verified npm CLI | |
| uses: ./.github/actions/install-npm-cli | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: npm-package-${{ inputs.tag || github.ref_name }} | |
| path: npm-package | |
| - name: Stage the exact tested tarball via OIDC trusted publishing | |
| working-directory: npm-package | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tarball=(jacoboptimiza-devnav-*.tgz) | |
| test "${#tarball[@]}" -eq 1 | |
| npm stage publish "./${tarball[0]}" | |
| npm-recovery-package: | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.tag != 'v0.14.0' }} | |
| runs-on: windows-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.tag }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify requested tag and download canonical release assets | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RECOVERY_TAG: ${{ inputs.tag }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $tagCommit = (git rev-list -n 1 "$env:RECOVERY_TAG^{commit}").Trim() | |
| if (-not $tagCommit) { throw "Tag $env:RECOVERY_TAG does not exist." } | |
| if ((git rev-parse HEAD).Trim() -ne $tagCommit) { throw 'Checkout is not exactly the requested tag.' } | |
| New-Item -ItemType Directory -Path recovery-assets -Force | Out-Null | |
| gh release download $env:RECOVERY_TAG --repo $env:GITHUB_REPOSITORY --dir recovery-assets --clobber ` | |
| --pattern 'DevNavSetup-x64.exe' ` | |
| --pattern 'DevNavSetup-arm64.exe' ` | |
| --pattern 'release-manifest.json' | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| - name: Install verified npm CLI | |
| uses: ./.github/actions/install-npm-cli | |
| - name: Assemble and verify the recovery package | |
| shell: pwsh | |
| env: | |
| RECOVERY_TAG: ${{ inputs.tag }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $version = $env:RECOVERY_TAG.TrimStart('v') | |
| $stage = 'npm-recovery' | |
| New-Item -ItemType Directory -Path "$stage\payload" -Force | Out-Null | |
| Copy-Item packaging\npm\package.json, packaging\npm\README.md $stage | |
| Copy-Item packaging\npm\bin $stage -Recurse | |
| Copy-Item LICENSE $stage | |
| Copy-Item recovery-assets\DevNavSetup-x64.exe, recovery-assets\DevNavSetup-arm64.exe "$stage\payload" | |
| Copy-Item recovery-assets\release-manifest.json $stage | |
| $package = Get-Content "$stage\package.json" -Raw | ConvertFrom-Json | |
| if ($package.version -ne $version) { throw "package.json version $($package.version) does not match $version." } | |
| if ($package.PSObject.Properties['dependencies'] -or $package.PSObject.Properties['devDependencies']) { throw 'Dependencies are forbidden.' } | |
| if ($package.PSObject.Properties['scripts'] -and $package.scripts.PSObject.Properties['postinstall']) { throw 'postinstall is forbidden.' } | |
| $manifest = Get-Content "$stage\release-manifest.json" -Raw | ConvertFrom-Json | |
| if ($manifest.version -ne $version) { throw "release-manifest version $($manifest.version) does not match $version." } | |
| foreach ($name in @('installer-x64', 'installer-arm64')) { | |
| $entry = $manifest.artifacts.$name | |
| $file = Join-Path $stage "payload\$($entry.file)" | |
| if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "Missing $($entry.file)." } | |
| $actualHash = (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash | |
| if ($actualHash -ne $entry.sha256.ToUpperInvariant()) { throw "SHA-256 mismatch for $($entry.file)." } | |
| } | |
| - name: Inspect the recovery tarball before packing | |
| working-directory: npm-recovery | |
| run: npm pack --dry-run | |
| - name: Pack the recovery tarball | |
| working-directory: npm-recovery | |
| run: npm pack | |
| - name: Run the bootstrap unit tests | |
| run: node --test "tests/npm/**/*.test.mjs" | |
| - name: End-to-end install on x64 | |
| working-directory: npm-recovery | |
| shell: pwsh | |
| run: | | |
| node bin/devnav.mjs install | |
| if ($LASTEXITCODE -ne 0) { throw 'Bootstrap install failed.' } | |
| - name: Install pinned package managers | |
| shell: pwsh | |
| run: | | |
| npm install --global --allow-scripts=bun bun@1.3.14 pnpm@11.21.0 | |
| corepack enable | |
| corepack prepare yarn@4.18.0 --activate | |
| - name: Smoke test the recovery tarball | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $version = "${{ inputs.tag }}".TrimStart('v') | |
| $tarball = (Resolve-Path "npm-recovery\jacoboptimiza-devnav-$version.tgz").Path | |
| $commands = [ordered]@{ | |
| 'npm' = { & npx.cmd --yes --package $tarball devnav --version } | |
| 'bun' = { & bunx.cmd --bun "@jacoboptimiza/devnav@file:$tarball" --version } | |
| 'pnpm' = { & pnx.cmd $tarball --version } | |
| 'yarn' = { | |
| & corepack.cmd yarn@4.18.0 dlx ` | |
| -p "@jacoboptimiza/devnav@file:$tarball" ` | |
| devnav --version | |
| } | |
| } | |
| foreach ($manager in $commands.GetEnumerator()) { | |
| $sandbox = Join-Path $env:RUNNER_TEMP "devnav-recovery-dlx-$($manager.Key)" | |
| New-Item -ItemType Directory -Path $sandbox -Force | Out-Null | |
| Push-Location $sandbox | |
| try { | |
| $output = & $manager.Value | |
| if ($LASTEXITCODE -ne 0) { throw "$($manager.Key) recovery run failed." } | |
| $versionLineFound = $false | |
| foreach ($line in $output) { | |
| if (([string]$line).Trim() -eq $version) { $versionLineFound = $true; break } | |
| } | |
| if (-not $versionLineFound) { throw "$($manager.Key) did not emit an exact version line: $output" } | |
| } | |
| finally { Pop-Location } | |
| } | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: npm-recovery-${{ inputs.tag }} | |
| path: npm-recovery/*.tgz | |
| 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' }} | |
| needs: npm-recovery-package | |
| runs-on: ubuntu-latest | |
| environment: npm-production | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.tag }} | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| # Configure the npm registry so the recovery tarball staging below | |
| # publishes to npmjs through OIDC trusted publishing (no token is | |
| # stored). | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install verified npm CLI | |
| uses: ./.github/actions/install-npm-cli | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: npm-recovery-${{ inputs.tag }} | |
| path: npm-recovery | |
| - name: Stage the exact recovery tarball via OIDC trusted publishing | |
| working-directory: npm-recovery | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tarball=(jacoboptimiza-devnav-*.tgz) | |
| test "${#tarball[@]}" -eq 1 | |
| npm stage publish "./${tarball[0]}" |