diff --git a/docs/register/register.js b/docs/register/register.js index a436b2b..dcb9dd2 100644 --- a/docs/register/register.js +++ b/docs/register/register.js @@ -10,7 +10,16 @@ var REPO = "OpenTechEvents/opentechevents-spec"; var ISSUE_TEMPLATE = "adopter.yml"; - var SCHEMA_BASE = "../schema/v0.4/"; // published copies — same origin, no CORS + /* Published schema copies — same origin, no CORS. The required-field lists are read + from the version the feed itself declares: checking a valid 0.3 feed against 0.4's + lists would report a problem its publisher has no reason to consider one. Unknown + or absent version falls back to the newest. */ + var SCHEMA_VERSIONS = ["0.1.0", "0.2.0", "0.3.0", "0.4.0"]; + var LATEST_SCHEMA = SCHEMA_VERSIONS[SCHEMA_VERSIONS.length - 1]; + function schemaBase(version) { + var known = SCHEMA_VERSIONS.indexOf(version) !== -1 ? version : LATEST_SCHEMA; + return "../schema/v" + known.split(".").slice(0, 2).join(".") + "/"; + } var REGISTRY = "../data/adopters.json"; // the same file the site's adopter list renders from /* ---------- linking sources (pluggable) ---------- @@ -296,20 +305,22 @@ var feedStatus = document.getElementById("reg-feed-status"); var requiredFields = null; // { feed: [...], event: [...] } from the published schemas - function loadRequired() { - if (requiredFields) return Promise.resolve(requiredFields); + function loadRequired(version) { + var base = schemaBase(version); + if (requiredFields && requiredFields.base === base) return Promise.resolve(requiredFields); return Promise.all([ - fetch(SCHEMA_BASE + "feed.schema.json").then(function (r) { return r.json(); }), - fetch(SCHEMA_BASE + "event.schema.json").then(function (r) { return r.json(); }), + fetch(base + "feed.schema.json").then(function (r) { return r.json(); }), + fetch(base + "event.schema.json").then(function (r) { return r.json(); }), ]).then(function (schemas) { requiredFields = { + base: base, feed: schemas[0].required || [], event: (schemas[1].$defs && schemas[1].$defs.event && schemas[1].$defs.event.required) || [], }; return requiredFields; }).catch(function () { // Schemas unreachable (offline dev?): degrade to the structural checks only. - requiredFields = { feed: [], event: [] }; + requiredFields = { base: base, feed: [], event: [] }; return requiredFields; }); } @@ -355,12 +366,14 @@ renderFeedStatus(); }; - Promise.all([fetch(url), loadRequired()]).then(function (results) { - var res = results[0], req = results[1]; + fetch(url).then(function (res) { if (!res.ok) return done("invalid", [{ key: "errHttp", vars: { status: String(res.status) } }]); return res.json().then(function (doc) { - var errors = structuralErrors(doc, req); - done(errors.length ? "invalid" : "valid", errors); + // The feed's own version decides which required-field lists it is measured by. + return loadRequired(doc.specVersion).then(function (req) { + var errors = structuralErrors(doc, req); + done(errors.length ? "invalid" : "valid", errors); + }); }, function () { done("invalid", [{ key: "errJson", vars: {} }]); }); diff --git a/scripts/register-adopter.mjs b/scripts/register-adopter.mjs index c9945e5..e02fb7d 100644 --- a/scripts/register-adopter.mjs +++ b/scripts/register-adopter.mjs @@ -10,7 +10,8 @@ * Outputs (to $GITHUB_OUTPUT when set, stdout otherwise): * valid=true|false whether the feed validated * name= for commit message / PR title - * spec=v0.4 the spec version this run measured against, for the PR body + * spec=v0.3 the spec version this run measured against — the one the feed + * declares, not necessarily the newest — for the PR body * validator= the feed's permalink in the online validator, for the PR body * A human-readable report is written to $REPORT_PATH (default: adopter-report.md) * — that file becomes the comment on the issue, valid or not. It opens with a hidden @@ -22,14 +23,28 @@ * An invalid feed is a normal outcome, not a job failure: the script only exits * non-zero when it cannot do its work (missing fields, unreadable adopters.json). */ -import { readFileSync, writeFileSync, appendFileSync } from "node:fs"; +import { readFileSync, writeFileSync, appendFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; import { annotationKeywords, customFormats, customKeywords } from "../index.js"; -const SPEC = "spec/v0.4"; -const SPEC_VERSION = SPEC.split("/").pop(); // "v0.4" — the version every message quotes +/* A feed is measured against the version it declares, not against the newest one: + every published version keeps its schemas here, and telling someone whose valid + 0.3 feed we asked to register that it "does not validate" — because of the + specVersion const and nothing else — is a rejection at the front door for a feed + that was never broken. Older than the support window is a different matter: there + we do ask for a migration, and say so. Same rule as scripts/check-feeds.mjs. */ +const SUPPORT_WINDOW = Number(process.env.FEED_SUPPORT_WINDOW || 3); + +const versions = readdirSync("spec") + .filter((d) => /^v\d+\.\d+$/.test(d)) + .map((dir) => ({ dir, minor: dir.slice(1).split(".").map(Number) })) + .sort((a, b) => a.minor[0] - b.minor[0] || a.minor[1] - b.minor[1]); +const byVersion = new Map(versions.map((v) => [`${v.minor[0]}.${v.minor[1]}.0`, v.dir])); +const LATEST = [...byVersion.keys()].at(-1); +const supported = [...byVersion.keys()].slice(-SUPPORT_WINDOW); + const ADOPTERS = join("docs", "data", "adopters.json"); const REPORT = process.env.REPORT_PATH || "adopter-report.md"; @@ -94,7 +109,6 @@ if (!body) { const fields = parseIssue(body); setOutput("name", fields.name || ""); -setOutput("spec", SPEC_VERSION); if (!fields.name || !fields.feed) { report(false, [ @@ -114,9 +128,20 @@ setOutput("validator", validatorLink(fields.feed)); const ORIGIN = "https://opentechevents.org"; let cors = null; +async function fetchFeed(url) { + try { + return await fetch(url, { headers: { Origin: ORIGIN }, signal: AbortSignal.timeout(60000) }); + } catch (err) { + await new Promise((r) => setTimeout(r, 3000)); + return fetch(url, { headers: { Origin: ORIGIN }, signal: AbortSignal.timeout(60000) }); + } +} + let doc; try { - const res = await fetch(fields.feed, { headers: { Origin: ORIGIN }, signal: AbortSignal.timeout(20000) }); + // Generous, and retried once: a feed that takes 30s to build half a megabyte is slow, + // not broken, and a timeout here reads to the publisher as "your feed is wrong". + const res = await fetchFeed(fields.feed); if (!res.ok) throw new Error(`the URL answered HTTP ${res.status}`); const acao = res.headers.get("access-control-allow-origin"); if (acao !== "*") cors = acao ? `only allows reads from \`${acao}\`` : "sends no `Access-Control-Allow-Origin`"; @@ -136,6 +161,48 @@ try { process.exit(0); } +/* Which rules to measure this feed by. Absent or unknown means nobody can judge it: + there is no schema for a version this project never published. Older than the + support window is the one case where being behind really is the problem, and the + report says so instead of dressing it up as a validation error. */ +const declared = typeof doc.specVersion === "string" ? doc.specVersion : null; + +if (!declared || !byVersion.has(declared)) { + report(false, [ + "### ❌ Unknown `specVersion`", + "", + declared + ? `The feed declares \`specVersion: "${declared}"\`, which is not a published version of OTE Spec.` + : "The feed does not declare a `specVersion`, so there are no rules to check it against.", + "", + `Published versions: ${[...byVersion.keys()].map((v) => `\`${v}\``).join(", ")}. The current one is \`${LATEST}\`.`, + "", + `👉 **[Open your feed in the OTE validator](${validatorLink(fields.feed)})**. The [field reference](https://opentechevents.org/spec/) documents every field.`, + "", + RETRY, + ]); + process.exit(0); +} + +if (!supported.includes(declared)) { + report(false, [ + `### ❌ OTE Spec ${declared} is no longer supported`, + "", + `The registry tracks the last ${supported.length} versions (${supported.map((v) => `\`${v}\``).join(", ")}); this feed declares \`${declared}\`.`, + "", + `Its schemas are still in the repo under [\`spec/${byVersion.get(declared)}\`](https://github.com/OpenTechEvents/opentechevents-spec/tree/main/spec/${byVersion.get(declared)}), so nothing you publish stops working — but to be listed, please move the feed to \`${LATEST}\`.`, + "", + `👉 **[Open your feed in the OTE validator](${validatorLink(fields.feed)})** once you've migrated.`, + "", + RETRY, + ]); + process.exit(0); +} + +const SPEC = join("spec", byVersion.get(declared)); +const SPEC_VERSION = byVersion.get(declared); // "v0.3" — the version every message quotes +setOutput("spec", SPEC_VERSION); + // Same Ajv setup as scripts/validate.mjs — see the comment there about strictRequired. const ajv = new Ajv2020({ strict: true, strictRequired: false, allErrors: true }); addFormats(ajv); @@ -192,6 +259,14 @@ report(true, [ "A pull request adding the community to the adopters registry follows; a maintainer will review and merge it.", "", `Keep this link to re-check the feed whenever you change it: [validator.opentechevents.org](${validatorLink(fields.feed)}).`, + // Being behind is not a defect and does not hold up the registration: said once, as a + // note, so nobody reads a supported version as a problem they have to fix today. + ...(declared !== LATEST + ? [ + "", + `> ℹ️ This feed declares \`${declared}\` and the current version is \`${LATEST}\`. Both are supported and it changes nothing here — whenever you feel like migrating, the [changelog](https://opentechevents.org/history/) lists what moved.`, + ] + : []), ...(cors ? [ "",