| name | netlify-cli |
| description | GitHub Actions workflows that deploy to Netlify with netlify-cli, or running 'netlify dev' / 'netlify functions:serve' locally. Covers monorepo 'Projects detected' errors, pnpm workspace deploys, deploying pre-built directories, capturing deploy URLs, GitHub secrets setup, netlify.toml inheritance with branch deploys, isolating sub-site deploys, netlify dev with --filter, pnpm 10.x trust store errors, CLI crash workarounds. Keywords: netlify deploy, github actions netlify, netlify-cli, monorepo deploy, pnpm workspace netlify, netlify dev, netlify functions serve. |
Netlify CLI for GitHub Actions
Use this skill when writing or debugging GitHub Actions workflows that deploy to Netlify using netlify-cli. This skill contains critical knowledge about common pitfalls and solutions.
Key Flags Reference
netlify deploy \
--dir=<path>
--site=<site-id>
--auth=<token>
--prod
--build
--filter=<package>
--functions=<folder>
--alias=<name>
--message=<msg>
--json
IMPORTANT: There is NO --no-build flag in netlify-cli@19+. netlify deploy does NOT build by default. Use --build only if you want to opt-in to building.
Critical: Monorepo "Projects Detected" Error
When deploying from a pnpm workspace or monorepo, you will encounter:
Error: Projects detected: package-a, package-b. Configure the project you want to work with and try again.
Solution
Use the --filter flag to select the target package:
netlify deploy \
--dir=./doc/build \
--site=$NETLIFY_SITE_ID \
--auth=$NETLIFY_AUTH_TOKEN \
--filter=<package-name> \
--prod
--filter=<package-name>: Selects the package (use the name from package.json)
netlify deploy does NOT build by default, so no extra flag is needed to skip building
Critical: netlify.toml Inheritance Problem
The CLI always reads netlify.toml from the project root, regardless of the --dir flag. This means:
- Redirects defined in the root
netlify.toml apply to ALL deploys (including branch/alias deploys)
- Functions from the root config are bundled into every deploy
- Circular proxy: If root
netlify.toml has a redirect like /doc/* -> https://doc--mysite.netlify.app/doc/:splat with force = true, the branch deploy will proxy to itself, causing 404s
Solution: Deploy from an Isolated Directory
To prevent the root netlify.toml from affecting a sub-site deploy, deploy from a separate directory with its own minimal netlify.toml:
- name: Prepare deploy directory
run: |
mkdir -p deploy-output/doc
cp -r doc/build/* deploy-output/doc/
mkdir -p deploy-output/.empty-functions
cat > deploy-output/netlify.toml << 'TOML'
[build]
publish = "."
TOML
- name: Deploy to Netlify
working-directory: deploy-output
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
run: |
netlify deploy \
--dir=. \
--functions=.empty-functions \
--site=$NETLIFY_SITE_ID \
--auth=$NETLIFY_AUTH_TOKEN \
--alias=doc \
--message="Deploy: ${{ github.sha }}"
Key points:
working-directory: deploy-output makes the CLI find the local netlify.toml instead of the root one
--functions=.empty-functions overrides the root's functions directory to avoid bundling errors
- The local
netlify.toml has no redirects, so no circular proxy issues
--filter is not needed when deploying from an isolated directory (no monorepo detection)
Critical: basePath / baseUrl Wrapping for Proxied Deploys
When a sub-site is proxied from the main site under a path prefix (e.g., mysite.com/doc/ -> doc--mysite.netlify.app/doc/), the build output must be nested in a matching subdirectory.
Problem: netlify deploy --dir=doc/build deploys files at root /. But if the framework (e.g., Docusaurus with baseUrl: '/doc/') generates asset references like /doc/assets/main.js, they will 404 because the actual file is at /assets/main.js.
Solution: Wrap the build output in the correct subdirectory before deploying:
- name: Prepare deploy directory
run: |
mkdir -p deploy-output/doc
cp -r doc/build/* deploy-output/doc/
- name: Deploy
run: netlify deploy --dir=deploy-output ...
Now doc--mysite.netlify.app/doc/assets/main.js resolves correctly.
Standard Single-Project Deploy
For non-monorepo projects:
- name: Install Netlify CLI
run: npm install -g netlify-cli
- name: Deploy to Netlify
run: |
netlify deploy \
--dir=./out \
--site=${{ secrets.NETLIFY_SITE_ID }} \
--auth=${{ secrets.NETLIFY_AUTH_TOKEN }} \
--prod \
--message="Deploy from GitHub Actions - ${{ github.sha }}"
Capturing Deploy URL
Without --json (simpler, recommended)
- name: Deploy to Netlify
id: deploy
run: |
OUTPUT=$(netlify deploy \
--dir=./build \
--site=${{ secrets.NETLIFY_SITE_ID }} \
--auth=${{ secrets.NETLIFY_AUTH_TOKEN }} \
--prod \
--message="Deploy: ${{ github.sha }}" 2>&1)
echo "$OUTPUT"
DEPLOY_URL=$(echo "$OUTPUT" | grep -o 'https://[^ ]*netlify.app' | head -1)
echo "deploy-url=$DEPLOY_URL" >> $GITHUB_OUTPUT
With --json (for more data)
- name: Deploy to Netlify
id: deploy
run: |
OUTPUT=$(netlify deploy \
--dir=./build \
--site=${{ secrets.NETLIFY_SITE_ID }} \
--auth=${{ secrets.NETLIFY_AUTH_TOKEN }} \
--prod \
--json)
DEPLOY_URL=$(echo "$OUTPUT" | jq -r '.deploy_url')
echo "deploy-url=$DEPLOY_URL" >> $GITHUB_OUTPUT
Complete GitHub Action Example (Monorepo Sub-Site)
Deploys a Docusaurus sub-site from a monorepo as a branch deploy, isolated from the main site's config:
name: Deploy Documentation
on:
push:
branches: [doc]
concurrency:
group: doc-deploy
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install documentation dependencies
working-directory: doc
run: pnpm install --frozen-lockfile
- name: Build Docusaurus documentation
working-directory: doc
run: pnpm run
Common Errors and Solutions
Error: "Projects detected"
- Cause: Monorepo with multiple package.json files
- Solution: Add
--filter=<package-name> flag to netlify deploy
Error: "Projects detected" with netlify link
- Cause:
netlify link does NOT support --filter flag and fails in monorepos
- Solution: Don't use
netlify link in monorepos. It's unnecessary when using --site flag with netlify deploy. The --site flag directly specifies the target site, making explicit linking redundant.
Error: "unknown option '--no-build'"
- Cause:
--no-build does not exist in netlify-cli@19+
- Solution: Remove the flag.
netlify deploy does NOT build by default. Use --build only when you want to opt-in to building.
Error: "Could not resolve @netlify/blobs" (or other function bundling errors)
-
Cause: The CLI picks up functions from the root netlify.toml config, even when deploying a sub-site that doesn't need functions
-
Solution: Use --functions=<empty-dir> to override. Create an empty directory and point to it:
mkdir -p .empty-functions
netlify deploy --functions=.empty-functions ...
Error: Circular redirect / 404 on branch deploy
- Cause: Root
netlify.toml has a redirect like /doc/* -> https://doc--mysite.netlify.app/doc/:splat with force = true. On the branch deploy itself, this redirect proxies to itself.
- Solution: Deploy from an isolated directory with its own
netlify.toml that has no redirects. Use working-directory in the GitHub Actions step so the CLI finds the local config instead of the root one.
Error: "Build directory not found"
- Cause: Wrong
--dir path or build artifacts not downloaded
- Solution: Verify path and ensure artifacts are downloaded in CI
Error: "Invalid character in header"
- Cause: Whitespace in auth token
- Solution: Trim the token:
export NETLIFY_AUTH_TOKEN=$(echo "$TOKEN" | tr -d '[:space:]')
Setup Requirements
GitHub Secrets Needed
NETLIFY_SITE_ID - From Site settings -> General -> Site details -> API ID
NETLIFY_AUTH_TOKEN - From User settings -> Applications -> Personal access tokens
Disable Netlify Auto-Build
When using GitHub Actions for deployment, disable Netlify's built-in CI:
- Go to Site Settings -> Build & deploy
- Set Build command to empty or
echo "Disabled"
- Or create
netlify.toml:
[build]
command = "echo 'Build disabled - deploying from GitHub Actions'"
publish = "out"
Using nwtgck/actions-netlify Instead
For simpler cases without monorepo issues:
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: ./out
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: 'Deploy - ${{ github.sha }}'
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 5
Note: This action may not handle monorepo scenarios well - use CLI directly for those cases.
Local Development with netlify dev
Basic Usage (Monorepo)
In a monorepo, netlify dev requires --filter to select the project. Without it, the CLI enters an interactive project selection prompt that blocks non-interactive environments:
netlify dev --functions=netlify/functions --offline --filter my-package
netlify dev --functions=netlify/functions --offline
A typical package.json script:
{
"netlify:dev": "PNPM_DISABLE_TRUST_STORE=true pnpm --package=netlify-cli dlx netlify dev --functions=netlify/functions --offline --filter my-package"
}
What netlify dev Does
- Starts your framework's dev server (e.g., Next.js on port 34434)
- Starts a Netlify Functions server
- Runs a proxy on port 8888 that routes:
- Page requests → framework dev server
/.netlify/functions/* → functions server
- Applies
netlify.toml redirect rules (e.g., /api/products → /.netlify/functions/get-products)
- Injects
.env variables into the environment
pnpm 10.x Trust Store Error with dlx
On pnpm 10.x, pnpm dlx netlify-cli may fail with:
ERR_PNPM_TRUST_DOWNGRADE High-risk trust downgrade for "@netlify/edge-bundler@14.9.5"
The PNPM_DISABLE_TRUST_STORE=true env var may not work with pnpm dlx even though pnpm config list shows trust-store=false. This is a pnpm 10.x behavior where dlx creates its own install context.
Workarounds:
-
Install netlify-cli globally instead of using pnpm dlx:
npm install -g netlify-cli
netlify dev --functions=netlify/functions --offline --filter my-package
-
Use npx (if the global install exists):
npx netlify-cli dev --functions=netlify/functions --offline --filter my-package
netlify dev Crash Workaround
netlify dev may crash with "Netlify CLI has terminated unexpectedly" after the proxy starts (observed in v23.14.0+). The proxy on port 8888 starts successfully but terminates immediately.
Workaround: Run functions separately
Use netlify functions:serve to run just the functions server on port 9999, then run your framework dev server separately:
netlify functions:serve --functions=netlify/functions --offline --filter my-package
pnpm dev
Limitation: Without the netlify dev proxy, API rewrites from netlify.toml (e.g., /api/products → /.netlify/functions/get-products) won't work. The functions are accessible directly at:
http://localhost:9999/.netlify/functions/<function-name>
For frameworks with output: 'export' (static site generation), Next.js rewrites() cannot be used. In this case, test the functions directly at the port 9999 URL.
netlify functions:serve vs netlify dev
| Feature | netlify dev | netlify functions:serve |
|---|
| Port | 8888 (proxy) | 9999 |
| Framework dev server | Auto-started | Must start separately |
API rewrites (netlify.toml) | Applied | Not applied |
.env injection | Yes | Yes |
| Stability | May crash (v23+) | Stable |
--filter flag | Supported | Supported |