diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml
new file mode 100644
index 000000000..a20f6534a
--- /dev/null
+++ b/.github/workflows/preview-deploy.yml
@@ -0,0 +1,441 @@
+name: Coded App Preview Deployments
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - main
+ - 'support/**'
+
+# Deny-all default; jobs grant the minimum they need.
+permissions: {}
+
+env:
+ TURBO_TELEMETRY_DISABLED: 1
+ DO_NOT_TRACK: 1
+ UIP_GO_VERSION: 0.3.0
+
+concurrency:
+ group: coded-app-preview-pr-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ initialize-comment:
+ name: Initialize Apollo Coded App Preview Comment
+ if: github.event.action != 'closed' && github.event.pull_request.head.repo.fork == false
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+
+ steps:
+ - name: Initialize PR comment
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const identifier = '';
+ const timestamp = new Date().toLocaleString('en-US', {
+ timeZone: 'America/Los_Angeles',
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: true
+ });
+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
+ const deployingLink = `[Deploying...](${runUrl})`;
+ const logsLink = `[Logs](${runUrl})`;
+ const projects = ['apollo-design', 'apollo-docs', 'apollo-landing', 'apollo-vertex'];
+ const body = [
+ identifier,
+ 'Apollo Coded App preview deployments are running.',
+ '',
+ '| Project | Status | Preview | Updated (PT) |',
+ '|---------|--------|---------|--------------|',
+ ...projects.map(project => `| ${project} | ${deployingLink} | ${logsLink} | ${timestamp} |`)
+ ].join('\n');
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ per_page: 100,
+ });
+ const existing = comments.find(comment =>
+ comment.body?.includes(identifier) &&
+ comment.user?.type === 'Bot' &&
+ ['github-actions[bot]', 'github-actions'].includes(comment.user?.login)
+ );
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
+
+ deploy:
+ name: Deploy ${{ matrix.label }}
+ needs: initialize-comment
+ if: ${{ !cancelled() && github.event.action != 'closed' && needs.initialize-comment.result == 'success' && github.event.pull_request.head.repo.fork == false }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: read
+ strategy:
+ # One app per runner, in parallel. Keep going if one app fails so the
+ # others still publish; each leg reports status via an artifact and the
+ # job fails at the end if any leg failed.
+ fail-fast: false
+ matrix:
+ include:
+ - label: apollo-landing
+ - label: apollo-docs
+ - label: apollo-design
+ - label: apollo-vertex
+ # Live AI Chat: pass the runtime OAuth client so uip-go bakes the
+ # platform-auth context in and registers this preview's redirect URI.
+ platform_auth: true
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Install Node dependencies
+ uses: ./.github/actions/install-node-deps
+
+ - name: Restore Turborepo cache
+ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: .turbo
+ # Per-app key so parallel legs don't race to save one shared cache
+ # (which would persist only the first/fastest app's task outputs).
+ key: ${{ runner.os }}-turbo-${{ matrix.label }}-${{ github.ref_name }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-turbo-${{ matrix.label }}-${{ github.ref_name }}-
+
+ - name: Deploy Coded App preview with uip-go
+ id: deploy
+ env:
+ GH_NPM_REGISTRY_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ APP_LABEL: ${{ matrix.label }}
+ PLATFORM_AUTH: ${{ matrix.platform_auth == true }}
+ UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }}
+ UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID || secrets.UIPATH_CLIENT_ID }}
+ UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }}
+ UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }}
+ UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }}
+ UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }}
+ UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE || 'Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution' }}
+ run: |
+ set -euo pipefail
+
+ required=(GH_NPM_REGISTRY_TOKEN UIPATH_BASE_URL UIPATH_CLIENT_ID UIPATH_CLIENT_SECRET UIPATH_FOLDER_KEY UIPATH_ORG_NAME UIPATH_TENANT_NAME UIP_GO_VERSION)
+ missing=0
+ for name in "${required[@]}"; do
+ if [ -z "${!name:-}" ]; then
+ echo "::error::$name is required for the ${APP_LABEL} Coded App preview deployment."
+ missing=1
+ fi
+ done
+ if [ "$missing" -ne 0 ]; then
+ exit 1
+ fi
+
+ short_sha="${GITHUB_SHA:0:7}"
+ version="0.1.0-pr${PR_NUMBER}.${short_sha}.${GITHUB_RUN_ATTEMPT}"
+ app_name="${APP_LABEL}-pr-${PR_NUMBER}"
+ uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc"
+ uip_go_prefix="${RUNNER_TEMP}/uip-go"
+ {
+ printf '@uipath:registry=https://npm.pkg.github.com\n'
+ printf '//npm.pkg.github.com/:_authToken=%s\n' "$GH_NPM_REGISTRY_TOKEN"
+ } > "$uip_go_npmrc"
+ NPM_CONFIG_USERCONFIG="$uip_go_npmrc" npm install --prefix "$uip_go_prefix" "@uipath/uip-go@${UIP_GO_VERSION}"
+ UIP_GO="${uip_go_prefix}/node_modules/.bin/uip-go"
+
+ derive_app_url() {
+ APP_NAME="$1" node <<'NODE'
+ const baseUrl = process.env.UIPATH_BASE_URL || '';
+ const orgName = process.env.UIPATH_ORG_NAME || '';
+ const appName = process.env.APP_NAME || '';
+
+ let environment = '';
+ try {
+ const hostname = new URL(baseUrl).hostname;
+ let inferred = hostname.replace(/\.uipath\.com$/, '');
+ inferred = inferred.replace(/^api\./, '').replace(/\.api$/, '');
+ environment = inferred && inferred !== 'api' && inferred !== 'cloud' ? inferred : '';
+ } catch {
+ environment = '';
+ }
+
+ const suffix = environment ? `.${environment}` : '';
+ console.log(`https://${orgName}${suffix}.uipath.host/${appName}`);
+ NODE
+ }
+
+ # The platform-auth app (vertex) carries its client id in the recipe
+ # (first-party Uber.Client). Its Coded App host is pre-registered as a
+ # redirect URI via a host wildcard in first-party-service-metadata, so
+ # skip per-deployment redirect-URI registration.
+ extra_args=()
+ if [ "$PLATFORM_AUTH" = "true" ]; then
+ extra_args+=(--no-aichat-redirects)
+ fi
+
+ output_file="$(mktemp)"
+ echo "::group::uip-go ${APP_LABEL}"
+ set +e
+ "$UIP_GO" "$APP_LABEL" \
+ --name "$app_name" \
+ --path-name "$app_name" \
+ --tags "apollo,${APP_LABEL},preview" \
+ --version "$version" \
+ --folder-key "$UIPATH_FOLDER_KEY" \
+ --base-url "$UIPATH_BASE_URL" \
+ --org-name "$UIPATH_ORG_NAME" \
+ --tenant-name "$UIPATH_TENANT_NAME" \
+ --deploy-retries 3 \
+ --deploy-retry-delay-ms 10000 \
+ "${extra_args[@]}" 2>&1 | tee "$output_file"
+ command_exit=${PIPESTATUS[0]}
+ set -e
+ echo "::endgroup::"
+
+ app_url="$(sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' "$output_file" | tail -n 1)"
+ if [ -z "$app_url" ] && [ "$command_exit" -eq 0 ]; then
+ app_url="$(derive_app_url "$app_name")"
+ fi
+ if [ -n "$app_url" ]; then
+ case "$app_url" in
+ */) ;;
+ *) app_url="${app_url}/" ;;
+ esac
+ echo "url=$app_url" >> "$GITHUB_OUTPUT"
+ fi
+
+ if [ "$command_exit" -ne 0 ]; then
+ echo "::error::uip-go ${APP_LABEL} failed."
+ exit "$command_exit"
+ fi
+
+ - name: Save Turborepo cache
+ if: always()
+ uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: .turbo
+ key: ${{ runner.os }}-turbo-${{ matrix.label }}-${{ github.ref_name }}-${{ github.sha }}
+
+ - name: Save deploy result
+ if: always()
+ env:
+ APP_LABEL: ${{ matrix.label }}
+ DEPLOY_URL: ${{ steps.deploy.outputs.url }}
+ DEPLOY_OUTCOME: ${{ steps.deploy.outcome }}
+ run: |
+ mkdir -p "$RUNNER_TEMP/deploy-result"
+
+ if [ "$DEPLOY_OUTCOME" = "success" ]; then
+ outcome="success"
+ error=""
+ elif [ "$DEPLOY_OUTCOME" = "failure" ]; then
+ outcome="failure"
+ error="uip-go deploy failed for $APP_LABEL"
+ else
+ outcome="skipped"
+ error=""
+ fi
+
+ printf '%s' "${DEPLOY_URL:-}" > "$RUNNER_TEMP/deploy-result/url"
+ printf '%s' "$outcome" > "$RUNNER_TEMP/deploy-result/outcome"
+ printf '%s' "$error" > "$RUNNER_TEMP/deploy-result/error"
+
+ - name: Upload deploy result
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: deploy-result-${{ matrix.label }}
+ path: ${{ runner.temp }}/deploy-result/
+ retention-days: 1
+
+ update-comment:
+ name: Update Apollo Coded App Preview Comment
+ needs: [initialize-comment, deploy]
+ if: ${{ always() && github.event.action != 'closed' && github.event.pull_request.head.repo.fork == false && needs.initialize-comment.result != 'skipped' }}
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+
+ steps:
+ - name: Download deploy results
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ with:
+ pattern: deploy-result-*
+ path: ${{ runner.temp }}/results
+
+ - name: Update PR comment
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ RESULTS_DIR: ${{ runner.temp }}/results
+ with:
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+
+ const identifier = '';
+ const projects = ['apollo-design', 'apollo-docs', 'apollo-landing', 'apollo-vertex'];
+ const resultsDir = process.env.RESULTS_DIR;
+ const logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
+ const timestamp = new Date().toLocaleString('en-US', {
+ timeZone: 'America/Los_Angeles',
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: true
+ });
+
+ const readResult = (project) => {
+ const dir = path.join(resultsDir, `deploy-result-${project}`);
+ const read = (name) => {
+ try {
+ return fs.readFileSync(path.join(dir, name), 'utf8').trim();
+ } catch {
+ return '';
+ }
+ };
+ return { url: read('url'), outcome: read('outcome') || 'skipped' };
+ };
+
+ let anyFailed = false;
+ const rows = projects.map(project => {
+ const { url, outcome } = readResult(project);
+ const ready = outcome === 'success' && Boolean(url);
+ if (outcome === 'failure') anyFailed = true;
+ const status = ready ? 'Ready' : outcome === 'failure' ? 'Failed' : 'Skipped';
+ const preview = ready ? `[Preview](${url}) ยท ${logsLink}` : logsLink;
+ return `| ${project} | ${status} | ${preview} | ${timestamp} |`;
+ });
+
+ const body = [
+ identifier,
+ anyFailed
+ ? 'Apollo Coded App preview deployments finished with failures.'
+ : 'Apollo Coded App preview deployments are ready.',
+ '',
+ '| Project | Status | Preview | Updated (PT) |',
+ '|---------|--------|---------|--------------|',
+ ...rows
+ ].join('\n');
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ per_page: 100,
+ });
+ const existing = comments.find(comment =>
+ comment.body?.includes(identifier) &&
+ comment.user?.type === 'Bot' &&
+ ['github-actions[bot]', 'github-actions'].includes(comment.user?.login)
+ );
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
+
+ cleanup-previews:
+ name: Cleanup Apollo Coded App Previews
+ if: github.event.action == 'closed' && github.event.pull_request.head.repo.fork == false
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ fetch-depth: 1
+ persist-credentials: false
+ # The PR head, not the base: cleanup needs the recipes in the
+ # branch's .uip-go.json. Same-repo PRs only (job is fork-gated),
+ # and the deploy job already runs head code with these secrets.
+ ref: ${{ github.event.pull_request.head.sha }}
+
+ - name: Remove Apollo Coded App previews
+ env:
+ GH_NPM_REGISTRY_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }}
+ UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID || secrets.UIPATH_CLIENT_ID }}
+ UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }}
+ UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }}
+ UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }}
+ UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }}
+ UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE || 'Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution' }}
+ run: |
+ set -euo pipefail
+
+ missing=0
+ for name in GH_NPM_REGISTRY_TOKEN UIPATH_BASE_URL UIPATH_CLIENT_ID UIPATH_CLIENT_SECRET UIPATH_FOLDER_KEY UIPATH_ORG_NAME UIPATH_TENANT_NAME UIP_GO_VERSION; do
+ if [ -z "${!name:-}" ]; then
+ echo "::error::$name is required for Apollo Coded App preview cleanup."
+ missing=1
+ fi
+ done
+ if [ "$missing" -ne 0 ]; then
+ exit 1
+ fi
+
+ uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc"
+ uip_go_prefix="${RUNNER_TEMP}/uip-go"
+ {
+ printf '@uipath:registry=https://npm.pkg.github.com\n'
+ printf '//npm.pkg.github.com/:_authToken=%s\n' "$GH_NPM_REGISTRY_TOKEN"
+ } > "$uip_go_npmrc"
+ NPM_CONFIG_USERCONFIG="$uip_go_npmrc" npm install --prefix "$uip_go_prefix" "@uipath/uip-go@${UIP_GO_VERSION}"
+ UIP_GO="${uip_go_prefix}/node_modules/.bin/uip-go"
+
+ # No redirect-URI cleanup: previews reuse the first-party Uber.Client,
+ # whose Coded App host is covered by a static wildcard registration.
+ for label in apollo-landing apollo-docs apollo-design apollo-vertex; do
+ app_name="${label}-pr-${PR_NUMBER}"
+ if ! "$UIP_GO" "$label" \
+ --delete-app \
+ --name "$app_name" \
+ --path-name "$app_name" \
+ --folder-key "$UIPATH_FOLDER_KEY" \
+ --base-url "$UIPATH_BASE_URL" \
+ --org-name "$UIPATH_ORG_NAME" \
+ --tenant-name "$UIPATH_TENANT_NAME"; then
+ echo "::warning::Could not delete Coded App ${app_name}. External App auth can publish/deploy/upgrade, but the Apps delete API may reject CI tokens."
+ fi
+ done
diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml
new file mode 100644
index 000000000..a3d5d701d
--- /dev/null
+++ b/.github/workflows/production-deploy.yml
@@ -0,0 +1,152 @@
+name: Coded App Production Deployments
+
+# PR previews deploy per-PR Coded Apps via preview-deploy.yml; this workflow
+# keeps stable production Coded Apps of the merged pages up to date on main.
+on:
+ push:
+ branches: [main]
+
+# Deny-all at the workflow level; each job grants only what it needs.
+permissions: {}
+
+concurrency:
+ group: coded-app-production
+ cancel-in-progress: true
+
+env:
+ TURBO_TELEMETRY_DISABLED: 1
+ DO_NOT_TRACK: 1
+ UIP_GO_VERSION: 0.3.0
+
+jobs:
+ deploy:
+ name: Deploy Apollo Coded Apps
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Install Node dependencies
+ uses: ./.github/actions/install-node-deps
+
+ - name: Restore Turborepo cache
+ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: .turbo
+ key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-turbo-${{ github.ref_name }}-
+
+ - name: Deploy Coded Apps with uip-go
+ env:
+ GH_NPM_REGISTRY_TOKEN: ${{ github.token }}
+ UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }}
+ UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID || secrets.UIPATH_CLIENT_ID }}
+ UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }}
+ UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }}
+ UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }}
+ UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }}
+ UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE || 'Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution PM.OAuthApp' }}
+ UIPATH_RUNTIME_CLIENT_ID: ${{ vars.UIPATH_RUNTIME_CLIENT_ID || secrets.UIPATH_RUNTIME_CLIENT_ID }}
+ run: |
+ set -euo pipefail
+
+ missing=0
+ for name in GH_NPM_REGISTRY_TOKEN UIPATH_BASE_URL UIPATH_CLIENT_ID UIPATH_CLIENT_SECRET UIPATH_FOLDER_KEY UIPATH_ORG_NAME UIPATH_TENANT_NAME UIP_GO_VERSION UIPATH_RUNTIME_CLIENT_ID; do
+ if [ -z "${!name:-}" ]; then
+ echo "::error::$name is required for Coded App production deployments."
+ missing=1
+ fi
+ done
+ if [ "$missing" -ne 0 ]; then
+ exit 1
+ fi
+
+ short_sha="${GITHUB_SHA:0:7}"
+ version="0.1.0-main.${short_sha}.${GITHUB_RUN_ATTEMPT}"
+ uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc"
+ uip_go_prefix="${RUNNER_TEMP}/uip-go"
+ {
+ printf '@uipath:registry=https://npm.pkg.github.com\n'
+ printf '//npm.pkg.github.com/:_authToken=%s\n' "$GH_NPM_REGISTRY_TOKEN"
+ } > "$uip_go_npmrc"
+ NPM_CONFIG_USERCONFIG="$uip_go_npmrc" npm install --prefix "$uip_go_prefix" "@uipath/uip-go@${UIP_GO_VERSION}"
+ UIP_GO="${uip_go_prefix}/node_modules/.bin/uip-go"
+
+ run_uip_go() {
+ local label="$1"
+ local output_file
+ local command_exit
+ local app_url
+ local extra_args=()
+
+ output_file="$(mktemp)"
+ trap 'rm -f "$output_file"; trap - RETURN' RETURN
+ if [ "$label" = "apollo-vertex" ]; then
+ extra_args+=(--auth-client-id "$UIPATH_RUNTIME_CLIENT_ID")
+ fi
+ echo "::group::uip-go ${label}"
+ set +e
+ "$UIP_GO" "$label" \
+ --name "$label" \
+ --path-name "$label" \
+ --version "$version" \
+ --folder-key "$UIPATH_FOLDER_KEY" \
+ --base-url "$UIPATH_BASE_URL" \
+ --org-name "$UIPATH_ORG_NAME" \
+ --tenant-name "$UIPATH_TENANT_NAME" \
+ --deploy-retries 3 \
+ --deploy-retry-delay-ms 10000 \
+ "${extra_args[@]}" 2>&1 | tee "$output_file"
+ command_exit=${PIPESTATUS[0]}
+ set -e
+ echo "::endgroup::"
+
+ if [ "$command_exit" -ne 0 ]; then
+ echo "::error::uip-go ${label} failed."
+ return "$command_exit"
+ fi
+
+ app_url="$(sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' "$output_file" | tail -n 1)"
+ if [ -n "$app_url" ]; then
+ echo "โ
Deployed ${label}: ${app_url}" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "โ
Deployed ${label} (${version})" >> "$GITHUB_STEP_SUMMARY"
+ fi
+ }
+
+ failed_apps=()
+ deploy_or_record_failure() {
+ local label="$1"
+
+ if run_uip_go "$label"; then
+ return 0
+ fi
+
+ failed_apps+=("$label")
+ return 0
+ }
+
+ deploy_or_record_failure "apollo-landing"
+ deploy_or_record_failure "apollo-docs"
+ deploy_or_record_failure "apollo-design"
+ deploy_or_record_failure "apollo-vertex"
+
+ if [ "${#failed_apps[@]}" -ne 0 ]; then
+ echo "::error::Coded App production deployment failed for: ${failed_apps[*]}"
+ exit 1
+ fi
+
+ - name: Save Turborepo cache
+ if: always()
+ uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: .turbo
+ key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }}
diff --git a/.github/workflows/vercel-deploy.yml b/.github/workflows/vercel-deploy.yml
deleted file mode 100644
index 88b2095fc..000000000
--- a/.github/workflows/vercel-deploy.yml
+++ /dev/null
@@ -1,430 +0,0 @@
-name: Vercel Deployments
-
-on:
- pull_request:
- push:
- branches: [main]
-
-# Deny-all default; jobs grant the minimum they need.
-permissions: {}
-
-concurrency:
- group: vercel-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- pre-deploy:
- name: Initialize Deployment Status
- runs-on: ubuntu-latest
- if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
- permissions:
- pull-requests: write
-
- steps:
- - name: Post initial deployment status
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const identifier = '';
- const timestamp = new Date().toLocaleString('en-US', {
- timeZone: 'America/Los_Angeles',
- year: 'numeric',
- month: 'short',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: true
- });
-
- const projects = [
- 'apollo-design',
- 'apollo-docs',
- 'apollo-landing',
- 'apollo-vertex'
- ];
-
- const tableRows = projects.map(projectName => {
- const logsLink = `[Logs](https://github.com/${ context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
- return `| ${projectName} | ๐ก Deploying... | ${logsLink} | ${timestamp} |`;
- }).join('\n');
-
- const comment = [
- identifier,
- '',
- 'The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.com/docs/deployments/git).',
- '',
- '| Project | Deployment | Review | Updated (PT) |',
- '|---------|------------|--------|---------------|',
- tableRows
- ].join('\n');
-
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
-
- const existingComment = comments.find(c => c.body?.includes(identifier));
-
- if (existingComment) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existingComment.id,
- body: comment
- });
- } else {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: comment
- });
- }
-
- deploy:
- name: Deploy ${{ matrix.project_name }}
- runs-on: ubuntu-latest
- needs: pre-deploy
- if: ${{ !cancelled() && (github.event_name == 'push' || (needs.pre-deploy.result == 'success' && github.event.pull_request.head.repo.fork == false)) }}
- permissions:
- contents: read
- strategy:
- # Let every project deploy attempt complete so the PR comment surfaces all outcomes;
- # a failure in one entry should not cancel the others, but should still fail the workflow.
- fail-fast: false
- matrix:
- include:
- - project_name: apollo-design
- vercel_project_id_secret: VERCEL_PROJECT_ID_CANVAS
- build_filter: storybook-app
- build_task: storybook:build
- - project_name: apollo-docs
- vercel_project_id_secret: VERCEL_PROJECT_ID_DOCS
- build_filter: apollo-docs
- build_task: build
- - project_name: apollo-landing
- vercel_project_id_secret: VERCEL_PROJECT_ID_LANDING
- build_filter: apollo-landing
- build_task: build
- - project_name: apollo-vertex
- vercel_project_id_secret: VERCEL_PROJECT_ID_VERTEX
- build_filter: apollo-vertex
- build_task: build
-
- steps:
- - name: Checkout
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- with:
- persist-credentials: false
-
- - name: Install Node dependencies
- uses: ./.github/actions/install-node-deps
-
- - name: Restore Turborepo cache
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
- with:
- path: .turbo
- key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-turbo-${{ github.ref_name }}-
-
- - name: Cache Vercel CLI
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
- with:
- path: ~/.npm
- key: ${{ runner.os }}-vercel-cli-53.4.0-${{ github.ref_name }}-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-vercel-cli-53.4.0-${{ github.ref_name }}-
-
- - name: Install Vercel CLI
- run: npm install -g vercel@53.4.0
-
- - name: Set deployment variables
- id: vars
- run: |
- if [ "${{ github.event_name }}" == "pull_request" ]; then
- echo "prod_flag=" >> "$GITHUB_OUTPUT"
- else
- echo "prod_flag=--prod" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Set Vercel Project ID
- id: set-project-id
- run: |
- case "${{ matrix.vercel_project_id_secret }}" in
- VERCEL_PROJECT_ID_CANVAS)
- echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_CANVAS }}" >> "$GITHUB_ENV"
- ;;
- VERCEL_PROJECT_ID_DOCS)
- echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_DOCS }}" >> "$GITHUB_ENV"
- ;;
- VERCEL_PROJECT_ID_LANDING)
- echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_LANDING }}" >> "$GITHUB_ENV"
- ;;
- VERCEL_PROJECT_ID_UI_REACT)
- echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_UI_REACT }}" >> "$GITHUB_ENV"
- ;;
- VERCEL_PROJECT_ID_VERTEX)
- echo "VERCEL_PROJECT_ID=${{ secrets.VERCEL_PROJECT_ID_VERTEX }}" >> "$GITHUB_ENV"
- ;;
- *)
- echo "Error: Unknown vercel_project_id_secret value '${{ matrix.vercel_project_id_secret }}'. Please update the case statement in .github/workflows/vercel-deploy.yml." >&2
- exit 1
- ;;
- esac
-
- - name: Build project
- id: build
- run: pnpm turbo ${{ matrix.build_task }} --filter=${{ matrix.build_filter }}
-
- - name: Save Turborepo cache
- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
- with:
- path: .turbo
- key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }}
-
- - name: Build Vercel output
- id: vercel-build
- env:
- VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
- VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
- PROD_FLAG: ${{ steps.vars.outputs.prod_flag }}
- run: |
- # Run from repo root โ Vercel applies rootDirectory from project settings
- # to locate the app subdirectory. Using --cwd here would double the path
- # (e.g. apps/apollo-vertex/apps/apollo-vertex) because the dashboard
- # rootDirectory is applied on top of --cwd.
- VERCEL_ARGS=(build --yes)
- [[ -n "$PROD_FLAG" ]] && VERCEL_ARGS+=("$PROD_FLAG")
- vercel "${VERCEL_ARGS[@]}"
-
- - name: Deploy to Vercel
- id: deploy
- run: |
- ERROR_MSG=""
- DEPLOY_URL=""
- set +e
- # Run from repo root โ same reasoning as vercel build above.
- # Vercel applies rootDirectory from project settings to locate
- # the .vercel/output/ directory created by the build step.
- # --archive=tgz: package the prebuilt output as a single tarball before
- # upload. Without it, each file in .vercel/output/ counts as a separate
- # upload against the 40k/day api-upload-paid quota โ Storybook + shadcn
- # registries blow through that quickly across 5 projects ร N PRs/day.
- VERCEL_ARGS=(deploy --prebuilt --archive=tgz --yes)
- [[ -n "$PROD_FLAG" ]] && VERCEL_ARGS+=("$PROD_FLAG")
- DEPLOY_OUTPUT=$(vercel "${VERCEL_ARGS[@]}" 2>&1)
- DEPLOY_EXIT_CODE=$?
- set -e
-
- for __secret in "$VERCEL_TOKEN" "$VERCEL_ORG_ID"; do
- if [ -n "$__secret" ]; then
- DEPLOY_OUTPUT="${DEPLOY_OUTPUT//$__secret/***}"
- fi
- done
-
- # Always echo the full vercel CLI output so the runner log is traceable
- # without having to download the deploy-result artifact. Collapsible so
- # successful runs stay tidy.
- echo "::group::vercel CLI output (exit $DEPLOY_EXIT_CODE)"
- printf '%s\n' "$DEPLOY_OUTPUT"
- echo "::endgroup::"
-
- if [ "$DEPLOY_EXIT_CODE" -eq 0 ]; then
- if [ "$PROD_FLAG" == "--prod" ]; then
- DEPLOY_URL="https://${{ matrix.project_name }}.vercel.app"
- else
- DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -oP 'https://[^\s]+\.vercel\.app[^\s]*' | head -n 1)
- if [ -z "$DEPLOY_URL" ]; then
- DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | tail -n 1)
- echo "โ ๏ธ Warning: URL extraction fallback used for ${{ matrix.project_name }}"
- fi
- fi
- {
- echo "url=$DEPLOY_URL"
- echo "error_message="
- } >> "$GITHUB_OUTPUT"
- echo "โ
Deployed ${{ matrix.project_name }} to $DEPLOY_URL" >> "$GITHUB_STEP_SUMMARY"
- else
- # Prefer the actual `Error: โฆ` line over a blind `tail -n 5`, which
- # often grabs upload progress bars or Node stack-trace frames and
- # hides the root cause.
- ERROR_MSG=$(echo "$DEPLOY_OUTPUT" | grep -m1 -E '^Error: ' || true)
- if [ -z "$ERROR_MSG" ]; then
- ERROR_MSG=$(echo "$DEPLOY_OUTPUT" | tail -n 5 | tr '\n' ' ')
- fi
- if [ "${#ERROR_MSG}" -gt 500 ]; then
- ERROR_MSG="${ERROR_MSG:0:500}..."
- fi
- echo "error_message=$ERROR_MSG" >> "$GITHUB_OUTPUT"
- echo "โ Failed to deploy ${{ matrix.project_name }}: $ERROR_MSG" >> "$GITHUB_STEP_SUMMARY"
- # Escape %, CR, LF before interpolating into a workflow command so a
- # malformed/hostile error string can't break the annotation or inject
- # a second `::workflow-command::` via an embedded newline. Order
- # matters: % must be escaped first, since later subs emit %.
- ERROR_ANNOTATION="${ERROR_MSG//%/%25}"
- ERROR_ANNOTATION="${ERROR_ANNOTATION//$'\r'/%0D}"
- ERROR_ANNOTATION="${ERROR_ANNOTATION//$'\n'/%0A}"
- echo "::error title=Vercel deploy failed for ${{ matrix.project_name }}::$ERROR_ANNOTATION"
- exit 1
- fi
- env:
- PROD_FLAG: ${{ steps.vars.outputs.prod_flag }}
- VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
- VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
- CI: true
- NODE_ENV: production
-
- - name: Save deploy result
- if: always() && github.event_name == 'pull_request'
- env:
- DEPLOY_URL: ${{ steps.deploy.outputs.url }}
- DEPLOY_OUTCOME: ${{ steps.deploy.outcome }}
- DEPLOY_ERROR: ${{ steps.deploy.outputs.error_message }}
- BUILD_OUTCOME: ${{ steps.build.outcome }}
- VERCEL_BUILD_OUTCOME: ${{ steps.vercel-build.outcome }}
- PROJECT_NAME: ${{ matrix.project_name }}
- run: |
- mkdir -p "$RUNNER_TEMP/deploy-result"
-
- # Attribute the failure to the earliest step that broke so the PR
- # comment can show a meaningful status instead of a misleading
- # "Skipped" when build/prebuild dies before deploy ever runs.
- if [ "$DEPLOY_OUTCOME" = "success" ]; then
- OUTCOME="success"
- ERROR=""
- elif [ "$BUILD_OUTCOME" = "failure" ]; then
- OUTCOME="failure"
- ERROR="Build failed (pnpm turbo) for $PROJECT_NAME"
- elif [ "$VERCEL_BUILD_OUTCOME" = "failure" ]; then
- OUTCOME="failure"
- ERROR="Vercel build failed for $PROJECT_NAME"
- elif [ "$DEPLOY_OUTCOME" = "failure" ]; then
- OUTCOME="failure"
- ERROR="${DEPLOY_ERROR:-Deploy step failed}"
- else
- OUTCOME="skipped"
- ERROR=""
- fi
-
- printf '%s' "${DEPLOY_URL:-}" > "$RUNNER_TEMP/deploy-result/url"
- printf '%s' "$OUTCOME" > "$RUNNER_TEMP/deploy-result/outcome"
- printf '%s' "$ERROR" > "$RUNNER_TEMP/deploy-result/error"
-
- - name: Upload deploy result
- if: always() && github.event_name == 'pull_request'
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- with:
- name: deploy-result-${{ matrix.project_name }}
- path: ${{ runner.temp }}/deploy-result/
- retention-days: 1
-
- update-pr-comment:
- name: Update PR Comment
- needs: deploy
- if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
- runs-on: ubuntu-latest
- permissions:
- pull-requests: write
-
- steps:
- - name: Download deploy results
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
- with:
- pattern: deploy-result-*
- path: ${{ runner.temp }}/results
-
- - name: Update PR comment
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- RESULTS_DIR: ${{ runner.temp }}/results
- with:
- script: |
- const fs = require('fs');
- const path = require('path');
-
- const projects = [
- 'apollo-design',
- 'apollo-docs',
- 'apollo-landing',
- 'apollo-vertex'
- ];
-
- const resultsDir = process.env.RESULTS_DIR;
- const logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
- const timestamp = new Date().toLocaleString('en-US', {
- timeZone: 'America/Los_Angeles',
- year: 'numeric',
- month: 'short',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: true
- });
-
- const truncateError = (msg, maxLength = 100) => {
- if (!msg || msg.length <= maxLength) return msg;
- const truncated = msg.substring(0, maxLength);
- const lastSpace = truncated.lastIndexOf(' ');
- return lastSpace > 0 ? truncated.substring(0, lastSpace) + '...' : truncated + '...';
- };
-
- const tableRows = projects.map(project => {
- const dir = path.join(resultsDir, `deploy-result-${project}`);
- let url = '', outcome = 'skipped', error = '';
- if (fs.existsSync(dir)) {
- url = fs.readFileSync(path.join(dir, 'url'), 'utf8').trim();
- outcome = fs.readFileSync(path.join(dir, 'outcome'), 'utf8').trim();
- error = fs.readFileSync(path.join(dir, 'error'), 'utf8').trim();
- }
-
- let status;
- if (outcome === 'success') {
- status = '๐ข Ready';
- } else if (outcome === 'failure') {
- status = error ? `โ Failed: ${truncateError(error)}` : 'โ Failed';
- } else {
- status = 'โ ๏ธ Skipped';
- }
-
- const projectLink = url ? `[${project}](${url})` : project;
- const previewLink = url ? `[Preview](${url})` : 'N/A';
- return `| ${projectLink} | ${status} | ${previewLink}, ${logsLink} | ${timestamp} |`;
- }).join('\n');
-
- const identifier = '';
- const body = [
- identifier,
- ``,
- 'The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.com/docs/deployments/git).',
- '',
- '| Project | Deployment | Review | Updated (PT) |',
- '|---------|------------|--------|---------------|',
- tableRows
- ].join('\n');
-
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- });
-
- const existing = comments.find(c => c.body?.includes(identifier));
- if (existing) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing.id,
- body
- });
- } else {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body
- });
- }
diff --git a/.gitignore b/.gitignore
index dfd56f70d..dc6da60e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,5 +84,10 @@ lerna-debug.log*
.cache/
.turbo/
+# UiPath Coded Apps local packaging
+.uipath/
+.uipath-build/
+uipath.json
+
# Vercel
.vercel/
diff --git a/.uip-go.json b/.uip-go.json
new file mode 100644
index 000000000..ea4407218
--- /dev/null
+++ b/.uip-go.json
@@ -0,0 +1,80 @@
+{
+ "deployments": {
+ "apollo-docs": {
+ "type": "codedapp",
+ "build": {
+ "type": "staged-next-export",
+ "sourceDir": "apps/apollo-docs",
+ "stageDir": ".uipath-build/apollo-docs",
+ "preCommands": ["pnpm turbo build --filter=apollo-docs^..."],
+ "commands": ["pnpm exec next build"],
+ "copyOutput": "out",
+ "pagefind": true
+ },
+ "outputDir": "apps/apollo-docs/out",
+ "pathNameEnv": "APOLLO_CODED_APP_PATH",
+ "folderKeyEnv": "UIPATH_FOLDER_KEY",
+ "postprocess": ["static-next-export"],
+ "tags": "apollo,apollo-docs",
+ "env": {
+ "CI": "true",
+ "APOLLO_CODED_APP": "1"
+ }
+ },
+ "apollo-landing": {
+ "type": "codedapp",
+ "build": "pnpm turbo build --filter=apollo-landing",
+ "outputDir": "apps/landing/dist",
+ "pathNameEnv": "APOLLO_CODED_APP_PATH",
+ "folderKeyEnv": "UIPATH_FOLDER_KEY",
+ "postprocess": ["relative-root-assets"],
+ "tags": "apollo,apollo-landing",
+ "env": {
+ "CI": "true",
+ "APOLLO_CODED_APP": "1"
+ }
+ },
+ "apollo-design": {
+ "type": "codedapp",
+ "build": "pnpm turbo run storybook:build --filter=storybook-app",
+ "outputDir": "apps/storybook/storybook-static",
+ "pathNameEnv": "APOLLO_CODED_APP_PATH",
+ "folderKeyEnv": "UIPATH_FOLDER_KEY",
+ "postprocess": ["relative-root-assets"],
+ "tags": "apollo,apollo-design",
+ "env": {
+ "CI": "true",
+ "APOLLO_CODED_APP": "1"
+ }
+ },
+ "apollo-vertex": {
+ "type": "codedapp",
+ "build": {
+ "type": "staged-next-export",
+ "sourceDir": "apps/apollo-vertex",
+ "stageDir": ".uipath-build/apollo-vertex",
+ "exclude": ["app/api"],
+ "commands": [
+ "pnpm generate:theme",
+ "pnpm registry:build",
+ "pnpm exec next build"
+ ],
+ "copyOutput": "out",
+ "pagefind": true
+ },
+ "outputDir": "apps/apollo-vertex/out",
+ "pathNameEnv": "APOLLO_CODED_APP_PATH",
+ "folderKeyEnv": "UIPATH_FOLDER_KEY",
+ "postprocess": ["static-next-export"],
+ "authClientId": "1119a927-10ab-4543-bd1a-ad6bfbbc27f4",
+ "platformAuth": {
+ "redirectPath": "auth_callback/index.html"
+ },
+ "tags": "apollo,apollo-vertex",
+ "env": {
+ "CI": "true",
+ "APOLLO_CODED_APP": "1"
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index 35e162585..2431a1501 100644
--- a/README.md
+++ b/README.md
@@ -138,10 +138,12 @@ Framework-agnostic AI chat interface web component for vanilla JS, Vue, Angular,
## ๐จ Live Demos
-Explore our components in interactive Storybook environments:
+Explore our components in interactive Storybook environments. The sites are hosted as UiPath Coded Apps on the staging environment:
-- **[Apollo Vertex](https://apollo-vertex.vercel.app/)** - Complete design system showcase
-- **[Canvas Components](https://apollo-design.vercel.app/)** - Tailwind CSS components & Workflow and canvas demos
+- **[Apollo Vertex](https://engdogfood.staging.uipath.host/apollo-vertex/)** - Complete design system showcase
+- **[Apollo Docs](https://engdogfood.staging.uipath.host/apollo-docs/)** - Central documentation site
+- **[Canvas Components](https://engdogfood.staging.uipath.host/apollo-design/)** - Tailwind CSS components & Workflow and canvas demos
+- **[Landing](https://engdogfood.staging.uipath.host/apollo-landing/)** - Apollo landing page
- **[React Playground](https://apollo-ui-react.vercel.app/)** - Material UI components, design tokens, CSS vars, icons, and chat component
## ๐ Documentation
diff --git a/apps/apollo-docs/app/page.tsx b/apps/apollo-docs/app/page.tsx
index 58ce8d3d0..65f4f7259 100644
--- a/apps/apollo-docs/app/page.tsx
+++ b/apps/apollo-docs/app/page.tsx
@@ -1,5 +1,13 @@
import { redirect } from 'next/navigation';
+import OverviewPage from './introduction/overview/page.mdx';
+
+// Static Coded App exports cannot serve a redirect, so the root page renders
+// the overview inline instead of redirecting to it.
+const codedApp = process.env.APOLLO_CODED_APP === '1';
export default function Page() {
- redirect('/introduction/overview');
+ if (!codedApp) {
+ redirect('/introduction/overview');
+ }
+ return ;
}
diff --git a/apps/apollo-docs/mdx.d.ts b/apps/apollo-docs/mdx.d.ts
new file mode 100644
index 000000000..d35a16888
--- /dev/null
+++ b/apps/apollo-docs/mdx.d.ts
@@ -0,0 +1,6 @@
+declare module '*.mdx' {
+ import type { ComponentType } from 'react';
+
+ const MDXComponent: ComponentType;
+ export default MDXComponent;
+}
diff --git a/apps/apollo-docs/next.config.mjs b/apps/apollo-docs/next.config.mjs
index 8c4f3b887..0b25513bf 100644
--- a/apps/apollo-docs/next.config.mjs
+++ b/apps/apollo-docs/next.config.mjs
@@ -4,7 +4,17 @@ const withNextra = nextra({
defaultShowCopyCode: true,
});
+// Coded App preview builds (uip-go) need a static export served from a
+// sub-path of the Coded App host. Regular dev/prod builds are unaffected.
+const codedApp = process.env.APOLLO_CODED_APP === '1';
+const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replace(/^\/+|\/+$/g, '');
+
export default withNextra({
+ ...(codedApp && {
+ output: 'export',
+ trailingSlash: true,
+ ...(codedAppPath && { basePath: `/${codedAppPath}` }),
+ }),
reactCompiler: true,
turbopack: {
resolveAlias: {
diff --git a/apps/apollo-docs/package.json b/apps/apollo-docs/package.json
index b21473e44..a11c18109 100644
--- a/apps/apollo-docs/package.json
+++ b/apps/apollo-docs/package.json
@@ -31,6 +31,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.2",
+ "pagefind": "^1.4.0",
"typescript": "^5.9.3"
}
}
diff --git a/apps/apollo-vertex/app/auth_callback/page.tsx b/apps/apollo-vertex/app/auth_callback/page.tsx
index 0b8607665..0db2f0c21 100644
--- a/apps/apollo-vertex/app/auth_callback/page.tsx
+++ b/apps/apollo-vertex/app/auth_callback/page.tsx
@@ -1,22 +1,24 @@
"use client";
-import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { STORAGE_KEYS } from "@/lib/auth";
const FALLBACK_PATH = "/";
export default function AuthCallback() {
- const router = useRouter();
-
useEffect(() => {
const returnTo =
sessionStorage.getItem(STORAGE_KEYS.AUTH_RETURN_TO) ?? FALLBACK_PATH;
sessionStorage.removeItem(STORAGE_KEYS.AUTH_RETURN_TO);
+ // returnTo is an absolute path already including the Coded App base path
+ // (captured from window.location.pathname at sign-in), so navigate with
+ // window.location rather than Next's router, which would prepend basePath
+ // again and double it. Carries the ?code/?state through to the page that
+ // completes the token exchange.
const search = window.location.search;
- router.replace(`${returnTo}${search}`);
- }, [router]);
+ window.location.replace(`${returnTo}${search}`);
+ }, []);
return null;
}
diff --git a/apps/apollo-vertex/app/components/page.tsx b/apps/apollo-vertex/app/components/page.tsx
index 10f6c1118..de70eb0dd 100644
--- a/apps/apollo-vertex/app/components/page.tsx
+++ b/apps/apollo-vertex/app/components/page.tsx
@@ -1,5 +1,13 @@
import { redirect } from "next/navigation";
+import ComponentsOverviewPage from "./overview/page.mdx";
+
+// Static Coded App exports cannot serve a redirect, so this index renders the
+// overview inline instead of redirecting to it.
+const codedApp = process.env.APOLLO_CODED_APP === "1";
export default function ComponentsIndex() {
- redirect("/components/overview");
+ if (!codedApp) {
+ redirect("/components/overview");
+ }
+ return ;
}
diff --git a/apps/apollo-vertex/app/layout.tsx b/apps/apollo-vertex/app/layout.tsx
index 6deeae785..202dfec96 100644
--- a/apps/apollo-vertex/app/layout.tsx
+++ b/apps/apollo-vertex/app/layout.tsx
@@ -49,12 +49,16 @@ export const metadata = {
},
};
+// Coded App preview builds serve the app from a sub-path; Next.js basePath
+// does not rewrite plain image sources, so the logo path is prefixed here.
+const codedAppPath = process.env.NEXT_PUBLIC_APOLLO_CODED_APP_PATH;
+
const navbar = (
!process.env[name]);
+ if (missing.length > 0) {
+ throw new Error(
+ `Coded App build is missing platform-auth context: ${missing.join(", ")}. ` +
+ "uip-go injects these from the platformAuth section of .uip-go.json; check the deployment recipe.",
+ );
+ }
+}
+
export default withNextra({
+ ...(codedApp
+ ? {
+ output: "export",
+ trailingSlash: true,
+ ...(codedAppPath && { basePath: `/${codedAppPath}` }),
+ env: {
+ NEXT_PUBLIC_APOLLO_CODED_APP: "1",
+ NEXT_PUBLIC_APOLLO_CODED_APP_PATH: codedAppPath ?? "",
+ // Client id and scope are fixed constants in the app (first-party
+ // Uber.Client), so they are not injected here. uip-go still resolves
+ // the per-deployment platform context below.
+ NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_BASE_URL:
+ process.env.UIP_GO_PLATFORM_AUTH_BASE_URL ?? "",
+ NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_ORG_NAME:
+ process.env.UIP_GO_PLATFORM_AUTH_ORG_NAME ?? "",
+ NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_NAME:
+ process.env.UIP_GO_PLATFORM_AUTH_TENANT_NAME ?? "",
+ NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_ID:
+ process.env.UIP_GO_PLATFORM_AUTH_TENANT_ID ?? "",
+ NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_REDIRECT_PATH:
+ process.env.UIP_GO_PLATFORM_AUTH_REDIRECT_PATH ?? "",
+ },
+ }
+ : {
+ rewrites() {
+ return [
+ {
+ source: "/identity_/:path*",
+ destination: "https://alpha.uipath.com/identity_/:path*",
+ },
+ {
+ source: "/_proxy/portal/:orgId/:path*",
+ destination: "https://alpha.uipath.com/:orgId/portal_/api/:path*",
+ },
+ ];
+ },
+ headers() {
+ return [
+ {
+ source: "/:path*",
+ headers: [
+ {
+ key: "X-DNS-Prefetch-Control",
+ value: "on",
+ },
+ {
+ key: "Strict-Transport-Security",
+ value: "max-age=63072000; includeSubDomains; preload",
+ },
+ {
+ key: "X-Frame-Options",
+ value: "SAMEORIGIN",
+ },
+ {
+ key: "X-Content-Type-Options",
+ value: "nosniff",
+ },
+ {
+ key: "X-XSS-Protection",
+ value: "1; mode=block",
+ },
+ {
+ key: "Referrer-Policy",
+ value: "strict-origin-when-cross-origin",
+ },
+ {
+ key: "Permissions-Policy",
+ value: "camera=(), microphone=(), geolocation=()",
+ },
+ ],
+ },
+ ];
+ },
+ }),
reactCompiler: true,
turbopack: {
resolveAlias: {
"next-mdx-import-source-file": "./mdx-components.tsx",
},
},
- rewrites() {
- return [
- {
- source: "/identity_/:path*",
- destination: "https://alpha.uipath.com/identity_/:path*",
- },
- {
- source: "/_proxy/portal/:orgId/:path*",
- destination: "https://alpha.uipath.com/:orgId/portal_/api/:path*",
- },
- ];
- },
- headers() {
- return [
- {
- source: "/:path*",
- headers: [
- {
- key: "X-DNS-Prefetch-Control",
- value: "on",
- },
- {
- key: "Strict-Transport-Security",
- value: "max-age=63072000; includeSubDomains; preload",
- },
- {
- key: "X-Frame-Options",
- value: "SAMEORIGIN",
- },
- {
- key: "X-Content-Type-Options",
- value: "nosniff",
- },
- {
- key: "X-XSS-Protection",
- value: "1; mode=block",
- },
- {
- key: "Referrer-Policy",
- value: "strict-origin-when-cross-origin",
- },
- {
- key: "Permissions-Policy",
- value: "camera=(), microphone=(), geolocation=()",
- },
- ],
- },
- ];
- },
});
diff --git a/apps/apollo-vertex/templates/AiChatTemplate.tsx b/apps/apollo-vertex/templates/AiChatTemplate.tsx
index ef8fcc385..58a6f363b 100644
--- a/apps/apollo-vertex/templates/AiChatTemplate.tsx
+++ b/apps/apollo-vertex/templates/AiChatTemplate.tsx
@@ -11,6 +11,8 @@ import { ConversationalAgentChat } from "./ai-chat/AiChatConversationalAgentMode
import { AiChatLoginGate, type OrgTenantInfo } from "./ai-chat/AiChatLoginGate";
import {
AICHAT_CLIENT_ID,
+ AICHAT_DIRECT_BASE_URL,
+ AICHAT_REDIRECT_PATH,
AICHAT_SCOPE,
AICHAT_STORAGE_KEYS,
type ChatMode,
@@ -85,8 +87,8 @@ export function AiChatTemplate() {
diff --git a/apps/apollo-vertex/templates/ai-chat/AiChatAgentHubMode.tsx b/apps/apollo-vertex/templates/ai-chat/AiChatAgentHubMode.tsx
index e0adfd38d..1f2868f9b 100644
--- a/apps/apollo-vertex/templates/ai-chat/AiChatAgentHubMode.tsx
+++ b/apps/apollo-vertex/templates/ai-chat/AiChatAgentHubMode.tsx
@@ -42,7 +42,10 @@ import {
import type { MessageFeedbackType } from "@/registry/ai-chat/types";
import { DataFabricGate } from "./AiChatDataFabricGate";
import type { OrgTenantInfo } from "./AiChatLoginGate";
-import { createUiPathSdk } from "./ai-chat-example-utils";
+import {
+ AICHAT_DIRECT_BASE_URL,
+ createUiPathSdk,
+} from "./ai-chat-example-utils";
interface AgentHubChatProps {
accessToken: string;
@@ -61,7 +64,11 @@ function AgentHubChatInner({
entities,
}: AgentHubChatInnerProps) {
const { t } = useTranslation();
- const dataFabricBaseUrl = `/api/datafabric/${orgTenant.orgName}/${orgTenant.tenantName}/datafabric_/api`;
+ // Coded App builds have no server for the /api proxy routes, so the browser
+ // calls the platform host directly.
+ const dataFabricBaseUrl = AICHAT_DIRECT_BASE_URL
+ ? `${AICHAT_DIRECT_BASE_URL}/${orgTenant.orgName}/${orgTenant.tenantName}/datafabric_/api`
+ : `/api/datafabric/${orgTenant.orgName}/${orgTenant.tenantName}/datafabric_/api`;
const tableTool = createDataFabricTableTool({
entities,
@@ -138,7 +145,9 @@ function AgentHubChatInner({
].join("\n\n");
const connection = createAgentHubConnection({
- baseUrl: `/api/agenthub/${orgTenant.orgName}/${orgTenant.tenantName}/agenthub_/llm/api`,
+ baseUrl: AICHAT_DIRECT_BASE_URL
+ ? `${AICHAT_DIRECT_BASE_URL}/${orgTenant.orgName}/${orgTenant.tenantName}/agenthub_/llm/api`
+ : `/api/agenthub/${orgTenant.orgName}/${orgTenant.tenantName}/agenthub_/llm/api`,
model: { vendor: "openai", name: "gpt-4.1-mini-2025-04-14" },
accessToken: () => accessToken,
systemPrompt,
diff --git a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx
index 60b6b0914..fcb4e1b50 100644
--- a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx
+++ b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx
@@ -1,9 +1,9 @@
"use client";
+import { useLocalStorage } from "@mantine/hooks";
import { useQuery } from "@tanstack/react-query";
import { jwtDecode } from "jwt-decode";
import { ChevronRight, LogIn, LogOut } from "lucide-react";
-import { useLocalStorage } from "@mantine/hooks";
import type { ReactNode } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
@@ -17,7 +17,12 @@ import {
SelectValue,
} from "@/registry/select/select";
import { useAuth } from "@/registry/shell/shell-auth-provider";
-import { AICHAT_STORAGE_KEYS } from "./ai-chat-example-utils";
+import {
+ AICHAT_DIRECT_BASE_URL,
+ AICHAT_IS_CODED_APP,
+ AICHAT_STATIC_ORG,
+ AICHAT_STORAGE_KEYS,
+} from "./ai-chat-example-utils";
export interface OrgTenantInfo {
orgId: string;
@@ -86,6 +91,15 @@ function useOrgInfo(accessToken: string | null) {
queryFn: () => {
if (!orgId || !accessToken)
throw new Error("Missing orgId or accessToken");
+ // Coded App builds cannot reach the portal proxy that lists tenants, so
+ // the deployment's org/tenant is baked in at build time instead.
+ if (AICHAT_STATIC_ORG) {
+ return Promise.resolve({
+ orgId,
+ orgName: AICHAT_STATIC_ORG.orgName,
+ tenants: AICHAT_STATIC_ORG.tenants,
+ });
+ }
return fetchOrgInfo(orgId, accessToken);
},
enabled: !!accessToken && !!orgId,
@@ -179,16 +193,21 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
);
logout();
+ // Coded App builds reach identity on the baked platform host; dev reaches
+ // it through the same-origin proxy.
+ const identityOrigin = AICHAT_IS_CODED_APP
+ ? AICHAT_DIRECT_BASE_URL
+ : window.location.origin;
const endSessionUrl = new URL(
"/identity_/connect/endsession",
- window.location.origin,
+ identityOrigin,
);
if (idToken) {
endSessionUrl.searchParams.set("id_token_hint", idToken);
}
endSessionUrl.searchParams.set(
"post_logout_redirect_uri",
- `${window.location.origin}/portal_/cloudrpa`,
+ `${identityOrigin}/portal_/cloudrpa`,
);
window.location.href = endSessionUrl.toString();
}
diff --git a/apps/apollo-vertex/templates/ai-chat/ai-chat-example-utils.ts b/apps/apollo-vertex/templates/ai-chat/ai-chat-example-utils.ts
index 6e28ba0b4..eeb5e628a 100644
--- a/apps/apollo-vertex/templates/ai-chat/ai-chat-example-utils.ts
+++ b/apps/apollo-vertex/templates/ai-chat/ai-chat-example-utils.ts
@@ -1,4 +1,5 @@
import { UiPath } from "@uipath/uipath-typescript/core";
+import { z } from "zod";
import type { OrgTenantInfo } from "./AiChatLoginGate";
export type ChatMode = "agenthub" | "conversational-agent";
@@ -9,10 +10,90 @@ export const AICHAT_STORAGE_KEYS = {
AGENT: "apollo-vertex:ai-chat:agent",
} as const;
+// UiPath's first-party Portal client (Uber.Client). Its browser tokens skip
+// the external-app audience checks, so the demo reaches Data Fabric and
+// AgentHub with just the base OIDC scopes. Coded App preview hosts are
+// pre-registered as redirect URIs on this client via a host wildcard in
+// first-party-service-metadata, so there is no per-deployment client or
+// redirect-URI registration. Same client id in dev and in Coded App builds.
export const AICHAT_CLIENT_ID = "1119a927-10ab-4543-bd1a-ad6bfbbc27f4";
export const AICHAT_SCOPE = "openid profile offline_access";
-const UIPATH_BASE_URL = "https://alpha.uipath.com";
+export const AICHAT_IS_CODED_APP =
+ process.env.NEXT_PUBLIC_APOLLO_CODED_APP === "1";
+
+// Two modes, chosen at build time:
+// - Coded App export: there is no server, so the browser calls the platform
+// host directly. uip-go bakes the platform context (base URL, org, tenant,
+// redirect path) into the bundle, and it is required โ a missing value
+// fails the build here rather than shipping a chat that cannot sign in.
+// - Dev / regular build: the app reaches the platform through the
+// same-origin Next.js proxy routes, so there is no baked platform context
+// and the org/tenant are discovered at runtime.
+function loadCodedAppPlatformAuth() {
+ const parsed = z
+ .object({
+ baseUrl: z.string().min(1),
+ orgName: z.string().min(1),
+ tenantName: z.string().min(1),
+ tenantId: z.string().min(1),
+ redirectPath: z.string().min(1),
+ })
+ .safeParse({
+ baseUrl: process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_BASE_URL,
+ orgName: process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_ORG_NAME,
+ tenantName:
+ process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_NAME,
+ tenantId: process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_ID,
+ redirectPath:
+ process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_REDIRECT_PATH,
+ });
+ if (!parsed.success) {
+ const missing = parsed.error.issues
+ .map((issue) => issue.path.join("."))
+ .join(", ");
+ throw new Error(
+ `Coded App build is missing platform-auth context (${missing}). uip-go injects these from the platformAuth section of .uip-go.json; check the deployment recipe.`,
+ );
+ }
+ return parsed.data;
+}
+
+const codedAppPlatformAuth = AICHAT_IS_CODED_APP
+ ? loadCodedAppPlatformAuth()
+ : null;
+
+// Empty in dev (use the same-origin proxy); the platform host in a Coded App
+// build (call it directly).
+export const AICHAT_DIRECT_BASE_URL = codedAppPlatformAuth
+ ? codedAppPlatformAuth.baseUrl.replace(/\/+$/, "")
+ : "";
+
+export const AICHAT_REDIRECT_PATH = codedAppPlatformAuth
+ ? codedAppPlatformAuth.redirectPath.startsWith("/")
+ ? codedAppPlatformAuth.redirectPath
+ : `/${codedAppPlatformAuth.redirectPath}`
+ : "/auth_callback";
+
+// Coded App builds bake in the org/tenant the deployment targets because the
+// portal endpoint that lists them is not reachable cross-origin. In dev the
+// login gate discovers them at runtime instead.
+export const AICHAT_STATIC_ORG = codedAppPlatformAuth
+ ? {
+ orgName: codedAppPlatformAuth.orgName,
+ tenants: [
+ {
+ id: codedAppPlatformAuth.tenantId,
+ name: codedAppPlatformAuth.tenantName,
+ },
+ ],
+ }
+ : null;
+
+// Coded App builds call the baked host directly; dev targets alpha.
+const UIPATH_BASE_URL = AICHAT_IS_CODED_APP
+ ? AICHAT_DIRECT_BASE_URL
+ : "https://alpha.uipath.com";
export function createUiPathSdk(
accessToken: string,
@@ -26,7 +107,7 @@ export function createUiPathSdk(
orgName: orgTenant.orgName,
tenantName: orgTenant.tenantName,
clientId: AICHAT_CLIENT_ID,
- redirectUri: `${origin}/auth_callback`,
+ redirectUri: `${origin}${AICHAT_REDIRECT_PATH}`,
scope: AICHAT_SCOPE,
});
sdk.updateToken({ token: accessToken, type: "oauth" });
diff --git a/apps/apollo-vertex/types/mdx.d.ts b/apps/apollo-vertex/types/mdx.d.ts
new file mode 100644
index 000000000..2ecc39f19
--- /dev/null
+++ b/apps/apollo-vertex/types/mdx.d.ts
@@ -0,0 +1,7 @@
+// Typed default export for MDX page imports (e.g. the coded-app index pages).
+declare module "*.mdx" {
+ import type { ComponentType } from "react";
+
+ const MDXComponent: ComponentType;
+ export default MDXComponent;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c7daff635..94907537f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -160,6 +160,9 @@ importers:
'@types/react-dom':
specifier: ^19.2.2
version: 19.2.3(@types/react@19.2.8)
+ pagefind:
+ specifier: ^1.4.0
+ version: 1.4.0
typescript:
specifier: ^5.9.3
version: 5.9.3
diff --git a/turbo.json b/turbo.json
index e780f4810..46aff326b 100644
--- a/turbo.json
+++ b/turbo.json
@@ -5,6 +5,7 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
+ "env": ["APOLLO_CODED_APP", "APOLLO_CODED_APP_PATH", "UIP_GO_PLATFORM_AUTH_*", "UIP_GO_AICHAT_*"],
"inputs": [
"$TURBO_DEFAULT$",
"!src/**/*.stories.*",
@@ -65,6 +66,7 @@
},
"storybook:build": {
"dependsOn": ["^build"],
+ "env": ["APOLLO_CODED_APP", "APOLLO_CODED_APP_PATH"],
"outputs": ["storybook-static/**"]
},
"clean": {