Skip to content

Trigger Release

Trigger Release #168

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
- 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