Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/deploy-web-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Deploy web UI

on:
push:
branches: [main]
paths:
- "apps/web-ui/**"
# include the workflow itself so edits to this file re-trigger a deploy
- ".github/workflows/deploy-web-ui.yml"
pull_request:
branches: [main]
paths:
- "apps/web-ui/**"
- ".github/workflows/deploy-web-ui.yml"

Comment on lines +10 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/deploy-web-ui.yml

Repository: TechImmigrants/cv-builder

Length of output: 4257


🌐 Web query:

GitHub Actions secrets availability pull_request forked repositories

💡 Result:

For GitHub Actions, secrets are NOT available to workflow runs triggered by the pull_request event when the pull request comes from a fork (i.e., “pull request from a forked repository to the base repository”). GitHub withholds access to other secrets for untrusted fork code, leaving the workflow with a read-only GITHUB_TOKEN instead. [1][2] Specifically: - pull_request (from a fork): Actions secrets are not passed to workflows triggered by a pull request from a fork. [3] - pull_request_target (used to get secrets for fork PRs): workflows triggered by pull_request_target run with elevated trust in the base repository context, receiving the base repository’s GITHUB_TOKEN and access to repository/organization secrets (but this trigger must be handled carefully because it can run dangerous attacker-controlled behavior if you check out or execute code from the fork). [1][2] Practical implication: If your goal is “secrets should be accessible for a PR opened from a fork,” you generally cannot do that with pull_request; you must use pull_request_target (or another trusted path), and then follow GitHub’s security guidance to avoid checking out/running untrusted fork code with elevated privileges. [1][2] Note: GitHub also redacts secret values in workflow logs, but that doesn’t make secrets available to workflows that don’t have permission to access them in the first place. [3][4]

Citations:


Guard deploy for forked PRs (missing secrets in pull_request context).

Forked PRs triggering the pull_request event do not have access to repository secrets. Lines 53–54 use secrets.CLOUDFLARE_API_TOKEN and secrets.CLOUDFLARE_ACCOUNT_ID, causing the deploy job to fail on external contributions. Add a guard to allow same-repo PRs while skipping forks.

Suggested change
 jobs:
   deploy:
+    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
     runs-on: ubuntu-latest
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-web-ui.yml around lines 10 - 15, The pull_request
event does not provide access to repository secrets for forked repositories,
causing the deploy job to fail when lines 53-54 attempt to use
secrets.CLOUDFLARE_API_TOKEN and secrets.CLOUDFLARE_ACCOUNT_ID. Add a
conditional guard to the deploy job using
github.event.pull_request.head.repo.full_name == github.repository to ensure the
job only runs for pull requests from the same repository, skipping execution for
forked PRs that lack access to these secrets.

# Cancel an in-flight deploy for the same ref when a new commit lands.
# Prevents stale previews and saves Cloudflare build minutes.
concurrency:
group: deploy-web-ui-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # required to post the preview URL comment
deployments: write
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the workflow file
fd "deploy-web-ui.yml" -t f

Repository: TechImmigrants/cv-builder

Length of output: 51


🏁 Script executed:

# List all files in .github/workflows directory
ls -la .github/workflows/ 2>/dev/null || echo "Directory not found"

Repository: TechImmigrants/cv-builder

Length of output: 303


🏁 Script executed:

# Read the deploy-web-ui.yml file to examine line 30 and surrounding context
cat -n .github/workflows/deploy-web-ui.yml

Repository: TechImmigrants/cv-builder

Length of output: 4257


Disable persisted checkout credentials for untrusted PR code paths.

This workflow runs on pull_request events (line 10) and executes untrusted PR code via pnpm install and pnpm build (lines 40, 47) before the deploy step. Without persist-credentials: false, the GITHUB_TOKEN remains in git config and could be exploited by malicious PR scripts to push unauthorized commits or access repository resources. Add the setting to prevent this attack vector:

Suggested patch
      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 30-30: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 30-30: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-web-ui.yml at line 30, The actions/checkout@v4
