Skip to content
Draft
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
172 changes: 172 additions & 0 deletions .github/scripts/add-multisrc-source.cjs
Original file line number Diff line number Diff line change
@@ -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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚫 [eslint] <no-useless-escape> reported by reviewdog 🐶
Unnecessary escape character: /.

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);
68 changes: 66 additions & 2 deletions .github/workflows/theme_auto_label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ on:
issues:
types: [labeled]

permissions:
issues: write
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
Expand All @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand All @@ -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
Loading