-
Notifications
You must be signed in to change notification settings - Fork 119
ci(landing): deploy landing via Actions so fork PRs stop failing Vercel #1248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| name: Deploy Landing (Vercel) | ||
|
|
||
| # Replaces Vercel's Git integration for the landing site. | ||
| # | ||
| # Why: the Vercel project sits on a personal (Hobby) scope with | ||
| # `gitForkProtection: true`, so every fork PR gets a "requires authorization" | ||
| # commit status that reports as FAILED — on arrival, before any build runs. No | ||
| # vercel.json / ignoreCommand can suppress that, because the authorization gate | ||
| # is upstream of the build step, and fork authors cannot be added as team | ||
| # members on a Hobby scope. | ||
| # | ||
| # Driving the deploy from Actions instead means a PR we don't want to deploy is | ||
| # SKIPPED (neutral) rather than failed, and the gate is explicit and readable. | ||
| # GitHub also withholds secrets from fork `pull_request` runs, so a fork | ||
| # physically cannot deploy even if the `if:` below were wrong — the condition is | ||
| # the intent, the secret withholding is the enforcement. | ||
|
|
||
| on: | ||
| push: | ||
| branches: [release] | ||
| paths: | ||
| - "landing/**" | ||
| - ".github/workflows/landing-deploy.yml" | ||
| pull_request: | ||
| paths: | ||
| - "landing/**" | ||
| - ".github/workflows/landing-deploy.yml" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: landing-deploy-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| deploy: | ||
| # Same-repo refs only. Fork PRs skip → no check, rather than a red one. | ||
| if: >- | ||
| github.event_name != 'pull_request' || | ||
| github.event.pull_request.head.repo.full_name == github.repository | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| env: | ||
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | ||
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | ||
|
Comment on lines
+40
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== file exists and line count =="
if [ -f .github/workflows/landing-deploy.yml ]; then
wc -l .github/workflows/landing-deploy.yml
else
echo "missing .github/workflows/landing-deploy.yml"
fd -a 'landing-deploy\.yml$' .
fi
echo "== relevant workflow contents =="
if [ -f .github/workflows/landing-deploy.yml ]; then
nl -ba .github/workflows/landing-deploy.yml | sed -n '1,150p'
fi
echo "== workflow files containing VERCEL_TOKEN references =="
rg -n "VERCEL_(TOKEN|ORG_ID|PROJECT_ID)" .github/workflows || trueRepository: juspay/neurolink Length of output: 302 🌐 Web query:
💡 Result: GitHub Actions security policy for pull requests originating from forks is designed to prevent unauthorized access to sensitive repository secrets [1][2][3]. By default, workflows triggered by the pull_request event for a pull request from a fork do not have access to secrets, and they are provided with a read-only GITHUB_TOKEN [4][1][2]. If a workflow requires access to secrets for a pull request from a fork, developers often use the pull_request_target event [5][4][6]. This event runs in the context of the base repository and does have access to secrets and a read/write GITHUB_TOKEN [5][4][2]. However, using this event introduces significant security risks because the workflow uses the repository's workflow file and is considered trusted, even though it is processing an untrusted pull request [5][7]. To securely handle these scenarios, follow these best practices: 1. Avoid checking out untrusted code: Never use actions/checkout to check out the head of an untrusted pull request in a workflow that has access to secrets [5][2][6]. Doing so allows the pull request's code (such as malicious build scripts or dependencies) to execute with the full privileges of the workflow, potentially leading to secret exfiltration [8][2][6]. 2. Use separate workflows: If you must test code from a fork and also perform actions requiring secrets, split the task into two workflows [6]. Use a pull_request event (without secrets) to test the untrusted code, and a separate pull_request_target event to perform the privileged operations, ensuring the latter never processes or executes the untrusted pull request code [2][6]. 3. Understand checkout protections: As of June 2026, actions/checkout (v7 and later) includes built-in protections that refuse to fetch code from a fork when used within pull_request_target workflows to prevent common "pwn request" vulnerabilities [8]. You should not override these protections (e.g., by setting allow-unsafe-pr-checkout: true) unless you have thoroughly audited the workflow to ensure that no untrusted code is executed [5][8]. 4. Configuration for private repositories: For private repositories, repository owners can adjust settings to enable or disable access to secrets and write tokens for pull requests from forks, but this should be done with extreme caution as it increases the risk of privilege escalation [9]. In summary, standard pull_request workflows are inherently safe from secret exfiltration because they lack access to them [2]. The use of pull_request_target is an intentional design choice for automation that requires elevated permissions, but it mandates strict separation between code execution and privileged secret handling to remain secure [5][2][6]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== relevant workflow contents =="
awk '{printf "%5d %s\n", NR, $0}' .github/workflows/landing-deploy.yml | sed -n '1,140p'
echo
echo "== workflow files using Vercel tokens/secrets =="
rg -n "VERCEL_(TOKEN|ORG_ID|PROJECT_ID)|pull_request|pull_request_target|environment:" .github/workflows || true
echo
echo "== git diff stat/name/status =="
git diff --stat HEAD~1..HEAD 2>/dev/null || git diff --stat 2>/dev/null || trueRepository: juspay/neurolink Length of output: 10010 🌐 Web query:
💡 Result: GitHub Actions handles secret access for pull requests differently depending on the source of the pull request and the specific trigger used [1][2][3]. For pull requests originating from a fork, GitHub enforces strict isolation for the pull_request event by default [1][2][3]: - Workflows triggered by pull_request for pull requests from forks do not have access to repository or organization secrets [1][4][3]. - They are also issued a read-only GITHUB_TOKEN to prevent unauthorized modifications to the repository [1][5][3]. For pull requests originating from the same repository (i.e., branches created directly within the base repository), workflows triggered by pull_request have access to repository secrets [2]. This is permitted because the user creating the pull request already possesses write access to the repository [2]. To handle scenarios requiring secret access for pull requests from forks, the pull_request_target trigger is available [1][2][6]. This trigger operates with elevated trust, granting access to secrets and a read/write GITHUB_TOKEN [1][7][3]. However, it is critical to note that pull_request_target executes the workflow file from the base repository's default branch rather than the pull request's branch [1][7]. While this provides a security boundary, it remains highly dangerous if the workflow subsequently checks out and runs code from the untrusted pull request branch [1][6][8]. If a workflow triggered by pull_request_target executes untrusted code, that code can compromise the repository and access the secrets available to the workflow [9][8][3]. In summary, the security model is designed to prevent secrets from leaking to untrusted fork code by withholding them from the standard pull_request event [2][3]. The pull_request_target trigger is a powerful tool for automation, but it must be used with extreme caution to ensure no untrusted input or code influences the execution of privileged steps [1][8][3]. Citations:
Use a preview-scoped Vercel token for PR builds. This preview path now includes same-repository PRs and loads 🤖 Prompt for AI Agents |
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| # package.json pins "packageManager": "pnpm@10.15.1", so `vercel build` | ||
| # shells out to pnpm for the install. Without pnpm on the runner it dies | ||
| # with `spawn pnpm ENOENT` before the build starts. Version kept in step | ||
| # with the pin and with the other workflows in this repo. | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10.15.1 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "24" | ||
|
|
||
| - name: Install Vercel CLI | ||
| run: npm install --global vercel@latest | ||
|
Comment on lines
+58
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- workflow snippet ---\n'
sed -n '45,70p' .github/workflows/landing-deploy.yml 2>/dev/null || true
printf '\n--- package manager/lock files (common names) ---\n'
git ls-files | grep -Ei '(^|/)(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml)$' || true
printf '\n--- Vercel CLI npm metadata for latest/dist-tags ---\n'
python3 - <<'PY'
import json, urllib.request
url='https://registry.npmjs.org/vercel'
with urllib.request.urlopen(url, timeout=20) as r:
data=json.load(r)
print('current version:', data.get('dist-tags',{}).get('latest'))
print('dist-tags:', data.get('dist-tags'))
PY
printf '\n--- npm config relevant to global installs ---\n'
npm config get prefix 2>/dev/null || true
npm config get cache 2>/dev/null || trueRepository: juspay/neurolink Length of output: 245 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- workflow snippet ---'
sed -n '45,70p' .github/workflows/landing-deploy.yml 2>/dev/null || true
echo
echo '--- package manager/lock files (common names) ---'
git ls-files | grep -Ei '(^|/)(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml)$' || true
echo
echo '--- Vercel CLI npm metadata for latest/dist-tags ---'
python3 - <<'PY'
import json, urllib.request
url='https://registry.npmjs.org/vercel'
with urllib.request.urlopen(url, timeout=20) as r:
data=json.load(r)
print('current latest version:', data.get('dist-tags',{}).get('latest'))
print('dist-tags:', data.get('dist-tags'))
PY
echo
echo '--- npm config relevant to global installs ---'
npm config get prefix 2>/dev/null || true
npm config get cache 2>/dev/null || trueRepository: juspay/neurolink Length of output: 3495 Pin the Vercel CLI used for deployments.
Proposed fix- run: npm install --global vercel@latest
+ run: npm install --global vercel@<validated-version>🧰 Tools🪛 zizmor (1.28.0)[warning] 59-59: ad-hoc installation of packages (adhoc-packages): installs a package outside of a lockfile (adhoc-packages) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| # push → production, pull_request/dispatch → preview. | ||
| - name: Resolve target environment | ||
| id: target | ||
| run: | | ||
| if [ "${{ github.event_name }}" = "push" ]; then | ||
| echo "env=production" >> "$GITHUB_OUTPUT" | ||
| echo "flag=--prod" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "env=preview" >> "$GITHUB_OUTPUT" | ||
| echo "flag=" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| # Pulls project settings too — including rootDirectory=landing — so the | ||
| # build runs against the landing app without duplicating config here. | ||
| - name: Pull Vercel project settings | ||
| run: vercel pull --yes --environment=${{ steps.target.outputs.env }} --token=${{ secrets.VERCEL_TOKEN }} | ||
|
|
||
| - name: Build | ||
| run: vercel build ${{ steps.target.outputs.flag }} --token=${{ secrets.VERCEL_TOKEN }} | ||
|
|
||
| - name: Deploy | ||
| id: deploy | ||
| run: | | ||
| url="$(vercel deploy --prebuilt ${{ steps.target.outputs.flag }} --token=${{ secrets.VERCEL_TOKEN }})" | ||
| echo "url=$url" >> "$GITHUB_OUTPUT" | ||
| { | ||
| echo "### Landing deployed (${{ steps.target.outputs.env }})" | ||
| echo "" | ||
| echo "$url" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| # Restores the one thing the Git integration gave us for free. | ||
| - name: Comment preview URL on the PR | ||
| if: github.event_name == 'pull_request' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const url = ${{ toJSON(steps.deploy.outputs.url) }}; | ||
| const marker = '<!-- landing-preview -->'; | ||
| const body = `${marker}\n**Landing preview:** ${url}\n\n<sub>Deployed from \`${context.sha.slice(0, 7)}\` by the landing-deploy workflow.</sub>`; | ||
| const { owner, repo } = context.repo; | ||
| const issue_number = context.payload.pull_request.number; | ||
| const existing = ( | ||
| await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 }) | ||
| ).data.find((c) => c.body && c.body.includes(marker)); | ||
|
Comment on lines
+112
to
+114
Comment on lines
+103
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== workflow file excerpt =="
sed -n '1,160p' .github/workflows/landing-deploy.yml 2>/dev/null || true
echo
echo "== relevant GitHub Actions REST API usage =="
rg -n "issues.listComments|paginate|github-actions|preview|marker" .github/workflows/landing-deploy.yml .github/workflows 2>/dev/null || true
echo
echo "== package/workflow context =="
ls -la .github/workflows/landing-deploy.yml 2>/dev/null || true
git diff --stat HEAD~1..HEAD 2>/dev/null | sed -n '1,80p' || trueRepository: juspay/neurolink Length of output: 7410 🌐 Web query:
💡 Result: The GitHub REST API enforces a maximum limit of 100 results per page for listing issue comments [1][2]. The default value for the per_page parameter is 30 [2][3]. It is important to note that if you provide a value for per_page that exceeds 100, the API will silently clamp the value to the maximum of 100 [4]. The request will still return a 200 OK status, meaning that if your application logic expects a higher number of results per page, it may under-fetch data without explicitly indicating that the limit was capped [4]. Citations:
🌐 Web query:
💡 Result: The github.rest.issues.updateComment method is available and is the standard way to update issue and pull request comments when using the actions/github-script action [1][2][3]. In the actions/github-script action, the github object provides a pre-authenticated Octokit client that includes the Citations:
🌐 Web query:
💡 Result: When using Octokit to list issue comments, you can set per_page to 100 to optimize the number of requests made during pagination [1][2]. The per_page parameter defaults to 30, but the GitHub REST API supports a maximum of 100 per page [1][2]. You can use the octokit.paginate method in combination with the relevant REST endpoint method. Below is an example of how to fetch all issue comments for a specific issue using this configuration: const comments = await octokit.paginate(octokit.rest.issues.listComments, { owner: "OWNER", repo: "REPO", issue_number: 123, per_page: 100, }); In this example, octokit.paginate will automatically handle fetching subsequent pages until all results are retrieved [3][4]. The per_page: 100 setting ensures that each individual request retrieves the maximum number of items allowed, which helps minimize API overhead and reduce the likelihood of hitting rate limits [1][2]. Citations:
Make the sticky-comment lookup authoritative.
🤖 Prompt for AI Agents |
||
| if (existing) { | ||
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); | ||
| } else { | ||
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: juspay/neurolink
Length of output: 12935
🌐 Web query:
actions/checkout persist-credentials default true README💡 Result:
In the actions/checkout GitHub Action, the persist-credentials input determines whether the action configures the provided token or SSH key within the local Git configuration [1][2]. As of July 2026, the default value for persist-credentials remains true [1][3][4]. When set to true, the action configures the authentication credentials in the local Git configuration, which allows your workflow scripts to perform authenticated Git commands (such as git push or git fetch) [1][2]. The action automatically removes these credentials during the post-job cleanup process [1][2]. While there have been community discussions and proposals to change this default to false to further enhance security—specifically to minimize the risk of accidental credential exposure—the default behavior has not been altered in the official releases [5]. If you prefer to disable this behavior for security reasons, you can explicitly set the input to false in your workflow file: - uses: actions/checkout@v4 with: persist-credentials: false
Citations:
🌐 Web query:
GitHub security hardening of Actions permissions pull-requests write contents read actions checkout💡 Result:
To harden GitHub Actions security, you must adhere to the principle of least privilege, especially when using sensitive event triggers [1][2]. 1. Set Default Permissions to Read-Only At the organization or repository level, configure the default GITHUB_TOKEN permissions to read-only [3][4][5]. This ensures that, by default, workflows cannot perform destructive actions or modify the repository [1][5]. You should then explicitly grant higher permissions only at the job level within individual workflow files [1][3][4]. 2. Securely Use pull_request_target The pull_request_target event is a high-risk trigger because it executes in the context of the base repository's default branch, granting access to secrets and potentially high-privilege tokens [6][7][8]. * Prefer the standard pull_request event, which is safer because it automatically restricts GITHUB_TOKEN permissions to read-only and withholds secrets when triggered from a fork [1][7][8]. * Never combine pull_request_target with a checkout of untrusted pull request code [7][8]. Doing so creates a pwn request vulnerability, where malicious code from a fork executes with the base repository's secrets [9][3][6]. * As of June 18, 2026, actions/checkout v7 automatically blocks the checkout of fork pull request code in pull_request_target and certain workflow_run workflows [9][3]. If you must check out untrusted code for specific, verified use cases, you must explicitly set allow-unsafe-pr-checkout: true [10][9][3]. Treat this as a significant security decision [9]. 3. Implement Fine-Grained Permissions Use the permissions key to restrict the GITHUB_TOKEN for every job [1][4]. * Start with an empty permissions block (permissions: {}) or read-all, then add only the specific scopes required [4]. * For checkout, typically only contents: read is needed [4]. * If you do not need the GITHUB_TOKEN at all, set permissions: {} to disable it entirely [4]. 4. General Hardening Best Practices * Use OIDC for cloud authentication instead of storing long-lived secrets [3][4]. * Pin actions to full commit SHAs to prevent supply-chain attacks via compromised action versions [4]. * Avoid using self-hosted runners on public repositories, as they are susceptible to compromise by any user who can open a pull request [2].
Citations:
Keep the deployment job read-only and disable checkout credential persistence.
pull-requests: writeis job-scoped, so every step indeployinherits PR write capability even though later steps only build/deploy. Move any PR commentary/commit-status actions to a separate job withpull-requests: write, keep the deploy job scoped tocontents: read, and setpersist-credentials: falsefor checkout to avoid latergitcommands reusing the token locally.Proposed checkout hardening
- name: Checkout uses: actions/checkout@v4 + 与 + persist-credentials: false🤖 Prompt for AI Agents
Source: Linters/SAST tools