From claude-resources
Deploys to Netlify using netlify-cli in GitHub Actions; resolves monorepo 'Projects detected' errors, pnpm workspace issues, netlify.toml inheritance problems, branch deploys, and local netlify dev/functions:serve.
npx claudepluginhub takazudo/claude-resourcesThis skill uses the workspace's default tool permissions.
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.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
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.
netlify deploy \
--dir=<path> # Directory to deploy (required)
--site=<site-id> # Netlify site ID
--auth=<token> # Auth token (or use NETLIFY_AUTH_TOKEN env var)
--prod # Deploy to production (default: draft)
--build # Opt-in to running build before deploy (deploy does NOT build by default)
--filter=<package> # Select package in monorepo - CRITICAL for pnpm workspaces
--functions=<folder> # Override functions directory (useful to skip functions with empty dir)
--alias=<name> # Custom subdomain for draft deploys
--message=<msg> # Deployment message
--json # Output as JSON (for programmatic URL extraction)
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.
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.
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 buildingThe CLI always reads netlify.toml from the project root, regardless of the --dir flag. This means:
netlify.toml apply to ALL deploys (including branch/alias deploys)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 404sTo 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 errorsnetlify.toml has no redirects, so no circular proxy issues--filter is not needed when deploying from an isolated directory (no monorepo detection)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.
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 }}"
- 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
- 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
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 build
- name: Install Netlify CLI
run: npm install -g netlify-cli@19
- 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 (branch deploy)
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="Documentation deploy: ${{ github.sha }}"
--filter=<package-name> flag to netlify deploynetlify linknetlify link does NOT support --filter flag and fails in monoreposnetlify 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.--no-build does not exist in netlify-cli@19+netlify deploy does NOT build by default. Use --build only when you want to opt-in to building.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 ...
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.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.--dir path or build artifacts not downloadedexport NETLIFY_AUTH_TOKEN=$(echo "$TOKEN" | tr -d '[:space:]')NETLIFY_SITE_ID - From Site settings -> General -> Site details -> API IDNETLIFY_AUTH_TOKEN - From User settings -> Applications -> Personal access tokensWhen using GitHub Actions for deployment, disable Netlify's built-in CI:
echo "Disabled"netlify.toml:[build]
command = "echo 'Build disabled - deploying from GitHub Actions'"
publish = "out"
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.
netlify devIn 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:
# This works - selects the correct package
netlify dev --functions=netlify/functions --offline --filter my-package
# This gets stuck at interactive prompt
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"
}
netlify dev Does/.netlify/functions/* → functions servernetlify.toml redirect rules (e.g., /api/products → /.netlify/functions/get-products).env variables into the environmentdlxOn 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 Workaroundnetlify 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:
# Terminal 1: Start functions server (port 9999)
netlify functions:serve --functions=netlify/functions --offline --filter my-package
# Terminal 2: Start Next.js dev server (port 34434)
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 |