Skip to content
Merged
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
119 changes: 119 additions & 0 deletions .github/workflows/landing-deploy.yml
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
Comment on lines +34 to +36

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
sed -n '1,90p' .github/workflows/landing-deploy.yml 2>/dev/null || true

echo
echo "== checkout usages in workflows =="
rg -n "actions/checkout|permissions:|pull-requests:|persist-credentials" .github/workflows -C 2 || true

echo
echo "== git diff stat and relevant diff =="
git diff --stat || true
git diff -- .github/workflows/landing-deploy.yml | sed -n '1,160p' || true

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: write is job-scoped, so every step in deploy inherits PR write capability even though later steps only build/deploy. Move any PR commentary/commit-status actions to a separate job with pull-requests: write, keep the deploy job scoped to contents: read, and set persist-credentials: false for checkout to avoid later git commands reusing the token locally.

Proposed checkout hardening
       - name: Checkout
         uses: actions/checkout@v4
+
+          persist-credentials: false
🤖 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/landing-deploy.yml around lines 34 - 36, Update the deploy
job in the workflow to retain only contents: read permission, move any PR
commentary or commit-status steps into a separate job scoped with pull-requests:
write, and configure its checkout step with persist-credentials: false.

Source: Linters/SAST tools


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

Copy link
Copy Markdown

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:

#!/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 || true

Repository: juspay/neurolink

Length of output: 302


🌐 Web query:

GitHub Actions pull requests from forks secrets security no-secrets documentation

💡 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 || true

Repository: juspay/neurolink

Length of output: 10010


🌐 Web query:

GitHub Actions pull_request same repository secrets access default security

💡 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 VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_PROJECT_ID before running PR-controlled checkout/build commands. Forks are still excluded from secret-bearing runs, but not all same-repo PRs are equal; use a read-only preview deployment token for PRs and keep production-capable credentials behind a release-only job or protected environment. Also applies to the Vercel steps that use secrets.VERCEL_TOKEN.

🤖 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/landing-deploy.yml around lines 40 - 48, Update the PR
preview job in the workflow around the same-repository condition and Vercel
deployment steps to use a preview-scoped, read-only Vercel token for pull
request builds. Keep production-capable Vercel credentials available only to the
release-only job or protected environment, and replace any PR-path references to
secrets.VERCEL_TOKEN while preserving the existing fork exclusion behavior.

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

Copy link
Copy Markdown

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:

#!/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 || true

Repository: 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 || true

Repository: juspay/neurolink

Length of output: 3495


Pin the Vercel CLI used for deployments.

npm install --global vercel@latest installs the mutable latest registry tag on each fresh runner, separate from the repo’s checked-in pnpm-lock.yaml, so a newly published release can change deployment behavior without reviewing the workflow. Pin an exact validated version and update it explicitly.

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 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/landing-deploy.yml around lines 58 - 59, Update the
“Install Vercel CLI” workflow step to install an exact validated Vercel CLI
version instead of the mutable latest tag, and retain that explicit version in
the command so future upgrades require a deliberate workflow change.

Source: 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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' || true

Repository: juspay/neurolink

Length of output: 7410


🌐 Web query:

GitHub REST API list issue comments per_page maximum 100

💡 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:

docs github actions github-script github.rest.issues.updateComment available actions rest API

💡 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 @octokit/plugin-rest-endpoint-methods plugin [1][4]. Starting with version 5 of actions/github-script, all REST API methods were moved under the github.rest namespace [1][5]. To use github.rest.issues.updateComment, you must provide the following required parameters [6][2]: owner: The owner of the repository. repo: The name of the repository. comment_id: The unique integer ID of the comment you wish to update. body: The new content of the comment (string). Below is a typical example of how this is used within an actions/github-script workflow step: await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: comment_id_variable, body: 'New comment body content' }); For comprehensive details on the parameters and behavior of this endpoint, you should refer to the official GitHub REST API documentation for "Update an issue comment" [6][7]. The github-script action itself relies on the underlying octokit/rest.js client, so you can also reference the octokit/plugin-rest-endpoint-methods documentation for further technical details [1][5].

Citations:


🌐 Web query:

Octokit paginate list issue comments maxPerPage 100

💡 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.

listComments is capped at 100 results per page, so PRs with more than 100 comments can get a new preview comment instead of updating the existing one. Also pin the lookup to the workflow bot author, since any comment containing the marker will currently be updated. Paginate the comments and require c.user?.login === 'github-actions[bot]' before updating.

🤖 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/landing-deploy.yml around lines 103 - 105, Update the
sticky-comment lookup around github.rest.issues.listComments to paginate through
all comment pages rather than limiting the search to one page, and only select a
comment whose c.user?.login equals github-actions[bot] in addition to containing
marker. Preserve the existing update-or-create flow using the authoritative
matching comment.

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 });
}
Loading