diff --git a/.github/workflows/validate-vendor.yml b/.github/workflows/validate-vendor.yml index ac4fa1c..6ce5fe3 100644 --- a/.github/workflows/validate-vendor.yml +++ b/.github/workflows/validate-vendor.yml @@ -2,11 +2,14 @@ name: ✅ Validate Vendor PR on: pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review] paths: - "vendors/**" - - "vendors/_schema.json" - "logos/**" - - "scripts/validate-all.mjs" + - "scripts/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/validate-vendor.yml" permissions: contents: read @@ -30,3 +33,6 @@ jobs: - name: Run repository validator run: npm run validate + + - name: Run PR metadata validator + run: npm run validate:pr diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 43bb898..d23e42b 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -102,6 +102,7 @@ Both trigger the sync on merge. Both are transparent — the change is visible i ```bash npm install -npm run validate # validate all vendor files locally -npm run sync # manual sync (requires FRAMER_PROJECT_URL and FRAMER_API_KEY exported in your shell) +npm run validate # validate all vendor files locally +npm run validate:pr # validates PR title/body in GitHub Actions; skips locally without GITHUB_EVENT_PATH +npm run sync # manual sync (requires FRAMER_PROJECT_URL and FRAMER_API_KEY exported in your shell) ``` diff --git a/logos/ustarminer.png b/logos/ustarminer.png new file mode 100644 index 0000000..d4881b2 Binary files /dev/null and b/logos/ustarminer.png differ diff --git a/package.json b/package.json index 3dc1d63..42bde01 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "sync": "node scripts/sync-to-framer.mjs", - "validate": "node scripts/validate-all.mjs" + "validate": "node scripts/validate-all.mjs", + "validate:pr": "node scripts/validate-pr.mjs" }, "dependencies": { "framer-api": "0.1.1" diff --git a/scripts/validate-pr.mjs b/scripts/validate-pr.mjs new file mode 100644 index 0000000..12df9de --- /dev/null +++ b/scripts/validate-pr.mjs @@ -0,0 +1,93 @@ +/** + * validate-pr.mjs + * + * Validates PR title/body hygiene using the GitHub pull_request event payload. + * Run in CI with: npm run validate:pr + */ + +import fs from "node:fs" + +const eventPath = process.env.GITHUB_EVENT_PATH + +if (!eventPath) { + console.log("Skipping PR metadata validation: GITHUB_EVENT_PATH is not set") + process.exit(0) +} + +let payload +try { + payload = JSON.parse(fs.readFileSync(eventPath, "utf-8").replace(/^\uFEFF/, "")) +} catch (err) { + console.error(`Failed to read GitHub event payload: ${err.message}`) + process.exit(1) +} + +const pr = payload.pull_request +if (!pr) { + console.log("Skipping PR metadata validation: event is not a pull_request") + process.exit(0) +} + +const title = pr.title.trim() +const body = pr.body || "" +const errors = [] +const titleMatch = title.match(/^(Add vendor|Update vendor|Remove vendor|Deactivate vendor): .+/) +const prType = titleMatch?.[1] + +if (!titleMatch) { + errors.push("PR title must start with one of: 'Add vendor:', 'Update vendor:', 'Remove vendor:', or 'Deactivate vendor:'") +} + +const placeholders = [ + "Your shop name", + "https://yourshop.com", + "Your X, Discord, Instagram, TikTok, Nostr, or other public link", +] + +for (const placeholder of placeholders) { + if (body.includes(placeholder)) { + errors.push(`Replace PR template placeholder: ${placeholder}`) + } +} + +if (!/\|\s*\*\*Shop name\*\*\s*\|\s*\S/.test(body)) { + errors.push("PR body must include a completed Shop name row") +} + +if (!/\|\s*\*\*Website\*\*\s*\|\s*https:\/\/\S+/.test(body)) { + errors.push("PR body must include a completed HTTPS Website row") +} + +if (!/\|\s*\*\*Where to find you in the community\*\*\s*\|\s*\S/.test(body)) { + errors.push("PR body must include a completed community contact row") +} + +const confirmationRows = [ + "I sell genuine Bitaxe hardware", + "My shop is live and active", + "I am reachable in the community", + "I use the correct name `Bitaxe` on my Bitaxe product pages", + "My Bitaxe product pages use real product photos", + "My Bitaxe product pages link to `https://bitaxe.org`", + "I do not sell Bitaxe-derived products in ways that violate Bitaxe open-source license terms", + "I am not aware of open fraud or counterfeit reports about my shop", + "I understand listings can be removed if confirmed issues arise", +] + +if (prType === "Add vendor" || prType === "Update vendor") { + for (const label of confirmationRows) { + const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + const rowPattern = new RegExp(`\\|\\s*${escapedLabel}\\s*\\|\\s*(Yes|No)\\s*\\|`, "i") + if (!rowPattern.test(body)) { + errors.push(`Vendor confirmation must be answered Yes/No: ${label}`) + } + } +} + +if (errors.length > 0) { + console.error("PR metadata validation failed:") + for (const error of errors) console.error(`- ${error}`) + process.exit(1) +} + +console.log("PR metadata is valid") diff --git a/vendors/ustarminer.json b/vendors/ustarminer.json new file mode 100644 index 0000000..2d62718 --- /dev/null +++ b/vendors/ustarminer.json @@ -0,0 +1,16 @@ +{ + "name": "Ustarminer", + "slug": "ustarminer", + "website": "https://ustarminer.com", + "region": "Asia Pacific", + "country": "Hongkong", + "description": "Professional distributor of genuine Bitaxe solo miners. We are the creators of the Starminer APP, available on Google Play and the Apple App Store—the only specialized companion application of its kind in the world for managing your solo mining experience.", + "active": true, + "social": { + "x": "https://x.com/StarMiner01", + "instagram": "", + "youtube": "https://www.youtube.com/@starminer-s7o", + "tiktok": "", + "nostr": "" + } +}