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
2 changes: 1 addition & 1 deletion .github/workflows/release-dates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
else
gh pr create \
--title "docs(release-notes): mark shipped releases as released" \
--body "Automated daily check against [product-details](https://product-details.mozilla.org/1.0/firefox_history_major_releases.json).
--body "Automated daily check for upcoming releases whose heading date has arrived.

_Opened by the [\`.github/workflows/release-dates.yml\`](https://github.com/${{ github.repository }}/tree/main/.github/workflows/release-dates.yml) workflow._" \
--label "automated pr"
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ The following details matter for the generated pages:
- Headings are the version number and release date: a `process` hook strips the date and prefixes `Firefox`, so `## 154 - 2026-08-18` is published as `Firefox 154` at `/release-notes/version/firefox-154/`.
Any other heading text changes the page URL, so the build fails on a malformed date.
- For a release that has not shipped, use the expected date and add `_Upcoming, not yet released._` after one blank line under the heading.
The daily `release-dates.yml` workflow opens a PR that removes it and corrects the date once the release ships.
The daily `release-dates.yml` workflow opens a PR that removes it once the heading date arrives.
If a release slips, update the heading date before then.
- Link each policy to its reference page, at the lowercased policy name.
Builds fail on broken internal links, so a policy with no reference page yet can be left unlinked.
- Anything that deviates from co-shipping needs a scope note, like `Firefox 154 only.`, `Does not apply to the ESR.` at the end of the sentence before the bug link.
Expand Down
53 changes: 11 additions & 42 deletions scripts/sync-release-dates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,40 @@ import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";

// Upstream only lists a version when it's shipped
const RELEASES_URL = "https://product-details.mozilla.org/1.0/firefox_history_major_releases.json";

// Local config
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const NOTES_PATH = resolve(REPO_ROOT, "release-notes/firefox.md");

// An upcoming release heading is followed, after a blank line, by `_Upcoming, not yet released._`.
const UPCOMING = /^\s*[_*]Upcoming\b/;
const HEADING = /^## (\d+) - (\d{4}-\d{2}-\d{2})\s*$/;
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;

async function main() {
const res = await fetch(RELEASES_URL);
if (!res.ok) {
throw new Error(`Failed to fetch releases: ${res.status} ${res.statusText}`);
}
const releases = await res.json();
// ISO dates compare correctly as strings
const today = new Date().toISOString().slice(0, 10);

const content = await readFile(NOTES_PATH, "utf8");
const eol = content.includes("\r\n") ? "\r\n" : "\n";
const lines = content.split(/\r?\n/);

// Find each dated heading's Upcoming line before editing anything
const upcoming = [];
for (let i = 0; i < lines.length; i++) {
// Edit from the bottom up so earlier line numbers stay valid
const shipped = [];
for (let i = lines.length - 1; i >= 0; i--) {
const heading = HEADING.exec(lines[i]);
if (!heading) continue;
if (!heading || heading[2] > today) continue;

let status = i + 1;
while (status < lines.length && lines[status].trim() === "") status++;
if (UPCOMING.test(lines[status] ?? "")) {
upcoming.push({ index: i, status, version: heading[1], expected: heading[2] });
}
}

// Any other Upcoming line would never be updated, so fail rather than skip it
const known = new Set(upcoming.map(({ status }) => status));
const stray = lines.filter((line, n) => UPCOMING.test(line) && !known.has(n));
if (stray.length > 0) {
throw new Error(
`Upcoming line not directly under a \`## N - YYYY-MM-DD\` heading: ${stray.join(" | ")}`,
);
}

// Edit from the bottom up so earlier line numbers stay valid
const shipped = [];
for (const { index, status, version, expected } of upcoming.reverse()) {
const released = releases[`${version}.0`];
if (!released) continue;
if (!ISO_DATE.test(released)) {
throw new Error(`Unexpected date for ${version}.0 in product-details: ${released}`);
}
if (!UPCOMING.test(lines[status] ?? "")) continue;

lines[index] = `## ${version} - ${released}`;
// Drop the status line and the blank lines above it, keeping one blank line under the heading
lines.splice(index + 1, status - index);
if (lines[index + 1] !== "") lines.splice(index + 1, 0, "");
shipped.unshift(
released === expected ? version : `${version} (moved from ${expected} to ${released})`,
);
lines.splice(i + 1, status - i);
if (lines[i + 1] !== "") lines.splice(i + 1, 0, "");
shipped.unshift(heading[1]);
}

if (shipped.length === 0) {
console.log("No upcoming releases have shipped.");
console.log("No upcoming releases are due.");
return;
}

Expand Down
Loading