Skip to content

Release

Release #53

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: true
default: "patch"
type: choice
options: [patch, minor, major]
release_web:
description: "True to release core web application"
required: true
type: boolean
release_web_extension:
description: "True to release web extension"
required: true
type: boolean
release_desktop:
description: "True to release desktop application"
required: true
type: boolean
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: write
# Many of these are only needed for tests in CI environment
env:
NODE_OPTIONS: "--max_old_space_size=4096"
NX_CLOUD_DISTRIBUTED_EXECUTION: false
AUTH_SFDC_CLIENT_ID: ${{ secrets.SFDC_CONSUMER_KEY }}
AUTH_SFDC_CLIENT_SECRET: ${{ secrets.SFDC_CONSUMER_SECRET }}
CONTENTFUL_HOST: cdn.contentful.com
CONTENTFUL_SPACE: wuv9tl5d77ll
CONTENTFUL_TOKEN: ${{ secrets.CONTENTFUL_TOKEN }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GOOGLE_APP_ID: ${{ secrets.GOOGLE_APP_ID }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
JETSTREAM_AUTH_OTP_SECRET: ${{ secrets.JETSTREAM_AUTH_OTP_SECRET }}
JETSTREAM_AUTH_SECRET: ${{ secrets.JETSTREAM_AUTH_SECRET }}
JETSTREAM_AUTH_SSO_SECRET: ${{ secrets.JETSTREAM_AUTH_SSO_SECRET }}
JETSTREAM_CLIENT_URL: http://localhost:3333/app
JETSTREAM_POSTGRES_DBURI: postgres://postgres:postgres@localhost:5432/postgres
PRISMA_TEST_DB_URI: postgres://postgres:postgres@localhost:5432/postgres
JETSTREAM_SAML_SP_ENTITY_ID_PREFIX: urn:jetstream:test
JETSTREAM_SERVER_DOMAIN: localhost:3333
JETSTREAM_SERVER_URL: http://localhost:3333
JETSTREAM_SESSION_SECRET: ${{ secrets.JETSTREAM_SESSION_SECRET }}
NX_PUBLIC_AMPLITUDE_KEY: ${{ secrets.NX_PUBLIC_AMPLITUDE_KEY }}
NX_PUBLIC_CLIENT_URL: "http://localhost:3333/app"
NX_PUBLIC_SENTRY_DSN: ${{ secrets.NX_PUBLIC_SENTRY_DSN }}
NX_PUBLIC_SERVER_URL: "http://localhost:3333"
SFDC_API_VERSION: "65.0"
SFDC_CALLBACK_URL: http://localhost:3333/oauth/sfdc/callback
DESKTOP_ORG_ENCRYPTION_SECRET: ${{ secrets.DESKTOP_ORG_ENCRYPTION_SECRET }}
SFDC_CONSUMER_KEY: ${{ secrets.SFDC_CONSUMER_KEY }}
SFDC_CONSUMER_SECRET: ${{ secrets.SFDC_CONSUMER_SECRET }}
SFDC_ENCRYPTION_KEY: ${{ secrets.SFDC_ENCRYPTION_KEY }}
JWT_ENCRYPTION_KEY: ${{ secrets.JWT_ENCRYPTION_KEY }}
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 60
environment: production
services:
# Used for integration tests (notably cron job tests)
postgres:
image: postgres:16.1-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Validate inputs
env:
REF_NAME: ${{ github.ref_name }}
RELEASE_WEB: ${{ inputs.release_web }}
RELEASE_WEB_EXTENSION: ${{ inputs.release_web_extension }}
RELEASE_DESKTOP: ${{ inputs.release_desktop }}
run: |
if [[ "$REF_NAME" != "main" && ! "$REF_NAME" =~ ^hotfix/ ]]; then
echo "Error: This workflow can only be run on 'main' or a 'hotfix/*' branch. Current branch: $REF_NAME"
exit 1
fi
if [[ "$RELEASE_WEB" != "true" && "$RELEASE_WEB_EXTENSION" != "true" && "$RELEASE_DESKTOP" != "true" ]]; then
echo "Error: At least one of release_web, release_web_extension, or release_desktop must be true."
exit 1
fi
- uses: actions/checkout@v6
with:
fetch-depth: 0
# We override the remote URL later with a GitHub App installation token
# so release-it can push as the App (which is on the ruleset bypass list).
persist-credentials: false
- uses: pnpm/action-setup@v6
with:
version: 11.1.3
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Prisma client is no longer generated on install, so it must be generated explicitly.
# Required by tests and the web extension/desktop builds, which don't run `pnpm build`.
- name: Generate database client
run: pnpm db:generate
- name: Run database migration
run: pnpm db:migrate
- name: Run tests
run: pnpm test:all
# ENSURE BUILD SUCCEEDS BEFORE CREATING ANY RELEASES
- name: Build Web
if: ${{ inputs.release_web }}
run: pnpm build
- name: Build Web Extension
if: ${{ inputs.release_web_extension }}
run: pnpm nx run-many --output-style=static --target=build --parallel=3
--projects=jetstream-web-extension --configuration=production
- name: Build Desktop
if: ${{ inputs.release_desktop }}
run: pnpm nx run-many --output-style=static --target=build --parallel=3
--projects=jetstream-desktop,jetstream-desktop-client
--configuration=production
# Generate a GitHub App installation token for git pushes.
# The App's bot account is on the main-branch ruleset bypass list, which a PAT is not.
# Token is generated after build (not at job start) because installation tokens expire
# after 1 hour — generating late keeps the lifetime budget on the actual push operations.
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Get GitHub App user ID
id: app-user
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
USER_ID=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
echo "user-id=${USER_ID}" >> "$GITHUB_OUTPUT"
- name: Configure git for release
env:
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
USER_ID: ${{ steps.app-user.outputs.user-id }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
run: |
git config user.name "${APP_SLUG}[bot]"
git config user.email "${USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${APP_TOKEN}@github.com/${REPO}.git"
# RELEASE CORE WEB APPLICATION
# NOTE: Each application has its own tag format - so it is safe to have release-it run multiple times
- name: Release Web
if: ${{ inputs.release_web }}
id: release_web
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
BUMP: ${{ inputs.bump }}
run: pnpm release-it "$BUMP" --ci -VV --config .release-it.json
- name: Debug dirty working dir
if: failure() && steps.release_web.outcome == 'failure'
run: |
echo "=== git status ==="
git status
echo "=== git diff ==="
git diff
# Point the 'release' branch at the HEAD of whichever branch triggered this run
# (main for normal releases, hotfix/* for hotfixes). Force-push avoids merge commits
# and any divergence from prior runs. After a hotfix release, the hotfix/* branch
# should be merged back to main via PR so the fix is preserved in main's history.
- name: Push to release branch (triggers Render deploy)
if: ${{ inputs.release_web }}
run: git push origin HEAD:release --force
# RELEASE WEB EXTENSION
# NOTE: Each application has its own tag format - so it is safe to have release-it run multiple times
- name: Release Web Extension
if: ${{ inputs.release_web_extension }}
id: release_web_ext
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
BUMP: ${{ inputs.bump }}
WEB_EXTENSION_ID_CHROME: ${{ secrets.WEB_EXTENSION_ID_CHROME }}
GOOGLE_WEB_EXT_PUBLISH_CLIENT_ID: ${{ secrets.GOOGLE_WEB_EXT_PUBLISH_CLIENT_ID }}
GOOGLE_WEB_EXT_PUBLISH_CLIENT_SECRET: ${{ secrets.GOOGLE_WEB_EXT_PUBLISH_CLIENT_SECRET }}
GOOGLE_WEB_EXT_PUBLISH_REFRESH_TOKEN: ${{ secrets.GOOGLE_WEB_EXT_PUBLISH_REFRESH_TOKEN }}
run: pnpm release-it "$BUMP" --ci -VV --config .release-it-web-ext.json
- name: Upload web extension zips
if: ${{ inputs.release_web_extension }}
uses: actions/upload-artifact@v6
with:
name: web-extension-zips
path: dist/web-extension-build/*.zip
retention-days: 30
# RELEASE DESKTOP
# NOTE: Each application has its own tag format - so it is safe to have release-it run multiple times
- name: Release Desktop
if: ${{ inputs.release_desktop }}
id: release_desktop
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
BUMP: ${{ inputs.bump }}
run: pnpm release-it "$BUMP" --ci -VV --config .release-it-desktop.json
# CAPTURE VERSIONS FOR SUMMARY
- name: Capture web version
if: ${{ always() && inputs.release_web && steps.release_web.outcome == 'success'
}}
id: capture_web_version
run: echo "version=$(node -p "require('./package.json').version")" >>
"$GITHUB_OUTPUT"
- name: Capture web extension version
if: ${{ always() && inputs.release_web_extension &&
steps.release_web_ext.outcome == 'success' }}
id: capture_web_ext_version
run: echo "version=$(node -p
"require('./apps/jetstream-web-extension/src/manifest.json').version")"
>> "$GITHUB_OUTPUT"
- name: Capture desktop version
if: ${{ always() && inputs.release_desktop && steps.release_desktop.outcome ==
'success' }}
id: capture_desktop_version
run: echo "version=$(node -p
"require('./apps/jetstream-desktop/package.json').version")" >>
"$GITHUB_OUTPUT"
# GENERATE RELEASE SUMMARY
- name: Generate release summary
if: always()
env:
RELEASE_WEB: ${{ inputs.release_web }}
RELEASE_WEB_EXT: ${{ inputs.release_web_extension }}
RELEASE_DESKTOP: ${{ inputs.release_desktop }}
WEB_VERSION: ${{ steps.capture_web_version.outputs.version }}
WEB_EXT_VERSION: ${{ steps.capture_web_ext_version.outputs.version }}
DESKTOP_VERSION: ${{ steps.capture_desktop_version.outputs.version }}
WEB_OUTCOME: ${{ steps.release_web.outcome }}
WEB_EXT_OUTCOME: ${{ steps.release_web_ext.outcome }}
DESKTOP_OUTCOME: ${{ steps.release_desktop.outcome }}
BUMP: ${{ inputs.bump }}
REPO: ${{ github.repository }}
run: |
get_status() {
local requested="$1" outcome="$2"
if [[ "$requested" != "true" ]]; then
echo "⏭️ Skipped"
elif [[ "$outcome" == "success" ]]; then
echo "✅ Released"
elif [[ "$outcome" == "failure" ]]; then
echo "❌ Failed"
elif [[ "$outcome" == "skipped" ]]; then
echo "⏭️ Skipped"
elif [[ "$outcome" == "cancelled" ]]; then
echo "🚫 Cancelled"
else
echo "⚠️ Unknown"
fi
}
get_release_link() {
local requested="$1" outcome="$2" version="$3" tag_prefix="$4"
if [[ "$requested" == "true" && "$outcome" == "success" && -n "$version" ]]; then
echo "[${tag_prefix}${version}](https://github.com/${REPO}/releases/tag/${tag_prefix}${version})"
else
echo "-"
fi
}
WEB_STATUS=$(get_status "$RELEASE_WEB" "$WEB_OUTCOME")
WEB_EXT_STATUS=$(get_status "$RELEASE_WEB_EXT" "$WEB_EXT_OUTCOME")
DESKTOP_STATUS=$(get_status "$RELEASE_DESKTOP" "$DESKTOP_OUTCOME")
WEB_LINK=$(get_release_link "$RELEASE_WEB" "$WEB_OUTCOME" "$WEB_VERSION" "v")
WEB_EXT_LINK=$(get_release_link "$RELEASE_WEB_EXT" "$WEB_EXT_OUTCOME" "$WEB_EXT_VERSION" "web-ext-v")
DESKTOP_LINK=$(get_release_link "$RELEASE_DESKTOP" "$DESKTOP_OUTCOME" "$DESKTOP_VERSION" "desktop-v")
{
echo "## 🚀 Release Summary (\`${BUMP}\`)"
echo ""
echo "| Component | Status | Version | Release |"
echo "|-----------|--------|---------|---------|"
echo "| Web | ${WEB_STATUS} | ${WEB_VERSION:-\`-\`} | ${WEB_LINK} |"
echo "| Web Extension | ${WEB_EXT_STATUS} | ${WEB_EXT_VERSION:-\`-\`} | ${WEB_EXT_LINK} |"
echo "| Desktop | ${DESKTOP_STATUS} | ${DESKTOP_VERSION:-\`-\`} | ${DESKTOP_LINK} |"
} >> "$GITHUB_STEP_SUMMARY"