mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
chore(docs): add docs sync and CI quality checks
This commit is contained in:
parent
8f79190ada
commit
413df27517
7 changed files with 329 additions and 4 deletions
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -67,6 +67,10 @@ jobs:
|
|||
echo "NPM version: $(npm --version)"
|
||||
echo "Timezone: $(date)"
|
||||
echo "Jest version: $(npx jest --version)"
|
||||
|
||||
- name: Run docs quality checks
|
||||
run: npm run docs:check
|
||||
timeout-minutes: 3
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint || echo "Linting completed with warnings"
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ nav:
|
|||
- NLP API: nlp-api.md
|
||||
- Webhooks: webhooks.md
|
||||
- Workflows: workflows.md
|
||||
- Calendar Setup: calendar-setup.md
|
||||
- Migration Guide: migration-v3-to-v4.md
|
||||
- Development:
|
||||
- Translation Workflow: development/translations.md
|
||||
|
|
@ -91,4 +92,4 @@ markdown_extensions:
|
|||
- attr_list
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
- stylesheets/extra.css
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "tasknotes",
|
||||
"version": "4.3.0",
|
||||
"version": "4.3.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tasknotes",
|
||||
"version": "4.3.0",
|
||||
"version": "4.3.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/view": "^6.37.2",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
"e2e:docs:copy": "mkdir -p media/docs && cp test-results/docs/*.png media/docs/",
|
||||
"e2e:setup": "bash e2e-setup.sh",
|
||||
"e2e:launch": "bash e2e-launch.sh",
|
||||
"version": "node version-bump.mjs && node generate-release-notes-import.mjs && git add manifest.json versions.json src/releaseNotes.ts",
|
||||
"version": "node version-bump.mjs && node generate-release-notes-import.mjs && npm run docs:sync && git add manifest.json versions.json src/releaseNotes.ts docs/releases.md PRIVACY.md",
|
||||
"docs:sync": "node scripts/update-release-index.mjs && node scripts/sync-privacy.mjs",
|
||||
"docs:check": "node scripts/update-release-index.mjs --check && node scripts/sync-privacy.mjs --check && node scripts/check-docs.mjs",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
|
|
|
|||
141
scripts/check-docs.mjs
Normal file
141
scripts/check-docs.mjs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const docsDir = path.join(rootDir, 'docs');
|
||||
const mkdocsPath = path.join(rootDir, 'mkdocs.yml');
|
||||
const readmePath = path.join(rootDir, 'README.md');
|
||||
const manifestPath = path.join(rootDir, 'manifest.json');
|
||||
const releaseIndexPath = path.join(docsDir, 'releases.md');
|
||||
const privacyPath = path.join(docsDir, 'privacy.md');
|
||||
const rootPrivacyPath = path.join(rootDir, 'PRIVACY.md');
|
||||
|
||||
const errors = [];
|
||||
|
||||
function normalizeLink(link) {
|
||||
return decodeURIComponent(link.split('#')[0].split('?')[0]).trim();
|
||||
}
|
||||
|
||||
function stripMarkdownCodeBlocks(content) {
|
||||
return content
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
.replace(/`[^`\n]+`/g, '');
|
||||
}
|
||||
|
||||
function markdownFiles() {
|
||||
const files = [readmePath];
|
||||
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(docsDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
function verifyMkdocsNavTargetsExist() {
|
||||
const config = fs.readFileSync(mkdocsPath, 'utf8');
|
||||
const matches = [...config.matchAll(/:\s*([A-Za-z0-9_./-]+\.md)\s*$/gm)];
|
||||
|
||||
for (const match of matches) {
|
||||
const relative = match[1];
|
||||
const full = path.join(docsDir, relative);
|
||||
if (!fs.existsSync(full)) {
|
||||
errors.push(`mkdocs nav references missing file: docs/${relative}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.includes('calendar-setup.md')) {
|
||||
errors.push('mkdocs nav is missing docs/calendar-setup.md');
|
||||
}
|
||||
}
|
||||
|
||||
function verifyCanonicalDocsUrl() {
|
||||
const readme = fs.readFileSync(readmePath, 'utf8');
|
||||
if (!readme.includes('https://tasknotes.dev/')) {
|
||||
errors.push('README.md is missing canonical docs URL: https://tasknotes.dev/');
|
||||
}
|
||||
}
|
||||
|
||||
function verifyReleaseIndexMatchesManifest() {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const releaseIndex = fs.readFileSync(releaseIndexPath, 'utf8');
|
||||
const releaseFile = path.join(docsDir, 'releases', `${manifest.version}.md`);
|
||||
|
||||
if (!fs.existsSync(releaseFile)) {
|
||||
errors.push(`Missing release file for manifest version: docs/releases/${manifest.version}.md`);
|
||||
}
|
||||
|
||||
if (!releaseIndex.includes(`releases/${manifest.version}.md`)) {
|
||||
errors.push(`docs/releases.md does not include current manifest version ${manifest.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyPrivacyMirror() {
|
||||
const docsPrivacy = fs.readFileSync(privacyPath, 'utf8').trimEnd();
|
||||
const rootPrivacy = fs.readFileSync(rootPrivacyPath, 'utf8').trimEnd();
|
||||
|
||||
if (docsPrivacy !== rootPrivacy) {
|
||||
errors.push('PRIVACY.md is out of sync with docs/privacy.md');
|
||||
}
|
||||
}
|
||||
|
||||
function verifyLocalMarkdownLinks() {
|
||||
const files = markdownFiles();
|
||||
const mdLinkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.includes(`${path.sep}docs${path.sep}releases${path.sep}`)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const directory = path.dirname(file);
|
||||
const content = stripMarkdownCodeBlocks(fs.readFileSync(file, 'utf8'));
|
||||
|
||||
for (const match of content.matchAll(mdLinkPattern)) {
|
||||
const raw = match[1].replace(/^<|>$/g, '').trim();
|
||||
if (!raw) continue;
|
||||
if (raw.startsWith('#')) continue;
|
||||
if (raw.startsWith('http://') || raw.startsWith('https://')) continue;
|
||||
if (raw.startsWith('mailto:')) continue;
|
||||
if (raw.startsWith('data:')) continue;
|
||||
|
||||
const normalized = normalizeLink(raw);
|
||||
if (!normalized) continue;
|
||||
|
||||
const resolved = normalized.startsWith('/')
|
||||
? path.join(rootDir, normalized.slice(1))
|
||||
: path.resolve(directory, normalized);
|
||||
|
||||
if (!fs.existsSync(resolved)) {
|
||||
const relativeFile = path.relative(rootDir, file);
|
||||
errors.push(`Broken local link in ${relativeFile}: ${raw}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifyMkdocsNavTargetsExist();
|
||||
verifyCanonicalDocsUrl();
|
||||
verifyReleaseIndexMatchesManifest();
|
||||
verifyPrivacyMirror();
|
||||
verifyLocalMarkdownLinks();
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('Documentation checks failed:');
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Documentation checks passed.');
|
||||
24
scripts/sync-privacy.mjs
Normal file
24
scripts/sync-privacy.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const sourcePath = path.join(rootDir, 'docs', 'privacy.md');
|
||||
const targetPath = path.join(rootDir, 'PRIVACY.md');
|
||||
const checkMode = process.argv.includes('--check');
|
||||
|
||||
const source = fs.readFileSync(sourcePath, 'utf8').trimEnd();
|
||||
const output = `${source}\n`;
|
||||
|
||||
if (checkMode) {
|
||||
const current = fs.readFileSync(targetPath, 'utf8');
|
||||
if (current !== output) {
|
||||
console.error('PRIVACY.md is out of sync with docs/privacy.md. Run: npm run docs:sync');
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetPath, output, 'utf8');
|
||||
console.log('Synced PRIVACY.md from docs/privacy.md');
|
||||
153
scripts/update-release-index.mjs
Normal file
153
scripts/update-release-index.mjs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const releasesDir = path.join(rootDir, 'docs', 'releases');
|
||||
const outputPath = path.join(rootDir, 'docs', 'releases.md');
|
||||
const manifestPath = path.join(rootDir, 'manifest.json');
|
||||
const checkMode = process.argv.includes('--check');
|
||||
|
||||
function parseVersion(filename) {
|
||||
const stem = filename.replace(/\.md$/, '');
|
||||
const match = stem.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
raw: stem,
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
prerelease: match[4] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function compareIdentifiers(a, b) {
|
||||
const aNum = /^\d+$/.test(a) ? Number(a) : NaN;
|
||||
const bNum = /^\d+$/.test(b) ? Number(b) : NaN;
|
||||
|
||||
if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) {
|
||||
return aNum - bNum;
|
||||
}
|
||||
if (!Number.isNaN(aNum)) {
|
||||
return -1;
|
||||
}
|
||||
if (!Number.isNaN(bNum)) {
|
||||
return 1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
function comparePrerelease(a, b) {
|
||||
if (a === null && b === null) return 0;
|
||||
if (a === null) return 1;
|
||||
if (b === null) return -1;
|
||||
|
||||
const aParts = a.split('.');
|
||||
const bParts = b.split('.');
|
||||
const max = Math.max(aParts.length, bParts.length);
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const aPart = aParts[i];
|
||||
const bPart = bParts[i];
|
||||
|
||||
if (aPart === undefined) return -1;
|
||||
if (bPart === undefined) return 1;
|
||||
|
||||
const cmp = compareIdentifiers(aPart, bPart);
|
||||
if (cmp !== 0) {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortVersionsDesc(a, b) {
|
||||
if (a.major !== b.major) return b.major - a.major;
|
||||
if (a.minor !== b.minor) return b.minor - a.minor;
|
||||
if (a.patch !== b.patch) return b.patch - a.patch;
|
||||
return -comparePrerelease(a.prerelease, b.prerelease);
|
||||
}
|
||||
|
||||
function buildReleaseIndex(currentMajor, versions) {
|
||||
const groups = new Map();
|
||||
for (const version of versions) {
|
||||
const list = groups.get(version.major) ?? [];
|
||||
list.push(version);
|
||||
groups.set(version.major, list);
|
||||
}
|
||||
|
||||
const majors = [...groups.keys()].sort((a, b) => b - a);
|
||||
const lines = [];
|
||||
|
||||
lines.push('# Release Notes');
|
||||
lines.push('');
|
||||
lines.push('Welcome to the TaskNotes release notes. Here you can find detailed information about each version, including new features, bug fixes, and improvements.');
|
||||
lines.push('');
|
||||
lines.push('## Latest Releases');
|
||||
lines.push('');
|
||||
|
||||
for (const major of majors) {
|
||||
const title = major === 0
|
||||
? '### Early Versions (0.x)'
|
||||
: major === currentMajor
|
||||
? `### Version ${major}.x (Current)`
|
||||
: `### Version ${major}.x`;
|
||||
|
||||
lines.push(title);
|
||||
lines.push('');
|
||||
|
||||
const items = (groups.get(major) ?? []).sort(sortVersionsDesc);
|
||||
for (const item of items) {
|
||||
lines.push(`- [${item.raw}](releases/${item.raw}.md)`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('## Getting Updates');
|
||||
lines.push('');
|
||||
lines.push('To update TaskNotes:');
|
||||
lines.push('1. Open Obsidian');
|
||||
lines.push('2. Go to Settings → Community Plugins');
|
||||
lines.push('3. Find TaskNotes and click "Update"');
|
||||
lines.push('4. Restart Obsidian if prompted');
|
||||
lines.push('');
|
||||
lines.push('## Feedback');
|
||||
lines.push('');
|
||||
lines.push('Found a bug or have a feature request? Please:');
|
||||
lines.push('');
|
||||
lines.push('- Check existing [GitHub Issues](https://github.com/callumalpass/tasknotes/issues)');
|
||||
lines.push('- Create a new issue with details');
|
||||
lines.push('');
|
||||
|
||||
return `${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const currentMajor = Number(String(manifest.version).split('.')[0]);
|
||||
|
||||
const versions = fs
|
||||
.readdirSync(releasesDir)
|
||||
.filter((name) => name.endsWith('.md') && name !== 'unreleased.md')
|
||||
.map(parseVersion)
|
||||
.filter((value) => value !== null)
|
||||
.sort(sortVersionsDesc);
|
||||
|
||||
const output = buildReleaseIndex(currentMajor, versions);
|
||||
|
||||
if (checkMode) {
|
||||
const current = fs.readFileSync(outputPath, 'utf8');
|
||||
if (current !== output) {
|
||||
console.error('docs/releases.md is out of date. Run: npm run docs:sync');
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, output, 'utf8');
|
||||
console.log('Updated docs/releases.md');
|
||||
Loading…
Reference in a new issue