-
Notifications
You must be signed in to change notification settings - Fork 1
review: keep the pinned review.md ref in sync with the released version #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "review": patch | ||
| --- | ||
|
|
||
| Keep the pinned `ref:` inside review.md in sync with the released version. The release flow's version command now runs `utils/sync-review-version.ts` after `changeset version`, rewriting every `review-v<semver>` literal in review.md to the version being released so the bump lands in the same Version Packages commit that gets tagged; `workflows/review/version-sync.test.ts` fails CI whenever the ref and the `review` package version diverge. Also bumps the currently stale ref from review-v1.2.2 to review-v1.4.0 (v1.3.0 through v1.4.0 shipped with the lagging ref, so an un-overridden consumer checked out a lib without `lib/provenance.ts`). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import {describe, expect, it, vi} from "vitest"; | ||
| import { | ||
| syncReviewVersion, | ||
| syncReviewVersionContent, | ||
| } from "./sync-review-version-lib.ts"; | ||
|
|
||
| describe("syncReviewVersionContent", () => { | ||
| it("rewrites the pinned checkout ref to the given version", () => { | ||
| // Arrange: a stale ref, as shipped in review-v1.3.0 through v1.4.0. | ||
| const content = [ | ||
| "pre-agent-steps:", | ||
| " - uses: actions/checkout@abc # v5", | ||
| " with:", | ||
| " repository: Khan/actions", | ||
| " ref: review-v1.2.2", | ||
| ].join("\n"); | ||
|
|
||
| // Act | ||
| const result = syncReviewVersionContent(content, "9.9.9"); | ||
|
|
||
| // Assert | ||
| expect(result.replaced).toBe(1); | ||
| expect(result.content).toContain("ref: review-v9.9.9"); | ||
| expect(result.content).not.toContain("review-v1.2.2"); | ||
| }); | ||
|
|
||
| it("rewrites every semver literal but leaves templates alone", () => { | ||
| // Arrange | ||
| const content = | ||
| "ref: review-v1.2.2\n" + | ||
| "the marker is `v=review-v<version>` (a template)\n" + | ||
| "released as review-v1.3.0\n"; | ||
|
|
||
| // Act | ||
| const result = syncReviewVersionContent(content, "2.0.0"); | ||
|
|
||
| // Assert | ||
| expect(result.replaced).toBe(2); | ||
| expect(result.content).toBe( | ||
| "ref: review-v2.0.0\n" + | ||
| "the marker is `v=review-v<version>` (a template)\n" + | ||
| "released as review-v2.0.0\n", | ||
| ); | ||
| }); | ||
|
|
||
| it("returns unchanged content when already at the version", () => { | ||
| // Act | ||
| const result = syncReviewVersionContent("ref: review-v1.4.0", "1.4.0"); | ||
|
|
||
| // Assert | ||
| expect(result.replaced).toBe(1); | ||
| expect(result.content).toBe("ref: review-v1.4.0"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("syncReviewVersion", () => { | ||
| const makeDeps = (files: Record<string, string>) => { | ||
| const writes: Record<string, string> = {}; | ||
| return { | ||
| writes, | ||
| deps: { | ||
| readFileSyncImpl: ((p: string) => { | ||
| const content = files[p]; | ||
| if (content === undefined) { | ||
| throw new Error(`unexpected read: ${p}`); | ||
| } | ||
| return content; | ||
| }) as never, | ||
| writeFileSyncImpl: ((p: string, data: string) => { | ||
| writes[p] = data; | ||
| }) as never, | ||
| log: vi.fn(), | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| it("writes review.md with the package version substituted", () => { | ||
| // Arrange | ||
| const {writes, deps} = makeDeps({ | ||
| "workflows/review/package.json": | ||
| '{"name": "review", "version": "1.5.0"}', | ||
| "workflows/review/review.md": " ref: review-v1.2.2\n", | ||
| }); | ||
|
|
||
| // Act | ||
| syncReviewVersion(deps); | ||
|
|
||
| // Assert | ||
| expect(writes["workflows/review/review.md"]).toBe( | ||
| " ref: review-v1.5.0\n", | ||
| ); | ||
| }); | ||
|
|
||
| it("does not write when the ref is already current", () => { | ||
| // Arrange | ||
| const {writes, deps} = makeDeps({ | ||
| "workflows/review/package.json": | ||
| '{"name": "review", "version": "1.5.0"}', | ||
| "workflows/review/review.md": " ref: review-v1.5.0\n", | ||
| }); | ||
|
|
||
| // Act | ||
| syncReviewVersion(deps); | ||
|
|
||
| // Assert | ||
| expect(writes).toEqual({}); | ||
| }); | ||
|
|
||
| it("throws when review.md has no review-v literal to sync", () => { | ||
| // Arrange | ||
| const {deps} = makeDeps({ | ||
| "workflows/review/package.json": | ||
| '{"name": "review", "version": "1.5.0"}', | ||
| "workflows/review/review.md": "no pinned ref here\n", | ||
| }); | ||
|
|
||
| // Act + Assert | ||
| expect(() => syncReviewVersion(deps)).toThrow( | ||
| /No review-v<semver> literals/, | ||
| ); | ||
| }); | ||
|
|
||
| it("throws when package.json has no version", () => { | ||
| // Arrange | ||
| const {deps} = makeDeps({ | ||
| "workflows/review/package.json": '{"name": "review"}', | ||
| "workflows/review/review.md": "ref: review-v1.2.2\n", | ||
| }); | ||
|
|
||
| // Act + Assert | ||
| expect(() => syncReviewVersion(deps)).toThrow(/No version found/); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import * as fs from "fs"; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we make this generic so that it updates any workflow? We're bound to have more shared workflows so we might as well handle this in a single way.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 3eafaab: the script is now utils/sync-workflow-versions.ts. It discovers every directory under workflows/ (the tag is named after the directory, matching publishWorkflow in utils/publish.ts), reads its package.json version, and rewrites every |
||
|
|
||
| /** | ||
| * Keep the released-version literals inside workflows/review/review.md in | ||
| * sync with the "review" package version. | ||
| * | ||
| * review.md checks out Khan/actions at a pinned `ref: review-v<version>` tag | ||
| * to fetch the deterministic lib the prompt invokes at runtime; that ref is | ||
| * supposed to name the release the file ships in. Changesets bumps | ||
| * package.json but knows nothing about prose in a markdown file, so this | ||
| * script runs alongside `changeset version` (see the root package.json | ||
| * "version-packages" script, used by .github/workflows/release.yml) to | ||
| * rewrite every review-v<semver> literal in review.md to the version being | ||
| * released. That way the bump lands in the same Version Packages commit that | ||
| * gets tagged, and the tag's review.md pins its own release. | ||
| * | ||
| * workflows/review/version-sync.test.ts is the CI backstop: it fails when | ||
| * the literals and the package version diverge. | ||
| */ | ||
|
|
||
| export const REVIEW_MD_PATH = "workflows/review/review.md"; | ||
| export const REVIEW_PKG_PATH = "workflows/review/package.json"; | ||
|
|
||
| export const REVIEW_VERSION_RE = /review-v\d+\.\d+\.\d+/g; | ||
|
|
||
| export const syncReviewVersionContent = ( | ||
| content: string, | ||
| version: string, | ||
| ): {content: string; replaced: number} => { | ||
| let replaced = 0; | ||
| const next = content.replace(REVIEW_VERSION_RE, () => { | ||
| replaced += 1; | ||
| return `review-v${version}`; | ||
| }); | ||
| return {content: next, replaced}; | ||
| }; | ||
|
|
||
| export type SyncReviewVersionDeps = { | ||
| readFileSyncImpl?: typeof fs.readFileSync; | ||
| writeFileSyncImpl?: typeof fs.writeFileSync; | ||
| log?: (message: string) => void; | ||
| }; | ||
|
|
||
| export const syncReviewVersion = (deps: SyncReviewVersionDeps = {}): void => { | ||
| const readFileSyncImpl = deps.readFileSyncImpl ?? fs.readFileSync; | ||
| const writeFileSyncImpl = deps.writeFileSyncImpl ?? fs.writeFileSync; | ||
| const log = deps.log ?? console.log; | ||
|
|
||
| const pkg = JSON.parse(readFileSyncImpl(REVIEW_PKG_PATH, "utf-8")); | ||
| if (!pkg.version) { | ||
| throw new Error(`No version found in ${REVIEW_PKG_PATH}`); | ||
| } | ||
|
|
||
| const content = readFileSyncImpl(REVIEW_MD_PATH, "utf-8"); | ||
| const {content: synced, replaced} = syncReviewVersionContent( | ||
| content, | ||
| pkg.version, | ||
| ); | ||
|
|
||
| if (replaced === 0) { | ||
| // At minimum the pinned-checkout `ref:` line must match; finding | ||
| // nothing means the file was restructured and the sync went blind. | ||
| throw new Error( | ||
| `No review-v<semver> literals found in ${REVIEW_MD_PATH}; ` + | ||
| `expected at least the pinned checkout ref line.`, | ||
| ); | ||
| } | ||
|
|
||
| if (synced === content) { | ||
| log(`${REVIEW_MD_PATH}: already at review-v${pkg.version}`); | ||
| return; | ||
| } | ||
|
|
||
| writeFileSyncImpl(REVIEW_MD_PATH, synced); | ||
| log( | ||
| `${REVIEW_MD_PATH}: synced ${replaced} review-v literal(s) ` + | ||
| `to review-v${pkg.version}`, | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /** | ||
| * Rewrite the review-v<version> literals in workflows/review/review.md (the | ||
| * pinned Khan/actions checkout ref) to the current "review" package version. | ||
| * | ||
| * Runs as part of the changesets version command (root package.json | ||
| * "version-packages", wired into .github/workflows/release.yml) so the bump | ||
| * lands in the same Version Packages commit that gets tagged. | ||
| * | ||
| * Usage: node -r @swc-node/register utils/sync-review-version.ts | ||
| */ | ||
| import {syncReviewVersion} from "./sync-review-version-lib.ts"; | ||
|
|
||
| syncReviewVersion(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * CI backstop for the review.md version surface. | ||
| * | ||
| * review.md checks out Khan/actions at a pinned `ref: review-v<version>` tag | ||
| * to fetch the lib code the prompt invokes at runtime; that ref must name the | ||
| * release the file ships in, or consumers get a prompt from one version | ||
| * running code from another (review-v1.3.0 through v1.4.0 shipped this way, | ||
| * still pointing at v1.2.2). The release flow keeps the ref true by running | ||
| * utils/sync-review-version.ts alongside `changeset version`; this test fails | ||
| * any PR (the Version Packages PR included) where the literals in review.md | ||
| * do not match the "review" package version. | ||
| */ | ||
| import * as fs from "fs"; | ||
| import {describe, expect, it} from "vitest"; | ||
|
|
||
| const reviewMd = fs.readFileSync( | ||
| new URL("./review.md", import.meta.url), | ||
| "utf-8", | ||
| ); | ||
| const pkg = JSON.parse( | ||
| fs.readFileSync(new URL("./package.json", import.meta.url), "utf-8"), | ||
| ); | ||
|
|
||
| describe("review.md version surface", () => { | ||
| it("pins the Khan/actions checkout ref to this release's version", () => { | ||
| const refs = [...reviewMd.matchAll(/^\s*ref:\s*(\S+)\s*$/gm)].map( | ||
| (m) => m[1], | ||
| ); | ||
| expect(refs).toEqual([`review-v${pkg.version}`]); | ||
| }); | ||
|
|
||
| it("matches every review-v<semver> literal to the package version", () => { | ||
| const literals = reviewMd.match(/review-v\d+\.\d+\.\d+/g) ?? []; | ||
| expect(literals.length).toBeGreaterThan(0); | ||
| expect(new Set(literals)).toEqual(new Set([`review-v${pkg.version}`])); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded this comment as part of making the sync generic; it now leads with what version-packages does rather than the "Not the default" phrasing.