From 9f5da426a2c2fedcc98c52ef52cff1ae83fac767 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 15:54:09 -0700 Subject: [PATCH 01/29] ci(ci): replace vercel preview deployments Deploy Apollo preview apps through UiPath Coded Apps and remove the Vercel PR workflow. --- .github/workflows/preview-deploy.yml | 654 +++++++++++++++++++++++++++ .github/workflows/vercel-deploy.yml | 430 ------------------ .gitignore | 5 + 3 files changed, 659 insertions(+), 430 deletions(-) create mode 100644 .github/workflows/preview-deploy.yml delete mode 100644 .github/workflows/vercel-deploy.yml diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 000000000..4e9b671e6 --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,654 @@ +name: Coded App Preview Deployments + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + - 'support/**' + +# Deny-all default; jobs grant the minimum they need. +permissions: {} + +env: + TURBO_TELEMETRY_DISABLED: 1 + DO_NOT_TRACK: 1 + +concurrency: + group: coded-app-preview-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy: + name: Deploy Apollo Coded App Previews + if: github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + 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: Install UiPath CLI + run: npm install --global @uipath/cli@1.197.0 + + - name: Authenticate UiPath External App + env: + UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} + UIPATH_CLIENT_ID: ${{ secrets.UIPATH_CLIENT_ID }} + UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE }} + UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }} + UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }} + UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }} + run: | + missing=0 + for name in UIPATH_BASE_URL UIPATH_CLIENT_ID UIPATH_CLIENT_SECRET UIPATH_ORG_NAME UIPATH_TENANT_NAME; do + if [ -z "${!name}" ]; then + echo "::error::$name is required for Coded App preview deployments." + missing=1 + fi + done + if [ "$missing" -ne 0 ]; then + exit 1 + fi + + export UIPATH_CLIENT_SCOPE="${UIPATH_CLIENT_SCOPE:-Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution}" + + node <<'NODE' + const fs = require('node:fs'); + + const baseUrl = process.env.UIPATH_BASE_URL.replace(/\/+$/, ''); + const scope = process.env.UIPATH_CLIENT_SCOPE; + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: process.env.UIPATH_CLIENT_ID, + client_secret: process.env.UIPATH_CLIENT_SECRET, + scope, + }); + + function parseJwtPayload(token) { + const [, payload] = String(token).split('.'); + if (!payload) return {}; + return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); + } + + function appendEnv(name, value) { + fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); + } + + async function main() { + const tokenResponse = await fetch(`${baseUrl}/identity_/connect/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params, + }); + const tokenData = await tokenResponse.json().catch(() => ({})); + if (!tokenResponse.ok || !tokenData.access_token) { + throw new Error(`External App token exchange failed with HTTP ${tokenResponse.status}: ${JSON.stringify(tokenData)}`); + } + + const claims = parseJwtPayload(tokenData.access_token); + const orgId = claims.prt_id || claims.prtId || claims.organizationId; + if (!orgId) { + throw new Error('External App token did not include an organization id.'); + } + + const contextResponse = await fetch(`${baseUrl}/${orgId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`, { + headers: { Authorization: `Bearer ${tokenData.access_token}` }, + }); + const contextText = await contextResponse.text(); + if (!contextResponse.ok) { + throw new Error(`Could not fetch External App tenant context (HTTP ${contextResponse.status}): ${contextText}`); + } + const context = JSON.parse(contextText); + const tenant = (context.tenants || []).find((item) => item.name === process.env.UIPATH_TENANT_NAME); + if (!tenant) { + throw new Error(`External App cannot access tenant "${process.env.UIPATH_TENANT_NAME}".`); + } + + console.log(`::add-mask::${tokenData.access_token}`); + appendEnv('UIPATH_ACCESS_TOKEN', tokenData.access_token); + appendEnv('UIPATH_BASE_URL', baseUrl); + appendEnv('UIPATH_URL', baseUrl); + appendEnv('UIPATH_ORG_ID', orgId); + appendEnv('UIPATH_ORGANIZATION_ID', orgId); + appendEnv('UIPATH_ORG_NAME', process.env.UIPATH_ORG_NAME || context.organization?.name || ''); + appendEnv('UIPATH_TENANT_ID', tenant.id); + appendEnv('UIPATH_TENANT_NAME', tenant.name); + } + + main().catch((error) => { + console.error(error); + process.exit(1); + }); + NODE + + - name: Deploy Coded App previews + id: deploy + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }} + run: | + missing=0 + for name in UIPATH_ACCESS_TOKEN UIPATH_BASE_URL UIPATH_FOLDER_KEY UIPATH_ORG_NAME UIPATH_TENANT_NAME; do + if [ -z "${!name}" ]; then + echo "::error::$name is required for 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}" + + fix_relative_assets() { + local output_dir="$1" + OUTPUT_DIR="$output_dir" node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const root = process.env.OUTPUT_DIR; + const filePattern = /\.(?:html|css|js|json)$/i; + + function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.isFile() && filePattern.test(fullPath)) { + const current = fs.readFileSync(fullPath, 'utf8'); + const updated = current + .replace(/\b(src|href)=["']\/(?!\/|https?:|data:)/g, '$1="./') + .replace(/url\(\s*\/(?!\/|https?:|data:)/g, 'url(./'); + if (updated !== current) { + fs.writeFileSync(fullPath, updated); + } + } + } + } + + walk(root); + NODE + } + + deploy_coded_app() { + local label="$1" + local app_name="$2" + local output_dir="$3" + local tags="$4" + local asset_mode="${5:-relative}" + local uipath_dir=".uipath/${label}" + + if [ ! -d "$output_dir" ]; then + echo "::error::Build output directory not found: $output_dir" + exit 1 + fi + + if [ "$asset_mode" = "relative" ]; then + fix_relative_assets "$output_dir" + fi + + uip codedapp pack "$output_dir" \ + --name "$app_name" \ + --version "$version" \ + --output "$uipath_dir" + + uip codedapp publish \ + --name "$app_name" \ + --version "$version" \ + --type Web \ + --uipath-dir "$uipath_dir" \ + --base-url "$UIPATH_BASE_URL" \ + --tenant-name "$UIPATH_TENANT_NAME" \ + --access-token "$UIPATH_ACCESS_TOKEN" + + for attempt in 1 2 3 4; do + if uip codedapp deploy \ + --name "$app_name" \ + --base-url "$UIPATH_BASE_URL" \ + --org-name "$UIPATH_ORG_NAME" \ + --folder-key "$UIPATH_FOLDER_KEY" \ + --access-token "$UIPATH_ACCESS_TOKEN" \ + --tags "$tags"; then + return 0 + fi + + echo "Deploy attempt ${attempt} failed; waiting for package indexing..." + sleep 10 + done + + echo "::error::Failed to deploy $app_name after retries." + exit 1 + } + + stage_next_export() { + local source_dir="$1" + local stage_dir="$2" + local overlay="$3" + SOURCE_DIR="$source_dir" STAGE_DIR="$stage_dir" OVERLAY="$overlay" node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const sourceDir = path.resolve(process.env.SOURCE_DIR); + const stageDir = path.resolve(process.env.STAGE_DIR); + const overlay = process.env.OVERLAY; + + const docsNextConfig = [ + 'import nextra from "nextra";', + '', + 'const withNextra = nextra({', + ' defaultShowCopyCode: true,', + '});', + '', + 'const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replace(/^\\/+|\\/+$/g, "");', + 'const codedAppBasePath = codedAppPath ? `/${codedAppPath}` : undefined;', + '', + 'export default withNextra({', + ' output: "export",', + ' trailingSlash: true,', + ' ...(codedAppBasePath && { basePath: codedAppBasePath }),', + ' reactCompiler: true,', + ' turbopack: {', + ' resolveAlias: {', + ' "next-mdx-import-source-file": "./mdx-components.tsx",', + ' },', + ' },', + '});', + '', + ].join('\n'); + + const vertexNextConfig = [ + 'import nextra from "nextra";', + '', + 'const withNextra = nextra({', + ' defaultShowCopyCode: true,', + '});', + '', + 'const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replace(/^\\/+|\\/+$/g, "");', + 'const codedAppBasePath = codedAppPath ? `/${codedAppPath}` : undefined;', + '', + 'export default withNextra({', + ' output: "export",', + ' trailingSlash: true,', + ' env: {', + ' NEXT_PUBLIC_APOLLO_CODED_APP_PATH: codedAppPath ?? "",', + ' },', + ' ...(codedAppBasePath && { basePath: codedAppBasePath }),', + ' reactCompiler: true,', + ' turbopack: {', + ' resolveAlias: {', + ' "next-mdx-import-source-file": "./mdx-components.tsx",', + ' },', + ' },', + '});', + '', + ].join('\n'); + + const mdxDeclaration = [ + "declare module '*.mdx' {", + " import type { ComponentType } from 'react';", + '', + ' const MDXComponent: ComponentType;', + ' export default MDXComponent;', + '}', + '', + ].join('\n'); + + const disabledAiChatTemplate = [ + 'export function AiChatTemplate() {', + ' return (', + '
', + '

', + ' AI Chat is not available in Coded App preview', + '

', + '

', + ' The demo needs browser calls to UiPath backend services that are blocked', + ' by cross-origin preflight checks in the Coded App host. The rest of', + ' Apollo Vertex is still available in this preview.', + '

