name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: main
jobs:
detect:
name: Detect changed areas
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
code: ${{ steps.lanes.outputs.code }}
docs: ${{ steps.lanes.outputs.docs }}
desktop: ${{ steps.lanes.outputs.desktop }}
api: ${{ steps.lanes.outputs.api }}
remote: ${{ steps.lanes.outputs.remote }}
storybook: ${{ steps.lanes.outputs.storybook }}
surfaces: ${{ steps.lanes.outputs.surfaces }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 2
- name: Select lanes
id: lanes
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
run: |
if [[ "$EVENT_NAME" != "pull_request" ]]; then
changed="package.json"
else
# Without `--no-renames`, a moved file lists only its new path, and a move to `docs/`
# would hide the path that a lane reads.
changed="$(git diff --name-only --no-renames HEAD^1 HEAD)"
fi
# Markdown that no build or test reads changes no lane. Not every Markdown file: the API
# bundles CHANGELOG.md, and the renderer bundles Markdown from `resources/`.
docs='^((docs|plans|changelog\.d)/.+\.md|(.+/)?AGENTS\.md|(ANALYTICS|CODE_OF_CONDUCT|CONTRIBUTING|PRIVACY|README|SECURITY|SUPPORT)\.md)$'
all="$changed"
changed="$(grep -Ev "$docs" <<<"$changed" || true)"
shared='^(package\.json|bun\.lock|bunfig\.toml|\.nvmrc|biome\.json|tsconfig[^/]*\.json|\.github/|packages/|patches/|tools/|vendor/)'
lane() {
if grep -Eq "$shared|$2" <<<"$changed"; then
echo "$1=true" >> "$GITHUB_OUTPUT"
else
echo "$1=false" >> "$GITHUB_OUTPUT"
fi
}
# The inverse, for a lane that reads most of the repository: it runs when a changed path
# does not match the pattern.
lane_unless() {
if grep -Ev "$2" <<<"$changed" | grep -q .; then
echo "$1=true" >> "$GITHUB_OUTPUT"
else
echo "$1=false" >> "$GITHUB_OUTPUT"
fi
}
lane code '.'
# Every Markdown file, before the filter above removed some of them.
if grep -Eq "\.md$|^scripts/check-doc-links\.ts$" <<<"$all" || grep -Eq "$shared" <<<"$changed"; then
echo "docs=true" >> "$GITHUB_OUTPUT"
else
echo "docs=false" >> "$GITHUB_OUTPUT"
fi
lane_unless desktop '^(apps/(auth-api|mobile|mobile-updates|site-router|status)|docker|remote)/'
lane api '^(apps/(auth-api|site-router)|src/renderer|resources)/|^(apps/mobile/)?CHANGELOG\.md$'
lane remote '^(remote|scripts)/'
lane storybook '^(src|apps/(auth-api|status)|\.storybook|resources|marketplace|build)/'
lane surfaces '^(apps/(mobile|mobile-updates|site-router|status)|remote|scripts)/'
cat "$GITHUB_OUTPUT"
check:
name: Check
needs: detect
if: ${{ !cancelled() && (needs.detect.result != 'success' || needs.detect.outputs.code == 'true' || needs.detect.outputs.docs == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CODE: ${{ needs.detect.result != 'success' || needs.detect.outputs.code == 'true' }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
- name: Check unused files and dependencies
if: env.CODE == 'true'
run: bun run knip:check
- name: Check image asset directories
if: env.CODE == 'true'
run: bun run check:assets
- name: Check Markdown links and anchors
run: bun run check:doc-links
- name: Check desktop application
if: env.CODE == 'true'
run: bun run check:desktop:static
- parallel:
- name: Check translation catalogs
if: env.CODE == 'true'
run: bun run i18n:check
- name: Check ratcheted TypeScript options
if: env.CODE == 'true'
run: bun run types:ratchet
- name: Check agent context budget
if: env.CODE == 'true'
run: bun run check:agent-context
- name: Check translated What's new notes
if: env.CODE == 'true'
run: bun scripts/whats-new-translations.ts
browser:
name: Browser smoke
needs: detect
if: needs.detect.outputs.desktop == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- parallel:
- name: Set up Bun
uses: ./.github/actions/setup-bun
- name: Install a virtual display
shell: bash
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends --yes xvfb
- name: Run the browser smoke test
run: xvfb-run -a bun run test:browser
- name: Run the agent browser session E2E
run: xvfb-run -a bun run test:browser-agent-session
- name: Run the browser steps E2E
run: xvfb-run -a bun run test:browser-steps
- name: Run the browser dialogs E2E
run: xvfb-run -a bun run test:browser-dialogs
- name: Run the browser fingerprint E2E
run: xvfb-run -a bun run test:browser-fingerprint
- name: Run the browser ambiguous targets E2E
run: xvfb-run -a bun run test:browser-ambiguous-targets
tests-desktop:
name: Tests (desktop ${{ matrix.shard }}/2)
needs: detect
if: needs.detect.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
shard: [1, 2]
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run desktop tests
run: bun run test:desktop -- --shard=${{ matrix.shard }}/2
signal-e2e:
name: Signal E2E
needs: detect
if: needs.detect.outputs.remote == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the Signal E2E test
run: bun run test:signal
- name: Upload the Signal E2E report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: signal-security-e2e
path: .openbot-build/signal-security-e2e/
if-no-files-found: ignore
automation-cli-e2e:
name: Local scripts CLI E2E
needs: detect
if: needs.detect.outputs.desktop == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the Local scripts CLI E2E test
run: bun run test:automation-cli
- name: Upload the Local scripts CLI E2E report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: automation-cli-e2e
path: .openbot-build/automation-cli-e2e/
if-no-files-found: ignore
team-api-routes-e2e:
name: Team API routes E2E
needs: detect
if: needs.detect.outputs.desktop == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the Team API routes E2E test
run: bun run test:team-api-routes
- name: Upload the Team API routes E2E report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: team-api-routes-e2e
path: .openbot-build/team-api-routes-e2e/
if-no-files-found: ignore
reverse-actions-e2e:
name: Team API and reverse actions E2E
needs: detect
if: needs.detect.outputs.desktop == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the Team API legacy clients E2E test
if: ${{ !cancelled() }}
run: bun run test:team-legacy-clients
- name: Run the Team session revoke and reconnect E2E test
if: ${{ !cancelled() }}
run: bun run test:team-session-revoke
- name: Run the sign-in relay paste E2E test
if: ${{ !cancelled() }}
run: bun run test:sign-in-relay-paste
- name: Run the sign-in relay replay E2E test
if: ${{ !cancelled() }}
run: bun run test:sign-in-relay-replay
- name: Run the MCP member access E2E test
if: ${{ !cancelled() }}
run: bun run test:mcp-member-access
- name: Run the server mute and unmute E2E test
if: ${{ !cancelled() }}
run: bun run test:server-mute
- name: Run the desktop and phone notification E2E test
if: ${{ !cancelled() }}
run: bun run test:desktop-notifications
- name: Run the phone notification away and Live Activity E2E test
if: ${{ !cancelled() }}
run: bun run test:push-notification-away
- name: Run the computer activity phone notification E2E test
if: ${{ !cancelled() }}
run: bun run test:computer-activity
- name: Run the notification summary E2E test
if: ${{ !cancelled() }}
run: bun run test:notification-summary
- name: Run the side thread notification and Live Activity link E2E test
if: ${{ !cancelled() }}
run: bun run test:thread-notifications
- name: Run the routine pause and resume E2E test
if: ${{ !cancelled() }}
run: bun run test:routine-pause-resume
- name: Run the routine time zone move E2E test
if: ${{ !cancelled() }}
run: bun run test:routine-timezone-move
- name: Run the routine overnight window E2E test
if: ${{ !cancelled() }}
run: bun run test:routine-overnight-window
- name: Run the agent open-wait tools E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-waits-tools
- name: Run the agent hide and restore E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-hide-restore
- name: Run the messaging pause and resume E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-pause-resume
- name: Run the Discord role list and reply context E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-discord-access
- name: Run the Discord embeds E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-discord-embeds
- name: Run the place history E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-place-history
- name: Run the Telegram answer mentions E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-telegram-mentions
- name: Run the Slack ping reply E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-slack-ping-reply
- name: Run the turn input E2E test
if: ${{ !cancelled() }}
run: bun run test:turn-input
- name: Run the zero-filled settings file E2E test
if: ${{ !cancelled() }}
run: bun run test:zero-filled-settings
- name: Run the server key rotation E2E test
if: ${{ !cancelled() }}
run: bun run test:host-key-rotation
- name: Run the provider update during a turn E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-update-busy
- name: Run the provider update after an app upgrade E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-update-after-app-upgrade
- name: Run the provider CLI removal E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-runtime-remove
- name: Run the provider session after a process exit E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-exit-session
- name: Run the ACP subagent card E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-subagents
- name: Run the Codex subagent steps E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-subagent-steps
- name: Run the subagent status and failure alert E2E test
if: ${{ !cancelled() }}
run: bun run test:subagent-status
- name: Run the hung Codex app-server E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-hung-app-server
- name: Run the Codex mid-turn compaction E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-midturn-compaction
- name: Run the Codex missing rollout backfill E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-missing-rollout
- name: Run the Codex idle thread release E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-idle-thread-release
- name: Run the Codex usage metering E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-usage-metering
- name: Run the Codex usage reset E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-usage-reset
- name: Run the Codex model capacity retry E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-capacity-retry
- name: Run the Codex warning banner E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-warning-banner
- name: Run the usage read pacing E2E test
if: ${{ !cancelled() }}
run: bun run test:usage-read-pacing
- name: Run the routine session E2E test
if: ${{ !cancelled() }}
run: bun run test:routine-fresh-session
- name: Run the routine failure report E2E test
if: ${{ !cancelled() }}
run: bun run test:routine-report-failure
- name: Run the agent webhook routine E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-webhook-routine
- name: Run the agent channel management E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-channel-manage
- name: Run the agent channel creation E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-channel-create
- name: Run the Library folders agent tool E2E test
if: ${{ !cancelled() }}
run: bun run test:library-folders
- name: Run the agent workflow gate E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-workflow-gate
- name: Run the pinned message agent tool E2E test
if: ${{ !cancelled() }}
run: bun run test:pinned-message-agent-tool
- name: Run the messaging owner card E2E test
if: ${{ !cancelled() }}
run: bun run test:messaging-owner-card
- name: Run the agent profile accounts E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-profile-accounts
- name: Run the browser profile agents E2E test
if: ${{ !cancelled() }}
run: bun run test:browser-profile-agents
- name: Run the Claude usage after a resume E2E test
if: ${{ !cancelled() }}
run: bun run test:claude-usage-resume
- name: Run the Claude background hold at low memory E2E test
if: ${{ !cancelled() }}
run: bun run test:claude-background-hold
- name: Run the Claude turn context after a runtime refresh E2E test
if: ${{ !cancelled() }}
run: bun run test:claude-turn-context
- name: Run the Claude late steer E2E test
if: ${{ !cancelled() }}
run: bun run test:claude-late-steer
- name: Run the provider session reuse E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-session-reuse
- name: Run the backup model failover E2E test
if: ${{ !cancelled() }}
run: bun run test:backup-model-failover
- name: Run the Codex empty lead turn E2E test
if: ${{ !cancelled() }}
run: bun run test:codex-empty-lead-turn
- name: Run the ZIP attachment E2E test
if: ${{ !cancelled() }}
run: bun run test:zip-attachments
- name: Run the MP4 attachment E2E test
if: ${{ !cancelled() }}
run: bun run test:mp4-attachments
- name: Run the SVG attachment E2E test
if: ${{ !cancelled() }}
run: bun run test:svg-attachments
- name: Run the any file attachment E2E test
if: ${{ !cancelled() }}
run: bun run test:any-file-attachments
- name: Run the channel notifications and channel settings E2E test
if: ${{ !cancelled() }}
run: bun run test:channel-notifications
- name: Run the agent MCP server tools E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-mcp-tools
- name: Run the MCP sign-in client refusal E2E test
if: ${{ !cancelled() }}
run: bun run test:mcp-sign-in-client-refused
- name: Run the MCP sign-in client metadata document E2E test
if: ${{ !cancelled() }}
run: bun run test:mcp-sign-in-client-metadata-document
- name: Run the unreadable MCP server row E2E test
if: ${{ !cancelled() }}
run: bun run test:mcp-server-unreadable-row
- name: Run the agent bulk settings E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-update-agents
- name: Run the Gemini usage E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-usage
- name: Run the Gemini accounts E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-accounts
- name: Run the Gemini usage metering E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-usage-metering
- name: Run the Gemini quota failover E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-quota-failover
- name: Run the Gemini Windows zip E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-windows-zip
- name: Run the workflow model steps E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-model-steps
- name: Run the workflow Discord E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-discord
- name: Run the workflow Telegram E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-telegram
- name: Run the workflow Slack E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-slack
- name: Run the workflow channel output E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-channel-output
- name: Run the workflow agent tools E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-agent-tools
- name: Run the workflow join E2E test
if: ${{ !cancelled() }}
run: bun run test:workflow-join
- name: Run the Discord files to GitHub E2E test
if: ${{ !cancelled() }}
run: bun run test:discord-github-files
- name: Run the OpenCode usage metering E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-usage-metering
- name: Run the Claude API key E2E test
if: ${{ !cancelled() }}
run: bun run test:claude-api-key
- name: Run the Claude stored sign-in E2E test
if: ${{ !cancelled() }}
run: bun run test:claude-stored-sign-in
- name: Run the Cursor usage E2E test
if: ${{ !cancelled() }}
run: bun run test:cursor-usage
- name: Run the OpenCode Go usage E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-go-usage
- name: Run the Pi sign-in outside OpenBot E2E test
if: ${{ !cancelled() }}
run: bun run test:pi-sign-in-watch
- name: Run the provider launch order E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-launch-order
- name: Run the agent API key E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-api-keys
- name: Run the remote connect analytics E2E test
if: ${{ !cancelled() }}
run: bun run test:remote-connect-analytics
- name: Run the memory review E2E test
if: ${{ !cancelled() }}
run: bun run test:memory-review
- name: Run the local skill revision E2E test
if: ${{ !cancelled() }}
run: bun run test:skill-revision
- name: Upload the reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: reverse-actions-e2e
path: |
.openbot-build/team-api-legacy-clients-e2e/
.openbot-build/team-session-revoke-e2e/
.openbot-build/sign-in-relay-paste-e2e/
.openbot-build/sign-in-relay-replay-e2e/
.openbot-build/mcp-member-access-e2e/
.openbot-build/server-mute-e2e/
.openbot-build/desktop-notifications-e2e/
.openbot-build/push-notification-away-e2e/
.openbot-build/computer-activity-e2e/
.openbot-build/notification-summary-e2e/
.openbot-build/thread-notifications-e2e/
.openbot-build/routine-pause-resume-e2e/
.openbot-build/routine-timezone-move-e2e/
.openbot-build/routine-overnight-window-e2e/
.openbot-build/agent-waits-tools-e2e/
.openbot-build/agent-hide-restore-e2e/
.openbot-build/messaging-pause-resume-e2e/
.openbot-build/messaging-discord-access-e2e/
.openbot-build/messaging-discord-embeds-e2e/
.openbot-build/messaging-place-history-e2e/
.openbot-build/messaging-telegram-mentions-e2e/
.openbot-build/messaging-slack-ping-reply-e2e/
.openbot-build/turn-input-e2e/
.openbot-build/zero-filled-settings-e2e/
.openbot-build/provider-update-busy-e2e/
.openbot-build/provider-update-after-app-upgrade-e2e/
.openbot-build/provider-runtime-remove-e2e/
.openbot-build/provider-exit-session-e2e/
.openbot-build/acp-subagents-e2e/
.openbot-build/codex-subagent-steps-e2e/
.openbot-build/subagent-status-e2e/
.openbot-build/codex-hung-app-server-e2e/
.openbot-build/codex-midturn-compaction-e2e/
.openbot-build/codex-missing-rollout-e2e/
.openbot-build/codex-idle-thread-release-e2e/
.openbot-build/codex-usage-metering-e2e/
.openbot-build/codex-usage-reset-e2e/
.openbot-build/codex-capacity-retry-e2e/
.openbot-build/usage-read-pacing-e2e/
.openbot-build/routine-fresh-session-e2e/
.openbot-build/routine-report-failure-e2e/
.openbot-build/agent-webhook-routine-e2e/
.openbot-build/agent-channel-manage-e2e/
.openbot-build/agent-channel-create-e2e/
.openbot-build/library-folders-e2e/
.openbot-build/agent-workflow-gate-e2e/
.openbot-build/pinned-message-agent-tool-e2e/
.openbot-build/messaging-owner-card-e2e/
.openbot-build/agent-profile-accounts-e2e/
.openbot-build/browser-profile-agents-e2e/
.openbot-build/claude-usage-resume-e2e/
.openbot-build/claude-background-hold-e2e/
.openbot-build/claude-turn-context-e2e/
.openbot-build/claude-late-steer-e2e/
.openbot-build/provider-session-reuse-e2e/
.openbot-build/backup-model-failover-e2e/
.openbot-build/codex-empty-lead-turn-e2e/
.openbot-build/zip-attachment-e2e/
.openbot-build/mp4-attachment-e2e/
.openbot-build/svg-attachment-e2e/
.openbot-build/any-file-attachment-e2e/
.openbot-build/agent-mcp-tools-e2e/
.openbot-build/mcp-server-unreadable-row-e2e/
.openbot-build/agent-update-agents-e2e/
.openbot-build/antigravity-usage-e2e/
.openbot-build/antigravity-accounts-e2e/
.openbot-build/antigravity-usage-metering-e2e/
.openbot-build/antigravity-quota-failover-e2e/
.openbot-build/workflow-model-steps-e2e/
.openbot-build/workflow-discord-e2e/
.openbot-build/workflow-telegram-e2e/
.openbot-build/workflow-slack-e2e/
.openbot-build/workflow-channel-output-e2e/
.openbot-build/workflow-agent-tools-e2e/
.openbot-build/workflow-join-e2e/
.openbot-build/discord-github-files-e2e/
.openbot-build/opencode-usage-metering-e2e/
.openbot-build/claude-api-key-e2e/
.openbot-build/claude-stored-sign-in-e2e/
.openbot-build/cursor-usage-e2e/
.openbot-build/opencode-go-usage-e2e/
.openbot-build/pi-sign-in-watch-e2e/
.openbot-build/provider-launch-order-e2e/
.openbot-build/agent-api-keys-e2e/
.openbot-build/remote-connect-analytics-e2e/
.openbot-build/memory-review-e2e/
.openbot-build/skill-revision-e2e.json
if-no-files-found: ignore
provider-fakes-e2e:
name: Provider fakes E2E
needs: detect
if: needs.detect.outputs.desktop == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the custom ACP agents E2E test
if: ${{ !cancelled() }}
run: bun run test:custom-agents
- name: Run the custom ACP agent login shell PATH E2E test
if: ${{ !cancelled() }}
run: bun run test:custom-agents:login-path
- name: Run the Codex login shell PATH E2E test
if: ${{ !cancelled() }}
run: bun run test:codex:login-path
- name: Run the Antigravity login shell PATH E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity:login-path
- name: Run the Antigravity MCP load failure E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-mcp-load-failure
- name: Run the Antigravity expired token E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-expired-token
- name: Run the Antigravity tool filter E2E test
if: ${{ !cancelled() }}
run: bun run test:antigravity-tool-filter
- name: Run the ACP additional directories E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-directories
- name: Run the ACP flavors E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-flavors
- name: Run the ACP MCP server names E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-mcp-names
- name: Run the Copilot MCP relay E2E test
if: ${{ !cancelled() }}
run: bun run test:copilot-mcp-relay
- name: Run the ACP turn stall E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-turn-stall
- name: Run the agent database host start E2E test
if: ${{ !cancelled() }}
run: bun run test:agent-database-host-start
- name: Run the ACP instructions once E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-instructions-once
- name: Run the ACP audio input E2E test
if: ${{ !cancelled() }}
run: bun run test:acp-audio-input
- name: Run the Grok steer E2E test
if: ${{ !cancelled() }}
run: bun run test:grok-steer
- name: Run the Grok question E2E test
if: ${{ !cancelled() }}
run: bun run test:grok-question
- name: Run the Grok partial model list E2E test
if: ${{ !cancelled() }}
run: bun run test:grok-partial-model-list
- name: Run the Pi steer E2E test
if: ${{ !cancelled() }}
run: bun run test:pi-steer
- name: Run the Pi idle release E2E test
if: ${{ !cancelled() }}
run: bun run test:pi-idle-release
- name: Run the Muse instructions once E2E test
if: ${{ !cancelled() }}
run: bun run test:muse-instructions-once
- name: Run the provider conformance E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-conformance
- name: Run the OpenCode expired key E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-expired-key
- name: Run the OpenCode downgraded session E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-downgraded-session
- name: Run the OpenCode service failure E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-service-failure
- name: Run the OpenCode thought interleave E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-thought-interleave
- name: Run the OpenCode background turn E2E test
if: ${{ !cancelled() }}
run: bun run test:opencode-background-turn
- name: Run the Cursor stream closed E2E test
if: ${{ !cancelled() }}
run: bun run test:cursor-stream-closed
- name: Run the Cursor model set E2E test
if: ${{ !cancelled() }}
run: bun run test:cursor-model-set
- name: Run the Cline free models E2E test
if: ${{ !cancelled() }}
run: bun run test:cline-free-models
- name: Run the recorded provider transcripts E2E test
if: ${{ !cancelled() }}
run: bun run test:provider-transcript-replay
- name: Upload the reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: provider-fakes-e2e
path: |
.openbot-build/custom-acp-agent-e2e/
.openbot-build/custom-acp-agent-login-path-e2e/
.openbot-build/codex-login-path-e2e/
.openbot-build/antigravity-login-path-e2e/
.openbot-build/antigravity-mcp-load-failure-e2e/
.openbot-build/antigravity-expired-token-e2e/
.openbot-build/antigravity-tool-filter-e2e/
.openbot-build/acp-additional-directories-e2e/
.openbot-build/acp-flavor-e2e/
.openbot-build/acp-mcp-server-names-e2e/
.openbot-build/copilot-mcp-relay-e2e/
.openbot-build/acp-turn-stall-e2e/
.openbot-build/agent-database-host-start-e2e/
.openbot-build/acp-instructions-once-e2e/
.openbot-build/acp-audio-input-e2e/
.openbot-build/grok-steer-e2e/
.openbot-build/grok-question-e2e/
.openbot-build/grok-partial-model-list-e2e/
.openbot-build/pi-steer-e2e/
.openbot-build/pi-idle-release-e2e/
.openbot-build/muse-instructions-once-e2e/
.openbot-build/provider-conformance-e2e/
.openbot-build/opencode-expired-key-e2e/
.openbot-build/opencode-downgraded-session-e2e/
.openbot-build/opencode-service-failure-e2e/
.openbot-build/opencode-thought-interleave-e2e/
.openbot-build/opencode-background-turn-e2e/
.openbot-build/cursor-stream-closed-e2e/
.openbot-build/cursor-model-set-e2e/
.openbot-build/cline-free-models-e2e/
.openbot-build/provider-transcript-replay-e2e/
if-no-files-found: ignore
surfaces:
name: Surfaces
needs: detect
if: needs.detect.outputs.surfaces == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- parallel:
- name: Typecheck mobile
run: bun run mobile:typecheck
- name: Typecheck team client
run: bun run typecheck:team-client
- parallel:
- name: Typecheck site router
run: bun run typecheck:sites
- name: Typecheck mobile update server
run: bun run typecheck:updates
- name: Typecheck remote API
run: bun run typecheck:remote
- name: Typecheck status page
run: bun run typecheck:status
- parallel:
- name: Typecheck logging
run: bun run typecheck:logging
- name: Typecheck telemetry
run: bun run typecheck:telemetry
- name: Typecheck user errors
run: bun run typecheck:user-errors
- name: Typecheck i18n
run: bun run typecheck:i18n
- name: Validate the remote Compose files
run: bun run remote:check:compose
mobile-share-sender-e2e:
name: Share extension page E2E
needs: detect
if: needs.detect.outputs.surfaces == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the share extension page E2E test
run: bun run test:mobile-share-sender
- name: Upload the share extension page E2E report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: mobile-share-sender-e2e
path: .openbot-build/mobile-share-sender-e2e/report.json
if-no-files-found: ignore
mobile-ota-e2e:
name: Mobile update server E2E
needs: detect
if: needs.detect.outputs.surfaces == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Run the mobile update server E2E test
run: bun run test:mobile-ota
- name: Run the mobile OTA release notes E2E test
env:
GIT_AUTHOR_NAME: ci
GIT_AUTHOR_EMAIL: ci@example.invalid
GIT_COMMITTER_NAME: ci
GIT_COMMITTER_EMAIL: ci@example.invalid
run: bun run test:mobile-ota-notes
- name: Run the mobile release announcement E2E test
run: bun run test:mobile-release-announcement
- name: Upload the mobile update E2E reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: mobile-ota-e2e
path: |
.openbot-build/mobile-ota-e2e/report.json
.openbot-build/mobile-ota-release-notes-e2e.json
.openbot-build/mobile-release-announcement-e2e.json
if-no-files-found: ignore
mobile-runtime:
name: Mobile runtime version
needs: detect
if: github.event_name == 'pull_request' && needs.detect.outputs.surfaces == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 2
- name: Find changes that can reach the native build
id: inputs
shell: bash
run: |
if git diff --name-only --no-renames HEAD^1 HEAD | grep -Eq '^(apps/mobile/(app\.json|app\.config\.js|eas\.json|package\.json|fingerprint\.config\.js|\.gitignore|modules/|plugins/|certs/|assets/|src/features/share/extension/)|patches/|bun\.lock$|scripts/mobile-share-sender\.ts$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Mobile runtime: no change. No file that the native build reads changed." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Set up Bun
if: steps.inputs.outputs.changed == 'true'
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Install the base commit
if: steps.inputs.outputs.changed == 'true'
shell: bash
run: |
git worktree add --detach "$RUNNER_TEMP/base" HEAD^1
cd "$RUNNER_TEMP/base"
bun install --frozen-lockfile --ignore-scripts
- name: Compare the runtime versions
if: steps.inputs.outputs.changed == 'true'
run: bun scripts/mobile-ota.ts compare-runtime --base-dir "$RUNNER_TEMP/base"
api:
name: API
needs: detect
if: needs.detect.outputs.api == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- name: Look for the home page beam key
id: beam
env:
LASER_FLOW_DEPLOY_KEY: ${{ secrets.LASER_FLOW_DEPLOY_KEY }}
run: echo "present=$([ -n "$LASER_FLOW_DEPLOY_KEY" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Check out the home page beam
if: steps.beam.outputs.present == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: nightly-labs/openbot-laser-flow
ssh-key: ${{ secrets.LASER_FLOW_DEPLOY_KEY }}
path: apps/auth-api/src/vendor/laser-flow
persist-credentials: false
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Check API
run: bun run check:api
- name: Upload preview build
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: cloudflare-preview
path: apps/auth-api/dist
include-hidden-files: true
if-no-files-found: error
retention-days: 3
storybook:
name: Storybook build
needs: detect
if: needs.detect.outputs.storybook == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Bun
uses: ./.github/actions/setup-bun
with:
install: ignore-scripts
- name: Build Storybook
run: bun run build-storybook
env:
OPENBOT_STORYBOOK_CHECK: "true"
all-checks-pass:
name: All required checks pass
if: always()
needs: [detect, check, browser, tests-desktop, signal-e2e, automation-cli-e2e, team-api-routes-e2e, reverse-actions-e2e, provider-fakes-e2e, surfaces, mobile-share-sender-e2e, mobile-ota-e2e, api, storybook]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Fail on a failed or cancelled job
shell: bash
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
for result in $RESULTS; do
case "$result" in
success | skipped) ;;
*)
echo "A required job ended as $result." >&2
exit 1
;;
esac
done
deploy-production:
name: Deploy Cloudflare production
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [check, browser, tests-desktop, signal-e2e, automation-cli-e2e, team-api-routes-e2e, reverse-actions-e2e, provider-fakes-e2e, api, storybook, surfaces, mobile-share-sender-e2e]
runs-on: ubuntu-latest
timeout-minutes: 15
environment: cloudflare-production
concurrency:
group: cloudflare-production-worker
cancel-in-progress: false
queue: max
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Check out the home page beam
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: nightly-labs/openbot-laser-flow
ssh-key: ${{ secrets.LASER_FLOW_DEPLOY_KEY }}
path: apps/auth-api/src/vendor/laser-flow
persist-credentials: false
- name: Set up Bun
uses: ./.github/actions/setup-bun
- name: Build production Worker
run: bun run api:build
- name: Keep the code files of earlier builds
run: bun apps/auth-api/scripts/keep-previous-assets.ts
- name: Prepare runtime secrets
id: runtime-secrets
shell: bash
env:
EMAIL_SMTP_PASSWORD: ${{ secrets.EMAIL_SMTP_PASSWORD }}
SKILLS_ADMIN_TOKEN: ${{ secrets.SKILLS_ADMIN_TOKEN }}
SITE_OPERATIONS_ADMIN_TOKEN: ${{ secrets.SITE_OPERATIONS_ADMIN_TOKEN }}
SERVER_CLAIMS_ADMIN_TOKEN: ${{ secrets.SERVER_CLAIMS_ADMIN_TOKEN }}
SITE_REPORT_HASH_SECRET: ${{ secrets.SITE_REPORT_HASH_SECRET }}
REMOTE_TICKET_PRIVATE_JWK: ${{ secrets.REMOTE_TICKET_PRIVATE_JWK }}
REMOTE_TICKET_PUBLIC_JWKS: ${{ secrets.REMOTE_TICKET_PUBLIC_JWKS }}
REMOTE_AUTH_WEBHOOK_SECRET: ${{ secrets.REMOTE_AUTH_WEBHOOK_SECRET }}
OPENPANEL_CLIENT_ID: ${{ secrets.OPENPANEL_CLIENT_ID }}
OPENPANEL_CLIENT_SECRET: ${{ secrets.OPENPANEL_CLIENT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
BOAT_API_KEY: ${{ secrets.BOAT_API_KEY }}
BOAT_WEBHOOK_SECRET: ${{ secrets.BOAT_WEBHOOK_SECRET }}
OPENBOT_GITHUB_APP_PRIVATE_KEY: ${{ secrets.OPENBOT_GITHUB_APP_PRIVATE_KEY }}
APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }}
APNS_KEY_ID: ${{ secrets.APNS_KEY_ID }}
FCM_SERVICE_ACCOUNT: ${{ secrets.FCM_SERVICE_ACCOUNT }}
COMPANY_SIGN_IN_SECRET: ${{ secrets.COMPANY_SIGN_IN_SECRET }}
run: |
secrets_path="$RUNNER_TEMP/openbot-worker-secrets.json"
umask 077
SECRETS_PATH="$secrets_path" bun -e '
const smtp = process.env.EMAIL_SMTP_PASSWORD;
const skillsAdmin = process.env.SKILLS_ADMIN_TOKEN;
const siteOperationsAdmin = process.env.SITE_OPERATIONS_ADMIN_TOKEN;
const serverClaimsAdmin = process.env.SERVER_CLAIMS_ADMIN_TOKEN;
const siteReportHash = process.env.SITE_REPORT_HASH_SECRET;
const remotePrivateJwk = process.env.REMOTE_TICKET_PRIVATE_JWK;
const remotePublicJwks = process.env.REMOTE_TICKET_PUBLIC_JWKS;
const remoteWebhookSecret = process.env.REMOTE_AUTH_WEBHOOK_SECRET;
const openpanelClientId = process.env.OPENPANEL_CLIENT_ID?.trim();
const openpanelClientSecret = process.env.OPENPANEL_CLIENT_SECRET?.trim();
if (!openpanelClientId || !openpanelClientSecret) {
throw new Error("OPENPANEL_CLIENT_ID and OPENPANEL_CLIENT_SECRET are required for production analytics.");
}
const path = process.env.SECRETS_PATH;
if (
!smtp ||
!skillsAdmin ||
!siteReportHash ||
!remotePrivateJwk ||
!remotePublicJwks ||
!remoteWebhookSecret ||
!path
) {
throw new Error("Required production secrets are missing.");
}
// A Stripe or boat key without its webhook secret takes payments or makes sandboxes that
// the Worker never sees, so each pair is set together or not at all.
const optional = {};
for (const names of [
["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"],
["BOAT_API_KEY", "BOAT_WEBHOOK_SECRET"],
// The Live Activity relay stays off until both are set.
["APNS_PRIVATE_KEY", "APNS_KEY_ID"],
]) {
const present = names.filter((name) => process.env[name]?.trim());
if (present.length === 0) continue;
if (present.length !== names.length) throw new Error(`Set ${names.join(" and ")} together.`);
for (const name of names) optional[name] = process.env[name].trim();
}
const githubAppKey = process.env.OPENBOT_GITHUB_APP_PRIVATE_KEY?.trim();
if (githubAppKey) optional.GITHUB_APP_PRIVATE_KEY = githubAppKey;
// Company sign-in stays off until it is set. It encrypts the stored client secrets, so it never changes.
const companySignInSecret = process.env.COMPANY_SIGN_IN_SECRET?.trim();
if (companySignInSecret) {
if (new TextEncoder().encode(companySignInSecret).byteLength < 32) {
throw new Error("COMPANY_SIGN_IN_SECRET must have at least 32 bytes.");
}
optional.COMPANY_SIGN_IN_SECRET = companySignInSecret;
}
// The Android notification relay stays off until the Firebase service account key is set.
const fcmServiceAccount = process.env.FCM_SERVICE_ACCOUNT?.trim();
if (fcmServiceAccount) {
let parsed;
try {
parsed = JSON.parse(fcmServiceAccount);
} catch {
parsed = null;
}
if (parsed?.type !== "service_account") throw new Error("FCM_SERVICE_ACCOUNT is not a Firebase service account key file.");
optional.FCM_SERVICE_ACCOUNT = fcmServiceAccount;
}
const stripeKey = optional.STRIPE_SECRET_KEY;
if (stripeKey && !stripeKey.startsWith("sk_live_") && !stripeKey.startsWith("rk_live_")) {
throw new Error("STRIPE_SECRET_KEY is not a live key.");
}
const secrets = {
...optional,
OPENPANEL_CLIENT_ID: openpanelClientId,
OPENPANEL_CLIENT_SECRET: openpanelClientSecret,
EMAIL_SMTP_PASSWORD: smtp,
SKILLS_ADMIN_TOKEN: skillsAdmin,
SITE_REPORT_HASH_SECRET: siteReportHash,
...(siteOperationsAdmin ? { SITE_OPERATIONS_ADMIN_TOKEN: siteOperationsAdmin } : {}),
...(serverClaimsAdmin ? { SERVER_CLAIMS_ADMIN_TOKEN: serverClaimsAdmin } : {}),
REMOTE_TICKET_PRIVATE_JWK: remotePrivateJwk,
REMOTE_TICKET_PUBLIC_JWKS: remotePublicJwks,
REMOTE_AUTH_WEBHOOK_SECRET: remoteWebhookSecret,
};
await Bun.write(path, JSON.stringify(secrets));
'
echo "path=$secrets_path" >> "$GITHUB_OUTPUT"
- name: Apply production D1 migrations
working-directory: apps/auth-api
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PRODUCTION_DEPLOY_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: ../../node_modules/.bin/wrangler d1 migrations apply DB --remote --config wrangler.jsonc
- name: Deploy public site router
working-directory: apps/site-router
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_SITE_ROUTER_DEPLOY_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: ../../node_modules/.bin/wrangler deploy --config wrangler.jsonc --keep-vars
- name: Deploy email relay Worker
working-directory: apps/auth-api
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PRODUCTION_DEPLOY_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: ../../node_modules/.bin/wrangler deploy --config wrangler.email-relay.jsonc
- name: Deploy production Worker
working-directory: apps/auth-api
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PRODUCTION_DEPLOY_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: ../../node_modules/.bin/wrangler deploy --config dist/server/wrangler.json --keep-vars --secrets-file "${{ steps.runtime-secrets.outputs.path }}"
- name: Remove temporary runtime secrets
if: always() && steps.runtime-secrets.outputs.path != ''
run: rm -f "${{ steps.runtime-secrets.outputs.path }}"
- name: Verify production deployment
shell: bash
env:
SITE_HOSTING_EXPECTED_ENABLED: ${{ vars.SITE_HOSTING_EXPECTED_ENABLED }}
run: |
verification_dir="$(mktemp -d)"
trap 'rm -rf "$verification_dir"' EXIT
fail() {
echo "::error title=Verify production deployment::$1"
exit 1
}
require_status() {
test "$1" = "$2" || fail "$3 returned HTTP $1. Expected HTTP $2."
}
require_header() {
if ! tr -d '\r' < "$1" | grep -Eiq "$2"; then
echo "Response headers of $3:"
tr -d '\r' < "$1"
fail "$3 has no response header that matches '$2'."
fi
}
curl --fail --silent --show-error --retry 5 --retry-all-errors https://openbot.run/ >/dev/null
join_status="$(curl --silent --show-error --retry 5 --retry-all-errors \
--max-redirs 0 --dump-header "$verification_dir/join.headers" \
--output "$verification_dir/join.html" --write-out '%{http_code}' \
https://openbot.run/join)"
require_status "$join_status" 200 https://openbot.run/join
require_header "$verification_dir/join.headers" '^cache-control:.*no-store' https://openbot.run/join
require_header "$verification_dir/join.headers" '^referrer-policy:.*no-referrer' https://openbot.run/join
association_url=https://openbot.run/.well-known/apple-app-site-association
association_status="$(curl --silent --show-error --retry 5 --retry-all-errors \
--max-redirs 0 --dump-header "$verification_dir/association.headers" \
--output "$verification_dir/association.json" --write-out '%{http_code}' \
"$association_url")"
require_status "$association_status" 200 "$association_url"
require_header "$verification_dir/association.headers" '^content-type: application/json' "$association_url"
if ! jq -e '.applinks.details == [{"appID":"ZTRDTUL87R.app.openbot.desktop","paths":["/join"]},{"appID":"ZTRDTUL87R.run.openbot.mobile","paths":["/join","/agents/*"]},{"appID":"8A47NKRZSJ.run.openbot.mobile","paths":["/join","/agents/*"]}]' \
"$verification_dir/association.json" >/dev/null; then
echo "Response headers of $association_url:"
tr -d '\r' < "$verification_dir/association.headers"
echo "Response body of $association_url (first 4000 bytes):"
head -c 4000 "$verification_dir/association.json"
echo
fail "$association_url does not have the expected applinks.details."
fi
curl --fail --silent --show-error --retry 5 --retry-all-errors https://api.openbot.run/health/live >/dev/null
curl --fail --silent --show-error --retry 5 --retry-all-errors https://api.openbot.run/health/ready >/dev/null
site_url=https://deployment-smoke-check.openbot.site/
site_status="$(curl --silent --show-error --retry 5 --retry-all-errors \
--max-redirs 0 --dump-header "$verification_dir/site-router.headers" \
--output "$verification_dir/site-router.txt" --write-out '%{http_code}' \
"$site_url")"
case "$SITE_HOSTING_EXPECTED_ENABLED" in
true) require_status "$site_status" 404 "$site_url" ;;
false) require_status "$site_status" 503 "$site_url" ;;
*) fail "SITE_HOSTING_EXPECTED_ENABLED must be true or false." ;;
esac
require_header "$verification_dir/site-router.headers" '^cache-control:.*no-store' "$site_url"
require_header "$verification_dir/site-router.headers" '^x-content-type-options:.*nosniff' "$site_url"
require_header "$verification_dir/site-router.headers" '^x-robots-tag:.*noindex' "$site_url"