From 852e40c534ef1315fa6b168852ed245179ebfd14 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Sun, 26 Jul 2026 18:32:11 +0530 Subject: [PATCH] feat: Create MultiSrc Draft Pull Requests --- .github/scripts/add-multisrc-source.cjs | 172 ++++++++++++++++++++++++ .github/workflows/theme_auto_label.yml | 68 +++++++++- 2 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/add-multisrc-source.cjs diff --git a/.github/scripts/add-multisrc-source.cjs b/.github/scripts/add-multisrc-source.cjs new file mode 100644 index 000000000..b141981f7 --- /dev/null +++ b/.github/scripts/add-multisrc-source.cjs @@ -0,0 +1,172 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const themeDirectories = { + madara: 'madara', + lightnovelwp: 'lightnovelwp', + hotnovelpub: 'hotnovelpub', + fictioneer: 'fictioneer', +}; + +function issueField(body, label) { + const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = body.match( + new RegExp(`(?:^|\\n)### ${escapedLabel}\\s*\\n+([^\\n]+)`, 'i'), + ); + const value = match?.[1]?.trim(); + return value && value !== '_No response_' ? value : ''; +} + +function sourceId(name, hostname) { + const id = name + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, ''); + + return ( + id || + hostname + .replace(/^www\./, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '') + ); +} + +function canonicalLanguage(language) { + const directory = path.join( + process.cwd(), + 'plugins', + language.trim().toLowerCase(), + ); + if (!language || !fs.existsSync(directory)) { + throw new Error(`Unsupported language from issue: "${language}"`); + } + + return ( + language.trim().charAt(0).toUpperCase() + + language.trim().slice(1).toLowerCase() + ); +} + +function hotNovelPubLanguage(language) { + const codes = { + English: 'en', + Russian: 'ru', + Spanish: 'es', + Portuguese: 'pt', + Turkish: 'th', + }; + const code = codes[language]; + if (!code) { + throw new Error( + `HotNovelPub does not map the requested language "${language}"`, + ); + } + return code; +} + +function writeOutput(key, value) { + if (!process.env.GITHUB_OUTPUT) return; + fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`); +} + +const body = process.env.ISSUE_BODY || ''; +const theme = (process.env.THEME || '').toLowerCase(); +const themeDirectory = themeDirectories[theme]; + +if (!themeDirectory) { + throw new Error(`Unsupported detected theme: "${theme}"`); +} + +const submittedUrl = issueField(body, 'Website URL'); +const name = issueField(body, 'Plugin Name'); +const language = canonicalLanguage(issueField(body, 'Language')); + +if (!submittedUrl || !name) { + throw new Error('The issue is missing a Website URL or Plugin Name'); +} +if (name.length > 100 || /[\/\\\0\r\n]/.test(name)) { + throw new Error('The Plugin Name contains unsupported characters'); +} + +let url; +try { + url = new URL(submittedUrl); +} catch { + throw new Error(`Invalid Website URL from issue: "${submittedUrl}"`); +} + +if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error(`Unsupported Website URL protocol: "${url.protocol}"`); +} + +const sourceSite = url.origin; +const id = sourceId(name, url.hostname); +const sourcesPath = path.join( + process.cwd(), + 'plugins', + 'multisrc', + themeDirectory, + 'sources.json', +); +const sourcesText = fs.readFileSync(sourcesPath, 'utf8'); +const sources = JSON.parse(sourcesText); + +const duplicate = sources.find(source => { + try { + return ( + source.id.toLowerCase() === id.toLowerCase() || + new URL(source.sourceSite).hostname + .replace(/^www\./, '') + .toLowerCase() === url.hostname.replace(/^www\./, '').toLowerCase() + ); + } catch { + return source.id.toLowerCase() === id.toLowerCase(); + } +}); + +if (duplicate) { + console.log( + `Skipping ${sourceSite}: it already matches source "${duplicate.id}"`, + ); + writeOutput('changed', 'false'); + writeOutput('source_name', name.replace(/[\r\n]/g, ' ')); + process.exit(0); +} + +const source = { + id, + sourceSite, + sourceName: name, +}; + +if (theme === 'fictioneer') { + source.options = { + browsePage: 'browse', + ...(language === 'English' ? {} : { lang: language }), + }; +} else if (theme === 'hotnovelpub') { + const lang = hotNovelPubLanguage(language); + if (lang !== 'en') source.options = { lang }; +} else if (language !== 'English') { + source.options = { lang: language }; +} + +const serializedSource = JSON.stringify(source, null, 2) + .split('\n') + .map(line => ` ${line}`) + .join('\n'); +const updatedSources = sourcesText.replace( + /\n\]\s*$/, + `,\n${serializedSource}\n]\n`, +); +if (updatedSources === sourcesText) { + throw new Error(`Could not append a source to ${sourcesPath}`); +} +fs.writeFileSync(sourcesPath, updatedSources); + +console.log(`Added ${id} to ${path.relative(process.cwd(), sourcesPath)}`); +writeOutput('changed', 'true'); +writeOutput('source_name', name.replace(/[\r\n]/g, ' ')); +writeOutput('source_id', id); diff --git a/.github/workflows/theme_auto_label.yml b/.github/workflows/theme_auto_label.yml index a2cf74bad..3945f7725 100644 --- a/.github/workflows/theme_auto_label.yml +++ b/.github/workflows/theme_auto_label.yml @@ -4,8 +4,7 @@ on: issues: types: [labeled] -permissions: - issues: write +permissions: {} concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} @@ -16,6 +15,10 @@ jobs: name: Auto Label Theme if: contains(github.event.issue.labels.*.name, 'Plugin Request') runs-on: ubuntu-latest + permissions: + issues: write + outputs: + theme: ${{ steps.add_theme_label.outputs.theme }} env: REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }} @@ -112,6 +115,7 @@ jobs: fi - name: Add Theme Label To Issue + id: add_theme_label if: always() env: MADARA: ${{ steps.check_madara.outputs.madara }} @@ -124,15 +128,75 @@ jobs: if [[ "$MADARA" == "true" ]]; then echo "🏷️ Adding label: Theme: Madara" >> $GITHUB_STEP_SUMMARY gh issue edit $ISSUE_NUMBER --add-label "Theme: Madara" -R $REPO + echo "theme=madara" >> "$GITHUB_OUTPUT" elif [[ "$LIGHNOVELWP" == "true" ]]; then echo "🏷️ Adding label: Theme: Lighnovelwp" >> $GITHUB_STEP_SUMMARY gh issue edit $ISSUE_NUMBER --add-label "Theme: Lighnovelwp" -R $REPO + echo "theme=lightnovelwp" >> "$GITHUB_OUTPUT" elif [[ "$HOTNOVELPUB" == "true" ]]; then echo "🏷️ Adding label: Theme: Hotnovelpub" >> $GITHUB_STEP_SUMMARY gh issue edit $ISSUE_NUMBER --add-label "Theme: Hotnovelpub" -R $REPO + echo "theme=hotnovelpub" >> "$GITHUB_OUTPUT" elif [[ "$FICTIONEER" == "true" ]]; then echo "🏷️ Adding label: Theme: Fictioneer" >> $GITHUB_STEP_SUMMARY gh issue edit $ISSUE_NUMBER --add-label "Theme: Fictioneer" -R $REPO + echo "theme=fictioneer" >> "$GITHUB_OUTPUT" else echo "❌ No Theme Detected" >> $GITHUB_STEP_SUMMARY fi + + create-source-pr: + name: Create MultiSrc Draft PR + needs: auto-label + if: needs.auto-label.outputs.theme != '' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: master + + - name: Add Source Configuration + id: add_source + env: + ISSUE_BODY: ${{ github.event.issue.body }} + THEME: ${{ needs.auto-label.outputs.theme }} + run: node .github/scripts/add-multisrc-source.cjs + + - name: Setup Node.js + if: steps.add_source.outputs.changed == 'true' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + cache: npm + + - name: Install Dependencies + if: steps.add_source.outputs.changed == 'true' + run: npm ci --omit=dev --ignore-scripts + + - name: Validate Source Configuration + if: steps.add_source.outputs.changed == 'true' + run: | + node -e "JSON.parse(require('fs').readFileSync('plugins/multisrc/${{ needs.auto-label.outputs.theme }}/sources.json', 'utf8'))" + npm run build:multisrc + + - name: Create Draft Pull Request + if: steps.add_source.outputs.changed == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + commit-message: 'feat: Add ${{ steps.add_source.outputs.source_name }} MultiSrc Source' + branch: 'automation/multisrc-issue-${{ github.event.issue.number }}' + title: 'feat: Add ${{ steps.add_source.outputs.source_name }} MultiSrc Source' + body: | + Adds `${{ steps.add_source.outputs.source_name }}` to the `${{ needs.auto-label.outputs.theme }}` multi-source configuration. + + Closes #${{ github.event.issue.number }} + + This draft was generated from the theme detected for the plugin request. Before marking it ready, verify theme-specific options such as chapter ordering, browse paths, filters, custom parsing, and language. + labels: 'bot' + base: 'master' + delete-branch: true + draft: true