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
25 changes: 23 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: Release

on:
workflow_dispatch:
inputs:
version:
description: "Explicit package version for reviewed Baseline year contract changes"
required: false
type: string

permissions:
contents: write
Expand Down Expand Up @@ -73,7 +78,14 @@ jobs:
run: npm run test:typescript:full -- --typescript-dir ./.tmp/TypeScript --summary-out .tmp/typescript-integration-summary.md --baseline-diff-out .tmp/typescript-baseline-changes.diff --focused-baselines-out .tmp/typescript-focused-artifact --local-baselines-out .tmp/typescript-raw-local-baselines

- name: Stage publishable baseline package
run: npm run pack:baseline
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
args=()
if [[ -n "$RELEASE_VERSION" ]]; then
args+=(--version "$RELEASE_VERSION")
fi
npm run pack:baseline -- "${args[@]}"

- name: Upload integration summary and focused artifacts
if: always()
Expand All @@ -96,6 +108,15 @@ jobs:

# Tokenless publish via OIDC trusted publishing; provenance only on public repos.
- name: Publish changed package and create GitHub release
run: npm run release:publish -- ${{ !github.event.repository.private && '--provenance' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
args=()
if [[ -n "$RELEASE_VERSION" ]]; then
args+=(--version "$RELEASE_VERSION")
fi
if [[ "${{ github.event.repository.private }}" != "true" ]]; then
args+=(--provenance)
fi
npm run release:publish -- "${args[@]}"
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,30 @@ import "core-js/proposals/promise-with-resolvers";

Only entries approved in `registry/allowlist.json` are public. The registry is a permanent path contract: after every registered compat key becomes Baseline widely available, the same entry remains as an alias to the base package. Limited availability features with `baselineStatus: false` are rejected.

## Target a Baseline year

Baseline year targets contain the cumulative JavaScript features that were Baseline newly available by the end of a completed calendar year. For example, Baseline 2024 includes `Promise.withResolvers`:

```json
{
"compilerOptions": {
"noLib": true,
"types": ["typescript-baseline-lib/year/2024"]
}
}
```

Year entrypoints are generated from each compat row's `baselineLowDate`. They are independent alternatives to the widely available base entrypoint, not additions to it. The current year is omitted until it is complete. The package currently starts at 2020 because the generator cannot yet close the 2015-2019 TypeScript declaration graph without importing symbols from later years; this is an implementation limitation, not a Baseline specification boundary.

Do not combine a `year/*` entrypoint with the base package or an `allow/*` entrypoint. Each year file is a complete historical target, while `allow/*` additions are generated only for the current widely available base.

Each year contract reports declaration-backed compat keys and explicitly managed upstream gaps in `derived/current/generation.json`. The generator never fabricates declarations for behavior that TypeScript cannot model.

Year boundaries apply to runtime JavaScript APIs. Erased TypeScript helper types come from the pinned TypeScript toolchain and are not historical runtime features.

## Current contract

- Target is `baseline` only.
- Public targets are Baseline widely available, completed cumulative Baseline years from 2020 onward, and audited `allow/*` additions for explicitly polyfilled APIs.
- Scope is TypeScript-declarable JavaScript surfaces: `javascript.builtins.*` plus the `arguments` object.
- DOM, Web Worker, syntax, grammar, statements, and operators are out of scope.
- Special compat rows are managed in `registry/compat-management.json` with a source URL for each.
Expand Down
4 changes: 4 additions & 0 deletions deploy/deployChangedPackage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ if (!args.dryRun && !args.yes && process.env.CI !== "true") {
const releasePlans = await collectReleasePlans({
packageId: args.package,
versionOverride: args.version,
preview: args.dryRun,
});

/** @type {Array<Record<string, unknown>>} */
Expand Down Expand Up @@ -56,6 +57,9 @@ for (const releasePlan of releasePlans) {
console.log(`Next version: ${releasePlan.packageVersion}`);
console.log(`Latest published: ${releasePlan.publishedVersion ?? "none"}`);
console.log(`Changed: ${releasePlan.changed ? "yes" : "no"}`);
if (releasePlan.requiredVersionBump && !args.version) {
console.log(`Reviewed release version required: ${releasePlan.requiredVersionBump} bump`);
}
if (releasePlan.changedFiles.length) {
console.log(`Changed files: ${releasePlan.changedFiles.join(", ")}`);
}
Expand Down
199 changes: 196 additions & 3 deletions deploy/package-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import os from "node:os";
import path from "node:path";
import { retryAsync, retrySync } from "../lib/net-retry.mjs";
import { compareYearContracts } from "../lib/year-contracts.mjs";
import { packages, repoRoot } from "./package-registry.mjs";

/**
Expand Down Expand Up @@ -114,6 +115,7 @@ export function assertTypeScriptPeerRange(range, versions) {
* packageId?: string;
* versionOverride?: string;
* stageDirectoryRoot?: string;
* preview?: boolean;
* }} [options]
*/
export async function collectReleasePlans(options = {}) {
Expand All @@ -122,7 +124,11 @@ export async function collectReleasePlans(options = {}) {
/** @type {ReleasePlan[]} */
const releasePlans = [];
for (const stageSummary of stageSummaries) {
releasePlans.push(await buildReleasePlan(stageSummary));
releasePlans.push(await buildReleasePlan(
stageSummary,
options.versionOverride !== undefined,
options.preview === true,
));
}

return releasePlans;
Expand Down Expand Up @@ -234,13 +240,15 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
typesVersions: {
"*": {
"allow/*": ["allow/*/index.d.ts"],
"year/*": ["year/*/index.d.ts"],
},
},
files: [
"index.d.ts",
"baseline.d.ts",
"snapshot.json",
"allow/",
"year/",
"reports/",
"README.md",
"LICENSE",
Expand Down Expand Up @@ -291,8 +299,10 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag

/**
* @param {PackageStageSummary} stageSummary
* @param {boolean} reviewedVersion
* @param {boolean} preview
*/
async function buildReleasePlan(stageSummary) {
async function buildReleasePlan(stageSummary, reviewedVersion, preview) {
const published = await getPublishedPackageState(stageSummary.packageConfig);
const stagedSnapshot = await readComparablePackageSnapshot(stageSummary.stageDirectory);

Expand All @@ -312,11 +322,25 @@ async function buildReleasePlan(stageSummary) {
}

const removedFiles = [...publishedPaths].sort(compareStrings);
if (reviewedVersion) {
assertExplicitVersionIncrease(published.version, stageSummary.packageVersion);
}
assertNoRemovedAllowEntries(removedFiles);
assertAllowEntryContractsPreserved(
published.snapshot.get("reports/generation.json"),
stagedSnapshot.get("reports/generation.json"),
);
assertNoRemovedYearEntryPoints(removedFiles);
const requiredVersionBump = assertYearContractsPreserved(
published.snapshot.get("reports/generation.json"),
stagedSnapshot.get("reports/generation.json"),
{
reviewedVersion,
preview,
publishedVersion: published.version,
stagedVersion: stageSummary.packageVersion,
},
);
const changed = !published.version || changedFiles.length > 0 || removedFiles.length > 0;

return {
Expand All @@ -328,6 +352,7 @@ async function buildReleasePlan(stageSummary) {
changedFiles,
unchangedFiles,
removedFiles,
requiredVersionBump,
notesMarkdown: renderReleaseNotes({
stageSummary,
publishedVersion: published.version,
Expand All @@ -348,6 +373,16 @@ export function assertNoRemovedAllowEntries(removedFiles) {
}
}

/**
* @param {string[]} removedFiles
*/
export function assertNoRemovedYearEntryPoints(removedFiles) {
const removedEntryPoints = removedFiles.filter(relativePath => /^year\/\d{4}\/index\.d\.ts$/.test(relativePath));
if (removedEntryPoints.length) {
throw new Error(`Published Baseline year entrypoints cannot be removed: ${removedEntryPoints.join(", ")}`);
}
}

/**
* @param {string | undefined} publishedReportText
* @param {string | undefined} stagedReportText
Expand All @@ -370,6 +405,163 @@ export function assertAllowEntryContractsPreserved(publishedReportText, stagedRe
}
}

/**
* @param {string | undefined} publishedReportText
* @param {string | undefined} stagedReportText
* @param {{ reviewedVersion?: boolean; preview?: boolean; publishedVersion?: string; stagedVersion?: string; }} [options]
*/
export function assertYearContractsPreserved(publishedReportText, stagedReportText, options = {}) {
if (!publishedReportText) {
return undefined;
}
if (!stagedReportText) {
throw new Error("The staged package is missing reports/generation.json");
}

const comparison = compareYearContracts(publishedReportText, stagedReportText);
const removedYear = comparison.changes.find(change => change.kind === "removed");
if (removedYear) {
throw new Error(`Published Baseline year contract is missing: year/${removedYear.year}`);
}
if (!comparison.requiredVersionBump) {
return undefined;
}
if (!options.reviewedVersion) {
if (options.preview) {
return comparison.requiredVersionBump;
}
throw new Error(
`Baseline year contracts require review (${comparison.changes.map(change => change.year).join(", ")}); `
+ "pass an explicit --version after inspecting the generated diff",
);
}
assertVersionBump(
options.publishedVersion,
options.stagedVersion,
comparison.requiredVersionBump,
);
return comparison.requiredVersionBump;
}

/**
* @param {string | undefined} publishedVersion
* @param {string | undefined} stagedVersion
* @param {"major" | "minor"} requiredBump
*/
function assertVersionBump(publishedVersion, stagedVersion, requiredBump) {
if (!publishedVersion || !stagedVersion) {
throw new Error("Reviewed Baseline year changes require published and staged package versions");
}
const published = parseVersion(publishedVersion);
const staged = parseVersion(stagedVersion);
const sufficient = requiredBump === "major"
? staged.major > published.major
: staged.major > published.major
|| (staged.major === published.major && staged.minor > published.minor);
if (!sufficient) {
throw new Error(
`Baseline year contract changes require a ${requiredBump} version increase from ${publishedVersion}; got ${stagedVersion}`,
);
}
}

/**
* @param {string | undefined} publishedVersion
* @param {string | undefined} stagedVersion
*/
export function assertExplicitVersionIncrease(publishedVersion, stagedVersion) {
if (!stagedVersion) {
throw new Error("Explicit package version is missing");
}
const staged = parseVersion(stagedVersion);
if (!publishedVersion) {
return;
}
if (compareVersion(staged, parseVersion(publishedVersion)) <= 0) {
throw new Error(
`Explicit package version must be greater than ${publishedVersion}; got ${stagedVersion}`,
);
}
}

/**
* @param {string} value
*/
function parseVersion(value) {
const numericIdentifier = "(?:0|[1-9]\\d*)";
const dotSeparatedIdentifiers = "[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*";
const match = new RegExp(
`^(${numericIdentifier})\\.(${numericIdentifier})\\.(${numericIdentifier})`
+ `(?:-(${dotSeparatedIdentifiers}))?(?:\\+${dotSeparatedIdentifiers})?$`,
).exec(value);
if (
!match
|| match[4]?.split(".").some(identifier => /^\d+$/.test(identifier) && identifier.length > 1 && identifier[0] === "0")
) {
throw new Error(`Unsupported package version format: ${value}`);
}
const parsed = {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
prerelease: match[4],
};
if (![parsed.major, parsed.minor, parsed.patch].every(Number.isSafeInteger)) {
throw new Error(`Unsupported package version format: ${value}`);
}
return parsed;
}

/**
* @param {ReturnType<typeof parseVersion>} left
* @param {ReturnType<typeof parseVersion>} right
*/
function compareVersion(left, right) {
const coreDifference = left.major - right.major
|| left.minor - right.minor
|| left.patch - right.patch;
if (coreDifference) {
return coreDifference;
}
if (left.prerelease === right.prerelease) {
return 0;
}
if (!left.prerelease) {
return 1;
}
if (!right.prerelease) {
return -1;
}
const leftParts = left.prerelease.split(".");
const rightParts = right.prerelease.split(".");
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index++) {
const leftPart = leftParts[index];
const rightPart = rightParts[index];
if (leftPart === undefined) {
return -1;
}
if (rightPart === undefined) {
return 1;
}
if (leftPart === rightPart) {
continue;
}
const leftNumeric = /^\d+$/.test(leftPart);
const rightNumeric = /^\d+$/.test(rightPart);
if (leftNumeric && rightNumeric) {
if (leftPart.length !== rightPart.length) {
return leftPart.length - rightPart.length;
}
return leftPart < rightPart ? -1 : 1;
}
if (leftNumeric !== rightNumeric) {
return leftNumeric ? -1 : 1;
}
return leftPart < rightPart ? -1 : 1;
}
return 0;
}

/**
* @param {string} reportText
* @param {string} label
Expand Down Expand Up @@ -576,7 +768,7 @@ function renderNotice(packageConfig, snapshot) {
"",
"Contents:",
"- `LICENSE` contains the Apache License 2.0 text for this package.",
"- `baseline.d.ts` is derived from the npm `typescript` package and retains the upstream Microsoft license notice at file header.",
"- Generated declaration files under `baseline.d.ts`, `allow/`, and `year/` are derived from the npm `typescript` package and retain the upstream Microsoft license notice.",
"- `reports/` contains generator audit artifacts for the exact packaged snapshot.",
"",
"Snapshot:",
Expand Down Expand Up @@ -795,6 +987,7 @@ function compareStrings(left, right) {
* changedFiles: string[];
* unchangedFiles: string[];
* removedFiles: string[];
* requiredVersionBump?: "major" | "minor";
* notesMarkdown: string;
* }} ReleasePlan
*/
Expand Down
4 changes: 4 additions & 0 deletions deploy/package-registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const baselinePackage = {
from: path.join(repoRoot, "generated", "current", "allow"),
to: "allow",
},
{
from: path.join(repoRoot, "generated", "current", "year"),
to: "year",
},
{
from: path.join(repoRoot, "derived", "current", "classification.json"),
to: path.join("reports", "classification.json"),
Expand Down
Loading