This is an automated email from the ASF dual-hosted git repository.
kparisa pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iggy-website.git
The following commit(s) were added to refs/heads/main by this push:
new c1b4b806 ci: add typecheck, docs and licence checks (#76)
c1b4b806 is described below
commit c1b4b806405f65d72a1b798ef11107415428d012
Author: Justin Mclean <[email protected]>
AuthorDate: Thu Sep 3 17:06:15 2026 +1000
ci: add typecheck, docs and licence checks (#76)
Adds a `checks` job that runs alongside the existing build:
- `npm run typecheck` — `tsc --noEmit`. There is no linter and no
typecheck script today, so type errors only surface at the end of a full
static export of every page.
- `npm run check:links` — internal `/docs` links resolve, every page is
listed in its `meta.json`, and every local asset referenced from
`content/` exists under `public/`.
- `npm run check:headers` — ASF licence headers on source files under
`src/` and `scripts/`.
- `.asf.yaml` parses as YAML. A typo there is silently ignored by infra
today.
All four pass on main as of this branch, so they lock in the current
state rather than fixing a broken one. Three files were missing licence
headers and now have them: `src/app/error.tsx`,
`src/components/benchmark-chart.tsx`,
`src/components/force-dark-theme.tsx`.
Separately, this makes the GitHub star fetch non-destructive. It now
sends `GITHUB_TOKEN` when one is available, since the anonymous API
limit is per IP and shared runners hit it routinely; and when a fetch
fails it keeps the previously fetched value instead of overwriting it
with a hardcoded fallback that had drifted about 800 stars behind.
---------
Co-authored-by: Kranti Parisa <[email protected]>
---
.github/workflows/ci.yml | 36 ++++++++++++
package.json | 4 ++
scripts/check-docs-links.mjs | 111 ++++++++++++++++++++++++++++++++++++
scripts/check-license-headers.mjs | 57 ++++++++++++++++++
scripts/fetch-github-stars.mjs | 47 ++++++++++++---
src/app/error.tsx | 19 ++++++
src/components/benchmark-chart.tsx | 19 ++++++
src/components/force-dark-theme.tsx | 19 ++++++
8 files changed, 303 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 453967b6..33220b82 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,40 @@ concurrency:
cancel-in-progress: true
jobs:
+ checks:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+
+ - name: Install Dependencies
+ run: npm ci
+
+ # pretypecheck generates src/github-stars.json, which is gitignored and
so
+ # absent on a fresh checkout. GITHUB_TOKEN lifts the anonymous API limit;
+ # the script falls back safely if the fetch fails.
+ - name: Typecheck
+ run: npm run typecheck
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Check docs links
+ run: npm run check:links
+
+ - name: Check licence headers
+ run: npm run check:headers
+
+ - name: Validate .asf.yaml
+ run: >-
+ python3 -c "import sys,yaml; yaml.safe_load(open('.asf.yaml'));
+ print('.asf.yaml parses')"
+
build:
runs-on: ubuntu-latest
steps:
@@ -28,3 +62,5 @@ jobs:
- name: Build Website
run: npm run build
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/package.json b/package.json
index 1bca9fdb..3ead87ce 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,10 @@
"prebuild": "node scripts/fetch-github-stars.mjs && node
scripts/generate-docs-markdown.mjs",
"build": "next build",
"start": "next start",
+ "pretypecheck": "node scripts/fetch-github-stars.mjs",
+ "typecheck": "tsc --noEmit",
+ "check:links": "node scripts/check-docs-links.mjs",
+ "check:headers": "node scripts/check-license-headers.mjs",
"postinstall": "fumadocs-mdx"
},
"dependencies": {
diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs
new file mode 100644
index 00000000..fb09fea6
--- /dev/null
+++ b/scripts/check-docs-links.mjs
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Fails if an internal /docs link points at a page that does not exist, if an
+ * .mdx page is missing from its directory's meta.json, or if a local asset
+ * referenced from content/ is not present under public/.
+ *
+ * All three are clean as of this commit, so this locks in a good state rather
+ * than fixing a broken one. No dependencies: plain Node.
+ */
+
+import { readdirSync, readFileSync, statSync, existsSync } from "fs";
+import { join, relative } from "path";
+
+const ROOT = "content/docs";
+
+function walk(dir) {
+ return readdirSync(dir).flatMap((name) => {
+ const p = join(dir, name);
+ return statSync(p).isDirectory() ? walk(p) : [p];
+ });
+}
+
+const files = walk(ROOT).filter((f) => f.endsWith(".mdx"));
+
+const routes = new Set();
+for (const f of files) {
+ const rel = relative(ROOT, f).slice(0, -4);
+ routes.add("/docs/" + rel.replace(/\/index$/, ""));
+ if (rel.endsWith("index")) routes.add("/docs/" + rel.slice(0, -6));
+}
+
+const problems = [];
+
+const linkPattern = /\]\((\/docs\/[^)#\s]*)/g;
+const links = new Map();
+for (const f of files) {
+ const text = readFileSync(f, "utf8");
+ for (const m of text.matchAll(linkPattern)) {
+ const target = m[1].replace(/\/$/, "");
+ if (!links.has(target)) links.set(target, new Set());
+ links.get(target).add(f);
+ }
+}
+for (const [target, sources] of links) {
+ if (!routes.has(target)) {
+ problems.push(`unresolved link ${target} (in ${[...sources].sort().join(",
")})`);
+ }
+}
+
+const dirs = new Set(files.map((f) => f.slice(0, f.lastIndexOf("/"))));
+for (const dir of dirs) {
+ const metaPath = join(dir, "meta.json");
+ if (!existsSync(metaPath)) continue;
+ let listed;
+ try {
+ listed = new Set(JSON.parse(readFileSync(metaPath, "utf8")).pages ?? []);
+ } catch (e) {
+ problems.push(`unparseable ${metaPath}: ${e.message}`);
+ continue;
+ }
+ for (const f of files.filter((f) => f.startsWith(dir + "/") &&
!f.slice(dir.length + 1).includes("/"))) {
+ const stem = f.slice(dir.length + 1, -4);
+ if (stem !== "index" && !listed.has(stem)) {
+ problems.push(`${f} is not listed in ${metaPath}`);
+ }
+ }
+}
+
+// Local assets referenced from any content page must exist under public/.
+// A missing image does not fail the build, it just renders broken on the site.
+const CONTENT = "content";
+const assetPattern =
/(?:\]\(|src=")(\/[^)"\s]+\.(?:png|jpe?g|svg|gif|webp|ico|pdf))/g;
+const contentFiles = walk(CONTENT).filter((f) => f.endsWith(".mdx") ||
f.endsWith(".md"));
+const assets = new Map();
+for (const f of contentFiles) {
+ const text = readFileSync(f, "utf8");
+ for (const m of text.matchAll(assetPattern)) {
+ if (!assets.has(m[1])) assets.set(m[1], new Set());
+ assets.get(m[1]).add(f);
+ }
+}
+for (const [asset, sources] of assets) {
+ if (!existsSync(join("public", asset))) {
+ problems.push(`missing asset public${asset} (referenced in
${[...sources].sort().join(", ")})`);
+ }
+}
+
+console.log(
+ `docs: ${files.length} pages, ${links.size} distinct internal links, ` +
+ `${assets.size} local assets, ${problems.length} problem(s)`,
+);
+for (const p of problems) console.error(" " + p);
+process.exit(problems.length ? 1 : 0);
diff --git a/scripts/check-license-headers.mjs
b/scripts/check-license-headers.mjs
new file mode 100644
index 00000000..1b3b42d1
--- /dev/null
+++ b/scripts/check-license-headers.mjs
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Fails if a source file under src/ or scripts/ has no ASF licence header.
+ *
+ * Strictly speaking the ASF source-header policy governs files shipped in a
+ * release, and this website is published rather than released - so this is a
+ * house-style rule, not a compliance gate. Every file in scope carries a
+ * header as of this commit, which is why it can enforce rather than report:
+ * the bar is already met, and this keeps it met.
+ */
+
+import { readdirSync, readFileSync, statSync } from "fs";
+import { join } from "path";
+
+const DIRS = ["src", "scripts"];
+const EXTS = [".ts", ".tsx", ".mjs", ".css"];
+
+const MARKER = "Licensed to the Apache Software Foundation";
+
+function walk(dir) {
+ return readdirSync(dir).flatMap((name) => {
+ const p = join(dir, name);
+ return statSync(p).isDirectory() ? walk(p) : [p];
+ });
+}
+
+const files = DIRS.flatMap(walk).filter((f) => EXTS.some((e) =>
f.endsWith(e)));
+const missing = files.filter((f) => !readFileSync(f, "utf8").includes(MARKER));
+
+console.log(`licence headers: ${files.length} files checked, ${missing.length}
without a header`);
+for (const f of missing) console.log(" " + f);
+
+if (missing.length) {
+ console.error(
+ "\nAdd the ASF header (copy one from a neighbouring file) or, if the file
" +
+ "genuinely should not carry one, exclude it here with a comment saying
why.",
+ );
+ process.exit(1);
+}
diff --git a/scripts/fetch-github-stars.mjs b/scripts/fetch-github-stars.mjs
index ff90c603..de0e8797 100644
--- a/scripts/fetch-github-stars.mjs
+++ b/scripts/fetch-github-stars.mjs
@@ -17,32 +17,61 @@
* under the License.
*/
-import { writeFileSync } from "fs";
+import { writeFileSync, readFileSync, existsSync } from "fs";
-const FALLBACK = "3.9K";
+// Only used when there is no previously fetched value on disk. Keep it roughly
+// current: a stale fallback silently ships a wrong number on the site.
+const FALLBACK = "4.7K";
+
+const OUT = new URL("../src/github-stars.json", import.meta.url);
function formatStars(count) {
return count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count);
}
+function existingStars() {
+ if (!existsSync(OUT)) return null;
+ try {
+ const value = JSON.parse(readFileSync(OUT, "utf8")).stars;
+ return typeof value === "string" ? value : null;
+ } catch {
+ return null;
+ }
+}
+
async function main() {
- let stars = FALLBACK;
+ // The anonymous GitHub API limit is per IP, and shared CI runners hit it
+ // routinely. Actions exposes GITHUB_TOKEN, which raises the limit a long
way.
+ const token = process.env.GITHUB_TOKEN;
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
+
+ let stars = null;
try {
- const res = await fetch("https://api.github.com/repos/apache/iggy");
+ const res = await fetch("https://api.github.com/repos/apache/iggy", {
headers });
if (res.ok) {
const data = await res.json();
if (data.stargazers_count) {
stars = formatStars(data.stargazers_count);
}
+ } else {
+ console.warn(`GitHub stars: API returned ${res.status}
${res.statusText}`);
}
} catch (e) {
- console.warn("Failed to fetch GitHub stars, using fallback:", e.message);
+ console.warn("GitHub stars: fetch failed:", e.message);
+ }
+
+ if (stars === null) {
+ // Never overwrite a good value with a worse one.
+ const previous = existingStars();
+ stars = previous ?? FALLBACK;
+ console.warn(
+ previous
+ ? `GitHub stars: keeping previously fetched ${previous}`
+ : `GitHub stars: no previous value, using fallback ${FALLBACK}`,
+ );
}
- writeFileSync(
- new URL("../src/github-stars.json", import.meta.url),
- JSON.stringify({ stars }),
- );
+ writeFileSync(OUT, JSON.stringify({ stars }));
console.log(`GitHub stars: ${stars}`);
}
diff --git a/src/app/error.tsx b/src/app/error.tsx
index 5f4782e4..407bbf12 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -1,3 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
"use client";
export default function Error({
diff --git a/src/components/benchmark-chart.tsx
b/src/components/benchmark-chart.tsx
index 28db7e08..29b36ebc 100644
--- a/src/components/benchmark-chart.tsx
+++ b/src/components/benchmark-chart.tsx
@@ -1,3 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
"use client";
import Link from "next/link";
diff --git a/src/components/force-dark-theme.tsx
b/src/components/force-dark-theme.tsx
index 6939b7e3..5f625950 100644
--- a/src/components/force-dark-theme.tsx
+++ b/src/components/force-dark-theme.tsx
@@ -1,3 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
"use client";
import { useEffect } from "react";