1name: Portal release23on:4 workflow_dispatch:5 inputs:6 tag:7 description: Existing immutable vVERSION tag with a Sigil draft release8 required: true9 type: string1011permissions:12 contents: read1314concurrency:15 group: public-release-${{ inputs.tag }}16 cancel-in-progress: false1718jobs:19 policy:20 name: Verify release policy21 runs-on: ubuntu-24.0422 timeout-minutes: 1023 steps:24 - name: Check out exact tag25 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.126 with:27 ref: ${{ inputs.tag }}28 fetch-depth: 02930 - name: Verify source, tests, and website31 env:32 RELEASE_TAG: ${{ inputs.tag }}33 run: |34 python3 scripts/verify-portal-release.py source \35 --repo-dir "$GITHUB_WORKSPACE" \36 --release-tag "$RELEASE_TAG"37 ./scripts/tests/portal-release.sh38 ./scripts/verify-website.sh3940 preview:41 name: Build unpublished ${{ matrix.target }} preview42 needs: policy43 runs-on: ${{ matrix.os }}44 timeout-minutes: 9045 permissions:46 attestations: write47 contents: read48 id-token: write49 strategy:50 fail-fast: false51 matrix:52 include:53 - os: macos-2654 target: x86_64-apple-darwin55 platform: macos56 bundles: app,dmg57 - os: ubuntu-24.0458 target: x86_64-unknown-linux-gnu59 platform: linux60 bundles: deb,rpm,appimage61 - os: windows-202562 target: x86_64-pc-windows-msvc63 platform: windows64 bundles: msi,nsis65 env:66 RELEASE_TAG: ${{ inputs.tag }}67 steps:68 - name: Check out exact tag69 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.170 with:71 ref: ${{ inputs.tag }}72 fetch-depth: 07374 - name: Require workflow execution from the exact tag75 shell: bash76 run: |77 test "$GITHUB_REF" = "refs/tags/$RELEASE_TAG"78 test "$GITHUB_SHA" = "$(git rev-parse HEAD)"7980 - name: Install Linux build dependencies81 if: matrix.platform == 'linux'82 run: |83 sudo apt-get update84 sudo apt-get install --yes --no-install-recommends \85 build-essential \86 libayatana-appindicator3-dev \87 librsvg2-dev \88 libssl-dev \89 libudev-dev \90 libwebkit2gtk-4.1-dev \91 libxdo-dev \92 pkg-config9394 - name: Install pinned Tauri CLI95 shell: bash96 run: |97 rustup target add "${{ matrix.target }}"98 cargo install tauri-cli --version 2.11.3 --locked99100 # No optional features: a preview build must differ from the published101 # macOS arm64 release only by target and by the absence of signing.102 - name: Build unsigned preview bundles103 shell: bash104 run: |105 cargo tauri build --ci \106 --target "${{ matrix.target }}" \107 --bundles "${{ matrix.bundles }}" \108 -- --locked109110 - name: Collect preview artifacts and digests111 id: collect112 shell: bash113 env:114 TARGET: ${{ matrix.target }}115 PLATFORM: ${{ matrix.platform }}116 run: |117 digest() {118 if command -v sha256sum >/dev/null 2>&1; then119 sha256sum "$1" | awk '{ print $1 }'120 else121 shasum -a 256 "$1" | awk '{ print $1 }'122 fi123 }124125 version="${RELEASE_TAG#v}"126 staging="$RUNNER_TEMP/portal-preview"127 mkdir -p "$staging"128 bundle_root="target/$TARGET/release/bundle"129 test -d "$bundle_root" || {130 echo "the bundler produced no output at $bundle_root" >&2131 exit 1132 }133134 entries=""135 count=0136 while IFS= read -r artifact; do137 name="$(basename "$artifact")"138 # The manifest is assembled textually, so refuse any name that139 # could alter its structure rather than escaping after the fact.140 printf '%s' "$name" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]*$' || {141 echo "unsafe preview artifact name: $name" >&2142 exit 1143 }144 test ! -e "$staging/$name" || {145 echo "duplicate preview artifact name: $name" >&2146 exit 1147 }148 cp "$artifact" "$staging/$name"149 sha256="$(digest "$staging/$name")"150 printf '%s %s\n' "$sha256" "$name" >"$staging/$name.sha256"151 test "$count" -eq 0 || entries="$entries,"152 entries="$entries{\"name\":\"$name\",\"sha256\":\"$sha256\"}"153 count=$((count + 1))154 done < <(find "$bundle_root" -type f \155 \( -name '*.dmg' -o -name '*.deb' -o -name '*.rpm' \156 -o -name '*.AppImage' -o -name '*.msi' -o -name '*.exe' \) \157 | sort)158159 test "$count" -gt 0 || {160 echo "the bundler produced no recognised preview artifact" >&2161 exit 1162 }163164 cat >"$staging/preview-manifest.json" <<EOF165 {166 "artifacts": [$entries167 ],168 "format": 1,169 "git_commit": "$(git rev-parse HEAD)",170 "platform": "$PLATFORM",171 "product": "portal-client-preview",172 "published": false,173 "release_tag": "$RELEASE_TAG",174 "signed": false,175 "target": "$TARGET",176 "version": "$version"177 }178 EOF179 echo "staging=$staging" >>"$GITHUB_OUTPUT"180181 - name: Attest preview build provenance182 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.1.0183 with:184 subject-path: ${{ steps.collect.outputs.staging }}/*185186 # Deliberately an Actions artifact, never a release asset. The published187 # release contract stays exactly the macOS arm64 DMG set.188 - name: Retain unpublished preview build189 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2190 with:191 name: portal-preview-${{ inputs.tag }}-${{ matrix.target }}192 path: ${{ steps.collect.outputs.staging }}193 if-no-files-found: error194 retention-days: 14195196 publish:197 name: Build and publish ad-hoc macOS arm64198 needs: policy199 runs-on: macos-26200 timeout-minutes: 90201 environment:202 name: main203 permissions:204 artifact-metadata: write205 attestations: write206 contents: write207 id-token: write208 env:209 RELEASE_TAG: ${{ inputs.tag }}210 steps:211 - name: Check out exact tag212 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1213 with:214 ref: ${{ inputs.tag }}215 fetch-depth: 0216217 - name: Require workflow execution from the exact tag218 run: |219 test "$GITHUB_REF" = "refs/tags/$RELEASE_TAG"220 test "$GITHUB_SHA" = "$(git rev-parse HEAD)"221222 - name: Verify native arm64 runner223 run: |224 test "$(uname -s)" = Darwin225 test "$(uname -m)" = arm64226 rustup target add aarch64-apple-darwin227228 - name: Install pinned Tauri CLI229 run: cargo install tauri-cli --version 2.11.3 --locked230231 # Portal has no Apple Developer ID. The packaging gate refuses to run in232 # ad-hoc mode if any Apple credential is present in the environment, so233 # this job deliberately provides none.234 - name: Build and verify ad-hoc release assets235 run: |236 release_dir="$RUNNER_TEMP/portal-release"237 mkdir -m 0700 "$release_dir"238 ./scripts/package-macos-client.sh \239 --release-tag "$RELEASE_TAG" \240 --signing adhoc \241 --output-dir "$release_dir"242243 - name: Attest exact Portal build provenance244 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.1.0245 with:246 subject-path: '${{ runner.temp }}/portal-release/Portal-*'247248 - name: Require the exact Sigil candidate draft249 env:250 GH_TOKEN: ${{ github.token }}251 run: |252 release_json="$(gh release view "$RELEASE_TAG" --json isDraft,tagName,assets)"253 test "$(jq -r '.isDraft' <<<"$release_json")" = true254 test "$(jq -r '.tagName' <<<"$release_json")" = "$RELEASE_TAG"255 target_contract="$(<release/sigil-target-contract.txt)"256 test "$target_contract" = linux-glibc2.17-x86_64257 sigil_asset="sigil-$RELEASE_TAG-$target_contract.tar.gz"258 expected="$(printf '%s\n' "$sigil_asset" "$sigil_asset.sha256" | sort)"259 actual="$(jq -r '.assets[].name' <<<"$release_json" | sort)"260 test "$actual" = "$expected" || {261 echo "draft must contain only the two verified Sigil candidate assets before Portal upload" >&2262 exit 1263 }264265 - name: Upload exact Portal assets266 env:267 GH_TOKEN: ${{ github.token }}268 run: |269 version="${RELEASE_TAG#v}"270 release_dir="$RUNNER_TEMP/portal-release"271 dmg="Portal-${version}-arm64.dmg"272 checksum="${dmg}.sha256"273 manifest="Portal-${version}-arm64.json"274 gh release upload "$RELEASE_TAG" \275 "$release_dir/$dmg" \276 "$release_dir/$checksum" \277 "$release_dir/$manifest"278279 - name: Verify the combined draft remains complete and unpublished280 env:281 GH_TOKEN: ${{ github.token }}282 run: |283 release_json="$(gh release view "$RELEASE_TAG" --json isDraft,tagName,assets)"284 test "$(jq -r '.isDraft' <<<"$release_json")" = true285 test "$(jq -r '.tagName' <<<"$release_json")" = "$RELEASE_TAG"286 version="${RELEASE_TAG#v}"287 target_contract="$(<release/sigil-target-contract.txt)"288 test "$target_contract" = linux-glibc2.17-x86_64289 sigil_asset="sigil-$RELEASE_TAG-$target_contract.tar.gz"290 expected="$(printf '%s\n' \291 "$sigil_asset" \292 "$sigil_asset.sha256" \293 "Portal-${version}-arm64.dmg" \294 "Portal-${version}-arm64.dmg.sha256" \295 "Portal-${version}-arm64.json" | sort)"296 actual="$(jq -r '.assets[].name' <<<"$release_json" | sort)"297 test "$actual" = "$expected" || {298 echo "combined draft does not match the five-asset pre-signature contract" >&2299 exit 1300 }301 asset_names="$RUNNER_TEMP/portal-release-assets.json"302 jq '[.assets[].name | select(startswith("Portal-"))]' \303 <<<"$release_json" >"$asset_names"304 python3 scripts/verify-portal-release.py release-assets \305 --release-tag "$RELEASE_TAG" \306 --names-json "$asset_names"307