Kaynağa Gözat

feat(release): validate client build artifacts

imccyu 1 ay önce
ebeveyn
işleme
738dcced9b

+ 43 - 2
scripts/release/families.spec.ts

@@ -1,7 +1,10 @@
 /** Release family discovery, publish order, tag naming, and the bump judgements. */
 
-import { resolve } from 'node:path'
-import { describe, expect, it } from 'vitest'
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join, resolve } from 'node:path'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { officialClientBuildEnvironment, writeClientBuildRecord } from '../client-build-environment.ts'
 import { releaseFamily, type ReleaseMember } from './families.ts'
 import { compareVersions, nextVendorVersion, reachesPayload } from './bump.ts'
 
@@ -16,6 +19,27 @@ function member(directory: string, name: string, manifest: Record<string, unknow
   return { directory, name, version: '0.0.1', manifest }
 }
 
+const roots: string[] = []
+
+function write(path: string, content: string): void {
+  mkdirSync(dirname(path), { recursive: true })
+  writeFileSync(path, content)
+}
+
+function buildFixture(environment: Record<string, string>): string {
+  const root = mkdtempSync(join(tmpdir(), 'dsh-release-build-'))
+  roots.push(root)
+  write(join(root, 'apps/web/dist/index.html'), '<main></main>')
+  write(join(root, 'packages/client/example/lib/client.js'), 'module.exports = {}\n')
+  writeClientBuildRecord(root, environment)
+  return root
+}
+
+afterEach(() => {
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+  vi.unstubAllEnvs()
+})
+
 describe('release families', () => {
   it('excludes private experimental packages from the dsh release', () => {
     const members = releaseFamily('dsh').members(resolve(import.meta.dirname, '../..'))
@@ -57,6 +81,23 @@ describe('release families', () => {
     expect(() => { vendor.verifyVersions([{ ...members[0]!, version: 'latest' }]) }).toThrow(/unpublishable version/)
   })
 
+  it('requires a current official client build only for dsh artifacts', () => {
+    const dsh = releaseFamily('dsh')
+    const vendor = releaseFamily('vendor')
+    const officialEnvironment = officialClientBuildEnvironment(resolve(import.meta.dirname, '../..'))
+    vi.stubEnv('DSH_CLIENT_COMMIT_HASH', officialEnvironment.DSH_CLIENT_COMMIT_HASH)
+    const official = buildFixture(officialEnvironment)
+    const defaultBuild = buildFixture({})
+
+    expect(() => { dsh.verifyBuildArtifacts(official) }).not.toThrow()
+    expect(() => { dsh.verifyBuildArtifacts(defaultBuild) }).toThrow(/DSH_CLIENT_TITLE/)
+    expect(() => { dsh.verifyBuildArtifacts(join(defaultBuild, 'missing')) }).toThrow(/record.*missing/)
+    expect(() => { vendor.verifyBuildArtifacts(join(defaultBuild, 'missing')) }).not.toThrow()
+
+    write(join(official, 'packages/client/example/lib/client.js'), 'module.exports = { changed: true }\n')
+    expect(() => { dsh.verifyBuildArtifacts(official) }).toThrow(/artifacts differ/)
+  })
+
   it('publishes a dependency before its consumer, and orders ties by name', () => {
     const dsh = releaseFamily('dsh')
     const members = [

+ 16 - 0
scripts/release/families.ts

@@ -11,6 +11,10 @@
 
 import { globSync, readFileSync } from 'node:fs'
 import { resolve } from 'node:path'
+import {
+  officialClientBuildEnvironment,
+  readClientBuildRecord,
+} from '../client-build-environment.ts'
 import { validateTarballPayload } from '../publication-payload.ts'
 
 /**
@@ -111,6 +115,13 @@ export abstract class ReleaseFamily {
   /** Git tag prefix this family publishes from. */
   abstract readonly tagPrefix: string
 
+  /**
+   * Assert that built artifacts match this release family's required profile.
+   * Families without environment-selected artifacts accept every build tree.
+   * @param _root - repository root containing generated artifacts.
+   */
+  verifyBuildArtifacts(_root: string): void {}
+
   /**
    * Discover this family's members.
    * @param root - repository root.
@@ -311,6 +322,11 @@ class DshFamily extends ReleaseFamily {
   readonly patterns = ['packages/!(experimental)/*/package.json', 'apps/*/package.json'] as const
   readonly tagPrefix = 'dsh-v'
 
+  /** Require current artifacts from a complete official client build. */
+  override verifyBuildArtifacts(root: string): void {
+    readClientBuildRecord(root, officialClientBuildEnvironment(root))
+  }
+
   /**
    * Require one version across the family, the way a single tag can name it.
    * @param members - this family's members.

+ 1 - 0
scripts/release/pack.ts

@@ -46,6 +46,7 @@ function main(): void {
   const root = process.cwd()
   const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
   const members = family.publishOrder(family.members(root)).order
+  family.verifyBuildArtifacts(root)
   family.verifyVersions(members)
 
   rmSync(destination, { recursive: true, force: true })