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";

Reply via email to