action on line 30 is missing the persist-credentials: false configuration. Since
this workflow processes untrusted code from pull requests (executing pnpm
install and pnpm build), the GITHUB_TOKEN must be prevented from persisting in
git config to avoid potential exploitation by malicious scripts. Add the
persist-credentials: false parameter to the checkout action to disable
credential persistence.

Source: Linters/SAST tools


- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install
run: pnpm install --frozen-lockfile

- name: Build (static export → apps/web-ui/out)
working-directory: apps/web-ui
env:
# Plumbed for when the fastify backend lands; harmless local default until then. See issue #77.
NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL || 'http://localhost:3001' }}
run: pnpm build

- name: Deploy to Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# --commit-dirty=true is required: the wrangler-action sets CI=1 and would otherwise refuse a deploy with apps/web-ui/out/ as an uncommitted change.
command: pages deploy apps/web-ui/out --project-name=cv-builder-web --commit-dirty=true

- name: Post preview URL on PR
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
COMMENT_MARKER: cv-builder-web-preview-deploy
run: |
set -euo pipefail

BODY=$(cat <<EOF
<!-- ${COMMENT_MARKER} -->
🌐 **Preview deployment:** ${ALIAS_URL}

_Updated automatically on every push to this PR._
EOF
)

AUTH=(-H "Authorization: token ${GH_TOKEN}" -H "Accept: application/vnd.github+json")
JSON=$(jq -nc --arg body "${BODY}" '{body: $body}')

# Find the existing bot comment for this PR (if any) by the marker, so
# we update it instead of posting a duplicate on every push.
EXISTING_ID=$(curl -fsS "${AUTH[@]}" \
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
| jq -r --arg m "${COMMENT_MARKER}" \
'.[] | select(.body | contains($m)) | .id' \
| head -n1)
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/deploy-web-ui.yml | head -100

Repository: TechImmigrants/cv-builder

Length of output: 4257


🏁 Script executed:

# Check if there are any other instances of COMMENT_MARKER in the codebase
rg "COMMENT_MARKER|cv-builder-web-preview-deploy" --type yaml --type sh --type bash

Repository: TechImmigrants/cv-builder

Length of output: 98


🏁 Script executed:

rg "COMMENT_MARKER|cv-builder-web-preview-deploy"

Repository: TechImmigrants/cv-builder

Length of output: 51


Filter preview comment updates to bot-authored comments.

The jq filter at lines 84-86 selects any comment containing the marker string, which could match a user-authored comment if they include the marker. This would cause the bot to update that user's comment instead of posting its own. Filter by user.login == "github-actions[bot]" to ensure only the bot's own comments are updated.

Suggested change
           EXISTING_ID=$(curl -fsS "${AUTH[@]}" \
             "https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
             | jq -r --arg m "${COMMENT_MARKER}" \
-                '.[] | select(.body | contains($m)) | .id' \
+                '.[] | select((.user.login == "github-actions[bot]") and (.body | contains($m))) | .id' \
             | head -n1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| jq -r --arg m "${COMMENT_MARKER}" \
'.[] | select(.body | contains($m)) | .id' \
| head -n1)
| jq -r --arg m "${COMMENT_MARKER}" \
'.[] | select((.user.login == "github-actions[bot]") and (.body | contains($m))) | .id' \
| head -n1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-web-ui.yml around lines 84 - 86, The jq filter in
the comment selection logic only checks if the comment body contains the marker
string, but does not verify the comment author. This could cause the bot to
update a user's comment if they happen to include the marker. Modify the jq
select filter to add an additional condition that checks the user.login field
equals "github-actions[bot]" alongside the existing body contains check,
ensuring only comments authored by the bot are selected for update.


