Skip to content

feat: add versions.json version metadata contract#110

Open
ryzrr wants to merge 2 commits into
mainfrom
feat/add-versions-json
Open

feat: add versions.json version metadata contract#110
ryzrr wants to merge 2 commits into
mainfrom
feat/add-versions-json

Conversation

@ryzrr
Copy link
Copy Markdown
Member

@ryzrr ryzrr commented May 26, 2026

Links to : #105

Summary

Adds versions.json as the shared version metadata contract for the documentation pipeline. This is the single source of truth that update-versions.mjs, place-output.mjs, release.yml will all read from.

Real values: v5.x reflects the actual latest webpack release on npm (5.107.1) and the commit matches the current HEAD_COMMIT. The v6.x entry has null values since v6 has no npm release yet, these get filled automatically by release.yml when webpack tags a v6 release.

What kind of change does this PR introduce?

New file, no existing behaviour is modified.

Did you add tests for your changes?

Tests will come alongside update-versions.mjs which reads and mutates this file. Adding them here in isolation would not be meaningful without the script that uses it.

Does this PR introduce a breaking change?

No. Nothing currently depends on this file.

If relevant, what needs to be documented?

Schema is already documented and shared with the team.

Use of AI

Used for writing the PR Desc.

Copilot AI review requested due to automatic review settings May 26, 2026 12:27
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
webpack-doc-kit Ready Ready Preview, Comment May 27, 2026 3:43pm