', + '
', + ' );', + '}', + '', + ].join('\n'); + + function normalize(value) { + return value.split(path.sep).join('/'); + } + + function shouldCopy(source) { + const rel = normalize(path.relative(sourceDir, source)); + if (!rel) return true; + const excluded = ['.next', 'out', 'node_modules']; + if (overlay === 'vertex') excluded.push('app/api'); + return !excluded.some((item) => rel === item || rel.startsWith(`${item}/`) || path.basename(source) === item); + } + + function writeStageFile(relativePath, content) { + const target = path.join(stageDir, relativePath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content, 'utf8'); + } + + fs.rmSync(stageDir, { recursive: true, force: true }); + fs.mkdirSync(path.dirname(stageDir), { recursive: true }); + fs.cpSync(sourceDir, stageDir, { recursive: true, filter: shouldCopy }); + + const nodeModules = [ + path.join(sourceDir, 'node_modules'), + path.resolve('node_modules'), + ].find((candidate) => fs.existsSync(candidate)); + if (nodeModules) { + fs.symlinkSync(nodeModules, path.join(stageDir, 'node_modules'), 'dir'); + } + + if (overlay === 'docs') { + writeStageFile('next.config.mjs', docsNextConfig); + writeStageFile('mdx.d.ts', mdxDeclaration); + writeStageFile('app/page.tsx', 'import OverviewPage from "./introduction/overview/page.mdx";\n\nexport default function Page() {\n return ;\n}\n'); + } else if (overlay === 'vertex') { + writeStageFile('next.config.ts', vertexNextConfig); + writeStageFile('templates/AiChatTemplate.tsx', disabledAiChatTemplate); + writeStageFile('mdx.d.ts', mdxDeclaration); + writeStageFile('app/components/page.tsx', 'import ComponentsOverviewPage from "./overview/page.mdx";\n\nexport default function ComponentsIndex() {\n return ;\n}\n'); + } else { + throw new Error(`Unknown overlay: ${overlay}`); + } + NODE + } + + postprocess_next_export() { + local output_dir="$1" + local app_name="$2" + OUTPUT_DIR="$output_dir" APP_BASE="/${app_name}" node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const outputDir = path.resolve(process.env.OUTPUT_DIR); + const appBase = process.env.APP_BASE; + + function normalize(value) { + return value.split(path.sep).join('/'); + } + + function findHtmlFiles(dir) { + const files = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findHtmlFiles(fullPath)); + } else if (entry.isFile() && fullPath.endsWith('.html')) { + files.push(fullPath); + } + } + return files; + } + + function htmlRoute(htmlFile) { + const route = normalize(path.relative(outputDir, htmlFile)) + .replace(/\/index\.html$/i, '') + .replace(/\.html$/i, '') + .replace(/^\/+|\/+$/g, ''); + return route === 'index' ? '' : route; + } + + function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function escapeHtmlAttribute(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); + } + + function isAssetPath(route) { + return /(^|\/)(?:_next|_pagefind|r)(?:\/|$)/.test(route) || + /\.[a-z0-9]+(?:[?#].*)?$/i.test(route); + } + + function shouldPrefixRootPath(rootPath) { + if (!rootPath || rootPath.startsWith('//')) return false; + return rootPath !== appBase && !rootPath.startsWith(`${appBase}/`); + } + + function indexedHref(href) { + if (!href.startsWith(`${appBase}/`)) return href; + const match = href.match(/^([^?#]*)([?#].*)?$/); + if (!match) return href; + const [, pathname, suffix = ''] = match; + if (pathname === appBase || pathname === `${appBase}/`) return href; + const route = pathname.slice(appBase.length).replace(/^\/+|\/+$/g, ''); + if (!route || route.endsWith('index.html') || isAssetPath(route)) return href; + return `${appBase}/${route}/index.html${suffix}`; + } + + function normalizeRouteHrefs(content) { + return content.replace(/(^|[\s<])href=(["'])([^"']+)\2/g, (match, lead, quote, href) => { + const normalized = indexedHref(href); + return normalized === href ? match : `${lead}href=${quote}${normalized}${quote}`; + }); + } + + function unprefixDataHrefs(content) { + const unprefixed = content.replace( + new RegExp(`\\bdata-href=(["'])${escapeRegExp(appBase)}/`, 'g'), + 'data-href=$1/', + ); + return unprefixed.replace( + /\bdata-href=(["'])(\/[^"']*?)\/index\.html([?#][^"']*)?\1/g, + (_match, quote, route, suffix = '') => `data-href=${quote}${route}${suffix}${quote}`, + ); + } + + function prefixRootPaths(content) { + let updated = content.replace(/\b(src|href)=(["'])\/(?!\/)([^"']*)\2/g, (match, attr, quote, rest) => { + const rootPath = `/${rest}`; + if (!shouldPrefixRootPath(rootPath)) return match; + return `${attr}=${quote}${appBase}${rootPath}${quote}`; + }); + updated = updated.replace(/("src"\s*:\s*")\/(?!\/)([^"\\]*)/g, (match, prefix, rest) => { + const rootPath = `/${rest}`; + if (!shouldPrefixRootPath(rootPath)) return match; + return `${prefix}${appBase}${rootPath.slice(1) ? rootPath : '/'}`; + }); + updated = updated.replace(/(\\"src\\"\s*:\s*\\")\/(?!\/)([^"\\]*)/g, (match, prefix, rest) => { + const rootPath = `/${rest}`; + if (!shouldPrefixRootPath(rootPath)) return match; + return `${prefix}${appBase}${rootPath.slice(1) ? rootPath : '/'}`; + }); + return unprefixDataHrefs(normalizeRouteHrefs(updated)); + } + + function navigationShim() { + return [ + '(()=>{', + `const configuredBase=${JSON.stringify(appBase)};`, + 'const cleanBase=(value)=>(value||"").replace(/\\/+$/g,"");', + 'const appBase=()=>cleanBase(document.querySelector(\'meta[name="uipath:app-base"]\')?.content)||configuredBase;', + 'const cleanRoute=(value)=>(value||"").replace(/^\\/+|\\/+$/g,"");', + 'const routeMeta=()=>cleanRoute(document.querySelector(\'meta[name="uip-go:route"]\')?.content);', + 'const isAssetPath=(path)=>(/(^|\\/)(?:_next|_pagefind|r)(?:\\/|$)/.test(path)||/\\.[a-z0-9]+(?:[?#].*)?$/i.test(path));', + 'const relativeRoute=(pathname)=>{const base=appBase();if(base&&(pathname===base||pathname===`${base}/`))return "";if(base&&pathname.startsWith(`${base}/`))return pathname.slice(base.length+1);return pathname.replace(/^\\/+/, "");};', + 'const indexedUrl=(url)=>{const base=appBase();const route=cleanRoute(relativeRoute(url.pathname));if(!route||route.endsWith("index.html")||isAssetPath(route))return url;url.pathname=`${base}/${route}/index.html`;return url;};', + 'const route=cleanRoute(relativeRoute(window.location.pathname));', + 'if(route&&route!==routeMeta()&&!route.endsWith("index.html")&&!isAssetPath(route)){', + 'const url=new URL(window.location.href);indexedUrl(url);window.location.replace(url.href);return;', + '}', + 'document.addEventListener("click",(event)=>{', + 'if(event.defaultPrevented||event.metaKey||event.ctrlKey||event.shiftKey||event.altKey||event.button!==0)return;', + 'const anchor=event.target?.closest?.("a[href]");', + 'if(!anchor)return;', + 'const url=new URL(anchor.getAttribute("href"),window.location.href);', + 'if(url.origin!==window.location.origin)return;', + 'const base=appBase();', + 'if(base&&!(url.pathname===base||url.pathname.startsWith(`${base}/`))){const rootPath=url.pathname.replace(/^\\/+/, "");if(rootPath&&isAssetPath(rootPath))return;url.pathname=rootPath?`${base}/${rootPath}`:`${base}/`;}', + 'indexedUrl(url);', + 'if(url.hash&&url.pathname===window.location.pathname&&url.search===window.location.search)return;', + 'const nextRoute=cleanRoute(relativeRoute(url.pathname));', + 'if(isAssetPath(nextRoute))return;', + 'event.preventDefault();', + 'event.stopImmediatePropagation();', + 'window.location.assign(url.href);', + '},true);', + '})();', + ].join(''); + } + + function injectNavigationShim(content, route) { + const cleaned = content + .replace(//g, '') + .replace(/`; + const headMatch = cleaned.match(/]*>/i); + if (!headMatch || headMatch.index === undefined) return `${routeMeta}${shim}${cleaned}`; + const insertAt = headMatch.index + headMatch[0].length; + return `${cleaned.slice(0, insertAt)}${routeMeta}${shim}${cleaned.slice(insertAt)}`; + } + + let updatedCount = 0; + for (const htmlFile of findHtmlFiles(outputDir)) { + const original = fs.readFileSync(htmlFile, 'utf8'); + const updated = injectNavigationShim(prefixRootPaths(original), htmlRoute(htmlFile)); + if (updated !== original) { + fs.writeFileSync(htmlFile, updated, 'utf8'); + updatedCount += 1; + } + } + console.log(`Updated ${updatedCount} HTML files for ${appBase}.`); + NODE + } + + prepare_docs_export() { + APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$docs_app" CI=true pnpm turbo build --filter=apollo-docs + stage_next_export "apps/apollo-docs" ".uipath-build/apollo-docs" "docs" + (cd ".uipath-build/apollo-docs" && APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$docs_app" CI=true pnpm exec next build) + postprocess_next_export ".uipath-build/apollo-docs/out" "$docs_app" + rm -rf "apps/apollo-docs/out" + cp -R ".uipath-build/apollo-docs/out" "apps/apollo-docs/out" + } + + prepare_vertex_export() { + stage_next_export "apps/apollo-vertex" ".uipath-build/apollo-vertex" "vertex" + ( + cd ".uipath-build/apollo-vertex" + APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$vertex_app" CI=true pnpm generate:theme + APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$vertex_app" CI=true pnpm registry:build + APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$vertex_app" CI=true pnpm exec next build + ) + postprocess_next_export ".uipath-build/apollo-vertex/out" "$vertex_app" + rm -rf "apps/apollo-vertex/out" + cp -R ".uipath-build/apollo-vertex/out" "apps/apollo-vertex/out" + } + + pnpm turbo build --filter=apollo-landing + deploy_coded_app "apollo-landing" "$landing_app" "apps/landing/dist" "preview,apollo,apollo-landing" + + prepare_docs_export + deploy_coded_app "apollo-docs" "$docs_app" "apps/apollo-docs/out" "preview,apollo,apollo-docs" "none" + + pnpm turbo run storybook:build --filter=storybook-app + deploy_coded_app "apollo-design" "$design_app" "apps/storybook/storybook-static" "preview,apollo,apollo-design" + + prepare_vertex_export + deploy_coded_app "apollo-vertex" "$vertex_app" "apps/apollo-vertex/out" "preview,apollo,apollo-vertex" "none" + + host_env="$(echo "$UIPATH_BASE_URL" | sed -E 's#^https?://##; s#/.*$##; s#\\.uipath\\.com$##')" + if [ "$host_env" = "cloud" ]; then + app_host="${UIPATH_ORG_NAME}.uipath.host" + else + app_host="${UIPATH_ORG_NAME}.${host_env}.uipath.host" + fi + + { + echo "landing_url=https://${app_host}/${landing_app}" + echo "docs_url=https://${app_host}/${docs_app}" + echo "design_url=https://${app_host}/${design_app}" + echo "vertex_url=https://${app_host}/${vertex_app}" + } >> "$GITHUB_OUTPUT" + + - name: Save Turborepo cache + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }} + + - name: Update PR comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + APOLLO_DESIGN_URL: ${{ steps.deploy.outputs.design_url }} + APOLLO_DOCS_URL: ${{ steps.deploy.outputs.docs_url }} + APOLLO_LANDING_URL: ${{ steps.deploy.outputs.landing_url }} + APOLLO_VERTEX_URL: ${{ steps.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 body = [ + identifier, + 'Apollo Coded App preview deployments are ready.', + '', + '| Project | Preview | Updated (PT) |', + '|---------|---------|--------------|', + `| apollo-design | [Preview](${process.env.APOLLO_DESIGN_URL}) · ${logsLink} | ${timestamp} |`, + `| apollo-docs | [Preview](${process.env.APOLLO_DOCS_URL}) · ${logsLink} | ${timestamp} |`, + `| apollo-landing | [Preview](${process.env.APOLLO_LANDING_URL}) · ${logsLink} | ${timestamp} |`, + `| apollo-vertex | [Preview](${process.env.APOLLO_VERTEX_URL}) · ${logsLink} | ${timestamp} |` + ].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(comment => comment.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/.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/ From 89da62ea08e3996c0caa05cdde1041e7c1f35ffe Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 16:01:10 -0700 Subject: [PATCH 02/29] ci(ci): pin published UiPath CLI version --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 4e9b671e6..0102892ba 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -46,7 +46,7 @@ jobs: ${{ runner.os }}-turbo-${{ github.ref_name }}- - name: Install UiPath CLI - run: npm install --global @uipath/cli@1.197.0 + run: npm install --global @uipath/cli@1.196.4 - name: Authenticate UiPath External App env: From a70e4af5d6549257f39d34f37964c2a46db674fd Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 16:06:05 -0700 Subject: [PATCH 03/29] ci(ci): read UiPath client ID from variables --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 0102892ba..39550e8c3 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -51,7 +51,7 @@ jobs: - name: Authenticate UiPath External App env: UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} - UIPATH_CLIENT_ID: ${{ secrets.UIPATH_CLIENT_ID }} + UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID }} UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE }} UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }} UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }} From 9a689575fdc6769d85b6ec8d841ab04adf538fc2 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 18:20:17 -0700 Subject: [PATCH 04/29] ci(ci): improve coded app preview links --- .github/workflows/preview-deploy.yml | 179 +++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 23 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 39550e8c3..8739b5579 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -34,6 +34,54 @@ jobs: fetch-depth: 0 persist-credentials: false + - 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 logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + 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} | Deploying... | ${logsLink} | ${timestamp} |`) + ].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(comment => comment.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, + }); + } + - name: Install Node dependencies uses: ./.github/actions/install-node-deps @@ -193,6 +241,71 @@ jobs: NODE } + postprocess_static_app() { + local output_dir="$1" + local app_name="$2" + OUTPUT_DIR="$output_dir" APP_BASE="/${app_name}/" node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const root = process.env.OUTPUT_DIR; + const appBase = process.env.APP_BASE; + const filePattern = /\.(?:html|css|js|json)$/i; + + function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.isFile() && filePattern.test(fullPath)) { + const current = fs.readFileSync(fullPath, 'utf8'); + let updated = current + .replace(/\b(src|href)=(["'])\/(?!\/|https?:|data:)([^"']*)\2/g, (_match, attr, quote, rest) => `${attr}=${quote}${appBase}${rest}${quote}`) + .replace(/url\(\s*\/(?!\/|https?:|data:)([^)"'\s]*)/g, (_match, rest) => `url(${appBase}${rest}`); + + if (fullPath.endsWith('.html')) { + const baseTag = ``; + updated = updated.replace(/]*>\n?/g, ''); + const headMatch = updated.match(/]*>/i); + if (headMatch?.index !== undefined) { + const insertAt = headMatch.index + headMatch[0].length; + updated = `${updated.slice(0, insertAt)}${baseTag}${updated.slice(insertAt)}`; + } + } + + if (updated !== current) { + fs.writeFileSync(fullPath, updated); + } + } + } + } + + walk(root); + NODE + } + + 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 + } + deploy_coded_app() { local label="$1" local app_name="$2" @@ -200,6 +313,10 @@ jobs: local tags="$4" local asset_mode="${5:-relative}" local uipath_dir=".uipath/${label}" + local deploy_output + local deploy_exit + local app_url + local output_name if [ ! -d "$output_dir" ]; then echo "::error::Build output directory not found: $output_dir" @@ -208,6 +325,7 @@ jobs: if [ "$asset_mode" = "relative" ]; then fix_relative_assets "$output_dir" + postprocess_static_app "$output_dir" "$app_name" fi uip codedapp pack "$output_dir" \ @@ -225,13 +343,29 @@ jobs: --access-token "$UIPATH_ACCESS_TOKEN" for attempt in 1 2 3 4; do - if uip codedapp deploy \ + set +e + deploy_output="$(uip codedapp deploy \ --name "$app_name" \ --base-url "$UIPATH_BASE_URL" \ --org-name "$UIPATH_ORG_NAME" \ --folder-key "$UIPATH_FOLDER_KEY" \ --access-token "$UIPATH_ACCESS_TOKEN" \ - --tags "$tags"; then + --tags "$tags" 2>&1)" + deploy_exit=$? + set -e + printf '%s\n' "$deploy_output" + + if [ "$deploy_exit" -eq 0 ]; then + app_url="$(printf '%s\n' "$deploy_output" | sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' | 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 + output_name="${label#apollo-}_url" + echo "${output_name}=${app_url}" >> "$GITHUB_OUTPUT" return 0 fi @@ -578,20 +712,6 @@ jobs: prepare_vertex_export deploy_coded_app "apollo-vertex" "$vertex_app" "apps/apollo-vertex/out" "preview,apollo,apollo-vertex" "none" - host_env="$(echo "$UIPATH_BASE_URL" | sed -E 's#^https?://##; s#/.*$##; s#\\.uipath\\.com$##')" - if [ "$host_env" = "cloud" ]; then - app_host="${UIPATH_ORG_NAME}.uipath.host" - else - app_host="${UIPATH_ORG_NAME}.${host_env}.uipath.host" - fi - - { - echo "landing_url=https://${app_host}/${landing_app}" - echo "docs_url=https://${app_host}/${docs_app}" - echo "design_url=https://${app_host}/${design_app}" - echo "vertex_url=https://${app_host}/${vertex_app}" - } >> "$GITHUB_OUTPUT" - - name: Save Turborepo cache uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: @@ -599,8 +719,10 @@ jobs: key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }} - name: Update PR comment + if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: + DEPLOY_OUTCOME: ${{ steps.deploy.outcome }} APOLLO_DESIGN_URL: ${{ steps.deploy.outputs.design_url }} APOLLO_DOCS_URL: ${{ steps.deploy.outputs.docs_url }} APOLLO_LANDING_URL: ${{ steps.deploy.outputs.landing_url }} @@ -619,16 +741,27 @@ jobs: 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, - 'Apollo Coded App preview deployments are ready.', + deploySucceeded + ? 'Apollo Coded App preview deployments are ready.' + : 'Apollo Coded App preview deployments did not complete.', '', - '| Project | Preview | Updated (PT) |', - '|---------|---------|--------------|', - `| apollo-design | [Preview](${process.env.APOLLO_DESIGN_URL}) · ${logsLink} | ${timestamp} |`, - `| apollo-docs | [Preview](${process.env.APOLLO_DOCS_URL}) · ${logsLink} | ${timestamp} |`, - `| apollo-landing | [Preview](${process.env.APOLLO_LANDING_URL}) · ${logsLink} | ${timestamp} |`, - `| apollo-vertex | [Preview](${process.env.APOLLO_VERTEX_URL}) · ${logsLink} | ${timestamp} |` + '| Project | Status | Preview | Updated (PT) |', + '|---------|--------|---------|--------------|', + ...rows ].join('\n'); const { data: comments } = await github.rest.issues.listComments({ From 9661d1551246a11760f0b747af3b944d1eebd4e6 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 20:36:58 -0700 Subject: [PATCH 05/29] ci(ci): fix apollo design logo preview path --- .github/workflows/preview-deploy.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 8739b5579..5b60f671b 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -261,7 +261,11 @@ jobs: const current = fs.readFileSync(fullPath, 'utf8'); let updated = current .replace(/\b(src|href)=(["'])\/(?!\/|https?:|data:)([^"']*)\2/g, (_match, attr, quote, rest) => `${attr}=${quote}${appBase}${rest}${quote}`) - .replace(/url\(\s*\/(?!\/|https?:|data:)([^)"'\s]*)/g, (_match, rest) => `url(${appBase}${rest}`); + .replace(/url\(\s*\/(?!\/|https?:|data:)([^)"'\s]*)/g, (_match, rest) => `url(${appBase}${rest}`) + .replace(/(["'`])\.\/brand\//g, (_match, quote) => `${quote}${appBase}brand/`) + .replace(/(["'`])\/brand\//g, (_match, quote) => `${quote}${appBase}brand/`) + .replace(/url\(\s*(["']?)\.\/brand\//g, (_match, quote) => `url(${quote}${appBase}brand/`) + .replace(/url\(\s*(["']?)\/brand\//g, (_match, quote) => `url(${quote}${appBase}brand/`); if (fullPath.endsWith('.html')) { const baseTag = ``; From d4db8bd3a46bba428e8fc0668541dc5bddcf6208 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 20:47:17 -0700 Subject: [PATCH 06/29] ci(ci): address copilot preview workflow feedback --- .github/workflows/preview-deploy.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 5b60f671b..dc0c8c17c 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -140,6 +140,16 @@ jobs: fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); } + function safeOAuthError(data) { + const summary = {}; + for (const key of ['error', 'error_description', 'error_uri']) { + if (typeof data?.[key] === 'string') { + summary[key] = data[key]; + } + } + return Object.keys(summary).length ? JSON.stringify(summary) : 'No safe error details returned.'; + } + async function main() { const tokenResponse = await fetch(`${baseUrl}/identity_/connect/token`, { method: 'POST', @@ -148,7 +158,7 @@ jobs: }); const tokenData = await tokenResponse.json().catch(() => ({})); if (!tokenResponse.ok || !tokenData.access_token) { - throw new Error(`External App token exchange failed with HTTP ${tokenResponse.status}: ${JSON.stringify(tokenData)}`); + throw new Error(`External App token exchange failed with HTTP ${tokenResponse.status}: ${safeOAuthError(tokenData)}`); } const claims = parseJwtPayload(tokenData.access_token); @@ -228,8 +238,8 @@ jobs: } else if (entry.isFile() && filePattern.test(fullPath)) { const current = fs.readFileSync(fullPath, 'utf8'); const updated = current - .replace(/\b(src|href)=["']\/(?!\/|https?:|data:)/g, '$1="./') - .replace(/url\(\s*\/(?!\/|https?:|data:)/g, 'url(./'); + .replace(/\b(src|href)=(["'])\/(?!\/|https?:|data:)([^"']*)\2/g, (_match, attr, quote, rest) => `${attr}=${quote}./${rest}${quote}`) + .replace(/url\(\s*(["']?)\/(?!\/|https?:|data:)([^)"'\s]*)/g, (_match, quote, rest) => `url(${quote}./${rest}`); if (updated !== current) { fs.writeFileSync(fullPath, updated); } @@ -619,6 +629,11 @@ jobs: if (!shouldPrefixRootPath(rootPath)) return match; return `${prefix}${appBase}${rootPath.slice(1) ? rootPath : '/'}`; }); + updated = updated.replace(/url\(\s*(["']?)\/(?!\/|https?:|data:)([^)"'\s]*)/g, (match, quote, rest) => { + const rootPath = `/${rest}`; + if (!shouldPrefixRootPath(rootPath)) return match; + return `url(${quote}${appBase}${rootPath}`; + }); return unprefixDataHrefs(normalizeRouteHrefs(updated)); } From 92781018a06ba2830467b76f468e197932d5d5bb Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 21:06:59 -0700 Subject: [PATCH 07/29] ci(ci): smooth coded app docs navigation --- .github/workflows/preview-deploy.yml | 134 +++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index dc0c8c17c..5f2b45e71 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -416,6 +416,9 @@ jobs: 'export default withNextra({', ' output: "export",', ' trailingSlash: true,', + ' env: {', + ' NEXT_PUBLIC_APOLLO_CODED_APP_PATH: codedAppPath ?? "",', + ' },', ' ...(codedAppBasePath && { basePath: codedAppBasePath }),', ' reactCompiler: true,', ' turbopack: {', @@ -454,6 +457,106 @@ jobs: '', ].join('\n'); + const codedAppNavigationComponent = [ + "'use client';", + '', + "import { useEffect } from 'react';", + "import { useRouter } from 'next/navigation';", + '', + 'const configuredBase = process.env.NEXT_PUBLIC_APOLLO_CODED_APP_PATH', + ' ? `/${process.env.NEXT_PUBLIC_APOLLO_CODED_APP_PATH.replace(/^\\/+|\\/+$/g, "")}`', + " : '';", + '', + 'function cleanBase(value: string) {', + ' return value.replace(/\\/+$/g, "");', + '}', + '', + 'function cleanRoute(value: string) {', + ' return value.replace(/^\\/+|\\/+$/g, "");', + '}', + '', + 'function appBase() {', + ' const metaBase = document.querySelector(\'meta[name="uipath:app-base"]\')?.content;', + ' return cleanBase(metaBase || configuredBase);', + '}', + '', + 'function isAssetPath(route: string) {', + ' return /(^|\\/)(?:_next|_pagefind|r)(?:\\/|$)/.test(route) ||', + ' /\\.[a-z0-9]+(?:[?#].*)?$/i.test(route);', + '}', + '', + 'function toNextRoute(rawHref: string) {', + ' const url = new URL(rawHref, window.location.href);', + ' if (url.origin !== window.location.origin) return null;', + '', + ' const base = appBase();', + ' let relative = "";', + ' if (base && (url.pathname === base || url.pathname === `${base}/`)) {', + ' relative = "";', + ' } else if (base && url.pathname.startsWith(`${base}/`)) {', + ' relative = url.pathname.slice(base.length + 1);', + ' } else if (base) {', + ' relative = url.pathname.replace(/^\\/+/, "");', + ' } else {', + ' relative = url.pathname.replace(/^\\/+/, "");', + ' }', + '', + ' relative = cleanRoute(relative);', + ' if (relative === "index.html") relative = "";', + ' if (relative.endsWith("/index.html")) {', + ' relative = relative.slice(0, -"/index.html".length);', + ' }', + ' relative = cleanRoute(relative);', + ' if (relative && isAssetPath(relative)) return null;', + '', + ' const routePath = relative ? `/${relative}` : "/";', + ' const routeHref = `${routePath}${url.search}${url.hash}`;', + ' const indexedPath = relative ? `${base}/${relative}/index.html` : `${base || ""}/`;', + ' const indexedHref = `${indexedPath}${url.search}${url.hash}`;', + ' return { routeHref, indexedHref };', + '}', + '', + 'export function CodedAppNavigation() {', + ' const router = useRouter();', + '', + ' useEffect(() => {', + ' function onClick(event: MouseEvent) {', + ' if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;', + ' const target = event.target instanceof Element ? event.target : null;', + ' const anchor = target?.closest("a[href]");', + ' if (!anchor || anchor.download) return;', + ' if (anchor.target && anchor.target !== "_self") return;', + '', + ' const href = anchor.getAttribute("href");', + ' if (!href || href.startsWith("#")) return;', + '', + ' const nextRoute = toNextRoute(href);', + ' if (!nextRoute) return;', + '', + ' const currentRoute = toNextRoute(window.location.href);', + ' if (currentRoute?.routeHref === nextRoute.routeHref) return;', + '', + ' event.preventDefault();', + ' event.stopImmediatePropagation();', + ' router.push(nextRoute.routeHref);', + '', + ' window.setTimeout(() => {', + ' const visibleRoute = toNextRoute(window.location.href);', + ' if (visibleRoute?.routeHref === nextRoute.routeHref) {', + ' window.history.replaceState(window.history.state, "", nextRoute.indexedHref);', + ' }', + ' }, 250);', + ' }', + '', + ' document.addEventListener("click", onClick, true);', + ' return () => document.removeEventListener("click", onClick, true);', + ' }, [router]);', + '', + ' return null;', + '}', + '', + ].join('\n'); + const mdxDeclaration = [ "declare module '*.mdx' {", " import type { ComponentType } from 'react';", @@ -500,6 +603,19 @@ jobs: fs.writeFileSync(target, content, 'utf8'); } + function installCodedAppNavigation() { + writeStageFile('app/_components/coded-app-navigation.tsx', codedAppNavigationComponent); + const layoutPath = path.join(stageDir, 'app/layout.tsx'); + let layout = fs.readFileSync(layoutPath, 'utf8'); + if (!layout.includes('coded-app-navigation')) { + layout = `import { CodedAppNavigation } from "./_components/coded-app-navigation";\n${layout}`; + } + if (!layout.includes('')) { + layout = layout.replace(/]*)>/, '\n '); + } + fs.writeFileSync(layoutPath, layout, 'utf8'); + } + fs.rmSync(stageDir, { recursive: true, force: true }); fs.mkdirSync(path.dirname(stageDir), { recursive: true }); fs.cpSync(sourceDir, stageDir, { recursive: true, filter: shouldCopy }); @@ -516,11 +632,13 @@ jobs: writeStageFile('next.config.mjs', docsNextConfig); writeStageFile('mdx.d.ts', mdxDeclaration); writeStageFile('app/page.tsx', 'import OverviewPage from "./introduction/overview/page.mdx";\n\nexport default function Page() {\n return ;\n}\n'); + installCodedAppNavigation(); } else if (overlay === 'vertex') { writeStageFile('next.config.ts', vertexNextConfig); writeStageFile('templates/AiChatTemplate.tsx', disabledAiChatTemplate); writeStageFile('mdx.d.ts', mdxDeclaration); writeStageFile('app/components/page.tsx', 'import ComponentsOverviewPage from "./overview/page.mdx";\n\nexport default function ComponentsIndex() {\n return ;\n}\n'); + installCodedAppNavigation(); } else { throw new Error(`Unknown overlay: ${overlay}`); } @@ -652,22 +770,6 @@ jobs: 'if(route&&route!==routeMeta()&&!route.endsWith("index.html")&&!isAssetPath(route)){', 'const url=new URL(window.location.href);indexedUrl(url);window.location.replace(url.href);return;', '}', - 'document.addEventListener("click",(event)=>{', - 'if(event.defaultPrevented||event.metaKey||event.ctrlKey||event.shiftKey||event.altKey||event.button!==0)return;', - 'const anchor=event.target?.closest?.("a[href]");', - 'if(!anchor)return;', - 'const url=new URL(anchor.getAttribute("href"),window.location.href);', - 'if(url.origin!==window.location.origin)return;', - 'const base=appBase();', - 'if(base&&!(url.pathname===base||url.pathname.startsWith(`${base}/`))){const rootPath=url.pathname.replace(/^\\/+/, "");if(rootPath&&isAssetPath(rootPath))return;url.pathname=rootPath?`${base}/${rootPath}`:`${base}/`;}', - 'indexedUrl(url);', - 'if(url.hash&&url.pathname===window.location.pathname&&url.search===window.location.search)return;', - 'const nextRoute=cleanRoute(relativeRoute(url.pathname));', - 'if(isAssetPath(nextRoute))return;', - 'event.preventDefault();', - 'event.stopImmediatePropagation();', - 'window.location.assign(url.href);', - '},true);', '})();', ].join(''); } From e644a242fbb8b6d50a4c1eec2a887a5c479e690e Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 21:15:19 -0700 Subject: [PATCH 08/29] ci(ci): remove failed coded app nav shim --- .github/workflows/preview-deploy.yml | 171 +-------------------------- 1 file changed, 1 insertion(+), 170 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 5f2b45e71..b0a6b14b8 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -416,9 +416,6 @@ jobs: 'export default withNextra({', ' output: "export",', ' trailingSlash: true,', - ' env: {', - ' NEXT_PUBLIC_APOLLO_CODED_APP_PATH: codedAppPath ?? "",', - ' },', ' ...(codedAppBasePath && { basePath: codedAppBasePath }),', ' reactCompiler: true,', ' turbopack: {', @@ -457,106 +454,6 @@ jobs: '', ].join('\n'); - const codedAppNavigationComponent = [ - "'use client';", - '', - "import { useEffect } from 'react';", - "import { useRouter } from 'next/navigation';", - '', - 'const configuredBase = process.env.NEXT_PUBLIC_APOLLO_CODED_APP_PATH', - ' ? `/${process.env.NEXT_PUBLIC_APOLLO_CODED_APP_PATH.replace(/^\\/+|\\/+$/g, "")}`', - " : '';", - '', - 'function cleanBase(value: string) {', - ' return value.replace(/\\/+$/g, "");', - '}', - '', - 'function cleanRoute(value: string) {', - ' return value.replace(/^\\/+|\\/+$/g, "");', - '}', - '', - 'function appBase() {', - ' const metaBase = document.querySelector(\'meta[name="uipath:app-base"]\')?.content;', - ' return cleanBase(metaBase || configuredBase);', - '}', - '', - 'function isAssetPath(route: string) {', - ' return /(^|\\/)(?:_next|_pagefind|r)(?:\\/|$)/.test(route) ||', - ' /\\.[a-z0-9]+(?:[?#].*)?$/i.test(route);', - '}', - '', - 'function toNextRoute(rawHref: string) {', - ' const url = new URL(rawHref, window.location.href);', - ' if (url.origin !== window.location.origin) return null;', - '', - ' const base = appBase();', - ' let relative = "";', - ' if (base && (url.pathname === base || url.pathname === `${base}/`)) {', - ' relative = "";', - ' } else if (base && url.pathname.startsWith(`${base}/`)) {', - ' relative = url.pathname.slice(base.length + 1);', - ' } else if (base) {', - ' relative = url.pathname.replace(/^\\/+/, "");', - ' } else {', - ' relative = url.pathname.replace(/^\\/+/, "");', - ' }', - '', - ' relative = cleanRoute(relative);', - ' if (relative === "index.html") relative = "";', - ' if (relative.endsWith("/index.html")) {', - ' relative = relative.slice(0, -"/index.html".length);', - ' }', - ' relative = cleanRoute(relative);', - ' if (relative && isAssetPath(relative)) return null;', - '', - ' const routePath = relative ? `/${relative}` : "/";', - ' const routeHref = `${routePath}${url.search}${url.hash}`;', - ' const indexedPath = relative ? `${base}/${relative}/index.html` : `${base || ""}/`;', - ' const indexedHref = `${indexedPath}${url.search}${url.hash}`;', - ' return { routeHref, indexedHref };', - '}', - '', - 'export function CodedAppNavigation() {', - ' const router = useRouter();', - '', - ' useEffect(() => {', - ' function onClick(event: MouseEvent) {', - ' if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;', - ' const target = event.target instanceof Element ? event.target : null;', - ' const anchor = target?.closest("a[href]");', - ' if (!anchor || anchor.download) return;', - ' if (anchor.target && anchor.target !== "_self") return;', - '', - ' const href = anchor.getAttribute("href");', - ' if (!href || href.startsWith("#")) return;', - '', - ' const nextRoute = toNextRoute(href);', - ' if (!nextRoute) return;', - '', - ' const currentRoute = toNextRoute(window.location.href);', - ' if (currentRoute?.routeHref === nextRoute.routeHref) return;', - '', - ' event.preventDefault();', - ' event.stopImmediatePropagation();', - ' router.push(nextRoute.routeHref);', - '', - ' window.setTimeout(() => {', - ' const visibleRoute = toNextRoute(window.location.href);', - ' if (visibleRoute?.routeHref === nextRoute.routeHref) {', - ' window.history.replaceState(window.history.state, "", nextRoute.indexedHref);', - ' }', - ' }, 250);', - ' }', - '', - ' document.addEventListener("click", onClick, true);', - ' return () => document.removeEventListener("click", onClick, true);', - ' }, [router]);', - '', - ' return null;', - '}', - '', - ].join('\n'); - const mdxDeclaration = [ "declare module '*.mdx' {", " import type { ComponentType } from 'react';", @@ -603,19 +500,6 @@ jobs: fs.writeFileSync(target, content, 'utf8'); } - function installCodedAppNavigation() { - writeStageFile('app/_components/coded-app-navigation.tsx', codedAppNavigationComponent); - const layoutPath = path.join(stageDir, 'app/layout.tsx'); - let layout = fs.readFileSync(layoutPath, 'utf8'); - if (!layout.includes('coded-app-navigation')) { - layout = `import { CodedAppNavigation } from "./_components/coded-app-navigation";\n${layout}`; - } - if (!layout.includes('')) { - layout = layout.replace(/]*)>/, '\n '); - } - fs.writeFileSync(layoutPath, layout, 'utf8'); - } - fs.rmSync(stageDir, { recursive: true, force: true }); fs.mkdirSync(path.dirname(stageDir), { recursive: true }); fs.cpSync(sourceDir, stageDir, { recursive: true, filter: shouldCopy }); @@ -632,13 +516,11 @@ jobs: writeStageFile('next.config.mjs', docsNextConfig); writeStageFile('mdx.d.ts', mdxDeclaration); writeStageFile('app/page.tsx', 'import OverviewPage from "./introduction/overview/page.mdx";\n\nexport default function Page() {\n return ;\n}\n'); - installCodedAppNavigation(); } else if (overlay === 'vertex') { writeStageFile('next.config.ts', vertexNextConfig); writeStageFile('templates/AiChatTemplate.tsx', disabledAiChatTemplate); writeStageFile('mdx.d.ts', mdxDeclaration); writeStageFile('app/components/page.tsx', 'import ComponentsOverviewPage from "./overview/page.mdx";\n\nexport default function ComponentsIndex() {\n return ;\n}\n'); - installCodedAppNavigation(); } else { throw new Error(`Unknown overlay: ${overlay}`); } @@ -655,10 +537,6 @@ jobs: const outputDir = path.resolve(process.env.OUTPUT_DIR); const appBase = process.env.APP_BASE; - function normalize(value) { - return value.split(path.sep).join('/'); - } - function findHtmlFiles(dir) { const files = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { @@ -672,26 +550,10 @@ jobs: return files; } - function htmlRoute(htmlFile) { - const route = normalize(path.relative(outputDir, htmlFile)) - .replace(/\/index\.html$/i, '') - .replace(/\.html$/i, '') - .replace(/^\/+|\/+$/g, ''); - return route === 'index' ? '' : route; - } - function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } - function escapeHtmlAttribute(value) { - return String(value) - .replaceAll('&', '&') - .replaceAll('"', '"') - .replaceAll('<', '<') - .replaceAll('>', '>'); - } - function isAssetPath(route) { return /(^|\/)(?:_next|_pagefind|r)(?:\/|$)/.test(route) || /\.[a-z0-9]+(?:[?#].*)?$/i.test(route); @@ -755,41 +617,10 @@ jobs: return unprefixDataHrefs(normalizeRouteHrefs(updated)); } - function navigationShim() { - return [ - '(()=>{', - `const configuredBase=${JSON.stringify(appBase)};`, - 'const cleanBase=(value)=>(value||"").replace(/\\/+$/g,"");', - 'const appBase=()=>cleanBase(document.querySelector(\'meta[name="uipath:app-base"]\')?.content)||configuredBase;', - 'const cleanRoute=(value)=>(value||"").replace(/^\\/+|\\/+$/g,"");', - 'const routeMeta=()=>cleanRoute(document.querySelector(\'meta[name="uip-go:route"]\')?.content);', - 'const isAssetPath=(path)=>(/(^|\\/)(?:_next|_pagefind|r)(?:\\/|$)/.test(path)||/\\.[a-z0-9]+(?:[?#].*)?$/i.test(path));', - 'const relativeRoute=(pathname)=>{const base=appBase();if(base&&(pathname===base||pathname===`${base}/`))return "";if(base&&pathname.startsWith(`${base}/`))return pathname.slice(base.length+1);return pathname.replace(/^\\/+/, "");};', - 'const indexedUrl=(url)=>{const base=appBase();const route=cleanRoute(relativeRoute(url.pathname));if(!route||route.endsWith("index.html")||isAssetPath(route))return url;url.pathname=`${base}/${route}/index.html`;return url;};', - 'const route=cleanRoute(relativeRoute(window.location.pathname));', - 'if(route&&route!==routeMeta()&&!route.endsWith("index.html")&&!isAssetPath(route)){', - 'const url=new URL(window.location.href);indexedUrl(url);window.location.replace(url.href);return;', - '}', - '})();', - ].join(''); - } - - function injectNavigationShim(content, route) { - const cleaned = content - .replace(//g, '') - .replace(/`; - const headMatch = cleaned.match(/]*>/i); - if (!headMatch || headMatch.index === undefined) return `${routeMeta}${shim}${cleaned}`; - const insertAt = headMatch.index + headMatch[0].length; - return `${cleaned.slice(0, insertAt)}${routeMeta}${shim}${cleaned.slice(insertAt)}`; - } - let updatedCount = 0; for (const htmlFile of findHtmlFiles(outputDir)) { const original = fs.readFileSync(htmlFile, 'utf8'); - const updated = injectNavigationShim(prefixRootPaths(original), htmlRoute(htmlFile)); + const updated = prefixRootPaths(original); if (updated !== original) { fs.writeFileSync(htmlFile, updated, 'utf8'); updatedCount += 1; From 8d62e23b01e2b3d6c4718cff88765c02f71fc33c Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 21:27:32 -0700 Subject: [PATCH 09/29] ci(ci): harden coded app preview workflow --- .github/workflows/preview-deploy.yml | 38 +++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index b0a6b14b8..6271896ed 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -64,8 +64,13 @@ jobs: 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)); + 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, @@ -97,6 +102,7 @@ jobs: run: npm install --global @uipath/cli@1.196.4 - name: Authenticate UiPath External App + id: auth env: UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID }} @@ -119,6 +125,7 @@ jobs: export UIPATH_CLIENT_SCOPE="${UIPATH_CLIENT_SCOPE:-Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution}" node <<'NODE' + const crypto = require('node:crypto'); const fs = require('node:fs'); const baseUrl = process.env.UIPATH_BASE_URL.replace(/\/+$/, ''); @@ -140,6 +147,11 @@ jobs: fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); } + function appendOutput(name, value) { + const delimiter = `ghadelim_${crypto.randomUUID()}`; + fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}<<${delimiter}\n${value}\n${delimiter}\n`); + } + function safeOAuthError(data) { const summary = {}; for (const key of ['error', 'error_description', 'error_uri']) { @@ -181,7 +193,7 @@ jobs: } console.log(`::add-mask::${tokenData.access_token}`); - appendEnv('UIPATH_ACCESS_TOKEN', tokenData.access_token); + appendOutput('uipath_access_token', tokenData.access_token); appendEnv('UIPATH_BASE_URL', baseUrl); appendEnv('UIPATH_URL', baseUrl); appendEnv('UIPATH_ORG_ID', orgId); @@ -201,6 +213,7 @@ jobs: id: deploy env: PR_NUMBER: ${{ github.event.pull_request.number }} + UIPATH_ACCESS_TOKEN: ${{ steps.auth.outputs.uipath_access_token }} UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }} run: | missing=0 @@ -379,7 +392,7 @@ jobs: *) app_url="${app_url}/" ;; esac output_name="${label#apollo-}_url" - echo "${output_name}=${app_url}" >> "$GITHUB_OUTPUT" + write_output "$output_name" "$app_url" return 0 fi @@ -391,6 +404,18 @@ jobs: exit 1 } + 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" + } + stage_next_export() { local source_dir="$1" local stage_dir="$2" @@ -720,8 +745,13 @@ jobs: 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)); + 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, From 7ca7bb4bfe0f5db2a4149766ec4b611d75b48f29 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 21:42:54 -0700 Subject: [PATCH 10/29] ci(ci): try coded app previews through uip-go --- .github/workflows/preview-deploy.yml | 122 ++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 6271896ed..84d3bbce5 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -25,6 +25,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: read pull-requests: write steps: @@ -101,7 +102,8 @@ jobs: - name: Install UiPath CLI run: npm install --global @uipath/cli@1.196.4 - - name: Authenticate UiPath External App + - name: Authenticate UiPath External App (manual path disabled) + if: ${{ false }} id: auth env: UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} @@ -209,8 +211,124 @@ jobs: }); NODE - - name: Deploy Coded App previews + - 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 }} + UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE }} + 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 }} + 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; 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}" + + 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 + + output_file="$(mktemp)" + echo "::group::uip-go ${label}" + set +e + npm exec --yes --registry=https://npm.pkg.github.com --package @uipath/uip-go -- \ + uip-go "$label" \ + --name "$app_name" \ + --path-name "$app_name" \ + --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 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)" + rm -f "$output_file" + 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" + } + + run_uip_go "apollo-landing" "$landing_app" + run_uip_go "apollo-docs" "$docs_app" + run_uip_go "apollo-design" "$design_app" + run_uip_go "apollo-vertex" "$vertex_app" + + - name: Deploy Coded App previews (manual path disabled) + if: ${{ false }} + id: deploy_manual env: PR_NUMBER: ${{ github.event.pull_request.number }} UIPATH_ACCESS_TOKEN: ${{ steps.auth.outputs.uipath_access_token }} From 371279a6c207e730859c477c22e5589159ef5920 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 21:45:11 -0700 Subject: [PATCH 11/29] ci(ci): isolate uip-go package registry config --- .github/workflows/preview-deploy.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 84d3bbce5..5091dd50f 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -243,6 +243,14 @@ jobs: 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="${uip_go_prefix}/node_modules/.bin/uip-go" write_output() { local name="$1" @@ -289,8 +297,7 @@ jobs: output_file="$(mktemp)" echo "::group::uip-go ${label}" set +e - npm exec --yes --registry=https://npm.pkg.github.com --package @uipath/uip-go -- \ - uip-go "$label" \ + "$UIP_GO" "$label" \ --name "$app_name" \ --path-name "$app_name" \ --version "$version" \ From 41548f9b15c230d64ca82d1f9a94190847266d91 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 22:02:36 -0700 Subject: [PATCH 12/29] ci(ci): make uip-go the coded app preview path --- .github/workflows/preview-deploy.yml | 590 --------------------------- 1 file changed, 590 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 5091dd50f..3639129d4 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -102,115 +102,6 @@ jobs: - name: Install UiPath CLI run: npm install --global @uipath/cli@1.196.4 - - name: Authenticate UiPath External App (manual path disabled) - if: ${{ false }} - id: auth - env: - UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} - UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID }} - UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE }} - UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }} - UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }} - UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }} - run: | - missing=0 - for name in UIPATH_BASE_URL UIPATH_CLIENT_ID UIPATH_CLIENT_SECRET UIPATH_ORG_NAME UIPATH_TENANT_NAME; do - if [ -z "${!name}" ]; then - echo "::error::$name is required for Coded App preview deployments." - missing=1 - fi - done - if [ "$missing" -ne 0 ]; then - exit 1 - fi - - export UIPATH_CLIENT_SCOPE="${UIPATH_CLIENT_SCOPE:-Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution}" - - node <<'NODE' - const crypto = require('node:crypto'); - const fs = require('node:fs'); - - const baseUrl = process.env.UIPATH_BASE_URL.replace(/\/+$/, ''); - const scope = process.env.UIPATH_CLIENT_SCOPE; - const params = new URLSearchParams({ - grant_type: 'client_credentials', - client_id: process.env.UIPATH_CLIENT_ID, - client_secret: process.env.UIPATH_CLIENT_SECRET, - scope, - }); - - function parseJwtPayload(token) { - const [, payload] = String(token).split('.'); - if (!payload) return {}; - return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); - } - - function appendEnv(name, value) { - fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); - } - - function appendOutput(name, value) { - const delimiter = `ghadelim_${crypto.randomUUID()}`; - fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}<<${delimiter}\n${value}\n${delimiter}\n`); - } - - function safeOAuthError(data) { - const summary = {}; - for (const key of ['error', 'error_description', 'error_uri']) { - if (typeof data?.[key] === 'string') { - summary[key] = data[key]; - } - } - return Object.keys(summary).length ? JSON.stringify(summary) : 'No safe error details returned.'; - } - - async function main() { - const tokenResponse = await fetch(`${baseUrl}/identity_/connect/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: params, - }); - const tokenData = await tokenResponse.json().catch(() => ({})); - if (!tokenResponse.ok || !tokenData.access_token) { - throw new Error(`External App token exchange failed with HTTP ${tokenResponse.status}: ${safeOAuthError(tokenData)}`); - } - - const claims = parseJwtPayload(tokenData.access_token); - const orgId = claims.prt_id || claims.prtId || claims.organizationId; - if (!orgId) { - throw new Error('External App token did not include an organization id.'); - } - - const contextResponse = await fetch(`${baseUrl}/${orgId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`, { - headers: { Authorization: `Bearer ${tokenData.access_token}` }, - }); - const contextText = await contextResponse.text(); - if (!contextResponse.ok) { - throw new Error(`Could not fetch External App tenant context (HTTP ${contextResponse.status}): ${contextText}`); - } - const context = JSON.parse(contextText); - const tenant = (context.tenants || []).find((item) => item.name === process.env.UIPATH_TENANT_NAME); - if (!tenant) { - throw new Error(`External App cannot access tenant "${process.env.UIPATH_TENANT_NAME}".`); - } - - console.log(`::add-mask::${tokenData.access_token}`); - appendOutput('uipath_access_token', tokenData.access_token); - appendEnv('UIPATH_BASE_URL', baseUrl); - appendEnv('UIPATH_URL', baseUrl); - appendEnv('UIPATH_ORG_ID', orgId); - appendEnv('UIPATH_ORGANIZATION_ID', orgId); - appendEnv('UIPATH_ORG_NAME', process.env.UIPATH_ORG_NAME || context.organization?.name || ''); - appendEnv('UIPATH_TENANT_ID', tenant.id); - appendEnv('UIPATH_TENANT_NAME', tenant.name); - } - - main().catch((error) => { - console.error(error); - process.exit(1); - }); - NODE - - name: Deploy Coded App previews with uip-go id: deploy env: @@ -333,487 +224,6 @@ jobs: run_uip_go "apollo-design" "$design_app" run_uip_go "apollo-vertex" "$vertex_app" - - name: Deploy Coded App previews (manual path disabled) - if: ${{ false }} - id: deploy_manual - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - UIPATH_ACCESS_TOKEN: ${{ steps.auth.outputs.uipath_access_token }} - UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }} - run: | - missing=0 - for name in UIPATH_ACCESS_TOKEN UIPATH_BASE_URL UIPATH_FOLDER_KEY UIPATH_ORG_NAME UIPATH_TENANT_NAME; do - if [ -z "${!name}" ]; then - echo "::error::$name is required for 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}" - - fix_relative_assets() { - local output_dir="$1" - OUTPUT_DIR="$output_dir" node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - - const root = process.env.OUTPUT_DIR; - const filePattern = /\.(?:html|css|js|json)$/i; - - function walk(dir) { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - walk(fullPath); - } else if (entry.isFile() && filePattern.test(fullPath)) { - const current = fs.readFileSync(fullPath, 'utf8'); - const updated = current - .replace(/\b(src|href)=(["'])\/(?!\/|https?:|data:)([^"']*)\2/g, (_match, attr, quote, rest) => `${attr}=${quote}./${rest}${quote}`) - .replace(/url\(\s*(["']?)\/(?!\/|https?:|data:)([^)"'\s]*)/g, (_match, quote, rest) => `url(${quote}./${rest}`); - if (updated !== current) { - fs.writeFileSync(fullPath, updated); - } - } - } - } - - walk(root); - NODE - } - - postprocess_static_app() { - local output_dir="$1" - local app_name="$2" - OUTPUT_DIR="$output_dir" APP_BASE="/${app_name}/" node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - - const root = process.env.OUTPUT_DIR; - const appBase = process.env.APP_BASE; - const filePattern = /\.(?:html|css|js|json)$/i; - - function walk(dir) { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - walk(fullPath); - } else if (entry.isFile() && filePattern.test(fullPath)) { - const current = fs.readFileSync(fullPath, 'utf8'); - let updated = current - .replace(/\b(src|href)=(["'])\/(?!\/|https?:|data:)([^"']*)\2/g, (_match, attr, quote, rest) => `${attr}=${quote}${appBase}${rest}${quote}`) - .replace(/url\(\s*\/(?!\/|https?:|data:)([^)"'\s]*)/g, (_match, rest) => `url(${appBase}${rest}`) - .replace(/(["'`])\.\/brand\//g, (_match, quote) => `${quote}${appBase}brand/`) - .replace(/(["'`])\/brand\//g, (_match, quote) => `${quote}${appBase}brand/`) - .replace(/url\(\s*(["']?)\.\/brand\//g, (_match, quote) => `url(${quote}${appBase}brand/`) - .replace(/url\(\s*(["']?)\/brand\//g, (_match, quote) => `url(${quote}${appBase}brand/`); - - if (fullPath.endsWith('.html')) { - const baseTag = ``; - updated = updated.replace(/]*>\n?/g, ''); - const headMatch = updated.match(/]*>/i); - if (headMatch?.index !== undefined) { - const insertAt = headMatch.index + headMatch[0].length; - updated = `${updated.slice(0, insertAt)}${baseTag}${updated.slice(insertAt)}`; - } - } - - if (updated !== current) { - fs.writeFileSync(fullPath, updated); - } - } - } - } - - walk(root); - NODE - } - - 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 - } - - deploy_coded_app() { - local label="$1" - local app_name="$2" - local output_dir="$3" - local tags="$4" - local asset_mode="${5:-relative}" - local uipath_dir=".uipath/${label}" - local deploy_output - local deploy_exit - local app_url - local output_name - - if [ ! -d "$output_dir" ]; then - echo "::error::Build output directory not found: $output_dir" - exit 1 - fi - - if [ "$asset_mode" = "relative" ]; then - fix_relative_assets "$output_dir" - postprocess_static_app "$output_dir" "$app_name" - fi - - uip codedapp pack "$output_dir" \ - --name "$app_name" \ - --version "$version" \ - --output "$uipath_dir" - - uip codedapp publish \ - --name "$app_name" \ - --version "$version" \ - --type Web \ - --uipath-dir "$uipath_dir" \ - --base-url "$UIPATH_BASE_URL" \ - --tenant-name "$UIPATH_TENANT_NAME" \ - --access-token "$UIPATH_ACCESS_TOKEN" - - for attempt in 1 2 3 4; do - set +e - deploy_output="$(uip codedapp deploy \ - --name "$app_name" \ - --base-url "$UIPATH_BASE_URL" \ - --org-name "$UIPATH_ORG_NAME" \ - --folder-key "$UIPATH_FOLDER_KEY" \ - --access-token "$UIPATH_ACCESS_TOKEN" \ - --tags "$tags" 2>&1)" - deploy_exit=$? - set -e - printf '%s\n' "$deploy_output" - - if [ "$deploy_exit" -eq 0 ]; then - app_url="$(printf '%s\n' "$deploy_output" | sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' | 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 - output_name="${label#apollo-}_url" - write_output "$output_name" "$app_url" - return 0 - fi - - echo "Deploy attempt ${attempt} failed; waiting for package indexing..." - sleep 10 - done - - echo "::error::Failed to deploy $app_name after retries." - exit 1 - } - - 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" - } - - stage_next_export() { - local source_dir="$1" - local stage_dir="$2" - local overlay="$3" - SOURCE_DIR="$source_dir" STAGE_DIR="$stage_dir" OVERLAY="$overlay" node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - - const sourceDir = path.resolve(process.env.SOURCE_DIR); - const stageDir = path.resolve(process.env.STAGE_DIR); - const overlay = process.env.OVERLAY; - - const docsNextConfig = [ - 'import nextra from "nextra";', - '', - 'const withNextra = nextra({', - ' defaultShowCopyCode: true,', - '});', - '', - 'const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replace(/^\\/+|\\/+$/g, "");', - 'const codedAppBasePath = codedAppPath ? `/${codedAppPath}` : undefined;', - '', - 'export default withNextra({', - ' output: "export",', - ' trailingSlash: true,', - ' ...(codedAppBasePath && { basePath: codedAppBasePath }),', - ' reactCompiler: true,', - ' turbopack: {', - ' resolveAlias: {', - ' "next-mdx-import-source-file": "./mdx-components.tsx",', - ' },', - ' },', - '});', - '', - ].join('\n'); - - const vertexNextConfig = [ - 'import nextra from "nextra";', - '', - 'const withNextra = nextra({', - ' defaultShowCopyCode: true,', - '});', - '', - 'const codedAppPath = process.env.APOLLO_CODED_APP_PATH?.replace(/^\\/+|\\/+$/g, "");', - 'const codedAppBasePath = codedAppPath ? `/${codedAppPath}` : undefined;', - '', - 'export default withNextra({', - ' output: "export",', - ' trailingSlash: true,', - ' env: {', - ' NEXT_PUBLIC_APOLLO_CODED_APP_PATH: codedAppPath ?? "",', - ' },', - ' ...(codedAppBasePath && { basePath: codedAppBasePath }),', - ' reactCompiler: true,', - ' turbopack: {', - ' resolveAlias: {', - ' "next-mdx-import-source-file": "./mdx-components.tsx",', - ' },', - ' },', - '});', - '', - ].join('\n'); - - const mdxDeclaration = [ - "declare module '*.mdx' {", - " import type { ComponentType } from 'react';", - '', - ' const MDXComponent: ComponentType;', - ' export default MDXComponent;', - '}', - '', - ].join('\n'); - - const disabledAiChatTemplate = [ - 'export function AiChatTemplate() {', - ' return (', - '
', - '

