Skip to content
Open
Show file tree
Hide file tree
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
12 changes: 12 additions & 0 deletions .github/workflows/build_n_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 runs per ref so two quick merges to main can't race the :main tag
# (last push wins regardless of commit order). Queued main builds are never
# cancelled mid-push; superseded PR builds are.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
docs_build_n_push:
runs-on: ubuntu-latest
Expand Down
45 changes: 40 additions & 5 deletions .github/workflows/generate_api_pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ on:
default: "refs/tags/vX.Y.Z"
type: string

# 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
Expand All @@ -23,6 +32,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

Expand All @@ -43,16 +62,25 @@ 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

- 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"
Expand All @@ -64,6 +92,13 @@ jobs:
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 16 additions & 28 deletions scripts/git-dates.mjs
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
import { execSync } from 'child_process'

/**
* Get the last modified date for a file from git history.
* Returns YYYY-MM-DD or null if the file is not tracked / git is unavailable.
*
* Prefer buildGitDateMap() when you need dates for many files — this spawns a
* git process per call, which is ~300x slower across a full page tree.
*/
export function getGitLastModified(filePath) {
try {
const date = execSync(`git log -1 --format=%cI -- "${filePath}"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'],
}).trim()

return date ? date.split('T')[0] : null
} catch {
return null
}
}

let _dateMapCache

/**
Expand All @@ -31,22 +11,28 @@ let _dateMapCache
* most recent commit — the same value `git log -1 -- <path>` returns. Paths are
* repo-relative with forward slashes, matching path.relative(repoRoot, file).
*
* Returns an empty map if git is unavailable (e.g. inside the Docker image,
* which has no git binary) or the checkout is shallow; callers then fall back
* to no date, exactly as the per-file getGitLastModified() did.
* Note: `git log --name-only` lists no files for merge commits, so content
* introduced by a conflict-resolving merge is attributed to its source
* commits. That matches this repo's squash-merge workflow; revisit with
* `--diff-merges=first-parent` if long-lived branches are ever merged.
*
* Returns an empty map — with a warning, since every page then renders without
* an "Updated" date and the sitemap loses <lastmod> — when git is unavailable
* or the checkout is shallow (e.g. actions/checkout without fetch-depth: 0,
* where git log would attribute one identical, wrong date to every file).
*/
export function buildGitDateMap() {
if (_dateMapCache) return _dateMapCache
const map = new Map()
try {
// On a shallow clone (e.g. actions/checkout without fetch-depth: 0) git log
// only sees the fetched commits, so every file would report the same wrong
// date. Emit no dates rather than wrong ones.
const shallow = execSync('git rev-parse --is-shallow-repository', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'],
}).trim()
if (shallow === 'true') {
console.warn(
'[git-dates] shallow clone detected — emitting no per-page dates (fetch full history to enable them)'
)
_dateMapCache = map
return map
}
Expand All @@ -68,8 +54,10 @@ export function buildGitDateMap() {
}
if (currentDate && !map.has(line)) map.set(line, currentDate)
}
} catch {
// git unavailable — return the empty map, callers fall back to null.
} catch (err) {
console.warn(
`[git-dates] could not read git history — emitting no per-page dates: ${err.message}`
)
}
_dateMapCache = map
return map
Expand Down
Loading