Request Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new root-level versions.json file intended to act as the shared “version metadata contract” for the documentation/release pipeline (per #105), capturing the current latest released webpack major (v5) and reserving an entry for v6 before an npm release exists.

Changes:

  • Introduces versions.json containing a latest pointer plus a versions[] list of version metadata.
  • Populates v5 with an exact npm version and a commit SHA matching HEAD_COMMIT; scaffolds v6 with null fields.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ryzrr ryzrr self-assigned this May 26, 2026
Comment thread versions.json
"frozen": false
},
{
"label": "v6.x",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is object for v6 required now?
update-version.mjs can add it when v6 is released

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a placeholder to validate the multi-version schema early rather than we discovering issues when v6 actually ships. I believe no cost to keeping it since the script update-version.mjs handles both cases.

Copy link
Copy Markdown
Member

@avivkeller avivkeller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the structure of the file looks good, I feel weird about adding an unused file, can you add the YAML and MJS files you mentioned to this PR?

@ryzrr
Copy link
Copy Markdown
Member Author

ryzrr commented May 27, 2026

Sure, will add update-versions.mjs and release.yml and also the place-output.mjs to this PR. Give me a bit to get release.yml ready and I'll push everything together.

@ryzrr
Copy link
Copy Markdown
Member Author

ryzrr commented May 27, 2026

Dropped place-output.mjs - the frozen protection is already handled in release.yml via git diff on versions.json, so a separate guard script adds no real value here.

Copy link
Copy Markdown
Member

@TusharThakur04 TusharThakur04 May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we expecting to manually set the prev version's frozen property to be true?
cause this script doesn't set it to true.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently , Yes manually though it can be automated as well but this ensures we have explicit control of when a version gets frozen.

Comment thread versions.json
"major": 5,
"exactVersion": "5.107.1",
"commit": "0b60f1cba10640d886510999c1bfbca5f4ba7ab6",
"frozen": false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize we don't actually need a frozen..., there simply won't be any new releases

Copy link
Copy Markdown
Member

@avivkeller avivkeller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My review is purely a suggestion, and I'm not 100% sure that it's the way to go. Curious what you think about consolidating versions.json to just a string

Comment on lines +1 to +141
name: Release

on:
release:
types: [published]

permissions:
contents: write
pull-requests: write

jobs:
sync-docs:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*
cache: 'npm'

# Packages go stale as doc-kit ships new features. Always reinstall from
# the lockfile before anything else.
- name: Install dependencies
run: npm ci

- name: Parse release tag and commit
id: release
run: |
TAG="${{ github.event.release.tag_name }}"
COMMIT="${{ github.event.release.target_commitish }}"
# npm install doesn't accept a leading v - strip it here
CLEAN_TAG="${TAG#v}"
MAJOR=$(echo "$CLEAN_TAG" | cut -d. -f1)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "commit=$COMMIT" >> "$GITHUB_OUTPUT"
echo "clean_tag=$CLEAN_TAG" >> "$GITHUB_OUTPUT"
echo "clean_tag_major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "Triggered by: $TAG @ $COMMIT"

- name: Update versions.json
id: versions
run: |
node scripts/update-versions.mjs "${{ steps.release.outputs.tag }}" "${{ steps.release.outputs.commit }}"
# If versions.json is unchanged after the script runs, this major is
# frozen. No point regenerating docs - nothing will differ.
if git diff --quiet versions.json; then
echo "frozen=true" >> "$GITHUB_OUTPUT"
echo "Version is frozen, skipping doc generation."
else
echo "frozen=false" >> "$GITHUB_OUTPUT"
fi

# Installing the exact release version updates package.json and
# package-lock.json so the lockfile stays in sync with what was used.
- name: Install webpack at release version
if: steps.versions.outputs.frozen == 'false'
run: npm install webpack@${{ steps.release.outputs.clean_tag }}

- name: Pin HEAD_COMMIT to release commit
if: steps.versions.outputs.frozen == 'false'
run: echo "${{ steps.release.outputs.commit }}" > HEAD_COMMIT

# Clone must come after npm install. TypeDoc needs to resolve types against
# the freshly installed webpack version, not whatever was in the lockfile.
- name: Clone webpack at release commit
if: steps.versions.outputs.frozen == 'false'
run: npm run clone-webpack

- name: Generate documentation
if: steps.versions.outputs.frozen == 'false'
run: npm run generate-docs

- name: Verify generated output
if: steps.versions.outputs.frozen == 'false'
run: |
LABEL="v${{ steps.release.outputs.clean_tag_major }}.x"
DOCS_DIR="pages/${LABEL}"
if [ ! -d "$DOCS_DIR" ]; then
echo "error: $DOCS_DIR was not generated"
exit 1
fi
if [ ! -f "$DOCS_DIR/type-map.json" ]; then
echo "error: type-map.json missing from $DOCS_DIR"
exit 1
fi
if [ -z "$(ls -A "$DOCS_DIR")" ]; then
echo "error: $DOCS_DIR is empty"
exit 1
fi
echo "$DOCS_DIR looks good"

- name: Write job summary
if: steps.versions.outputs.frozen == 'false'
run: |
FILES=$(git diff --name-only pages/ | wc -l)
CATEGORIES=$(git diff --name-only pages/ \
| sed 's|pages/[^/]*/||' \
| cut -d/ -f1 \
| sort -u \
| tr '\n' ',' \
| sed 's/,$//')
{
echo "## Doc generation summary"
echo "- **Release:** ${{ steps.release.outputs.tag }}"
echo "- **Commit:** \`${{ steps.release.outputs.commit }}\`"
echo "- **Pages updated:** ${FILES}"
echo "- **Affected categories:** ${CATEGORIES:-none detected}"
} >> "$GITHUB_STEP_SUMMARY"

- name: Open pull request
if: steps.versions.outputs.frozen == 'false'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
add-paths: |
versions.json
HEAD_COMMIT
package.json
package-lock.json
pages/
commit-message: "docs: sync API documentation to webpack@${{ steps.release.outputs.tag }}"
title: "docs: sync API documentation to webpack@${{ steps.release.outputs.tag }}"
body: |
Automated doc sync triggered by the `${{ steps.release.outputs.tag }}` release.

**webpack commit:** `${{ steps.release.outputs.commit }}`

**Files in this PR:**
- `versions.json` -- updated `exactVersion` and `commit` for this release
- `HEAD_COMMIT` -- pinned to release commit
- `package.json` / `package-lock.json` -- webpack bumped to `${{ steps.release.outputs.tag }}`
- `pages/` -- regenerated API documentation

Check the Actions summary tab for a file count and category breakdown. CI must pass before merging.
labels: documentation
branch: "docs/release-${{ steps.release.outputs.tag }}"
delete-branch: true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name: Release
on:
release:
types: [published]
permissions:
contents: write
pull-requests: write
jobs:
sync-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*
cache: 'npm'
# Packages go stale as doc-kit ships new features. Always reinstall from
# the lockfile before anything else.
- name: Install dependencies
run: npm ci
- name: Parse release tag and commit
id: release
run: |
TAG="${{ github.event.release.tag_name }}"
COMMIT="${{ github.event.release.target_commitish }}"
# npm install doesn't accept a leading v - strip it here
CLEAN_TAG="${TAG#v}"
MAJOR=$(echo "$CLEAN_TAG" | cut -d. -f1)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "commit=$COMMIT" >> "$GITHUB_OUTPUT"
echo "clean_tag=$CLEAN_TAG" >> "$GITHUB_OUTPUT"
echo "clean_tag_major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "Triggered by: $TAG @ $COMMIT"
- name: Update versions.json
id: versions
run: |
node scripts/update-versions.mjs "${{ steps.release.outputs.tag }}" "${{ steps.release.outputs.commit }}"
# If versions.json is unchanged after the script runs, this major is
# frozen. No point regenerating docs - nothing will differ.
if git diff --quiet versions.json; then
echo "frozen=true" >> "$GITHUB_OUTPUT"
echo "Version is frozen, skipping doc generation."
else
echo "frozen=false" >> "$GITHUB_OUTPUT"
fi
# Installing the exact release version updates package.json and
# package-lock.json so the lockfile stays in sync with what was used.
- name: Install webpack at release version
if: steps.versions.outputs.frozen == 'false'
run: npm install webpack@${{ steps.release.outputs.clean_tag }}
- name: Pin HEAD_COMMIT to release commit
if: steps.versions.outputs.frozen == 'false'
run: echo "${{ steps.release.outputs.commit }}" > HEAD_COMMIT
# Clone must come after npm install. TypeDoc needs to resolve types against
# the freshly installed webpack version, not whatever was in the lockfile.
- name: Clone webpack at release commit
if: steps.versions.outputs.frozen == 'false'
run: npm run clone-webpack
- name: Generate documentation
if: steps.versions.outputs.frozen == 'false'
run: npm run generate-docs
- name: Verify generated output
if: steps.versions.outputs.frozen == 'false'
run: |
LABEL="v${{ steps.release.outputs.clean_tag_major }}.x"
DOCS_DIR="pages/${LABEL}"
if [ ! -d "$DOCS_DIR" ]; then
echo "error: $DOCS_DIR was not generated"
exit 1
fi
if [ ! -f "$DOCS_DIR/type-map.json" ]; then
echo "error: type-map.json missing from $DOCS_DIR"
exit 1
fi
if [ -z "$(ls -A "$DOCS_DIR")" ]; then
echo "error: $DOCS_DIR is empty"
exit 1
fi
echo "$DOCS_DIR looks good"
- name: Write job summary
if: steps.versions.outputs.frozen == 'false'
run: |
FILES=$(git diff --name-only pages/ | wc -l)
CATEGORIES=$(git diff --name-only pages/ \
| sed 's|pages/[^/]*/||' \
| cut -d/ -f1 \
| sort -u \
| tr '\n' ',' \
| sed 's/,$//')
{
echo "## Doc generation summary"
echo "- **Release:** ${{ steps.release.outputs.tag }}"
echo "- **Commit:** \`${{ steps.release.outputs.commit }}\`"
echo "- **Pages updated:** ${FILES}"
echo "- **Affected categories:** ${CATEGORIES:-none detected}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Open pull request
if: steps.versions.outputs.frozen == 'false'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
add-paths: |
versions.json
HEAD_COMMIT
package.json
package-lock.json
pages/
commit-message: "docs: sync API documentation to webpack@${{ steps.release.outputs.tag }}"
title: "docs: sync API documentation to webpack@${{ steps.release.outputs.tag }}"
body: |
Automated doc sync triggered by the `${{ steps.release.outputs.tag }}` release.
**webpack commit:** `${{ steps.release.outputs.commit }}`
**Files in this PR:**
- `versions.json` -- updated `exactVersion` and `commit` for this release
- `HEAD_COMMIT` -- pinned to release commit
- `package.json` / `package-lock.json` -- webpack bumped to `${{ steps.release.outputs.tag }}`
- `pages/` -- regenerated API documentation
Check the Actions summary tab for a file count and category breakdown. CI must pass before merging.
labels: documentation
branch: "docs/release-${{ steps.release.outputs.tag }}"
delete-branch: true
name: Release
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. v5.99.0)'
required: true
type: string
commit:
description: 'Target commit SHA or branch'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
sync:
runs-on: ubuntu-latest
env:
TAG: ${{ inputs.tag }}
COMMIT: ${{ inputs.commit }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Update files
shell: bash
run: |
set -euo pipefail
node scripts/update-versions.mjs "$TAG" "$COMMIT"
echo "$COMMIT" > HEAD_COMMIT
- name: Open pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: sync to webpack@${{ env.TAG }}"
title: "core: sync to webpack@${{ env.TAG }}"
body: |
Automated version sync triggered by the `${{ env.TAG }}` release of `${{ env.COMMIT }}`.
branch: "update-versions"
draft: true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasons here:

  1. We need to workflow_dispatch, since we are running on webpack releases, not releases in this repo
  2. We don't need to generate anything, since that'll happen on deployment (we'll need to update our generation to do the checkout of multiple versions, this can be done in a follow-up)
  3. We don't need to check frozen-ness, since frozen means nothing (in the sense that frozen will be implied by the lack of new releases)
  4. We need to draft: true, it's a GitHub actions quirk (see https://github.com/community/maintainers/discussions/740, note this repository is only accessible to verified maintainers)

Comment on lines +8 to +11
function fail(msg) {
console.error(`error: ${msg}`);
process.exit(1);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's replace all fail() with throw new Error()

Comment on lines +1 to +59
// Called from release.yml when a new webpack tag is published.
// Usage: node scripts/update-versions.mjs v5.108.0 <commit-sha>
import { readFileSync, writeFileSync } from 'fs';
import { major, valid } from 'semver';

const VERSIONS_FILE = './versions.json';

function fail(msg) {
console.error(`error: ${msg}`);
process.exit(1);
}

const [tag, commit] = process.argv.slice(2);

if (!tag) fail('missing release tag (e.g. v5.108.0)');
if (!commit) fail('missing commit SHA');
if (!valid(tag)) fail(`"${tag}" is not a valid semver tag`);

const data = JSON.parse(readFileSync(VERSIONS_FILE, 'utf8'));

const incomingMajor = major(tag);
const label = `v${incomingMajor}.x`;
const exactVersion = tag.replace(/^v/, '');

const existing = data.versions.find(v => v.major === incomingMajor);

if (existing) {
// frozen means the docs for this major are locked - don't touch them
if (existing.frozen) {
console.log(`${label} is frozen. Skipping.`);
process.exit(0);
}

existing.exactVersion = exactVersion;
existing.commit = commit;
console.log(`updated ${label} → ${exactVersion} @ ${commit}`);
} else {
// first release of a new major - create the entry from scratch
data.versions.push({
label,
major: incomingMajor,
exactVersion,
commit,
frozen: false,
});
console.log(`created new entry for ${label} → ${exactVersion} @ ${commit}`);
}

// only move latest forward, never back
const currentLatestMajor =
data.versions.find(v => v.label === data.latest)?.major ?? 0;
if (incomingMajor > currentLatestMajor) {
data.latest = label;
console.log(`latest promoted to ${label}`);
}

writeFileSync(VERSIONS_FILE, JSON.stringify(data, null, 2) + '\n');
console.log(`versions.json written`);
process.exit(0);
Copy link
Copy Markdown
Member

@avivkeller avivkeller May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Called from release.yml when a new webpack tag is published.
// Usage: node scripts/update-versions.mjs v5.108.0 <commit-sha>
import { readFileSync, writeFileSync } from 'fs';
import { major, valid } from 'semver';
const VERSIONS_FILE = './versions.json';
function fail(msg) {
console.error(`error: ${msg}`);
process.exit(1);
}
const [tag, commit] = process.argv.slice(2);
if (!tag) fail('missing release tag (e.g. v5.108.0)');
if (!commit) fail('missing commit SHA');
if (!valid(tag)) fail(`"${tag}" is not a valid semver tag`);
const data = JSON.parse(readFileSync(VERSIONS_FILE, 'utf8'));
const incomingMajor = major(tag);
const label = `v${incomingMajor}.x`;
const exactVersion = tag.replace(/^v/, '');
const existing = data.versions.find(v => v.major === incomingMajor);
if (existing) {
// frozen means the docs for this major are locked - don't touch them
if (existing.frozen) {
console.log(`${label} is frozen. Skipping.`);
process.exit(0);
}
existing.exactVersion = exactVersion;
existing.commit = commit;
console.log(`updated ${label}${exactVersion} @ ${commit}`);
} else {
// first release of a new major - create the entry from scratch
data.versions.push({
label,
major: incomingMajor,
exactVersion,
commit,
frozen: false,
});
console.log(`created new entry for ${label}${exactVersion} @ ${commit}`);
}
// only move latest forward, never back
const currentLatestMajor =
data.versions.find(v => v.label === data.latest)?.major ?? 0;
if (incomingMajor > currentLatestMajor) {
data.latest = label;
console.log(`latest promoted to ${label}`);
}
writeFileSync(VERSIONS_FILE, JSON.stringify(data, null, 2) + '\n');
console.log(`versions.json written`);
process.exit(0);
import { readFileSync, writeFileSync } from 'node:fs';
import { major, valid } from 'semver';
const VERSIONS_FILE = './versions.json';
const [tag] = process.argv.slice(2);
if (!tag) throw new Error('Missing release tag (e.g. v5.108.0)');
if (!valid(tag)) throw new Error(`"${tag}" is not a valid semver tag`);
const latestMajor = major(tag);
const data = JSON.parse(readFileSync(VERSIONS_FILE, 'utf8'));
const existingIndex = data.findIndex((v) => major(v) === latestMajor);
if (existingIndex !== -1) {
data[existingIndex] = tag;
console.log(`Updated v${latestMajor}.x to ${tag}`);
} else {
data.unshift(tag);
console.log(`Created new entry for v${latestMajor}.x: ${tag}`);
}
writeFileSync(VERSIONS_FILE, JSON.stringify(data, null, 2));
console.log('versions.json written');

Copy link
Copy Markdown
Member

@avivkeller avivkeller May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explanation of changes:

  1. As mentioned previously, we really only need the tag
  2. Rather than using .latest, we can use .[0], so versions are sorted (.unshift())
  3. fail => throw new Error

Comment thread versions.json
Comment on lines +1 to +19
{
"latest": "v5.x",
"versions": [
{
"label": "v5.x",
"major": 5,
"exactVersion": "5.107.1",
"commit": "0b60f1cba10640d886510999c1bfbca5f4ba7ab6",
"frozen": false
},
{
"label": "v6.x",
"major": 6,
"exactVersion": null,
"commit": null,
"frozen": false
}
]
} No newline at end of file
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{
"latest": "v5.x",
"versions": [
{
"label": "v5.x",
"major": 5,
"exactVersion": "5.107.1",
"commit": "0b60f1cba10640d886510999c1bfbca5f4ba7ab6",
"frozen": false
},
{
"label": "v6.x",
"major": 6,
"exactVersion": null,
"commit": null,
"frozen": false
}
]
}
["v5.107.1"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we only include the versions like this? That's really all the info we need, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants