diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 000000000..5e8b0cb81 --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,427 @@ +name: Coded App Preview Deployments + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - main + - 'support/**' + +# Deny-all default; jobs grant the minimum they need. +permissions: {} + +env: + TURBO_TELEMETRY_DISABLED: 1 + DO_NOT_TRACK: 1 + UIP_GO_VERSION: 0.1.15 + +concurrency: + group: coded-app-preview-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + initialize-comment: + name: Initialize Apollo Coded App Preview Comment + if: github.event.action != 'closed' && github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - name: Initialize PR comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const identifier = ''; + const timestamp = new Date().toLocaleString('en-US', { + timeZone: 'America/Los_Angeles', + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true + }); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const deployingLink = `[Deploying...](${runUrl})`; + const logsLink = `[Logs](${runUrl})`; + const projects = ['apollo-design', 'apollo-docs', 'apollo-landing', 'apollo-vertex']; + const body = [ + identifier, + 'Apollo Coded App preview deployments are running.', + '', + '| Project | Status | Preview | Updated (PT) |', + '|---------|--------|---------|--------------|', + ...projects.map(project => `| ${project} | ${deployingLink} | ${logsLink} | ${timestamp} |`) + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + const existing = comments.find(comment => + comment.body?.includes(identifier) && + comment.user?.type === 'Bot' && + ['github-actions[bot]', 'github-actions'].includes(comment.user?.login) + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + deploy: + name: Deploy Apollo Coded App Previews + needs: initialize-comment + if: ${{ !cancelled() && github.event.action != 'closed' && needs.initialize-comment.result == 'success' && github.event.pull_request.head.repo.fork == false }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + outputs: + design_url: ${{ steps.deploy.outputs.design_url }} + docs_url: ${{ steps.deploy.outputs.docs_url }} + landing_url: ${{ steps.deploy.outputs.landing_url }} + vertex_url: ${{ steps.deploy.outputs.vertex_url }} + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Node dependencies + uses: ./.github/actions/install-node-deps + + - name: Restore Turborepo cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo-${{ github.ref_name }}- + + - name: Deploy Coded App previews with uip-go + id: deploy + env: + GH_NPM_REGISTRY_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} + UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID || secrets.UIPATH_CLIENT_ID }} + UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }} + UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }} + UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }} + UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }} + UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE || 'Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution PM.OAuthApp' }} + 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 APOLLO_VERTEX_AICHAT_CLIENT_ID; do + if [ -z "${!name:-}" ]; then + echo "::error::$name is required for uip-go Coded App preview deployments." + missing=1 + fi + done + if [ "$missing" -ne 0 ]; then + exit 1 + fi + + short_sha="${GITHUB_SHA:0:7}" + version="0.1.0-pr${PR_NUMBER}.${short_sha}.${GITHUB_RUN_ATTEMPT}" + landing_app="apollo-landing-pr-${PR_NUMBER}" + docs_app="apollo-docs-pr-${PR_NUMBER}" + design_app="apollo-design-pr-${PR_NUMBER}" + vertex_app="apollo-vertex-pr-${PR_NUMBER}" + uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc" + uip_go_prefix="${RUNNER_TEMP}/uip-go" + { + printf '@uipath:registry=https://npm.pkg.github.com\n' + printf '//npm.pkg.github.com/:_authToken=%s\n' "$GH_NPM_REGISTRY_TOKEN" + } > "$uip_go_npmrc" + NPM_CONFIG_USERCONFIG="$uip_go_npmrc" npm install --prefix "$uip_go_prefix" "@uipath/uip-go@${UIP_GO_VERSION}" + UIP_GO="${uip_go_prefix}/node_modules/.bin/uip-go" + + write_output() { + local name="$1" + local value="$2" + local delimiter + delimiter="ghadelim_$(openssl rand -hex 16)" + { + printf '%s<<%s\n' "$name" "$delimiter" + printf '%s\n' "$value" + printf '%s\n' "$delimiter" + } >> "$GITHUB_OUTPUT" + } + + derive_app_url() { + local app_name="$1" + APP_NAME="$app_name" node <<'NODE' + const baseUrl = process.env.UIPATH_BASE_URL || ''; + const orgName = process.env.UIPATH_ORG_NAME || ''; + const appName = process.env.APP_NAME || ''; + + let environment = ''; + try { + const hostname = new URL(baseUrl).hostname; + let inferred = hostname.replace(/\.uipath\.com$/, ''); + inferred = inferred.replace(/^api\./, '').replace(/\.api$/, ''); + environment = inferred && inferred !== 'api' && inferred !== 'cloud' ? inferred : ''; + } catch { + environment = ''; + } + + const suffix = environment ? `.${environment}` : ''; + console.log(`https://${orgName}${suffix}.uipath.host/${appName}`); + NODE + } + + run_uip_go() { + local label="$1" + local app_name="$2" + local output_name="${label#apollo-}_url" + local output_file + local command_exit + local app_url + local extra_args=() + + output_file="$(mktemp)" + trap 'rm -f "$output_file"; trap - RETURN' RETURN + if [ "$label" = "apollo-vertex" ]; then + extra_args+=(--auth-client-id "$APOLLO_VERTEX_AICHAT_CLIENT_ID") + fi + echo "::group::uip-go ${label}" + set +e + "$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 \ + "${extra_args[@]}" 2>&1 | tee "$output_file" + command_exit=${PIPESTATUS[0]} + set -e + echo "::endgroup::" + + if [ "$command_exit" -ne 0 ]; then + echo "::error::uip-go ${label} failed." + return "$command_exit" + fi + + app_url="$(sed -nE 's/^[[:space:]]*App URL:[[:space:]]*//p' "$output_file" | tail -n 1)" + if [ -z "$app_url" ]; then + app_url="$(derive_app_url "$app_name")" + fi + case "$app_url" in + */) ;; + *) app_url="${app_url}/" ;; + esac + write_output "$output_name" "$app_url" + } + + failed_apps=() + deploy_or_record_failure() { + local label="$1" + local app_name="$2" + + if run_uip_go "$label" "$app_name"; then + return 0 + fi + + failed_apps+=("$label") + return 0 + } + + deploy_or_record_failure "apollo-landing" "$landing_app" + deploy_or_record_failure "apollo-docs" "$docs_app" + deploy_or_record_failure "apollo-design" "$design_app" + deploy_or_record_failure "apollo-vertex" "$vertex_app" + + if [ "${#failed_apps[@]}" -ne 0 ]; then + echo "::error::uip-go deployment failed for: ${failed_apps[*]}" + exit 1 + fi + + - name: Save Turborepo cache + if: always() + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }} + + update-comment: + name: Update Apollo Coded App Preview Comment + needs: [initialize-comment, deploy] + if: ${{ always() && github.event.action != 'closed' && github.event.pull_request.head.repo.fork == false && needs.initialize-comment.result != 'skipped' }} + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - name: Update PR comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + DEPLOY_OUTCOME: ${{ needs.deploy.result }} + APOLLO_DESIGN_URL: ${{ needs.deploy.outputs.design_url }} + APOLLO_DOCS_URL: ${{ needs.deploy.outputs.docs_url }} + APOLLO_LANDING_URL: ${{ needs.deploy.outputs.landing_url }} + APOLLO_VERTEX_URL: ${{ needs.deploy.outputs.vertex_url }} + with: + script: | + const identifier = ''; + const timestamp = new Date().toLocaleString('en-US', { + timeZone: 'America/Los_Angeles', + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true + }); + const logsLink = `[Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + const deploySucceeded = process.env.DEPLOY_OUTCOME === 'success'; + const rows = [ + ['apollo-design', process.env.APOLLO_DESIGN_URL], + ['apollo-docs', process.env.APOLLO_DOCS_URL], + ['apollo-landing', process.env.APOLLO_LANDING_URL], + ['apollo-vertex', process.env.APOLLO_VERTEX_URL], + ].map(([project, url]) => { + const ready = Boolean(url); + const status = ready ? 'Ready' : deploySucceeded ? 'Skipped' : 'Failed'; + const preview = ready ? `[Preview](${url}) ยท ${logsLink}` : logsLink; + return `| ${project} | ${status} | ${preview} | ${timestamp} |`; + }); + const body = [ + identifier, + deploySucceeded + ? 'Apollo Coded App preview deployments are ready.' + : 'Apollo Coded App preview deployments did not complete.', + '', + '| Project | Status | Preview | Updated (PT) |', + '|---------|--------|---------|--------------|', + ...rows + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + const existing = comments.find(comment => + comment.body?.includes(identifier) && + comment.user?.type === 'Bot' && + ['github-actions[bot]', 'github-actions'].includes(comment.user?.login) + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + cleanup-ai-chat-redirects: + name: Cleanup Apollo Coded App Previews + if: github.event.action == 'closed' && github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 1 + persist-credentials: false + ref: ${{ github.event.pull_request.base.sha }} + + - name: Remove Apollo Coded App previews + env: + GH_NPM_REGISTRY_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + UIPATH_BASE_URL: ${{ vars.UIPATH_BASE_URL || secrets.UIPATH_BASE_URL }} + UIPATH_CLIENT_ID: ${{ vars.UIPATH_CLIENT_ID || secrets.UIPATH_CLIENT_ID }} + UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }} + UIPATH_FOLDER_KEY: ${{ vars.UIPATH_FOLDER_KEY || secrets.UIPATH_FOLDER_KEY }} + UIPATH_ORG_NAME: ${{ vars.UIPATH_ORG_NAME || secrets.UIPATH_ORG_NAME }} + UIPATH_TENANT_NAME: ${{ vars.UIPATH_TENANT_NAME || secrets.UIPATH_TENANT_NAME }} + UIPATH_CLIENT_SCOPE: ${{ vars.UIPATH_CLIENT_SCOPE || 'Apps Apps.Read Apps.Write OR.Folders.Read OR.Folders.Write OR.Execution PM.OAuthApp' }} + 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 APOLLO_VERTEX_AICHAT_CLIENT_ID; do + if [ -z "${!name:-}" ]; then + echo "::error::$name is required for Apollo Coded App preview cleanup." + missing=1 + fi + done + if [ "$missing" -ne 0 ]; then + exit 1 + fi + + vertex_app="apollo-vertex-pr-${PR_NUMBER}" + uip_go_npmrc="${RUNNER_TEMP}/uip-go.npmrc" + uip_go_prefix="${RUNNER_TEMP}/uip-go" + { + printf '@uipath:registry=https://npm.pkg.github.com\n' + printf '//npm.pkg.github.com/:_authToken=%s\n' "$GH_NPM_REGISTRY_TOKEN" + } > "$uip_go_npmrc" + NPM_CONFIG_USERCONFIG="$uip_go_npmrc" npm install --prefix "$uip_go_prefix" "@uipath/uip-go@${UIP_GO_VERSION}" + UIP_GO="${uip_go_prefix}/node_modules/.bin/uip-go" + + "$UIP_GO" apollo-vertex \ + --aichat-redirects remove \ + --name "$vertex_app" \ + --path-name "$vertex_app" \ + --base-url "$UIPATH_BASE_URL" \ + --org-name "$UIPATH_ORG_NAME" \ + --tenant-name "$UIPATH_TENANT_NAME" \ + --auth-client-id "$APOLLO_VERTEX_AICHAT_CLIENT_ID" + + for label in apollo-landing apollo-docs apollo-design apollo-vertex; do + app_name="${label}-pr-${PR_NUMBER}" + if ! "$UIP_GO" "$label" \ + --delete-app \ + --name "$app_name" \ + --path-name "$app_name" \ + --folder-key "$UIPATH_FOLDER_KEY" \ + --base-url "$UIPATH_BASE_URL" \ + --org-name "$UIPATH_ORG_NAME" \ + --tenant-name "$UIPATH_TENANT_NAME"; then + echo "::warning::Could not delete Coded App ${app_name}. External App auth can publish/deploy/upgrade, but the Apps delete API may reject CI tokens." + fi + done diff --git a/.github/workflows/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/