From fdf33752c65810eb08c5564bd7d85acb2d92d025 Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Fri, 3 Apr 2026 10:12:51 +0200 Subject: [PATCH 1/2] Add upstream compatibility baseline regression check --- .github/workflows/dev-containers.yml | 2 + README.md | 16 ++++++ TODO.md | 9 ++- build/check-upstream-compatibility.js | 55 +++++++++++++++++++ docs/upstream/compatibility-baseline.json | 5 ++ package.json | 3 +- .../migration/upstreamCompatibility.ts | 28 ++++++++++ src/test/upstreamCompatibility.test.ts | 27 +++++++++ 8 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 build/check-upstream-compatibility.js create mode 100644 docs/upstream/compatibility-baseline.json diff --git a/.github/workflows/dev-containers.yml b/.github/workflows/dev-containers.yml index 3397f2d28..b8abfb0fc 100644 --- a/.github/workflows/dev-containers.yml +++ b/.github/workflows/dev-containers.yml @@ -28,6 +28,8 @@ jobs: scope: '@microsoft' - name: Install Dependencies run: yarn install --frozen-lockfile + - name: Upstream Compatibility Baseline + run: yarn check-upstream-compatibility - name: Type-Check run: yarn type-check - name: Lint diff --git a/README.md b/README.md index c560c1afe..0b6973f06 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,22 @@ When updating upstream, prefer an explicit workflow: 3. Fix regressions in project-owned code. 4. Merge once CI is green. +Recommended command sequence: + +```bash +git submodule update --init --recursive +git -C upstream fetch origin +git -C upstream checkout +git add upstream +git rev-parse HEAD:upstream +npm run check-upstream-compatibility +npm test +``` + +If `npm run check-upstream-compatibility` reports a commit change, update +[`docs/upstream/compatibility-baseline.json`](./docs/upstream/compatibility-baseline.json) +in the same change after parity checks pass. + If you clone this repository without submodules, initialize them before building/testing: ```bash diff --git a/TODO.md b/TODO.md index deaf88059..8065c3509 100644 --- a/TODO.md +++ b/TODO.md @@ -204,9 +204,12 @@ Move all vendored upstream TypeScript CLI sources out of repo root and treat `up ### 3) Compatibility target versioning - [x] Define the compatibility contract as: “this repo targets the exact commit pinned in `upstream/`.” - Added `resolvePinnedUpstreamCommit(...)` and `formatUpstreamCompatibilityContract(...)` helpers (with tests) to make the pinned-commit contract explicit and machine-resolvable. -- [ ] Expose the pinned upstream commit in test output/logging for traceability. -- [ ] Add a dedicated CI check that reports diffs/regressions when submodule commit changes. -- [ ] Create an “update upstream” workflow (bump submodule -> run parity suite -> fix breakages -> merge). +- [x] Expose the pinned upstream commit in test output/logging for traceability. + - Added `formatUpstreamCommitTraceLine(...)` and a CI-emitted `[upstream-compat] pinned upstream commit: ...` log line so pinned commit traces appear in automated output. +- [x] Add a dedicated CI check that reports diffs/regressions when submodule commit changes. + - Added `build/check-upstream-compatibility.js`, baseline metadata in `docs/upstream/compatibility-baseline.json`, npm script `check-upstream-compatibility`, and wired it into the CI workflow. +- [x] Create an “update upstream” workflow (bump submodule -> run parity suite -> fix breakages -> merge). + - Expanded `README.md` with a concrete command sequence for submodule bump, compatibility checks, parity tests, and baseline update expectations. ### 4) Documentation updates - [ ] Update `README.md` with: diff --git a/build/check-upstream-compatibility.js b/build/check-upstream-compatibility.js new file mode 100644 index 000000000..62513241f --- /dev/null +++ b/build/check-upstream-compatibility.js @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const cp = require('child_process'); + +const repositoryRoot = path.join(__dirname, '..'); +const baselineFilePath = path.join(repositoryRoot, 'docs/upstream/compatibility-baseline.json'); +const submodulePath = 'upstream'; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function resolveCurrentPinnedCommit() { + return cp.execFileSync('git', ['rev-parse', `HEAD:${submodulePath}`], { + cwd: repositoryRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); +} + +if (!fs.existsSync(baselineFilePath)) { + fail(`Missing compatibility baseline file: ${path.relative(repositoryRoot, baselineFilePath)}`); +} + +const baselineRaw = fs.readFileSync(baselineFilePath, 'utf8'); +/** @type {{ submodulePath?: string; pinnedCommit?: string; compatibilityContract?: string; }} */ +const baseline = JSON.parse(baselineRaw); + +if (!baseline.pinnedCommit || typeof baseline.pinnedCommit !== 'string') { + fail(`Invalid pinnedCommit in ${path.relative(repositoryRoot, baselineFilePath)}`); +} + +const currentCommit = resolveCurrentPinnedCommit(); +const recordedCommit = baseline.pinnedCommit.trim(); + +console.log(`[upstream-compat] pinned upstream commit: ${currentCommit}`); + +if (recordedCommit !== currentCommit) { + fail( + [ + `Pinned upstream commit changed from ${recordedCommit} to ${currentCommit}.`, + 'Run parity tests and update docs/upstream/compatibility-baseline.json in the same change.', + ].join('\n'), + ); +} + +console.log('[upstream-compat] compatibility baseline matches pinned upstream commit.'); diff --git a/docs/upstream/compatibility-baseline.json b/docs/upstream/compatibility-baseline.json new file mode 100644 index 000000000..8032e6afe --- /dev/null +++ b/docs/upstream/compatibility-baseline.json @@ -0,0 +1,5 @@ +{ + "submodulePath": "upstream", + "pinnedCommit": "39685cf1aa58b5b11e90085bd32562fad61f4103", + "compatibilityContract": "This repository targets upstream/ at commit 39685cf1aa58b5b11e90085bd32562fad61f4103." +} diff --git a/package.json b/package.json index a4a79b8d2..268a5cb4c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "test-container-features-cli": "env TS_NODE_PROJECT=upstream/src/test/tsconfig.json mocha -r ts-node/register --exit upstream/src/test/container-features/featuresCLICommands.test.ts", "test-container-templates": "env TS_NODE_PROJECT=upstream/src/test/tsconfig.json mocha -r ts-node/register --exit upstream/src/test/container-templates/*.test.ts", "check-setup-separation": "node build/check-setup-separation.js", - "check-upstream-submodule": "node build/check-upstream-submodule.js" + "check-upstream-submodule": "node build/check-upstream-submodule.js", + "check-upstream-compatibility": "node build/check-upstream-compatibility.js" }, "files": [ "CHANGELOG.md", diff --git a/src/spec-node/migration/upstreamCompatibility.ts b/src/spec-node/migration/upstreamCompatibility.ts index 5cd8cfa9a..d26aafd86 100644 --- a/src/spec-node/migration/upstreamCompatibility.ts +++ b/src/spec-node/migration/upstreamCompatibility.ts @@ -29,3 +29,31 @@ export function resolvePinnedUpstreamCommit(options: ResolvePinnedUpstreamCommit export function formatUpstreamCompatibilityContract(commit: string) { return `This repository targets upstream/ at commit ${commit}.`; } + +export function formatUpstreamCommitTraceLine(commit: string) { + return `[upstream-compat] pinned upstream commit: ${commit}`; +} + +interface UpstreamCommitRegressionInput { + recordedCommit: string; + currentCommit: string; +} + +interface UpstreamCommitRegressionReport { + hasRegression: boolean; + summary: string; +} + +export function reportUpstreamCommitRegression(input: UpstreamCommitRegressionInput): UpstreamCommitRegressionReport { + if (input.recordedCommit === input.currentCommit) { + return { + hasRegression: false, + summary: `Pinned upstream commit unchanged at ${input.currentCommit}.`, + }; + } + + return { + hasRegression: true, + summary: `Pinned upstream commit changed from ${input.recordedCommit} to ${input.currentCommit}.`, + }; +} diff --git a/src/test/upstreamCompatibility.test.ts b/src/test/upstreamCompatibility.test.ts index 54e035691..b04a58bcb 100644 --- a/src/test/upstreamCompatibility.test.ts +++ b/src/test/upstreamCompatibility.test.ts @@ -1,7 +1,9 @@ import { expect } from 'chai'; import { + formatUpstreamCommitTraceLine, formatUpstreamCompatibilityContract, + reportUpstreamCommitRegression, resolvePinnedUpstreamCommit, } from '../spec-node/migration/upstreamCompatibility'; @@ -25,6 +27,31 @@ describe('upstream compatibility contract helpers', () => { ); }); + it('formats a traceable pinned commit log line', () => { + const line = formatUpstreamCommitTraceLine('0123456789abcdef0123456789abcdef01234567'); + expect(line).to.equal('[upstream-compat] pinned upstream commit: 0123456789abcdef0123456789abcdef01234567'); + }); + + it('reports a regression summary when pinned commit changes', () => { + const report = reportUpstreamCommitRegression({ + recordedCommit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + currentCommit: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }); + expect(report.hasRegression).to.equal(true); + expect(report.summary).to.equal( + 'Pinned upstream commit changed from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa to bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.', + ); + }); + + it('reports no regression when pinned commit is unchanged', () => { + const report = reportUpstreamCommitRegression({ + recordedCommit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + currentCommit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }); + expect(report.hasRegression).to.equal(false); + expect(report.summary).to.equal('Pinned upstream commit unchanged at aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.'); + }); + it('throws when git output is empty', () => { expect(() => resolvePinnedUpstreamCommit({ repositoryRoot: '/workspace/devcontainer-cli', From 2a7c3f201f3ee851c1d29ee17c9ff60ad9ce3c80 Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Fri, 3 Apr 2026 10:25:17 +0200 Subject: [PATCH 2/2] Read upstream gitlink from index in compatibility check --- build/check-upstream-compatibility.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/check-upstream-compatibility.js b/build/check-upstream-compatibility.js index 62513241f..b0d124fa3 100644 --- a/build/check-upstream-compatibility.js +++ b/build/check-upstream-compatibility.js @@ -19,7 +19,7 @@ function fail(message) { } function resolveCurrentPinnedCommit() { - return cp.execFileSync('git', ['rev-parse', `HEAD:${submodulePath}`], { + return cp.execFileSync('git', ['rev-parse', `:${submodulePath}`], { cwd: repositoryRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],