Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,27 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: node -e "const fs=require('fs'); const dir='deploy/grafana/dashboards'; fs.readdirSync(dir).filter(f=>f.endsWith('.json')).forEach(f=>{const d=JSON.parse(fs.readFileSync(dir+'/'+f,'utf8'));if(!d.uid||!d.schemaVersion)throw new Error('missing uid/schemaVersion in '+f);if(!d.templating||!d.templating.list.some(v=>v.name==='organization_id'))throw new Error('missing organization_id variable in '+f);console.log('OK',f)})"

docs:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Validate frontmatter
run: npx tsx scripts/validate-frontmatter.ts
- name: Build static site
run: npm run build
- name: Generate Pagefind index
run: npx pagefind --site out
- name: Check dead links

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] --offline フラグにより外部リンクが検査されない

該当箇所:

        args: --offline --include-fragments './out/**/*.html'

lychee--offline オプションは HTTP/HTTPS リクエストを発行せず、ローカルファイル参照のみを検証する。ドキュメントサイトに外部 URL が含まれる場合、それらは一切チェックされない。タスク要件では「dead link detection」が求められているが、外部リンク切れをこのジョブでは検出できない。

  • CI コストを抑えつつ外部リンクも検証したい場合は --offline を除去し、--max-concurrency で並列数を制限する。
  • 意図的にオフラインのみにするなら、ステップ名や PR 説明に明示すべき。
@@ -118,1 +118,1 @@
-          args: --offline --include-fragments './out/**/*.html'
+          args: --include-fragments './out/**/*.html'

code.ci.dead_link_offline_only | confidence: 0.90

uses: lycheeverse/lychee-action@v2
with:
args: --offline --include-fragments --root-dir docs/out --fallback-extensions html --exclude-path docs/out/404.html './docs/out/**/*.html'
fail: true
71 changes: 71 additions & 0 deletions docs/__tests__/scripts/validate-frontmatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import fs from "fs";
import os from "os";
import path from "path";
import { afterEach, describe, expect, it } from "vitest";
import {
validateFrontmatter,
validateFrontmatterOrThrow,
} from "../../scripts/validate-frontmatter";

const tempDirs: string[] = [];

function createTempPagesDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mdx-frontmatter-"));
tempDirs.push(dir);
return dir;
}

afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});

describe("validateFrontmatter", () => {
it("returns an error when title is missing", () => {
const pagesDir = createTempPagesDir();
fs.writeFileSync(
path.join(pagesDir, "missing-title.mdx"),
"---\ndescription: test\n---\n# Hello",
);

const result = validateFrontmatter(pagesDir);

expect(result.errors).toHaveLength(1);
expect(result.errors[0]?.message).toMatch(/title/i);
expect(() => validateFrontmatterOrThrow(pagesDir)).toThrow(/title/i);
});

it("succeeds when title is present", () => {
const pagesDir = createTempPagesDir();
fs.writeFileSync(
path.join(pagesDir, "valid.mdx"),
"---\ntitle: Hello World\n---\n# Hello",
);

const result = validateFrontmatter(pagesDir);

expect(result.errors).toHaveLength(0);
expect(result.successCount).toBe(1);
expect(validateFrontmatterOrThrow(pagesDir).successCount).toBe(1);
});

it("flags draft pages in production mode", () => {
const pagesDir = createTempPagesDir();
fs.writeFileSync(
path.join(pagesDir, "draft.mdx"),
"---\ntitle: Draft Page\ndraft: true\n---\n# Draft",
);

const devResult = validateFrontmatter(pagesDir, { production: false });
const prodResult = validateFrontmatter(pagesDir, { production: true });

expect(devResult.errors).toHaveLength(0);
expect(devResult.successCount).toBe(1);
expect(prodResult.errors).toHaveLength(1);
expect(prodResult.errors[0]?.message).toMatch(/draft/i);
expect(() =>
validateFrontmatterOrThrow(pagesDir, { production: true }),
).toThrow(/draft/i);
});
});
8 changes: 4 additions & 4 deletions docs/components/EditPageLink.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useConfig } from 'nextra-theme-docs';

type EditPageLinkProps = {
filePath: string;
};

const DOCS_REPOSITORY_BASE =
'https://github.com/Corevice/open-git/blob/main/docs';

