Skip to content
Merged
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
26 changes: 22 additions & 4 deletions .github/workflows/app-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ jobs:
// Mirrors scripts/build-catalog.mjs — keep in lockstep.
const APP_TYPES = ['mobile', 'web', 'desktop', 'terminal', 'bot', 'integration'];
const APP_IMAGE_HOSTS = ['raw.githubusercontent.com', 'user-images.githubusercontent.com', 'github.com'];
const MAX_PLATFORM_LEN = 32;
const MAX_PLATFORMS = 6;

// --- 1. Parse the issue-form body into a { heading: value } map ---
// GitHub renders each form field as "### <label>\n\n<value>", with
Expand Down Expand Up @@ -98,8 +100,17 @@ jobs:
try { const u = new URL(v); return u.protocol === 'http:' || u.protocol === 'https:'; }
catch { return false; }
};
// github.com/<owner>/<repo>/blob/<ref>/<path> serves an HTML page, not
// the image — it passes the host allowlist but renders as a broken
// image on the site. Rewrite it to the raw host rather than bouncing
// the submission, since it's the link GitHub's UI hands people.
const rawify = (v) => {
const m = v.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/);
return m ? `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3]}` : v;
};
const checkImage = (v, field) => {
if (!v) return '';
v = rawify(v);
let u;
try { u = new URL(v); }
catch { problems.push(`**${field}** is not a valid URL.`); return ''; }
Expand All @@ -125,6 +136,17 @@ jobs:
problems.push(`**Description** must be 280 characters or fewer (yours is ${description.length}).`);
const iconUrl = checkImage(icon, 'Icon URL');
const shotUrl = checkImage(screenshot, 'Screenshot URL');
// Truncating a platform to fit would mangle it ("Progressive Web-App
// (PWA)" -> "...(PWA"), so say so instead and let the submitter edit.
const platforms = platformsRaw
.split(',').map((p) => p.trim()).filter(Boolean).slice(0, MAX_PLATFORMS);
for (const p of platforms) {
if (p.length > MAX_PLATFORM_LEN)
problems.push(
`**Platforms** entry \`${p}\` must be ${MAX_PLATFORM_LEN} characters or fewer ` +
`(it is ${p.length}).`,
);
}

const fail = async (lines) => {
await github.rest.issues.createComment({
Expand All @@ -146,10 +168,6 @@ jobs:
.replace(/^-+|-+$/g, '')
.slice(0, 48) || `app-${issue.number}`;

const platforms = platformsRaw
.split(',').map((p) => p.trim()).filter(Boolean)
.map((p) => p.slice(0, 24)).slice(0, 6);

const app = { name, url, type };
if (description) app.description = description;
if (author) app.author = author.slice(0, 60);
Expand Down
8 changes: 7 additions & 1 deletion scripts/build-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const APP_TYPES = ['mobile', 'web', 'desktop', 'terminal', 'bot', 'skin', 'integ
// web/lib/apps.ts — the web tier renders these through next/image, which refuses
// any host not in remotePatterns.
const APP_IMAGE_HOSTS = ['raw.githubusercontent.com', 'user-images.githubusercontent.com', 'github.com'];
const MAX_PLATFORM_LEN = 32;
const MAX_PLATFORMS = 6;

const errors = [];
const fail = (where, msg) => errors.push(`${where}: ${msg}`);
Expand Down Expand Up @@ -245,6 +247,10 @@ function appImageUrl(v, field, where) {
let u;
try { u = new URL(s); } catch { fail(where, `${field} "${s}" is not a valid URL`); return undefined; }
if (u.protocol !== 'https:') { fail(where, `${field} must be https://`); return undefined; }
// A /blob/ link renders an HTML page, not the image. Normalise it to the raw
// host so a hand-written JSON doesn't ship a broken <img> to the site.
const blob = s.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/);
if (blob) return `https://raw.githubusercontent.com/${blob[1]}/${blob[2]}/${blob[3]}`;
if (!APP_IMAGE_HOSTS.includes(u.host)) {
fail(where, `${field} host "${u.host}" not allowed — use one of ${APP_IMAGE_HOSTS.join(', ')}`);
return undefined;
Expand Down Expand Up @@ -279,7 +285,7 @@ async function buildApps() {
if (description.length > 280) fail(where, `description must be <=280 chars (is ${description.length})`);

const platforms = (Array.isArray(json.platforms) ? json.platforms : commaList(json.platforms))
.map(p => String(p).trim()).filter(Boolean).map(p => p.slice(0, 24)).slice(0, 6);
.map(p => String(p).trim()).filter(Boolean).map(p => p.slice(0, MAX_PLATFORM_LEN)).slice(0, MAX_PLATFORMS);

out.push(clean({
slug,
Expand Down
Loading