From 77e491bd04e43591ba3870694cac413946a157fb Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 9 Jul 2026 03:35:54 -0700 Subject: [PATCH 1/7] ci(ci): replace Vercel previews with Coded App previews Deploys per-PR previews of apollo-docs, apollo-landing, apollo-design, and apollo-vertex as UiPath Coded Apps via @uipath/uip-go 0.2.1, replacing the Vercel preview workflow. All deployment knowledge lives in this repo, so deploy changes never require a uip-go release: - .uip-go.json holds the four deployment recipes (staged Next.js static exports, pagefind indexing, static-next-export post-processing, AI Chat platform context via aiChat redirectPath + scope) - APOLLO_CODED_APP=1 gates static-export mode in the docs and vertex next configs (basePath from APOLLO_CODED_APP_PATH); index pages render the overview inline in coded app builds since static exports cannot redirect - the vertex AI Chat demo reads NEXT_PUBLIC_APOLLO_VERTEX_AICHAT_* (mapped from the UIP_GO_AICHAT_* values uip-go resolves at deploy time) to call the platform directly; dev keeps the same-origin proxies, and the demo renders a disabled state when no client id is configured - the preview workflow builds, deploys, comments preview links, registers the AI Chat redirect URI, and cleans up on PR close (checkout of the PR head so the recipes are present; app deletion stays best-effort) - turbo hashes APOLLO_CODED_APP/APOLLO_CODED_APP_PATH; pagefind is a real devDependency of apollo-docs --- .github/workflows/preview-deploy.yml | 429 +++++++++++++++++ .github/workflows/production-deploy.yml | 152 +++++++ .github/workflows/vercel-deploy.yml | 430 ------------------ .gitignore | 5 + .uip-go.json | 80 ++++ README.md | 8 +- apps/apollo-docs/app/page.tsx | 10 +- apps/apollo-docs/mdx.d.ts | 6 + apps/apollo-docs/next.config.mjs | 10 + apps/apollo-docs/package.json | 1 + apps/apollo-vertex/app/components/page.tsx | 10 +- apps/apollo-vertex/app/layout.tsx | 6 +- apps/apollo-vertex/next.config.ts | 134 ++++-- .../templates/AiChatTemplate.tsx | 25 +- .../templates/ai-chat/AiChatAgentHubMode.tsx | 15 +- .../templates/ai-chat/AiChatLoginGate.tsx | 24 +- .../ai-chat/ai-chat-example-utils.ts | 59 ++- apps/apollo-vertex/types/mdx.d.ts | 7 + pnpm-lock.yaml | 3 + turbo.json | 2 + 20 files changed, 918 insertions(+), 498 deletions(-) create mode 100644 .github/workflows/preview-deploy.yml create mode 100644 .github/workflows/production-deploy.yml delete mode 100644 .github/workflows/vercel-deploy.yml create mode 100644 .uip-go.json create mode 100644 apps/apollo-docs/mdx.d.ts create mode 100644 apps/apollo-vertex/types/mdx.d.ts diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 000000000..932a81408 --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,429 @@ +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 Apollo Coded App Previews + 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 + outputs: + design_url: ${{ steps.deploy.outputs.design_url }} + docs_url: ${{ steps.deploy.outputs.docs_url }} + landing_url: ${{ steps.deploy.outputs.landing_url }} + vertex_url: ${{ steps.deploy.outputs.vertex_url }} + + 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 App previews with uip-go + id: deploy + 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 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 uip-go Coded App preview deployments." + 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}" + landing_app="apollo-landing-pr-${PR_NUMBER}" + docs_app="apollo-docs-pr-${PR_NUMBER}" + design_app="apollo-design-pr-${PR_NUMBER}" + vertex_app="apollo-vertex-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" + + write_output() { + local name="$1" + local value="$2" + local delimiter + delimiter="ghadelim_$(openssl rand -hex 16)" + { + printf '%s<<%s\n' "$name" "$delimiter" + printf '%s\n' "$value" + printf '%s\n' "$delimiter" + } >> "$GITHUB_OUTPUT" + } + + derive_app_url() { + local app_name="$1" + APP_NAME="$app_name" 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 + } + + run_uip_go() { + local label="$1" + local app_name="$2" + local output_name="${label#apollo-}_url" + 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 "$app_name" \ + --path-name "$app_name" \ + --tags "apollo,${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::" + + 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 [ -z "$app_url" ]; then + app_url="$(derive_app_url "$app_name")" + fi + case "$app_url" in + */) ;; + *) app_url="${app_url}/" ;; + esac + write_output "$output_name" "$app_url" + } + + failed_apps=() + deploy_or_record_failure() { + local label="$1" + local app_name="$2" + + if run_uip_go "$label" "$app_name"; then + return 0 + fi + + failed_apps+=("$label") + return 0 + } + + deploy_or_record_failure "apollo-landing" "$landing_app" + deploy_or_record_failure "apollo-docs" "$docs_app" + deploy_or_record_failure "apollo-design" "$design_app" + deploy_or_record_failure "apollo-vertex" "$vertex_app" + + if [ "${#failed_apps[@]}" -ne 0 ]; then + echo "::error::uip-go 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 }} + + 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: Update PR comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + DEPLOY_OUTCOME: ${{ needs.deploy.result }} + APOLLO_DESIGN_URL: ${{ needs.deploy.outputs.design_url }} + APOLLO_DOCS_URL: ${{ needs.deploy.outputs.docs_url }} + APOLLO_LANDING_URL: ${{ needs.deploy.outputs.landing_url }} + APOLLO_VERTEX_URL: ${{ needs.deploy.outputs.vertex_url }} + 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 logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + const deploySucceeded = process.env.DEPLOY_OUTCOME === 'success'; + const rows = [ + ['apollo-design', process.env.APOLLO_DESIGN_URL], + ['apollo-docs', process.env.APOLLO_DOCS_URL], + ['apollo-landing', process.env.APOLLO_LANDING_URL], + ['apollo-vertex', process.env.APOLLO_VERTEX_URL], + ].map(([project, url]) => { + const ready = Boolean(url); + const status = ready ? 'Ready' : deploySucceeded ? 'Skipped' : 'Failed'; + const preview = ready ? `[Preview](${url}) ยท ${logsLink}` : logsLink; + return `| ${project} | ${status} | ${preview} | ${timestamp} |`; + }); + const body = [ + identifier, + deploySucceeded + ? 'Apollo Coded App preview deployments are ready.' + : 'Apollo Coded App preview deployments did not complete.', + '', + '| 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-ai-chat-redirects: + 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 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 Apollo Coded App preview cleanup." + missing=1 + fi + done + if [ "$missing" -ne 0 ]; then + exit 1 + fi + + vertex_app="apollo-vertex-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" + + "$UIP_GO" apollo-vertex \ + --aichat-redirects remove \ + --name "$vertex_app" \ + --path-name "$vertex_app" \ + --base-url "$UIPATH_BASE_URL" \ + --org-name "$UIPATH_ORG_NAME" \ + --tenant-name "$UIPATH_TENANT_NAME" \ + --auth-client-id "$UIPATH_RUNTIME_CLIENT_ID" + + 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..439ff1135 --- /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"], + "platformAuth": { + "redirectPath": "patterns/ai-chat/index.html", + "scope": "openid profile offline_access DataFabric.Data.Read DataFabric.Schema.Read OR.Execution OR.Folders OR.Jobs ConversationalAgents Traces.Api" + }, + "tags": "apollo,apollo-vertex", + "env": { + "CI": "true", + "APOLLO_CODED_APP": "1" + } + } + } +} diff --git a/README.md b/README.md index 35e162585..ddf11adc0 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 (internal access): -- **[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/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 = ( Apollo Vertex +

