feat: Create MultiSrc Draft Pull Requests (#2354)
This commit is contained in:
committed by
GitHub
parent
2901a7232b
commit
e1e369c495
@@ -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);
|
||||||
@@ -4,8 +4,7 @@ on:
|
|||||||
issues:
|
issues:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
issues: write
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||||
@@ -16,6 +15,10 @@ jobs:
|
|||||||
name: Auto Label Theme
|
name: Auto Label Theme
|
||||||
if: contains(github.event.issue.labels.*.name, 'Plugin Request')
|
if: contains(github.event.issue.labels.*.name, 'Plugin Request')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
outputs:
|
||||||
|
theme: ${{ steps.add_theme_label.outputs.theme }}
|
||||||
env:
|
env:
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
@@ -112,6 +115,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Add Theme Label To Issue
|
- name: Add Theme Label To Issue
|
||||||
|
id: add_theme_label
|
||||||
if: always()
|
if: always()
|
||||||
env:
|
env:
|
||||||
MADARA: ${{ steps.check_madara.outputs.madara }}
|
MADARA: ${{ steps.check_madara.outputs.madara }}
|
||||||
@@ -124,15 +128,75 @@ jobs:
|
|||||||
if [[ "$MADARA" == "true" ]]; then
|
if [[ "$MADARA" == "true" ]]; then
|
||||||
echo "🏷️ Adding label: Theme: Madara" >> $GITHUB_STEP_SUMMARY
|
echo "🏷️ Adding label: Theme: Madara" >> $GITHUB_STEP_SUMMARY
|
||||||
gh issue edit $ISSUE_NUMBER --add-label "Theme: Madara" -R $REPO
|
gh issue edit $ISSUE_NUMBER --add-label "Theme: Madara" -R $REPO
|
||||||
|
echo "theme=madara" >> "$GITHUB_OUTPUT"
|
||||||
elif [[ "$LIGHNOVELWP" == "true" ]]; then
|
elif [[ "$LIGHNOVELWP" == "true" ]]; then
|
||||||
echo "🏷️ Adding label: Theme: Lighnovelwp" >> $GITHUB_STEP_SUMMARY
|
echo "🏷️ Adding label: Theme: Lighnovelwp" >> $GITHUB_STEP_SUMMARY
|
||||||
gh issue edit $ISSUE_NUMBER --add-label "Theme: Lighnovelwp" -R $REPO
|
gh issue edit $ISSUE_NUMBER --add-label "Theme: Lighnovelwp" -R $REPO
|
||||||
|
echo "theme=lightnovelwp" >> "$GITHUB_OUTPUT"
|
||||||
elif [[ "$HOTNOVELPUB" == "true" ]]; then
|
elif [[ "$HOTNOVELPUB" == "true" ]]; then
|
||||||
echo "🏷️ Adding label: Theme: Hotnovelpub" >> $GITHUB_STEP_SUMMARY
|
echo "🏷️ Adding label: Theme: Hotnovelpub" >> $GITHUB_STEP_SUMMARY
|
||||||
gh issue edit $ISSUE_NUMBER --add-label "Theme: Hotnovelpub" -R $REPO
|
gh issue edit $ISSUE_NUMBER --add-label "Theme: Hotnovelpub" -R $REPO
|
||||||
|
echo "theme=hotnovelpub" >> "$GITHUB_OUTPUT"
|
||||||
elif [[ "$FICTIONEER" == "true" ]]; then
|
elif [[ "$FICTIONEER" == "true" ]]; then
|
||||||
echo "🏷️ Adding label: Theme: Fictioneer" >> $GITHUB_STEP_SUMMARY
|
echo "🏷️ Adding label: Theme: Fictioneer" >> $GITHUB_STEP_SUMMARY
|
||||||
gh issue edit $ISSUE_NUMBER --add-label "Theme: Fictioneer" -R $REPO
|
gh issue edit $ISSUE_NUMBER --add-label "Theme: Fictioneer" -R $REPO
|
||||||
|
echo "theme=fictioneer" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "❌ No Theme Detected" >> $GITHUB_STEP_SUMMARY
|
echo "❌ No Theme Detected" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user