export function EditPageLink({ filePath }: EditPageLinkProps) {
const { docsRepositoryBase } = useConfig();
const href = `${docsRepositoryBase}/tree/main/${filePath}`;
const href = `${DOCS_REPOSITORY_BASE}/${filePath}`;

return <a href={href}>このページを編集</a>;
}
5 changes: 3 additions & 2 deletions docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ const withNextra = nextra({
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
// Static export (required so the Pagefind postbuild can index `out/`) is not
// compatible with the default next/image optimization loader.
// Static export to `out/` (so the Pagefind postbuild and the docs CI
// workflow can index it) is not compatible with the default next/image
// optimization loader.
images: {
unoptimized: true,
},
Expand Down
18 changes: 14 additions & 4 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"validate-frontmatter": "tsx scripts/validate-frontmatter.ts",
"check-links": "blc http://localhost:3000 -ro --filter-level 3",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"postbuild": "pagefind --site out"
},
"dependencies": {
"gray-matter": "^4.0.3",
"next": "^15.0.0",
"nextra": "^3.0.0",
"nextra-theme-docs": "^3.0.0",
"next-themes": "^0.4.0",
"prismjs": "^1.29.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next-themes": "^0.4.0",
"gray-matter": "^4.0.3"
"react-dom": "^19.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.0.0",
Expand All @@ -29,9 +32,16 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.0.0",
"broken-link-checker": "^0.7.8",
"jsdom": "^25.0.0",
"pagefind": "^1.1.0",
"pagefind": "^1.3.0",
"tsx": "^4.19.0",
"typescript": "^5.4.0",
"vitest": "^2.0.0"
},
"allowScripts": {
"esbuild@0.28.1": true,
"esbuild@0.21.5": true,
"sharp@0.34.5": true
}
}
2 changes: 1 addition & 1 deletion docs/pages/contributing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Issue の報告、Pull Request の提出、ドキュメントの改善など、

## ライセンス

本プロジェクトは [LICENSE](/LICENSE) ファイルに記載されたライセンスの下で提供されています。コントリビューションを行う前にライセンス条項をご確認ください。
本プロジェクトは [LICENSE](https://github.com/Corevice/open-git/blob/main/LICENSE) ファイルに記載されたライセンスの下で提供されています。コントリビューションを行う前にライセンス条項をご確認ください。

## 関連リンク

Expand Down
115 changes: 115 additions & 0 deletions docs/scripts/validate-frontmatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";

export interface ValidationError {
file: string;
message: string;
}

export interface ValidationResult {
errors: ValidationError[];
successCount: number;
}

export interface ValidateOptions {
production?: boolean;
}

function isProduction(options: ValidateOptions): boolean {
if (options.production !== undefined) {
return options.production;
}
return process.env.NODE_ENV === "production";
}

function walkMdxFiles(dir: string, files: string[] = []): string[] {
if (!fs.existsSync(dir)) {
return files;
}

for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkMdxFiles(fullPath, files);
} else if (entry.isFile() && entry.name.endsWith(".mdx")) {
files.push(fullPath);
}
}

return files;
}

export function validateFrontmatter(
pagesDir: string,
options: ValidateOptions = {},
): ValidationResult {
const production = isProduction(options);
const errors: ValidationError[] = [];
let successCount = 0;

for (const filePath of walkMdxFiles(pagesDir)) {
const content = fs.readFileSync(filePath, "utf8");
const { data } = matter(content);

const title = data.title;
if (typeof title !== "string" || title.trim() === "") {
errors.push({
file: filePath,
message: 'Missing required "title" in frontmatter',
});
continue;
}

if (production && data.draft === true) {
errors.push({
file: filePath,
message: "Draft pages are not allowed in production builds",
});
continue;
}

successCount++;
}

return { errors, successCount };
}

export function validateFrontmatterOrThrow(
pagesDir: string,
options: ValidateOptions = {},
): ValidationResult {
const result = validateFrontmatter(pagesDir, options);
if (result.errors.length > 0) {
const details = result.errors
.map((error) => `${error.file}: ${error.message}`)
.join("\n");
throw new Error(`Frontmatter validation failed:\n${details}`);
}
return result;
}

function main(): void {
const pagesDir = path.join(process.cwd(), "pages");
const result = validateFrontmatter(pagesDir);

if (result.errors.length > 0) {
console.error("Frontmatter validation failed:");
for (const error of result.errors) {
console.error(` ${error.file}: ${error.message}`);
}
process.exit(1);
}

console.log(
`Frontmatter validation passed: ${result.successCount} file(s) checked`,
);
}

const isMainModule =
process.argv[1] !== undefined &&
path.resolve(process.argv[1]) === path.resolve(__filename);

if (isMainModule) {
main();
}
Loading