From 89f41dea53f2cb954bc9ca07fe9983e98ed5fb7b Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:29:40 -0500 Subject: [PATCH] ci: fix Android docs sync (remove stray workflow, prune stale pages) (#5955) Co-authored-by: Claude Opus 4.8 --- .github/workflows/sync-android-docs.yml | 53 ---------- scripts/sync-android-docs.js | 131 ++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 60 deletions(-) delete mode 100644 .github/workflows/sync-android-docs.yml diff --git a/.github/workflows/sync-android-docs.yml b/.github/workflows/sync-android-docs.yml deleted file mode 100644 index a01a12307..000000000 --- a/.github/workflows/sync-android-docs.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Sync Android App Documentation - -on: - schedule: - - cron: "0 0 * * 0" # Every Sunday at midnight UTC - workflow_dispatch: - -jobs: - sync: - runs-on: ubuntu-24.04 - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout meshtastic/meshtastic - uses: actions/checkout@v7.0.0 - with: - fetch-depth: 1 - - - name: Clone meshtastic/Meshtastic-Android - run: | - git clone --depth=1 --branch main https://github.com/meshtastic/Meshtastic-Android.git /tmp/Meshtastic-Android - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version-file: ".nvmrc" - - - name: Install cwebp - run: sudo apt-get update && sudo apt-get install -y webp - - - name: Run sync script - run: node /tmp/Meshtastic-Android/scripts/sync-android-docs.js /tmp/Meshtastic-Android --convert-webp - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "docs: sync latest Android app documentation" - branch: sync/android-docs - delete-branch: true - title: "docs: sync latest Android app documentation" - body: | - This PR was automatically created by the [sync-android-docs](../.github/workflows/sync-android-docs.yml) workflow. - - It synchronizes Markdown documentation and images from [meshtastic/Meshtastic-Android](https://github.com/meshtastic/Meshtastic-Android) into this repository: - - - Markdown files → `docs/software/android/` - - Image files → `static/img/android/docs/` - - Image paths in Markdown are rewritten to use the Docusaurus `/img/android/docs/` static path. - Screenshots are converted to WebP for optimal site performance. diff --git a/scripts/sync-android-docs.js b/scripts/sync-android-docs.js index 8281a5db6..b221a8eba 100755 --- a/scripts/sync-android-docs.js +++ b/scripts/sync-android-docs.js @@ -120,7 +120,7 @@ function rewriteImagePaths(content) { * e.g., `[text](connections)` → `[text](connections.md)` * e.g., `[text](../developer/testing)` → `[text](../developer/testing.md)` */ -function rewriteSiblingLinks(content, section) { +function rewriteSiblingLinks(content, section, isIndex = false) { const slugs = section === "user" ? KNOWN_USER_SLUGS : KNOWN_DEV_SLUGS; // Match [text](link) where link is NOT an absolute URL, NOT an anchor, NOT already .md @@ -132,6 +132,16 @@ function rewriteSiblingLinks(content, section) { return match; } + // Section landing page (user/index.md, developer/index.md): the source + // user.md/developer.md sit beside the section dir, so they link to children + // as "user/onboarding". From index.md inside that dir, strip the prefix. + if (isIndex) { + const sameSection = link.match(new RegExp(`^${section}/(.+)`)); + if (sameSection && slugs.has(sameSection[1])) { + return `[${text}](${sameSection[1]}.md)`; + } + } + // Check for cross-section links like ../developer/testing const crossMatch = link.match(/^\.\.\/(\w+)\/(.+)/); if (crossMatch) { @@ -213,19 +223,21 @@ function convertCallouts(content) { // ── Main ───────────────────────────────────────────────────────────────────── -function processMarkdown(srcPath, destPath, section) { +function processMarkdown(srcPath, destPath, section, isIndex = false) { let content = fs.readFileSync(srcPath, "utf-8"); content = transformFrontmatter(content, section); content = rewriteImagePaths(content); - content = rewriteSiblingLinks(content, section); + content = rewriteSiblingLinks(content, section, isIndex); content = convertCallouts(content); writeFile(destPath, content); } +/** Sync screenshots, returning the set of destination basenames written. */ function processImages() { + const written = new Set(); if (!fs.existsSync(SRC_SCREENSHOTS_DIR)) { console.log("No screenshots directory found, skipping image sync."); - return; + return written; } const images = fs.readdirSync(SRC_SCREENSHOTS_DIR) @@ -241,19 +253,70 @@ function processImages() { if (DRY_RUN) { console.log(`[dry-run] Would convert: ${srcPath} → ${destPath}`); + written.add(destName); } else { ensureDir(path.dirname(destPath)); try { execSync(`cwebp -q 80 "${srcPath}" -o "${destPath}"`, { stdio: "pipe" }); console.log(`Converted: ${destPath}`); + written.add(destName); } catch (err) { console.error(`Failed to convert ${img}: ${err.message}`); // Fall back to copying the original copyFile(srcPath, path.join(DEST_IMAGES_DIR, img)); + written.add(img); } } } else { copyFile(srcPath, path.join(DEST_IMAGES_DIR, img)); + written.add(img); + } + } + return written; +} + +/** Recursively collect files under a directory, as paths relative to it. */ +function collectFiles(dir) { + const results = []; + if (!fs.existsSync(dir)) return results; + (function walk(current) { + for (const entry of fs.readdirSync(current)) { + const full = path.join(current, entry); + if (fs.statSync(full).isDirectory()) walk(full); + else results.push(path.relative(dir, full)); + } + })(dir); + return results; +} + +/** + * Remove destination files this run did not produce, so renamed or deleted + * source pages (and the screenshots they referenced) don't linger on the site. + * Mirrors the Apple sync's cleanup pass. Scoped to docs pages (.md/.mdx) and + * images under the directories this script owns. + */ +function pruneStale(expectedDocPaths, expectedImageNames) { + for (const file of collectFiles(DEST_DOCS_DIR)) { + const ext = path.extname(file).toLowerCase(); + if (ext !== ".md" && ext !== ".mdx") continue; + if (expectedDocPaths.has(file.split(path.sep).join("/"))) continue; + const target = path.join(DEST_DOCS_DIR, file); + if (DRY_RUN) { + console.log(`[dry-run] Would remove stale page: ${target}`); + } else { + fs.unlinkSync(target); + console.log(`Removed stale page: ${target}`); + } + } + for (const file of collectFiles(DEST_IMAGES_DIR)) { + if (!IMAGE_EXTENSIONS.has(path.extname(file).toLowerCase())) continue; + if (expectedImageNames.has(path.basename(file))) continue; + const target = path.join(DEST_IMAGES_DIR, file); + if (DRY_RUN) { + console.log(`[dry-run] Would remove stale image: ${target}`); + } else { + fs.unlinkSync(target); + console.log(`Removed stale image: ${target}`); } } } @@ -277,12 +340,37 @@ Documentation for the [Meshtastic Android](https://github.com/meshtastic/Meshtas } function createCategoryFiles() { - const userCategory = `label: User Guide + // Top-level section category. Uses the synced index.md as the landing page + // (matching the Apple sync), replacing any hand-written generated-index that + // would otherwise collide with index.md. + // Unique `key` per category: the "User Guide" / "Developer Guide" labels also + // exist in the Apple section, and Docusaurus derives sidebar translation keys + // from the label, so without a key the two sections collide at build time. + const sectionCategory = `key: androidApp +label: Android App +collapsible: true position: 1 +link: + type: doc + id: software/android/index `; - const devCategory = `label: Developer Guide + const userCategory = `key: androidUserGuide +label: User Guide +collapsible: true +position: 1 +link: + type: doc + id: software/android/user/index +`; + const devCategory = `key: androidDeveloperGuide +label: Developer Guide +collapsible: true position: 2 +link: + type: doc + id: software/android/developer/index `; + writeFile(path.join(DEST_DOCS_DIR, "_category_.yml"), sectionCategory); writeFile(path.join(DEST_DOCS_DIR, "user", "_category_.yml"), userCategory); writeFile(path.join(DEST_DOCS_DIR, "developer", "_category_.yml"), devCategory); } @@ -294,6 +382,30 @@ function main() { console.log(`Dry run: ${DRY_RUN}`); console.log(""); + // Track everything this run produces so stale files can be pruned afterwards. + const expectedDocPaths = new Set([ + "index.md", + "_category_.yml", + "user/_category_.yml", + "developer/_category_.yml", + ]); + + // Section overview pages: docs/en/user.md → user/index.md and + // docs/en/developer.md → developer/index.md. Docusaurus uses /index.md + // as the category landing page, so the index.md "User Guide" / "Developer + // Guide" links resolve (matching the Apple sync). + const sections = [ + { src: "user.md", dest: ["user", "index.md"], section: "user", key: "user/index.md" }, + { src: "developer.md", dest: ["developer", "index.md"], section: "developer", key: "developer/index.md" }, + ]; + for (const { src, dest, section, key } of sections) { + const overview = path.join(SRC_DOCS_DIR, src); + if (fs.existsSync(overview)) { + processMarkdown(overview, path.join(DEST_DOCS_DIR, ...dest), section, true); + expectedDocPaths.add(key); + } + } + // Process user guide const userDir = path.join(SRC_DOCS_DIR, "user"); if (fs.existsSync(userDir)) { @@ -303,6 +415,7 @@ function main() { path.join(DEST_DOCS_DIR, "user", file), "user", ); + expectedDocPaths.add(`user/${file}`); } } @@ -315,6 +428,7 @@ function main() { path.join(DEST_DOCS_DIR, "developer", file), "developer", ); + expectedDocPaths.add(`developer/${file}`); } } @@ -323,7 +437,10 @@ function main() { createCategoryFiles(); // Process images - processImages(); + const expectedImageNames = processImages(); + + // Remove anything left over from a previous sync or the old hand-written docs. + pruneStale(expectedDocPaths, expectedImageNames); console.log("\nSync complete."); }