', - ' AI Chat is not available in Coded App preview', - '

', - '

', - ' The demo needs browser calls to UiPath backend services that are blocked', - ' by cross-origin preflight checks in the Coded App host. The rest of', - ' Apollo Vertex is still available in this preview.', - '

', - '
', - ' );', - '}', - '', - ].join('\n'); - - function normalize(value) { - return value.split(path.sep).join('/'); - } - - function shouldCopy(source) { - const rel = normalize(path.relative(sourceDir, source)); - if (!rel) return true; - const excluded = ['.next', 'out', 'node_modules']; - if (overlay === 'vertex') excluded.push('app/api'); - return !excluded.some((item) => rel === item || rel.startsWith(`${item}/`) || path.basename(source) === item); - } - - function writeStageFile(relativePath, content) { - const target = path.join(stageDir, relativePath); - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, content, 'utf8'); - } - - fs.rmSync(stageDir, { recursive: true, force: true }); - fs.mkdirSync(path.dirname(stageDir), { recursive: true }); - fs.cpSync(sourceDir, stageDir, { recursive: true, filter: shouldCopy }); - - const nodeModules = [ - path.join(sourceDir, 'node_modules'), - path.resolve('node_modules'), - ].find((candidate) => fs.existsSync(candidate)); - if (nodeModules) { - fs.symlinkSync(nodeModules, path.join(stageDir, 'node_modules'), 'dir'); - } - - if (overlay === 'docs') { - writeStageFile('next.config.mjs', docsNextConfig); - writeStageFile('mdx.d.ts', mdxDeclaration); - writeStageFile('app/page.tsx', 'import OverviewPage from "./introduction/overview/page.mdx";\n\nexport default function Page() {\n return ;\n}\n'); - } else if (overlay === 'vertex') { - writeStageFile('next.config.ts', vertexNextConfig); - writeStageFile('templates/AiChatTemplate.tsx', disabledAiChatTemplate); - writeStageFile('mdx.d.ts', mdxDeclaration); - writeStageFile('app/components/page.tsx', 'import ComponentsOverviewPage from "./overview/page.mdx";\n\nexport default function ComponentsIndex() {\n return ;\n}\n'); - } else { - throw new Error(`Unknown overlay: ${overlay}`); - } - NODE - } - - postprocess_next_export() { - local output_dir="$1" - local app_name="$2" - OUTPUT_DIR="$output_dir" APP_BASE="/${app_name}" node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - - const outputDir = path.resolve(process.env.OUTPUT_DIR); - const appBase = process.env.APP_BASE; - - function findHtmlFiles(dir) { - const files = []; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...findHtmlFiles(fullPath)); - } else if (entry.isFile() && fullPath.endsWith('.html')) { - files.push(fullPath); - } - } - return files; - } - - function escapeRegExp(value) { - return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - function isAssetPath(route) { - return /(^|\/)(?:_next|_pagefind|r)(?:\/|$)/.test(route) || - /\.[a-z0-9]+(?:[?#].*)?$/i.test(route); - } - - function shouldPrefixRootPath(rootPath) { - if (!rootPath || rootPath.startsWith('//')) return false; - return rootPath !== appBase && !rootPath.startsWith(`${appBase}/`); - } - - function indexedHref(href) { - if (!href.startsWith(`${appBase}/`)) return href; - const match = href.match(/^([^?#]*)([?#].*)?$/); - if (!match) return href; - const [, pathname, suffix = ''] = match; - if (pathname === appBase || pathname === `${appBase}/`) return href; - const route = pathname.slice(appBase.length).replace(/^\/+|\/+$/g, ''); - if (!route || route.endsWith('index.html') || isAssetPath(route)) return href; - return `${appBase}/${route}/index.html${suffix}`; - } - - function normalizeRouteHrefs(content) { - return content.replace(/(^|[\s<])href=(["'])([^"']+)\2/g, (match, lead, quote, href) => { - const normalized = indexedHref(href); - return normalized === href ? match : `${lead}href=${quote}${normalized}${quote}`; - }); - } - - function unprefixDataHrefs(content) { - const unprefixed = content.replace( - new RegExp(`\\bdata-href=(["'])${escapeRegExp(appBase)}/`, 'g'), - 'data-href=$1/', - ); - return unprefixed.replace( - /\bdata-href=(["'])(\/[^"']*?)\/index\.html([?#][^"']*)?\1/g, - (_match, quote, route, suffix = '') => `data-href=${quote}${route}${suffix}${quote}`, - ); - } - - function prefixRootPaths(content) { - let updated = content.replace(/\b(src|href)=(["'])\/(?!\/)([^"']*)\2/g, (match, attr, quote, rest) => { - const rootPath = `/${rest}`; - if (!shouldPrefixRootPath(rootPath)) return match; - return `${attr}=${quote}${appBase}${rootPath}${quote}`; - }); - updated = updated.replace(/("src"\s*:\s*")\/(?!\/)([^"\\]*)/g, (match, prefix, rest) => { - const rootPath = `/${rest}`; - if (!shouldPrefixRootPath(rootPath)) return match; - return `${prefix}${appBase}${rootPath.slice(1) ? rootPath : '/'}`; - }); - updated = updated.replace(/(\\"src\\"\s*:\s*\\")\/(?!\/)([^"\\]*)/g, (match, prefix, rest) => { - const rootPath = `/${rest}`; - if (!shouldPrefixRootPath(rootPath)) return match; - return `${prefix}${appBase}${rootPath.slice(1) ? rootPath : '/'}`; - }); - updated = updated.replace(/url\(\s*(["']?)\/(?!\/|https?:|data:)([^)"'\s]*)/g, (match, quote, rest) => { - const rootPath = `/${rest}`; - if (!shouldPrefixRootPath(rootPath)) return match; - return `url(${quote}${appBase}${rootPath}`; - }); - return unprefixDataHrefs(normalizeRouteHrefs(updated)); - } - - let updatedCount = 0; - for (const htmlFile of findHtmlFiles(outputDir)) { - const original = fs.readFileSync(htmlFile, 'utf8'); - const updated = prefixRootPaths(original); - if (updated !== original) { - fs.writeFileSync(htmlFile, updated, 'utf8'); - updatedCount += 1; - } - } - console.log(`Updated ${updatedCount} HTML files for ${appBase}.`); - NODE - } - - prepare_docs_export() { - APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$docs_app" CI=true pnpm turbo build --filter=apollo-docs - stage_next_export "apps/apollo-docs" ".uipath-build/apollo-docs" "docs" - (cd ".uipath-build/apollo-docs" && APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$docs_app" CI=true pnpm exec next build) - postprocess_next_export ".uipath-build/apollo-docs/out" "$docs_app" - rm -rf "apps/apollo-docs/out" - cp -R ".uipath-build/apollo-docs/out" "apps/apollo-docs/out" - } - - prepare_vertex_export() { - stage_next_export "apps/apollo-vertex" ".uipath-build/apollo-vertex" "vertex" - ( - cd ".uipath-build/apollo-vertex" - APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$vertex_app" CI=true pnpm generate:theme - APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$vertex_app" CI=true pnpm registry:build - APOLLO_CODED_APP=1 APOLLO_CODED_APP_PATH="$vertex_app" CI=true pnpm exec next build - ) - postprocess_next_export ".uipath-build/apollo-vertex/out" "$vertex_app" - rm -rf "apps/apollo-vertex/out" - cp -R ".uipath-build/apollo-vertex/out" "apps/apollo-vertex/out" - } - - pnpm turbo build --filter=apollo-landing - deploy_coded_app "apollo-landing" "$landing_app" "apps/landing/dist" "preview,apollo,apollo-landing" - - prepare_docs_export - deploy_coded_app "apollo-docs" "$docs_app" "apps/apollo-docs/out" "preview,apollo,apollo-docs" "none" - - pnpm turbo run storybook:build --filter=storybook-app - deploy_coded_app "apollo-design" "$design_app" "apps/storybook/storybook-static" "preview,apollo,apollo-design" - - prepare_vertex_export - deploy_coded_app "apollo-vertex" "$vertex_app" "apps/apollo-vertex/out" "preview,apollo,apollo-vertex" "none" - - name: Save Turborepo cache uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: From f63f0097dcc82babf54f9e4685acd2e43245bee2 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 22:06:57 -0700 Subject: [PATCH 13/29] ci(ci): link deploying status to preview run --- .github/workflows/preview-deploy.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 3639129d4..e6155ed00 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -50,7 +50,9 @@ jobs: second: '2-digit', hour12: true }); - const logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + 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, @@ -58,7 +60,7 @@ jobs: '', '| Project | Status | Preview | Updated (PT) |', '|---------|--------|---------|--------------|', - ...projects.map(project => `| ${project} | Deploying... | ${logsLink} | ${timestamp} |`) + ...projects.map(project => `| ${project} | ${deployingLink} | ${logsLink} | ${timestamp} |`) ].join('\n'); const { data: comments } = await github.rest.issues.listComments({ From bc74b5b832c7d06dda6cb4bb871609749e5eb611 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 22:12:30 -0700 Subject: [PATCH 14/29] ci(ci): harden uip-go preview workflow permissions --- .github/workflows/preview-deploy.yml | 60 +++++++++++++++++++--------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index e6155ed00..03025ebe1 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,28 +13,21 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 + UIP_GO_VERSION: 0.1.4 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: - deploy: - name: Deploy Apollo Coded App Previews + initialize-comment: + name: Initialize Apollo Coded App Preview Comment if: github.event.pull_request.head.repo.fork == false runs-on: ubuntu-latest permissions: - contents: read - packages: read - pull-requests: write + issues: write steps: - - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - persist-credentials: false - - name: Initialize PR comment uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: @@ -90,6 +83,27 @@ jobs: }); } + deploy: + name: Deploy Apollo Coded App Previews + needs: initialize-comment + if: ${{ !cancelled() && 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 @@ -120,7 +134,7 @@ jobs: 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; 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 uip-go Coded App preview deployments." missing=1 @@ -142,7 +156,7 @@ jobs: 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 + 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() { @@ -232,15 +246,23 @@ jobs: 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.pull_request.head.repo.fork == false && needs.initialize-comment.result != 'skipped' }} + runs-on: ubuntu-latest + permissions: + issues: write + + steps: - name: Update PR comment - if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: - DEPLOY_OUTCOME: ${{ steps.deploy.outcome }} - APOLLO_DESIGN_URL: ${{ steps.deploy.outputs.design_url }} - APOLLO_DOCS_URL: ${{ steps.deploy.outputs.docs_url }} - APOLLO_LANDING_URL: ${{ steps.deploy.outputs.landing_url }} - APOLLO_VERTEX_URL: ${{ steps.deploy.outputs.vertex_url }} + 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 = ''; From 34e3ae4bc4670179648f0f6082105045f5a5e989 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 22:25:28 -0700 Subject: [PATCH 15/29] ci(ci): rely on bundled uip-go cli --- .github/workflows/preview-deploy.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 03025ebe1..6fb1438ac 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.4 + UIP_GO_VERSION: 0.1.6 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} @@ -115,9 +115,6 @@ jobs: restore-keys: | ${{ runner.os }}-turbo-${{ github.ref_name }}- - - name: Install UiPath CLI - run: npm install --global @uipath/cli@1.196.4 - - name: Deploy Coded App previews with uip-go id: deploy env: From 3799592e7b64ec1e7f357fdb723a730f08ff97a4 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 22:35:18 -0700 Subject: [PATCH 16/29] ci(ci): use uip-go codedapp tool bundle --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 6fb1438ac..ff9098897 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.6 + UIP_GO_VERSION: 0.1.7 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} From c6ef045ba2111b05130348ab25749f5cc7283468 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 22:47:02 -0700 Subject: [PATCH 17/29] ci(ci): address preview workflow review feedback --- .github/workflows/preview-deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index ff9098897..a8f998bca 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -122,7 +122,6 @@ jobs: 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 }} - UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE }} 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 }} @@ -199,6 +198,7 @@ jobs: local app_url output_file="$(mktemp)" + trap 'rm -f "$output_file"; trap - RETURN' RETURN echo "::group::uip-go ${label}" set +e "$UIP_GO" "$label" \ @@ -221,7 +221,6 @@ jobs: fi app_url="$(sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' "$output_file" | tail -n 1)" - rm -f "$output_file" if [ -z "$app_url" ]; then app_url="$(derive_app_url "$app_name")" fi @@ -238,6 +237,7 @@ jobs: run_uip_go "apollo-vertex" "$vertex_app" - name: Save Turborepo cache + if: always() uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .turbo From a47f137c5257e6507090988f5b96b66434b11780 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Wed, 1 Jul 2026 23:59:25 -0700 Subject: [PATCH 18/29] ci(ci): paginate preview comment lookup --- .github/workflows/preview-deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index a8f998bca..945cfa81d 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -56,7 +56,7 @@ jobs: ...projects.map(project => `| ${project} | ${deployingLink} | ${logsLink} | ${timestamp} |`) ].join('\n'); - const { data: comments } = await github.rest.issues.listComments({ + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, @@ -297,7 +297,7 @@ jobs: ...rows ].join('\n'); - const { data: comments } = await github.rest.issues.listComments({ + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, From e39a83321202537a60c3dd8fd4701fe59a083c9e Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 2 Jul 2026 00:15:53 -0700 Subject: [PATCH 19/29] ci(ci): use uip-go search fix --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 945cfa81d..bef70e99e 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.7 + UIP_GO_VERSION: 0.1.8 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} From 0a853ca44949fb558b2ebe79d8b6e08f8c47bb56 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 2 Jul 2026 00:30:06 -0700 Subject: [PATCH 20/29] ci(ci): use uip-go pagefind fix --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index bef70e99e..76eb4eff4 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.8 + UIP_GO_VERSION: 0.1.9 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} From a67b6cd00eb9404189a9357c608d479e8cb3e6d3 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 2 Jul 2026 01:02:02 -0700 Subject: [PATCH 21/29] ci(ci): continue preview deployments after failure --- .github/workflows/preview-deploy.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 76eb4eff4..47423c07c 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -231,10 +231,28 @@ jobs: write_output "$output_name" "$app_url" } - run_uip_go "apollo-landing" "$landing_app" - run_uip_go "apollo-docs" "$docs_app" - run_uip_go "apollo-design" "$design_app" - run_uip_go "apollo-vertex" "$vertex_app" + 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() From aed05528b33ed2e5247e5c4eea15a0bf8b58e848 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 2 Jul 2026 01:11:41 -0700 Subject: [PATCH 22/29] ci(ci): allow client id secret fallback --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 47423c07c..8350d2f2b 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -121,7 +121,7 @@ jobs: 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 }} + 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 }} From f57262c1350766c0e31bf29ed643fa4ac0aa71e8 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 2 Jul 2026 21:50:03 -0700 Subject: [PATCH 23/29] ci(ci): enable vertex ai chat previews --- .github/workflows/preview-deploy.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 8350d2f2b..e0e031e73 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.9 + UIP_GO_VERSION: 0.1.10 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} @@ -126,11 +126,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 }} + APOLLO_VERTEX_AICHAT_CLIENT_ID: ${{ vars.APOLLO_VERTEX_AICHAT_CLIENT_ID || secrets.APOLLO_VERTEX_AICHAT_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; 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 APOLLO_VERTEX_AICHAT_CLIENT_ID; do if [ -z "${!name:-}" ]; then echo "::error::$name is required for uip-go Coded App preview deployments." missing=1 @@ -196,9 +197,13 @@ jobs: 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 "$APOLLO_VERTEX_AICHAT_CLIENT_ID") + fi echo "::group::uip-go ${label}" set +e "$UIP_GO" "$label" \ @@ -210,7 +215,8 @@ jobs: --org-name "$UIPATH_ORG_NAME" \ --tenant-name "$UIPATH_TENANT_NAME" \ --deploy-retries 3 \ - --deploy-retry-delay-ms 10000 2>&1 | tee "$output_file" + --deploy-retry-delay-ms 10000 \ + "${extra_args[@]}" 2>&1 | tee "$output_file" command_exit=${PIPESTATUS[0]} set -e echo "::endgroup::" From a3f2e41db00fd68dcbdd7f7fa7bcd7db6f8c80e3 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Thu, 2 Jul 2026 21:53:59 -0700 Subject: [PATCH 24/29] ci(ci): use uip-go 0.1.11 --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index e0e031e73..52d93c789 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.10 + UIP_GO_VERSION: 0.1.11 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} From 17265685dc26e5cfc62814c4c19a42874f31d11c Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Sat, 4 Jul 2026 03:48:50 -0700 Subject: [PATCH 25/29] ci(ci): allow preview comments on draft PRs --- .github/workflows/preview-deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 52d93c789..bdba2ebeb 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -26,6 +26,7 @@ jobs: runs-on: ubuntu-latest permissions: issues: write + pull-requests: write steps: - name: Initialize PR comment @@ -274,6 +275,7 @@ jobs: runs-on: ubuntu-latest permissions: issues: write + pull-requests: write steps: - name: Update PR comment From e2daabd0a5cd4dbfd884b1477cab6018cabe21b5 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Sat, 4 Jul 2026 04:28:33 -0700 Subject: [PATCH 26/29] ci(ci): use uip-go 0.1.12 --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index bdba2ebeb..913b32171 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.11 + UIP_GO_VERSION: 0.1.12 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} From bcf16abe9711db4cea0c91f5e8605a97c8ccd8ec Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Sat, 4 Jul 2026 04:56:51 -0700 Subject: [PATCH 27/29] ci(ci): request external app admin scope for previews --- .github/workflows/preview-deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 913b32171..fa521caf5 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -127,6 +127,7 @@ 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' }} APOLLO_VERTEX_AICHAT_CLIENT_ID: ${{ vars.APOLLO_VERTEX_AICHAT_CLIENT_ID || secrets.APOLLO_VERTEX_AICHAT_CLIENT_ID }} run: | set -euo pipefail From 2c636819e9af14b74940b6610bae964f8712c669 Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Sat, 4 Jul 2026 05:10:37 -0700 Subject: [PATCH 28/29] ci(ci): use uip-go 0.1.13 --- .github/workflows/preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index fa521caf5..6ec68750f 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -13,7 +13,7 @@ permissions: {} env: TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 - UIP_GO_VERSION: 0.1.12 + UIP_GO_VERSION: 0.1.13 concurrency: group: coded-app-preview-pr-${{ github.event.pull_request.number }} From ebbd85774bdd7d5cf483ec126c59359af90d616e Mon Sep 17 00:00:00 2001 From: Fikewa Olatunji Date: Sat, 4 Jul 2026 14:46:01 -0700 Subject: [PATCH 29/29] ci(ci): clean up vertex ai chat redirects --- .github/workflows/preview-deploy.yml | 68 ++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 6ec68750f..1a7deef4b 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -2,7 +2,7 @@ name: Coded App Preview Deployments on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, closed] branches: - main - 'support/**' @@ -22,7 +22,7 @@ concurrency: jobs: initialize-comment: name: Initialize Apollo Coded App Preview Comment - if: github.event.pull_request.head.repo.fork == false + if: github.event.action != 'closed' && github.event.pull_request.head.repo.fork == false runs-on: ubuntu-latest permissions: issues: write @@ -87,7 +87,7 @@ jobs: deploy: name: Deploy Apollo Coded App Previews needs: initialize-comment - if: ${{ !cancelled() && needs.initialize-comment.result == 'success' && github.event.pull_request.head.repo.fork == false }} + 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 @@ -272,7 +272,7 @@ jobs: update-comment: name: Update Apollo Coded App Preview Comment needs: [initialize-comment, deploy] - if: ${{ always() && github.event.pull_request.head.repo.fork == false && needs.initialize-comment.result != 'skipped' }} + 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 @@ -350,3 +350,63 @@ jobs: body, }); } + + cleanup-ai-chat-redirects: + name: Cleanup Apollo Vertex AI Chat Redirects + 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 + ref: ${{ github.event.pull_request.base.sha }} + + - name: Remove Apollo Vertex AI Chat redirect URIs + 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_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' }} + APOLLO_VERTEX_AICHAT_CLIENT_ID: ${{ vars.APOLLO_VERTEX_AICHAT_CLIENT_ID || secrets.APOLLO_VERTEX_AICHAT_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_ORG_NAME UIPATH_TENANT_NAME UIP_GO_VERSION APOLLO_VERTEX_AICHAT_CLIENT_ID; do + if [ -z "${!name:-}" ]; then + echo "::error::$name is required for Apollo Vertex AI Chat redirect 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 "$APOLLO_VERTEX_AICHAT_CLIENT_ID"