+ AI Chat is not configured for this deployment +

+

+ The demo needs an AI Chat External App client id and platform context + at build time to call UiPath services directly. This deployment was + built without them, so the demo is disabled. The rest of Apollo Vertex + is fully available. +

+ + ); + } + return (
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..52e579b7f 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,11 @@ 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_STATIC_ORG, + AICHAT_STORAGE_KEYS, +} from "./ai-chat-example-utils"; export interface OrgTenantInfo { orgId: string; @@ -86,6 +90,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 +192,19 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { ); logout(); + // In Coded App builds the identity endpoints live on the platform host, + // not on the origin serving the static export. + const identityOrigin = 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..31ad79281 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 @@ -9,10 +9,61 @@ export const AICHAT_STORAGE_KEYS = { AGENT: "apollo-vertex:ai-chat:agent", } as const; -export const AICHAT_CLIENT_ID = "1119a927-10ab-4543-bd1a-ad6bfbbc27f4"; -export const AICHAT_SCOPE = "openid profile offline_access"; +// Coded App preview builds (uip-go) inject the NEXT_PUBLIC_APOLLO_VERTEX_* +// values via next.config.ts. In dev and regular builds they are undefined and +// the defaults below apply, with platform calls going through the same-origin +// Next.js proxy routes. +// +// The ?? here is load-bearing: coded-app builds define the env vars +// unconditionally (as "" when uip-go injected nothing), so an empty string +// survives and the disabled-state check in AiChatTemplate can detect a +// missing client id. The dev fallbacks only apply when the vars are truly +// undefined, which never happens in a coded-app build. +export const AICHAT_CLIENT_ID = + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_CLIENT_ID ?? + "1119a927-10ab-4543-bd1a-ad6bfbbc27f4"; +export const AICHAT_SCOPE = + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_SCOPE ?? + "openid profile offline_access"; -const UIPATH_BASE_URL = "https://alpha.uipath.com"; +// Non-empty only in Coded App builds, where there is no server for the proxy +// routes and the browser must call the platform host directly. +export const AICHAT_DIRECT_BASE_URL = ( + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_BASE_URL ?? "" +).replace(/\/+$/, ""); + +const rawRedirectPath = + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_REDIRECT_PATH ?? ""; +export const AICHAT_REDIRECT_PATH = rawRedirectPath + ? rawRedirectPath.startsWith("/") + ? rawRedirectPath + : `/${rawRedirectPath}` + : "/auth_callback"; + +export const AICHAT_IS_CODED_APP = + process.env.NEXT_PUBLIC_APOLLO_CODED_APP === "1"; + +const staticOrgName = + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_ORG_NAME ?? ""; +const staticTenantName = + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_NAME ?? ""; +const staticTenantId = + process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_ID ?? + staticTenantName; + +// Coded App builds bake in the org/tenant the deployment targets because the +// portal endpoint that lists them is not reachable cross-origin. +export const AICHAT_STATIC_ORG = + staticOrgName && staticTenantName + ? { + orgName: staticOrgName, + tenants: [{ id: staticTenantId, name: staticTenantName }], + } + : null; + +const UIPATH_BASE_URL = AICHAT_DIRECT_BASE_URL + ? AICHAT_DIRECT_BASE_URL + : "https://alpha.uipath.com"; export function createUiPathSdk( accessToken: string, @@ -26,7 +77,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": { From 4ad0b5d8bd277b327f6796ede89806d21284c8fe Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Mon, 13 Jul 2026 14:34:44 -0700 Subject: [PATCH 2/7] ci(ci): run Coded App previews in parallel via matrix The four preview apps were deploying sequentially in one job (~8 min wall clock = sum of all four builds). Split into a per-app matrix (fail-fast: false) so each app builds and deploys on its own runner in parallel, matching the structure the Vercel workflow already used. Wall clock now floors at the slowest single app (apollo-design Storybook) rather than the sum. - Each leg writes url/outcome to a deploy-result- artifact; a new update-comment job fans them in to rebuild the PR comment (same pattern as vercel-deploy.yml), replacing the single deploy job's outputs. - Per-app Turborepo cache keys so parallel legs don't race on one shared key. - --auth-client-id (platformAuth) applied only to the apollo-vertex leg, which is also the only leg that requires UIPATH_RUNTIME_CLIENT_ID. - Cleanup job unchanged. --- .github/workflows/preview-deploy.yml | 244 +++++++++++++++------------ 1 file changed, 133 insertions(+), 111 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 932a81408..c40b51b2f 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -84,18 +84,27 @@ jobs: } deploy: - name: Deploy Apollo Coded App Previews + 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 - outputs: - design_url: ${{ steps.deploy.outputs.design_url }} - docs_url: ${{ steps.deploy.outputs.docs_url }} - landing_url: ${{ steps.deploy.outputs.landing_url }} - vertex_url: ${{ steps.deploy.outputs.vertex_url }} + 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 @@ -111,15 +120,19 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .turbo - key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }} + # 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-${{ github.ref_name }}- + ${{ runner.os }}-turbo-${{ matrix.label }}-${{ github.ref_name }}- - - name: Deploy Coded App previews with uip-go + - 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 }} @@ -131,10 +144,15 @@ jobs: run: | set -euo pipefail + # Runtime OAuth client id is only needed for the platform-auth app. + 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) + if [ "$PLATFORM_AUTH" = "true" ]; then + required+=(UIPATH_RUNTIME_CLIENT_ID) + fi 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 + for name in "${required[@]}"; do if [ -z "${!name:-}" ]; then - echo "::error::$name is required for uip-go Coded App preview deployments." + echo "::error::$name is required for the ${APP_LABEL} Coded App preview deployment." missing=1 fi done @@ -144,10 +162,7 @@ jobs: short_sha="${GITHUB_SHA:0:7}" version="0.1.0-pr${PR_NUMBER}.${short_sha}.${GITHUB_RUN_ATTEMPT}" - landing_app="apollo-landing-pr-${PR_NUMBER}" - docs_app="apollo-docs-pr-${PR_NUMBER}" - design_app="apollo-design-pr-${PR_NUMBER}" - vertex_app="apollo-vertex-pr-${PR_NUMBER}" + app_name="${APP_LABEL}-pr-${PR_NUMBER}" uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc" uip_go_prefix="${RUNNER_TEMP}/uip-go" { @@ -157,21 +172,8 @@ jobs: 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" - write_output() { - local name="$1" - local value="$2" - local delimiter - delimiter="ghadelim_$(openssl rand -hex 16)" - { - printf '%s<<%s\n' "$name" "$delimiter" - printf '%s\n' "$value" - printf '%s\n' "$delimiter" - } >> "$GITHUB_OUTPUT" - } - derive_app_url() { - local app_name="$1" - APP_NAME="$app_name" node <<'NODE' + 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 || ''; @@ -191,75 +193,45 @@ jobs: NODE } - run_uip_go() { - local label="$1" - local app_name="$2" - local output_name="${label#apollo-}_url" - 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 "$app_name" \ - --path-name "$app_name" \ - --tags "apollo,${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::" - - if [ "$command_exit" -ne 0 ]; then - echo "::error::uip-go ${label} failed." - return "$command_exit" - fi + extra_args=() + if [ "$PLATFORM_AUTH" = "true" ]; then + extra_args+=(--auth-client-id "$UIPATH_RUNTIME_CLIENT_ID") + fi - app_url="$(sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' "$output_file" | tail -n 1)" - if [ -z "$app_url" ]; then - app_url="$(derive_app_url "$app_name")" - 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 - write_output "$output_name" "$app_url" - } - - failed_apps=() - deploy_or_record_failure() { - local label="$1" - local app_name="$2" - - if run_uip_go "$label" "$app_name"; then - return 0 - fi - - failed_apps+=("$label") - return 0 - } - - deploy_or_record_failure "apollo-landing" "$landing_app" - deploy_or_record_failure "apollo-docs" "$docs_app" - deploy_or_record_failure "apollo-design" "$design_app" - deploy_or_record_failure "apollo-vertex" "$vertex_app" + echo "url=$app_url" >> "$GITHUB_OUTPUT" + fi - if [ "${#failed_apps[@]}" -ne 0 ]; then - echo "::error::uip-go deployment failed for: ${failed_apps[*]}" - exit 1 + if [ "$command_exit" -ne 0 ]; then + echo "::error::uip-go ${APP_LABEL} failed." + exit "$command_exit" fi - name: Save Turborepo cache @@ -267,7 +239,39 @@ jobs: uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .turbo - key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }} + 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 @@ -278,17 +282,25 @@ jobs: 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: - DEPLOY_OUTCOME: ${{ needs.deploy.result }} - APOLLO_DESIGN_URL: ${{ needs.deploy.outputs.design_url }} - APOLLO_DOCS_URL: ${{ needs.deploy.outputs.docs_url }} - APOLLO_LANDING_URL: ${{ needs.deploy.outputs.landing_url }} - APOLLO_VERTEX_URL: ${{ needs.deploy.outputs.vertex_url }} + 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', @@ -299,24 +311,34 @@ jobs: second: '2-digit', hour12: true }); - const logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; - const deploySucceeded = process.env.DEPLOY_OUTCOME === 'success'; - const rows = [ - ['apollo-design', process.env.APOLLO_DESIGN_URL], - ['apollo-docs', process.env.APOLLO_DOCS_URL], - ['apollo-landing', process.env.APOLLO_LANDING_URL], - ['apollo-vertex', process.env.APOLLO_VERTEX_URL], - ].map(([project, url]) => { - const ready = Boolean(url); - const status = ready ? 'Ready' : deploySucceeded ? 'Skipped' : 'Failed'; + + 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, - deploySucceeded - ? 'Apollo Coded App preview deployments are ready.' - : 'Apollo Coded App preview deployments did not complete.', + anyFailed + ? 'Apollo Coded App preview deployments finished with failures.' + : 'Apollo Coded App preview deployments are ready.', '', '| Project | Status | Preview | Updated (PT) |', '|---------|--------|---------|--------------|', From 037e5b38c38689122e4ebb930eeb317881531792 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Mon, 13 Jul 2026 18:59:19 -0700 Subject: [PATCH 3/7] docs(repo): drop inaccurate (internal access) note on demo sites The Coded App demo sites are publicly reachable, not internal-access. Per review feedback on #896. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ddf11adc0..2431a1501 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Framework-agnostic AI chat interface web component for vanilla JS, Vue, Angular, ## ๐ŸŽจ Live Demos -Explore our components in interactive Storybook environments. The sites are hosted as UiPath Coded Apps on the staging environment (internal access): +Explore our components in interactive Storybook environments. The sites are hosted as UiPath Coded Apps on the staging environment: - **[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 From 2ad1a35a92bdcf01bebf0bdf229ac1cf2289476a Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Mon, 20 Jul 2026 23:51:23 -0700 Subject: [PATCH 4/7] refactor(apollo-vertex): sign in AI Chat with first-party Uber.Client Use the first-party Portal client (Uber.Client) for AI Chat sign-in instead of a per-PR non-confidential External App; its tokens skip the external-app audience checks, so the base OIDC scopes reach Data Fabric and AgentHub. Coded App previews use a shallow //auth_callback/index.html redirect covered by a host wildcard in first-party-service-metadata. Address review feedback (no silent fallbacks): validate the injected platform context with zod and fail the build when a Coded App is misconfigured, instead of ?? "" defaults plus a disabled-state panel. Replace the identity-origin fallback with an explicit coded-app-vs-dev branch. auth_callback navigates with window.location so the Coded App base path is not doubled. --- apps/apollo-vertex/app/auth_callback/page.tsx | 12 +- apps/apollo-vertex/next.config.ts | 7 +- .../templates/AiChatTemplate.tsx | 19 --- .../templates/ai-chat/AiChatLoginGate.tsx | 9 +- .../ai-chat/ai-chat-example-utils.ts | 122 +++++++++++------- 5 files changed, 92 insertions(+), 77 deletions(-) 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/next.config.ts b/apps/apollo-vertex/next.config.ts index 5a6c276cd..c75c4ef57 100644 --- a/apps/apollo-vertex/next.config.ts +++ b/apps/apollo-vertex/next.config.ts @@ -23,10 +23,9 @@ export default withNextra({ env: { NEXT_PUBLIC_APOLLO_CODED_APP: "1", NEXT_PUBLIC_APOLLO_CODED_APP_PATH: codedAppPath ?? "", - NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_CLIENT_ID: - process.env.UIP_GO_PLATFORM_AUTH_CLIENT_ID ?? "", - NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_SCOPE: - process.env.UIP_GO_PLATFORM_AUTH_SCOPE ?? "", + // 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: diff --git a/apps/apollo-vertex/templates/AiChatTemplate.tsx b/apps/apollo-vertex/templates/AiChatTemplate.tsx index c3249212b..58a6f363b 100644 --- a/apps/apollo-vertex/templates/AiChatTemplate.tsx +++ b/apps/apollo-vertex/templates/AiChatTemplate.tsx @@ -12,7 +12,6 @@ import { AiChatLoginGate, type OrgTenantInfo } from "./ai-chat/AiChatLoginGate"; import { AICHAT_CLIENT_ID, AICHAT_DIRECT_BASE_URL, - AICHAT_IS_CODED_APP, AICHAT_REDIRECT_PATH, AICHAT_SCOPE, AICHAT_STORAGE_KEYS, @@ -82,24 +81,6 @@ function AiChatWithConnection({ } export function AiChatTemplate() { - // Coded App previews without an AI Chat External App configured cannot sign - // in, so the demo is disabled instead of failing at the login step. - if (AICHAT_IS_CODED_APP && (!AICHAT_DIRECT_BASE_URL || !AICHAT_CLIENT_ID)) { - return ( -
-

- AI Chat is not configured for this deployment -

-

- The demo needs an AI Chat External App client id and platform context - at build time to call UiPath services directly. This deployment was - built without them, so the demo is disabled. The rest of Apollo Vertex - is fully available. -

-
- ); - } - return (
diff --git a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx index 52e579b7f..fcb4e1b50 100644 --- a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx +++ b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx @@ -19,6 +19,7 @@ import { import { useAuth } from "@/registry/shell/shell-auth-provider"; import { AICHAT_DIRECT_BASE_URL, + AICHAT_IS_CODED_APP, AICHAT_STATIC_ORG, AICHAT_STORAGE_KEYS, } from "./ai-chat-example-utils"; @@ -192,9 +193,11 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) { ); logout(); - // In Coded App builds the identity endpoints live on the platform host, - // not on the origin serving the static export. - const identityOrigin = AICHAT_DIRECT_BASE_URL || window.location.origin; + // 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", identityOrigin, 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 31ad79281..92ff2bb68 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,59 +10,88 @@ export const AICHAT_STORAGE_KEYS = { AGENT: "apollo-vertex:ai-chat:agent", } as const; -// Coded App preview builds (uip-go) inject the NEXT_PUBLIC_APOLLO_VERTEX_* -// values via next.config.ts. In dev and regular builds they are undefined and -// the defaults below apply, with platform calls going through the same-origin -// Next.js proxy routes. -// -// The ?? here is load-bearing: coded-app builds define the env vars -// unconditionally (as "" when uip-go injected nothing), so an empty string -// survives and the disabled-state check in AiChatTemplate can detect a -// missing client id. The dev fallbacks only apply when the vars are truly -// undefined, which never happens in a coded-app build. -export const AICHAT_CLIENT_ID = - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_CLIENT_ID ?? - "1119a927-10ab-4543-bd1a-ad6bfbbc27f4"; -export const AICHAT_SCOPE = - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_SCOPE ?? - "openid profile offline_access"; - -// Non-empty only in Coded App builds, where there is no server for the proxy -// routes and the browser must call the platform host directly. -export const AICHAT_DIRECT_BASE_URL = ( - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_BASE_URL ?? "" -).replace(/\/+$/, ""); - -const rawRedirectPath = - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_REDIRECT_PATH ?? ""; -export const AICHAT_REDIRECT_PATH = rawRedirectPath - ? rawRedirectPath.startsWith("/") - ? rawRedirectPath - : `/${rawRedirectPath}` - : "/auth_callback"; +// 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"; export const AICHAT_IS_CODED_APP = process.env.NEXT_PUBLIC_APOLLO_CODED_APP === "1"; -const staticOrgName = - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_ORG_NAME ?? ""; -const staticTenantName = - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_NAME ?? ""; -const staticTenantId = - process.env.NEXT_PUBLIC_APOLLO_VERTEX_PLATFORM_AUTH_TENANT_ID ?? - staticTenantName; +// 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).optional(), + 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. -export const AICHAT_STATIC_ORG = - staticOrgName && staticTenantName - ? { - orgName: staticOrgName, - tenants: [{ id: staticTenantId, name: staticTenantName }], - } - : null; +// 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 ?? codedAppPlatformAuth.tenantName, + name: codedAppPlatformAuth.tenantName, + }, + ], + } + : null; -const UIPATH_BASE_URL = AICHAT_DIRECT_BASE_URL +// 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"; From ab162f67a1d90cb1a64dccd3e998c54f8be38737 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Mon, 20 Jul 2026 23:51:24 -0700 Subject: [PATCH 5/7] ci(ci): use first-party Uber.Client for Coded App previews Point the apollo-vertex recipe at Uber.Client (authClientId) with a shallow auth_callback redirect, and drop the per-PR runtime OAuth client machinery from the preview workflow: no --auth-client-id, no UIPATH_RUNTIME_CLIENT_ID, no PM.OAuthApp scope, no --aichat-redirects cleanup. Redirect URIs are covered by a static host wildcard in first-party-service-metadata. --- .github/workflows/preview-deploy.yml | 30 ++++++++++------------------ .uip-go.json | 4 ++-- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index c40b51b2f..63131a384 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -139,16 +139,11 @@ jobs: 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 }} + UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE || 'Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution' }} run: | set -euo pipefail - # Runtime OAuth client id is only needed for the platform-auth app. 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) - if [ "$PLATFORM_AUTH" = "true" ]; then - required+=(UIPATH_RUNTIME_CLIENT_ID) - fi missing=0 for name in "${required[@]}"; do if [ -z "${!name:-}" ]; then @@ -193,9 +188,13 @@ jobs: 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+=(--auth-client-id "$UIPATH_RUNTIME_CLIENT_ID") + extra_args+=(--no-aichat-redirects) fi output_file="$(mktemp)" @@ -401,13 +400,12 @@ jobs: 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 }} + 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 UIPATH_RUNTIME_CLIENT_ID; do + 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 @@ -417,7 +415,6 @@ jobs: exit 1 fi - vertex_app="apollo-vertex-pr-${PR_NUMBER}" uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc" uip_go_prefix="${RUNNER_TEMP}/uip-go" { @@ -427,15 +424,8 @@ jobs: 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" - "$UIP_GO" apollo-vertex \ - --aichat-redirects remove \ - --name "$vertex_app" \ - --path-name "$vertex_app" \ - --base-url "$UIPATH_BASE_URL" \ - --org-name "$UIPATH_ORG_NAME" \ - --tenant-name "$UIPATH_TENANT_NAME" \ - --auth-client-id "$UIPATH_RUNTIME_CLIENT_ID" - + # 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" \ diff --git a/.uip-go.json b/.uip-go.json index 439ff1135..ea4407218 100644 --- a/.uip-go.json +++ b/.uip-go.json @@ -66,9 +66,9 @@ "pathNameEnv": "APOLLO_CODED_APP_PATH", "folderKeyEnv": "UIPATH_FOLDER_KEY", "postprocess": ["static-next-export"], + "authClientId": "1119a927-10ab-4543-bd1a-ad6bfbbc27f4", "platformAuth": { - "redirectPath": "patterns/ai-chat/index.html", - "scope": "openid profile offline_access DataFabric.Data.Read DataFabric.Schema.Read OR.Execution OR.Folders OR.Jobs ConversationalAgents Traces.Api" + "redirectPath": "auth_callback/index.html" }, "tags": "apollo,apollo-vertex", "env": { From 2de7b1c42c73634c92dc2441ff95b802f40edff3 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Tue, 21 Jul 2026 00:11:49 -0700 Subject: [PATCH 6/7] fix(apollo-vertex): validate platform-auth at build time (review) Address Copilot review on the Uber.Client refactor: - Move the required-context check into next.config, which runs in Node during next build, so a misconfigured Coded App fails the build instead of crashing in the browser (a throw in the client module was not a reliable build gate). - Make tenantId a required field. uip-go always injects it (its resolver falls back to the tenant name), so the previous optional + min(1) combination would have thrown on the injected empty string; requiring it is correct and simpler. --- apps/apollo-vertex/next.config.ts | 21 +++++++++++++++++++ .../ai-chat/ai-chat-example-utils.ts | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/apollo-vertex/next.config.ts b/apps/apollo-vertex/next.config.ts index c75c4ef57..9d9fd83f0 100644 --- a/apps/apollo-vertex/next.config.ts +++ b/apps/apollo-vertex/next.config.ts @@ -14,6 +14,27 @@ const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replaceAll( "", ); +// A Coded App build calls the platform directly, so uip-go must have injected +// the platform context. Validate it here โ€” next.config runs in Node during +// `next build`, so a misconfigured deployment fails the build immediately +// rather than shipping a bundle that crashes in the browser. (tenantId is +// optional; it falls back to the tenant name.) +if (codedApp) { + const missing = [ + "UIP_GO_PLATFORM_AUTH_BASE_URL", + "UIP_GO_PLATFORM_AUTH_ORG_NAME", + "UIP_GO_PLATFORM_AUTH_TENANT_NAME", + "UIP_GO_PLATFORM_AUTH_TENANT_ID", + "UIP_GO_PLATFORM_AUTH_REDIRECT_PATH", + ].filter((name) => !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 ? { 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 92ff2bb68..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 @@ -36,7 +36,7 @@ function loadCodedAppPlatformAuth() { baseUrl: z.string().min(1), orgName: z.string().min(1), tenantName: z.string().min(1), - tenantId: z.string().min(1).optional(), + tenantId: z.string().min(1), redirectPath: z.string().min(1), }) .safeParse({ @@ -83,7 +83,7 @@ export const AICHAT_STATIC_ORG = codedAppPlatformAuth orgName: codedAppPlatformAuth.orgName, tenants: [ { - id: codedAppPlatformAuth.tenantId ?? codedAppPlatformAuth.tenantName, + id: codedAppPlatformAuth.tenantId, name: codedAppPlatformAuth.tenantName, }, ], From 048bfa5905254a3be8b52941690ea208fa51b5f5 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Tue, 21 Jul 2026 00:22:08 -0700 Subject: [PATCH 7/7] fix(apollo-vertex): correct tenantId comment and rename cleanup job (review) - next.config: the tenantId comment said "optional" but the check requires it; update the comment to match (uip-go always injects it). - preview-deploy: rename the close-event job from cleanup-ai-chat-redirects to cleanup-previews, since it now only deletes preview apps (no redirect sync). --- .github/workflows/preview-deploy.yml | 2 +- apps/apollo-vertex/next.config.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 63131a384..a20f6534a 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -371,7 +371,7 @@ jobs: }); } - cleanup-ai-chat-redirects: + 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 diff --git a/apps/apollo-vertex/next.config.ts b/apps/apollo-vertex/next.config.ts index 9d9fd83f0..7f14b2d0a 100644 --- a/apps/apollo-vertex/next.config.ts +++ b/apps/apollo-vertex/next.config.ts @@ -17,8 +17,9 @@ const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replaceAll( // A Coded App build calls the platform directly, so uip-go must have injected // the platform context. Validate it here โ€” next.config runs in Node during // `next build`, so a misconfigured deployment fails the build immediately -// rather than shipping a bundle that crashes in the browser. (tenantId is -// optional; it falls back to the tenant name.) +// rather than shipping a bundle that crashes in the browser. uip-go always +// resolves the tenant id (falling back to the tenant name), so it is required +// too. if (codedApp) { const missing = [ "UIP_GO_PLATFORM_AUTH_BASE_URL",