Release #51
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: | |
| 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" |