if [[ "${EXISTING_ID}" =~ ^[0-9]+$ ]]; then
curl -fsS -X PATCH "${AUTH[@]}" \
"https://api.github.com/repos/${REPO}/issues/comments/${EXISTING_ID}" \
-d "${JSON}"
echo "Updated existing comment ${EXISTING_ID}"
else
curl -fsS -X POST "${AUTH[@]}" \
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments" \
-d "${JSON}"
echo "Posted new comment"
fi
Comment on lines +58 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make PR-comment API calls non-critical or retryable to avoid false failed deploys.

The deployment can succeed but the job can still fail on transient GitHub API issues in this step due to set -euo pipefail. That degrades CI reliability and obscures actual deploy status.

Suggested hardening
       - name: Post preview URL on PR
         if: github.event_name == 'pull_request'
+        continue-on-error: true
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@
-          EXISTING_ID=$(curl -fsS "${AUTH[@]}" \
+          EXISTING_ID=$(curl -fsS --retry 3 --retry-all-errors --connect-timeout 10 "${AUTH[@]}" \
             "https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
             | jq -r --arg m "${COMMENT_MARKER}" \
                 '.[] | select(.body | contains($m)) | .id' \
             | head -n1)
@@
-            curl -fsS -X PATCH "${AUTH[@]}" \
+            curl -fsS --retry 3 --retry-all-errors --connect-timeout 10 -X PATCH "${AUTH[@]}" \
               "https://api.github.com/repos/${REPO}/issues/comments/${EXISTING_ID}" \
               -d "${JSON}"
@@
-            curl -fsS -X POST "${AUTH[@]}" \
+            curl -fsS --retry 3 --retry-all-errors --connect-timeout 10 -X POST "${AUTH[@]}" \
               "https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments" \
               -d "${JSON}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-web-ui.yml around lines 58 - 98, The "Post preview
URL on PR" step uses set -euo pipefail which causes the entire job to fail if
any GitHub API curl calls fail due to transient issues, obscuring actual
deployment status. Wrap the curl commands that post/update the PR comment (the
EXISTING_ID lookup curl and the subsequent PATCH or POST curl calls) with error
handling to make them non-critical. This can be done by either temporarily
disabling pipefail with set +e before the comment posting logic and set -e
afterwards, or by adding || true to the curl commands, or by implementing retry
logic to handle transient API failures gracefully without failing the job.

20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide.

---

## Deployment

The web UI (`apps/web-ui/`) is deployed to **Cloudflare Pages**. Every push to a
PR that changes `apps/web-ui/**` triggers the `Deploy web UI` workflow, which
builds a static export and posts a preview URL as a comment on the PR. Pushes
to `main` deploy to the production site.

### Required repo secrets

| Secret | Where to get it |
| --- | --- |
| `CLOUDFLARE_API_TOKEN` | Cloudflare dashboard → My Profile → API Tokens → Create Token → use the "Edit Cloudflare Pages" template, scoped to the account and `cv-builder-web` project |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare dashboard → Workers & Pages → right sidebar |

The `cv-builder-web` Cloudflare Pages project must exist before the first
deploy — create it once via the dashboard or
`wrangler pages project create cv-builder-web`.

---

## Supported Role Archetypes

Currently built-in:
Expand Down
11 changes: 11 additions & 0 deletions apps/web-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ Then open [http://localhost:3000](http://localhost:3000).
- `src/app/layout.tsx` - app shell and metadata
- `src/app/page.tsx` - homepage
- `src/app/globals.css` - global styles

## Previews

PRs that touch this package automatically get a Cloudflare Pages preview link,
posted as a comment on the PR by the `Deploy web UI` workflow. Pushing new
commits updates the same comment instead of posting duplicates. Merging to
`main` redeploys the production site at `https://cv-builder-web.pages.dev`.

The build is a static export (`output: "export"` in `next.config.ts`) — the
app must remain free of server-only features (API routes, server actions,
middleware, etc.).
3 changes: 2 additions & 1 deletion apps/web-ui/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
// Static export for Cloudflare Pages; the app must stay server-feature-free. See issue #77.
output: "export",
reactCompiler: true,
};

Expand Down
Loading