Trigger Release #170
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: Trigger Release | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| # Default all jobs to the minimum token needed to read the release source. | |
| # Jobs that create tags or mutate a GitHub Release opt into write below. | |
| contents: read | |
| # Every release shell step must stop on command, unset-variable, or pipeline | |
| # failures. A release workflow must never proceed to publish after a partial | |
| # verification command or a failed command hidden by a pipeline. | |
| defaults: | |
| run: | |
| shell: bash -euo pipefail {0} | |
| concurrency: | |
| group: trigger-release | |
| cancel-in-progress: false | |
| jobs: | |
| smoke-test: | |
| name: Smoke Test | |
| runs-on: ubuntu-latest | |
| # Bound dependency, audit, and smoke execution so a stalled runner cannot | |
| # indefinitely retain a workflow token or block the serialized release. | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 24.15.0 | |
| cache: npm | |
| - name: Install dependencies | |
| run: | | |
| npm install --global npm@12.0.2 | |
| npm ci | |
| # Builds stage the built-in extensions without re-running their suites; | |
| # CI covers them through `test:workspaces`, so the release path runs them | |
| # here explicitly rather than once per build. | |
| - name: Test built-in extensions | |
| run: npm run test:built-in-extensions | |
| - name: Run smoke checks | |
| run: npm run smoke | |
| - name: Validate release evidence and production dependency audit | |
| run: npm run test:release-evidence | |
| release: | |
| name: Create Release | |
| runs-on: ubuntu-latest | |
| # Tagging and notes generation are bounded independently of packaging. | |
| timeout-minutes: 30 | |
| needs: smoke-test | |
| permissions: | |
| contents: write | |
| outputs: | |
| tag: ${{ steps.release_tag.outputs.tag }} | |
| no_release: ${{ steps.release_tag.outputs.no_release }} | |
| source_commit: ${{ steps.release_source.outputs.commit }} | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 24.15.0 | |
| cache: npm | |
| - name: Install dependencies | |
| run: | | |
| npm install --global npm@12.0.2 | |
| npm ci | |
| - name: Record immutable release source and tag baseline | |
| id: release_source | |
| run: | | |
| SOURCE_COMMIT="$(git rev-parse HEAD)" | |
| echo "commit=$SOURCE_COMMIT" >> "$GITHUB_OUTPUT" | |
| git tag --points-at "$SOURCE_COMMIT" --list 'v*' --sort=version:refname \ | |
| | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | |
| | LC_ALL=C sort -u \ | |
| > "$RUNNER_TEMP/terminay-release-tags-before" || true | |
| - name: Create release tag | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: npm run release | |
| - name: Resolve and verify newly created release tag | |
| id: release_tag | |
| env: | |
| SOURCE_COMMIT: ${{ steps.release_source.outputs.commit }} | |
| run: | | |
| test -n "$SOURCE_COMMIT" | |
| AFTER_TAGS="$RUNNER_TEMP/terminay-release-tags-after" | |
| git tag --points-at "$SOURCE_COMMIT" --list 'v*' --sort=version:refname \ | |
| | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | |
| | LC_ALL=C sort -u \ | |
| > "$AFTER_TAGS" || true | |
| NEW_TAGS="$RUNNER_TEMP/terminay-release-tags-new" | |
| comm -13 "$RUNNER_TEMP/terminay-release-tags-before" "$AFTER_TAGS" > "$NEW_TAGS" | |
| NEW_TAG_COUNT="$(wc -l < "$NEW_TAGS" | tr -d '[:space:]')" | |
| if [ "$NEW_TAG_COUNT" = 0 ]; then | |
| echo "no_release=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| test "$NEW_TAG_COUNT" = 1 | |
| TAG="$(cat "$NEW_TAGS")" | |
| [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] | |
| test "$(git rev-parse "$TAG^{commit}")" = "$SOURCE_COMMIT" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Detect AI release-notes credential | |
| id: release_notes_credential | |
| if: steps.release_tag.outputs.no_release != 'true' | |
| env: | |
| OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| run: | | |
| if [ -n "$OPENROUTER_API_KEY" ]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Generate AI release notes | |
| id: generate_release_notes | |
| if: steps.release_tag.outputs.no_release != 'true' && steps.release_notes_credential.outputs.available == 'true' | |
| continue-on-error: true | |
| env: | |
| OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| run: node scripts/generate-release-notes.mjs "${{ steps.release_tag.outputs.tag }}" | |
| - name: Use fallback release notes | |
| if: steps.release_tag.outputs.no_release != 'true' && (steps.release_notes_credential.outputs.available != 'true' || steps.generate_release_notes.outcome != 'success') | |
| run: node scripts/generate-fallback-release-notes.mjs "${{ steps.release_tag.outputs.tag }}" | |
| - name: Upload release notes artifact | |
| if: steps.release_tag.outputs.no_release != 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: release-notes | |
| path: RELEASE.md | |
| if-no-files-found: error | |
| include-hidden-files: false | |
| build-binaries: | |
| name: Build ${{ matrix.label }} Binary | |
| runs-on: ${{ matrix.os }} | |
| # Native packaging, notarization, and upload must fail closed on a stalled | |
| # runner rather than leaving a write-capable job alive without a bound. | |
| timeout-minutes: 45 | |
| needs: release | |
| if: needs.release.outputs.no_release != 'true' | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| label: macOS | |
| script: npm run build:mac | |
| pattern: release/**/*.dmg | |
| asset_template: Terminay-Mac-%VERSION%-Installer.dmg | |
| - os: ubuntu-latest | |
| label: Linux | |
| script: npm run build:linux | |
| pattern: release/**/*.AppImage | |
| asset_template: Terminay-Linux-%VERSION%.AppImage | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| # Build the immutable release tag, never a branch that could advance | |
| # after the release job creates the tag. | |
| ref: ${{ needs.release.outputs.tag }} | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 24.15.0 | |
| cache: npm | |
| - name: Verify immutable release tag source | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| EXPECTED_COMMIT: ${{ needs.release.outputs.source_commit }} | |
| run: | | |
| test -n "$TAG" | |
| test -n "$EXPECTED_COMMIT" | |
| TAG_COMMIT="$(git rev-parse "$TAG^{commit}")" | |
| CHECKED_OUT_COMMIT="$(git rev-parse HEAD)" | |
| test "$TAG_COMMIT" = "$EXPECTED_COMMIT" | |
| test "$CHECKED_OUT_COMMIT" = "$EXPECTED_COMMIT" | |
| - name: Install dependencies | |
| run: | | |
| npm install --global npm@12.0.2 | |
| npm ci | |
| - name: Check macOS signing secrets | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ vars.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} | |
| run: | | |
| missing="" | |
| for name in MACOS_CERTIFICATE_P12 MACOS_CERTIFICATE_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do | |
| if [ -z "$(printenv "$name")" ]; then | |
| missing="$missing $name" | |
| fi | |
| done | |
| if [ -n "$missing" ]; then | |
| echo "Refusing to publish an unsigned or unnotarized macOS release; missing:$missing" >&2 | |
| exit 1 | |
| fi | |
| - name: Import Apple signing certificate | |
| if: matrix.os == 'macos-latest' | |
| uses: apple-actions/import-codesign-certs@2dbeb2d7c37642111f938c56ef0feb5d51dad55d # v4.0.1 | |
| with: | |
| p12-file-base64: ${{ secrets.MACOS_CERTIFICATE_P12 }} | |
| p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| - name: Sync package version to release tag | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| TARGET_VERSION="${TAG#v}" | |
| node scripts/sync-package-version.mjs "$TARGET_VERSION" | |
| - name: Build packaged app | |
| env: | |
| APPLE_ID: ${{ matrix.os == 'macos-latest' && vars.APPLE_ID || '' }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }} | |
| APPLE_TEAM_ID: ${{ matrix.os == 'macos-latest' && vars.APPLE_TEAM_ID || '' }} | |
| CSC_IDENTITY_AUTO_DISCOVERY: ${{ matrix.os == 'macos-latest' && 'true' || 'false' }} | |
| run: | | |
| test ! -e release | |
| node scripts/stage-selected-secure-werift-runtime.mjs | |
| ${{ matrix.script }} | |
| - name: Verify exact release asset selection | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| ASSET_TEMPLATE: ${{ matrix.asset_template }} | |
| run: | | |
| if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Release tag is not a supported semantic version: $TAG" >&2 | |
| exit 1 | |
| fi | |
| VERSION="${TAG#v}" | |
| EXPECTED_ASSET="${ASSET_TEMPLATE//%VERSION%/$VERSION}" | |
| EXPECTED_FILE="release/$VERSION/$EXPECTED_ASSET" | |
| test -f "$EXPECTED_FILE" | |
| test ! -L "$EXPECTED_FILE" | |
| CANDIDATE_COUNT="$(find release -type f \( -name '*.dmg' -o -name '*.AppImage' \) -print | wc -l | tr -d '[:space:]')" | |
| test "$CANDIDATE_COUNT" = 1 | |
| ACTUAL_FILE="$(find release -type f \( -name '*.dmg' -o -name '*.AppImage' \) -print | sort)" | |
| test "$ACTUAL_FILE" = "$EXPECTED_FILE" | |
| - name: Verify macOS microphone entitlement | |
| if: matrix.os == 'macos-latest' | |
| run: | | |
| APP_BUNDLE="$(find release -name 'Terminay.app' -type d | head -n 1)" | |
| if [ -z "$APP_BUNDLE" ]; then | |
| echo "Terminay.app was not found in release output." | |
| exit 1 | |
| fi | |
| ENTITLEMENTS_PLIST="$RUNNER_TEMP/terminay-entitlements.plist" | |
| codesign -d --entitlements :- "$APP_BUNDLE" > "$ENTITLEMENTS_PLIST" 2>/dev/null | |
| /usr/libexec/PlistBuddy -c 'Print :com.apple.security.device.audio-input' "$ENTITLEMENTS_PLIST" | grep -q true | |
| MICROPHONE_USAGE_DESCRIPTION="$(/usr/libexec/PlistBuddy -c 'Print :NSMicrophoneUsageDescription' "$APP_BUNDLE/Contents/Info.plist")" | |
| test -n "$(printf '%s' "$MICROPHONE_USAGE_DESCRIPTION" | tr -d '[:space:]')" | |
| - name: Verify macOS signed and notarized release DMG | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| APPLE_ID: ${{ vars.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| DMG="release/$VERSION/Terminay-Mac-$VERSION-Installer.dmg" | |
| test -f "$DMG" | |
| # electron-builder notarizes the application before it creates the | |
| # DMG. Submit the final container separately so the exact published | |
| # bytes receive a ticket that can be stapled and checked offline. | |
| xcrun notarytool submit "$DMG" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait | |
| xcrun stapler staple "$DMG" | |
| MOUNT_POINT="$RUNNER_TEMP/terminay-release-dmg" | |
| mkdir -p "$MOUNT_POINT" | |
| hdiutil attach "$DMG" -nobrowse -readonly -mountpoint "$MOUNT_POINT" | |
| cleanup() { | |
| hdiutil detach "$MOUNT_POINT" -force || true | |
| } | |
| trap cleanup EXIT | |
| # The signed application must come from this exact, read-only DMG. | |
| # Do not let a stale unpacked app or an ambiguous second bundle stand | |
| # in for the payload that will be published. | |
| APP_BUNDLE_COUNT="$(find "$MOUNT_POINT" -type d -name 'Terminay.app' -print | wc -l | tr -d '[:space:]')" | |
| test "$APP_BUNDLE_COUNT" = 1 | |
| APP_BUNDLE="$(find "$MOUNT_POINT" -type d -name 'Terminay.app' -print)" | |
| test ! -L "$APP_BUNDLE" | |
| APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/Terminay" | |
| test -f "$APP_EXECUTABLE" | |
| test ! -L "$APP_EXECUTABLE" | |
| codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" | |
| codesign -dvv "$APP_BUNDLE" 2>&1 | grep -F "TeamIdentifier=$APPLE_TEAM_ID" | |
| spctl --assess --type execute --verbose=4 "$APP_BUNDLE" | |
| xcrun stapler validate "$DMG" | |
| cleanup | |
| trap - EXIT | |
| # Chromium aborts on quit when the bundle stays on the read-only DMG. | |
| # Stage a writable copy of those exact signed bytes, then boot that. | |
| STAGED_APP="$(bash scripts/stage-macos-app-from-dmg.sh "$DMG" "$RUNNER_TEMP/terminay-release-staged")" | |
| codesign --verify --deep --strict --verbose=2 "$STAGED_APP" | |
| codesign -dvv "$STAGED_APP" 2>&1 | grep -F "TeamIdentifier=$APPLE_TEAM_ID" | |
| spctl --assess --type execute --verbose=4 "$STAGED_APP" | |
| export TERMINAY_PACKAGED_APP="$STAGED_APP" | |
| export TERMINAY_RELEASE_DIAGNOSTICS_DIR="$RUNNER_TEMP/terminay-packaged-smoke" | |
| bash scripts/run-packaged-macos-smoke.sh | |
| - name: Preserve packaged startup diagnostics | |
| if: ${{ always() && matrix.os == 'macos-latest' }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: packaged-startup-diagnostics-${{ matrix.label }} | |
| path: ${{ runner.temp }}/terminay-packaged-smoke | |
| if-no-files-found: ignore | |
| - name: Write release asset checksums | |
| env: | |
| ASSET_PATTERN: ${{ matrix.pattern }} | |
| run: | | |
| found=false | |
| for file in $ASSET_PATTERN; do | |
| test -f "$file" || continue | |
| test ! -L "$file" | |
| # A stale sidecar must not be followed or replaced through a | |
| # symlink when a runner retries packaging in its workspace. | |
| test ! -e "$file.sha256" | |
| test ! -L "$file.sha256" | |
| # Sidecars are published next to their payload. Do not embed the | |
| # runner-local release/ path: downloaded release assets must be | |
| # verifiable from any directory without recreating that path. | |
| checksum="$(shasum -a 256 "$file" | awk '{ print $1 }')" | |
| printf '%s %s\n' "$checksum" "$(basename "$file")" > "$file.sha256" | |
| found=true | |
| done | |
| test "$found" = true | |
| - name: Verify release asset checksums before upload | |
| env: | |
| ASSET_PATTERN: ${{ matrix.pattern }} | |
| run: | | |
| found=false | |
| for file in $ASSET_PATTERN; do | |
| test -f "$file" || continue | |
| test -f "$file.sha256" | |
| test ! -L "$file" | |
| test ! -L "$file.sha256" | |
| ( | |
| cd "$(dirname "$file")" | |
| shasum -a 256 -c "$(basename "$file").sha256" | |
| ) | |
| found=true | |
| done | |
| test "$found" = true | |
| - name: Upload workflow artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: ${{ matrix.label }} binary | |
| path: | | |
| ${{ matrix.pattern }} | |
| ${{ matrix.pattern }}.sha256 | |
| if-no-files-found: error | |
| include-hidden-files: false | |
| - name: Attach checksummed binaries to GitHub release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| ASSET_PATTERN: ${{ matrix.pattern }} | |
| run: | | |
| for file in $ASSET_PATTERN; do | |
| test -f "$file" | |
| test -f "$file.sha256" | |
| test ! -L "$file" | |
| test ! -L "$file.sha256" | |
| ( | |
| cd "$(dirname "$file")" | |
| shasum -a 256 -c "$(basename "$file").sha256" | |
| ) | |
| # Deliberately omit --clobber: a release asset name is immutable | |
| # once attached, and a retry must never silently replace its bytes. | |
| gh release upload "$TAG" "$file" "$file.sha256" --repo "$GH_REPO" | |
| done | |
| build-standalone-server: | |
| name: Build Standalone Server Artifact | |
| runs-on: ubuntu-latest | |
| # Packing, extracted-payload validation, and publication must not retain a | |
| # write-capable token indefinitely. | |
| timeout-minutes: 30 | |
| needs: release | |
| if: needs.release.outputs.no_release != 'true' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| ref: ${{ needs.release.outputs.tag }} | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 24.15.0 | |
| cache: npm | |
| - name: Verify immutable release tag source | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| EXPECTED_COMMIT: ${{ needs.release.outputs.source_commit }} | |
| run: | | |
| test -n "$TAG" | |
| test -n "$EXPECTED_COMMIT" | |
| test "$(git rev-parse "$TAG^{commit}")" = "$EXPECTED_COMMIT" | |
| test "$(git rev-parse HEAD)" = "$EXPECTED_COMMIT" | |
| - name: Install dependencies | |
| run: | | |
| npm install --global npm@12.0.2 | |
| npm ci | |
| - name: Sync package version to release tag | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| node scripts/sync-package-version.mjs "$VERSION" | |
| - name: Prepare integrity-pinned Secure Werift source mirror | |
| run: | | |
| node scripts/prove-secure-werift-offline-rebuild.mjs --prepare-mirror "$RUNNER_TEMP/secure-werift-source-mirror" | |
| - name: Prove clean network-independent Secure Werift rebuild | |
| env: | |
| npm_config_offline: "true" | |
| run: | | |
| node scripts/prove-secure-werift-offline-rebuild.mjs --prove-mirror "$RUNNER_TEMP/secure-werift-source-mirror" | |
| - name: Build and pack exact standalone server artifact | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| OUTPUT="release/$VERSION" | |
| mkdir -p "$OUTPUT" | |
| # The server's published declarations import these workspace | |
| # packages. Build the graph rather than compiling the server in | |
| # isolation, as a fresh release checkout has no cached dist output. | |
| npm run build:application-graph | |
| npm run build:server-postcompile | |
| node scripts/stage-selected-secure-werift-runtime.mjs | |
| cp -R build/webrtc-runtime apps/terminay-server/dist/webrtc-runtime | |
| node apps/terminay-server/scripts/write-release-integrity-manifest.mjs | |
| npm run verify:built-in-extensions | |
| npm pack --workspace @terminay/server --json --pack-destination "$OUTPUT" > "$RUNNER_TEMP/terminay-server-pack.json" | |
| EXPECTED="terminay-server-$VERSION.tgz" | |
| test "$(node scripts/npm-pack-result.mjs "$RUNNER_TEMP/terminay-server-pack.json")" = "$EXPECTED" | |
| test -f "$OUTPUT/$EXPECTED" | |
| test "$(find "$OUTPUT" -maxdepth 1 -type f -name '*.tgz' -print | wc -l | tr -d '[:space:]')" = 1 | |
| - name: Verify extracted standalone server payload before checksumming | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| ARCHIVE="release/$VERSION/terminay-server-$VERSION.tgz" | |
| EXTRACTED="release/$VERSION/extracted" | |
| mkdir -p "$EXTRACTED" | |
| tar -xzf "$ARCHIVE" -C "$EXTRACTED" | |
| test -d "$EXTRACTED/package" | |
| test -f "$EXTRACTED/package/dist/release-integrity.json" | |
| test -f "$EXTRACTED/package/dist/webrtc-runtime/artifact/lib/index.mjs" | |
| test -f "$EXTRACTED/package/dist/built-in-extensions/inventory.v1.json" | |
| test ! -L "$EXTRACTED/package" | |
| MANIFEST="$RUNNER_TEMP/terminay-server-artifact-manifest.json" | |
| node scripts/standalone-artifact.mjs "$EXTRACTED/package" "$MANIFEST" | |
| test "$(node -e 'process.stdout.write(require(process.argv[1]).package.version)' "$MANIFEST")" = "$VERSION" | |
| - name: Write and verify standalone server checksum | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| ARCHIVE="release/$VERSION/terminay-server-$VERSION.tgz" | |
| node scripts/release-checksum.mjs write "$ARCHIVE" "$ARCHIVE.sha256" | |
| node scripts/release-checksum.mjs verify "$ARCHIVE" "$ARCHIVE.sha256" | |
| - name: Sign and verify exact standalone server archive | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| TERMINAY_RELEASE_SIGNING_PRIVATE_KEY_B64: ${{ secrets.TERMINAY_RELEASE_SIGNING_PRIVATE_KEY_B64 }} | |
| TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64: ${{ vars.TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64 }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| ARCHIVE="release/$VERSION/terminay-server-$VERSION.tgz" | |
| test -n "$TERMINAY_RELEASE_SIGNING_PRIVATE_KEY_B64" | |
| test -n "$TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64" | |
| node scripts/release-signature.mjs sign "$ARCHIVE" "$ARCHIVE.sig" | |
| node scripts/release-signature.mjs verify "$ARCHIVE" "$ARCHIVE.sig" | |
| - name: Upload standalone server workflow artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: standalone-server | |
| path: | | |
| release/*/terminay-server-*.tgz | |
| release/*/terminay-server-*.tgz.sha256 | |
| release/*/terminay-server-*.tgz.sig | |
| if-no-files-found: error | |
| include-hidden-files: false | |
| - name: Attach checksummed standalone server to GitHub release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64: ${{ vars.TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64 }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| ARCHIVE="release/$VERSION/terminay-server-$VERSION.tgz" | |
| test -f "$ARCHIVE" | |
| test -f "$ARCHIVE.sha256" | |
| test -f "$ARCHIVE.sig" | |
| test ! -L "$ARCHIVE" | |
| test ! -L "$ARCHIVE.sha256" | |
| test ! -L "$ARCHIVE.sig" | |
| test -n "$TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64" | |
| node scripts/release-checksum.mjs verify "$ARCHIVE" "$ARCHIVE.sha256" | |
| node scripts/release-signature.mjs verify "$ARCHIVE" "$ARCHIVE.sig" | |
| # Deliberately omit --clobber: each published standalone asset name | |
| # is immutable, including its checksum and detached signature. | |
| gh release upload "$TAG" "$ARCHIVE" "$ARCHIVE.sha256" "$ARCHIVE.sig" --repo "$GH_REPO" | |
| publish-release-notes: | |
| name: Publish Release Notes | |
| runs-on: ubuntu-latest | |
| # Download, checksum verification, and the final release edit are bounded. | |
| timeout-minutes: 20 | |
| needs: [release, build-binaries, build-standalone-server] | |
| if: needs.release.outputs.no_release != 'true' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Check out release verification code | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| ref: ${{ needs.release.outputs.tag }} | |
| persist-credentials: false | |
| - name: Verify immutable release source before publication | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| EXPECTED_COMMIT: ${{ needs.release.outputs.source_commit }} | |
| run: | | |
| test -n "$TAG" | |
| test -n "$EXPECTED_COMMIT" | |
| test "$(git rev-parse "$TAG^{commit}")" = "$EXPECTED_COMMIT" | |
| test "$(git rev-parse HEAD)" = "$EXPECTED_COMMIT" | |
| - name: Download release notes artifact | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: release-notes | |
| - name: Verify immutable Desktop release assets before notes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: | | |
| test -n "$TAG" | |
| VERSION="${TAG#v}" | |
| ASSET_NAMES="$(gh release view "$TAG" --repo "$GH_REPO" --json assets --jq '.assets[].name' | sort)" | |
| EXPECTED_ASSET_NAMES="$(cat <<EOF | |
| Terminay-Linux-${VERSION}.AppImage | |
| Terminay-Linux-${VERSION}.AppImage.sha256 | |
| Terminay-Mac-${VERSION}-Installer.dmg | |
| Terminay-Mac-${VERSION}-Installer.dmg.sha256 | |
| terminay-server-${VERSION}.tgz | |
| terminay-server-${VERSION}.tgz.sha256 | |
| terminay-server-${VERSION}.tgz.sig | |
| EOF | |
| )" | |
| # A release retry must not publish notes for a mixture of the exact | |
| # verified assets and stale or substituted attachments. | |
| test "$ASSET_NAMES" = "$EXPECTED_ASSET_NAMES" | |
| - name: Verify published Desktop asset checksums before notes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64: ${{ vars.TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64 }} | |
| run: | | |
| test -n "$TAG" | |
| VERSION="${TAG#v}" | |
| ASSET_DIR="release/$VERSION" | |
| mkdir -p "$ASSET_DIR" | |
| for expected in \ | |
| "Terminay-Mac-${VERSION}-Installer.dmg" \ | |
| "Terminay-Mac-${VERSION}-Installer.dmg.sha256" \ | |
| "Terminay-Linux-${VERSION}.AppImage" \ | |
| "Terminay-Linux-${VERSION}.AppImage.sha256" \ | |
| "terminay-server-${VERSION}.tgz" \ | |
| "terminay-server-${VERSION}.tgz.sha256" \ | |
| "terminay-server-${VERSION}.tgz.sig"; do | |
| gh release download "$TAG" --repo "$GH_REPO" --dir "$ASSET_DIR" --pattern "$expected" | |
| test -f "$ASSET_DIR/$expected" | |
| done | |
| for asset in \ | |
| "$ASSET_DIR/Terminay-Mac-${VERSION}-Installer.dmg" \ | |
| "$ASSET_DIR/Terminay-Linux-${VERSION}.AppImage" \ | |
| "$ASSET_DIR/terminay-server-${VERSION}.tgz"; do | |
| ( | |
| cd "$ASSET_DIR" | |
| shasum -a 256 -c "$(basename "$asset").sha256" | |
| ) | |
| done | |
| test -n "$TERMINAY_RELEASE_SIGNING_PUBLIC_KEY_B64" | |
| node scripts/release-signature.mjs verify \ | |
| "$ASSET_DIR/terminay-server-${VERSION}.tgz" \ | |
| "$ASSET_DIR/terminay-server-${VERSION}.tgz.sig" | |
| - name: Append release asset summary | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| cat <<EOF >> RELEASE.md | |
| ## Release Assets | |
| - macOS installer: https://github.com/${REPOSITORY}/releases/download/${TAG}/Terminay-Mac-${TAG#v}-Installer.dmg | |
| - Linux AppImage: https://github.com/${REPOSITORY}/releases/download/${TAG}/Terminay-Linux-${TAG#v}.AppImage | |
| - Standalone server: https://github.com/${REPOSITORY}/releases/download/${TAG}/terminay-server-${TAG#v}.tgz | |
| EOF | |
| - name: Update GitHub release notes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| run: gh release edit "$TAG" --repo "$GH_REPO" --notes-file RELEASE.md |