diff --git a/.github/workflows/build_n_push.yml b/.github/workflows/build_n_push.yml
index 3f5f5cdb..2f77a6a2 100644
--- a/.github/workflows/build_n_push.yml
+++ b/.github/workflows/build_n_push.yml
@@ -8,11 +8,23 @@ on:
pull_request:
paths:
- 'docker/**'
+ - '.dockerignore'
- '.github/workflows/build_n_push.yml'
- 'next.config.mjs'
+ - 'package.json'
- 'package-lock.json'
workflow_dispatch:
+permissions:
+ contents: read
+
+# Serialise non-PR runs while allowing superseded PR validation to be canceled.
+# Builds publish only an immutable SHA tag; the mutable ref tag is promoted
+# separately after the build and a final ref check.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
jobs:
docs_build_n_push:
runs-on: ubuntu-latest
@@ -55,7 +67,10 @@ jobs:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- - name: Docker build and push
+ # Non-PR runs publish an immutable image first. A canceled or stale run
+ # can leave this tag behind, but it cannot change :main (or another
+ # mutable ref tag).
+ - name: Docker build and publish immutable image
uses: docker/build-push-action@v6
with:
context: .
@@ -64,5 +79,39 @@ jobs:
# Keep a single-arch manifest (no attestation index) so the server's
# `docker compose pull` stays happy.
provenance: false
- tags: ${{ steps.meta.outputs.tags }}
+ tags: netbirdio/docs.netbird.io:${{ github.sha }}
labels: ${{ steps.meta.outputs.labels }}
+
+ # Promotion is a single remote manifest-tag update after the expensive
+ # build. Combined with serialisation and the final ref comparison, an
+ # older run cannot replace the mutable tag after a newer run publishes.
+ - name: Promote current image to ref tag
+ if: github.event_name != 'pull_request'
+ env:
+ SOURCE_IMAGE: netbirdio/docs.netbird.io:${{ github.sha }}
+ TARGET_TAGS: ${{ steps.meta.outputs.tags }}
+ run: |
+ remote_sha="$(git ls-remote origin "${GITHUB_REF}" | awk 'NR == 1 { print $1 }')"
+ if [ -z "$remote_sha" ]; then
+ echo "Could not resolve ${GITHUB_REF} on origin" >&2
+ exit 1
+ fi
+
+ if [ "$GITHUB_SHA" != "$remote_sha" ]; then
+ echo "Skipping tag promotion: ${GITHUB_SHA} is stale; ${GITHUB_REF} is now ${remote_sha}"
+ exit 0
+ fi
+
+ if [ -z "$TARGET_TAGS" ]; then
+ echo "Docker metadata produced no target tags" >&2
+ exit 1
+ fi
+
+ while IFS= read -r target_tag; do
+ if [ -n "$target_tag" ]; then
+ docker buildx imagetools create \
+ --prefer-index=false \
+ --tag "$target_tag" \
+ "$SOURCE_IMAGE"
+ fi
+ done <<< "$TARGET_TAGS"
diff --git a/.github/workflows/generate_api_pages.yml b/.github/workflows/generate_api_pages.yml
index aa28d1a0..d720be59 100644
--- a/.github/workflows/generate_api_pages.yml
+++ b/.github/workflows/generate_api_pages.yml
@@ -8,6 +8,19 @@ on:
default: "refs/tags/vX.Y.Z"
type: string
+permissions:
+ actions: read
+ contents: read
+
+# One run at a time: overlapping dispatches (several release tags in a day
+# happen — see run history) regenerate the same files and would collide. A
+# queued run superseded by a newer dispatch is fine: every run regenerates the
+# whole directory from its own tag, so the latest dispatch is the end state.
+# NB "latest dispatched", not "newest tag" — re-dispatching an older tag after
+# a newer one regresses the pages to the older spec.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+
jobs:
generate_api_pages:
runs-on: macos-latest
@@ -23,6 +36,16 @@ jobs:
with:
token: ${{ secrets.DEV_GITHUB_TOKEN }}
+ # A run queued behind another (see concurrency above) checks out the
+ # commit pinned at its dispatch time, not the branch's current tip.
+ # Sync before generating so the diff is computed against reality —
+ # otherwise the pre-push rebase replays a stale-base snapshot, and a
+ # file the newer spec removed could silently survive from the prior run.
+ - name: Sync to branch tip
+ run: |
+ git fetch origin "${GITHUB_REF_NAME}"
+ git reset --hard "origin/${GITHUB_REF_NAME}"
+
- name: Create directory
run: mkdir -p generator/openapi
@@ -43,8 +66,17 @@ jobs:
- name: Remove old generated files
run: rm -rf src/pages/ipa/resources/*
- - name: Npm install
- run: npm install
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ # npm ci: this workflow never changes dependencies, so install exactly
+ # the committed lockfile and never mutate it (macos-latest's npm can
+ # differ from the one that generated package-lock.json).
+ - name: Install dependencies
+ run: npm ci
- name: Generate api pages for netbird main openapi definition
run: npx ts-node generator/index.ts gen --input generator/openapi/expanded.yml --output src/pages/ipa/resources
@@ -52,18 +84,53 @@ jobs:
- name: Check git diff and untracked files
id: git_diff
run: |
- if [ -n "$(git status --porcelain)" ]; then
+ if [ -n "$(git status --porcelain src/pages/ipa/resources)" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- - name: Commit and push changes
+ # Concurrency serialises runs but does not guarantee dispatch order. A
+ # delayed older run must not overwrite output from a newer dispatch.
+ - name: Check whether this is the latest dispatch
+ id: freshness
if: steps.git_diff.outputs.changed == 'true'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ latest_run_id="$(
+ curl --fail --silent --show-error \
+ -H "Accept: application/vnd.github+json" \
+ -H "Authorization: Bearer ${GH_TOKEN}" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ --get \
+ --data-urlencode "event=workflow_dispatch" \
+ --data-urlencode "branch=${GITHUB_REF_NAME}" \
+ --data-urlencode "per_page=1" \
+ "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/generate_api_pages.yml/runs" \
+ | python3 -c 'import json, sys; print(json.load(sys.stdin)["workflow_runs"][0]["id"])'
+ )"
+
+ if [ "$GITHUB_RUN_ID" = "$latest_run_id" ]; then
+ echo "push=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "push=false" >> "$GITHUB_OUTPUT"
+ echo "Skipping generated-page commit: run ${GITHUB_RUN_ID} was superseded by ${latest_run_id}"
+ fi
+
+ - name: Commit and push changes
+ if: steps.git_diff.outputs.changed == 'true' && steps.freshness.outputs.push == 'true'
run: |
git config --global user.email "dev@netbird.io"
git config --global user.name "netbirddev"
- git add -A
+ # Stage only the regenerated API pages — never sweep up incidental
+ # changes like a rewritten package-lock.json.
+ git add src/pages/ipa/resources
git commit -m "Update API pages with v${{ steps.semver_parser.outputs.fullversion }}"
- git push --force
+ # The run takes minutes; if the branch moved meanwhile, replay our
+ # single generated-files commit on top instead of failing the push.
+ # A conflict is only possible against a concurrent edit of the
+ # generated files themselves and fails the run loudly.
+ git pull --rebase origin "${GITHUB_REF_NAME}"
+ git push
diff --git a/CLAUDE.md b/CLAUDE.md
index ae23b402..9f56986c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -16,7 +16,7 @@ There is no test suite in this project. Validate changes with `npm run build`.
npm install # Install dependencies
npm run dev # Start dev server (also runs gen:llm, gen:edit-routes, gen:last-updated, gen:sitemap)
npm run build # Production build (also runs gen:llm, gen:edit-routes, gen:last-updated, gen:sitemap)
-npm run start # Serve the production build
+npm run start # Serve the production build (warns under `output: 'standalone'` — safe to ignore locally; prod runs `node server.js` from `.next/standalone`)
npm run lint # ESLint (next/core-web-vitals) on src/
npm run gen # Regenerate API docs from NetBird OpenAPI spec
npm run gen:llm # Regenerate LLM-friendly markdown (auto-runs with dev/build)
diff --git a/public/docs-static/files/io.netbird.client.plist b/public/docs-static/files/io.netbird.client.plist
index f42b6b3d..ed87c71d 100644
--- a/public/docs-static/files/io.netbird.client.plist
+++ b/public/docs-static/files/io.netbird.client.plist
@@ -66,6 +66,9 @@
disableAutoConnect
+ disableAutostart
+
+
disableClientRoutes
diff --git a/public/docs-static/files/netbird-macos.mobileconfig b/public/docs-static/files/netbird-macos.mobileconfig
index 53453db5..d5b54e73 100644
--- a/public/docs-static/files/netbird-macos.mobileconfig
+++ b/public/docs-static/files/netbird-macos.mobileconfig
@@ -103,6 +103,8 @@