From cc0d8fc0163f5a62b286b38dcf6ba0f333c59465 Mon Sep 17 00:00:00 2001 From: Adrian Webb Date: Thu, 10 Sep 2026 05:13:04 -0400 Subject: [PATCH] fix: hash normalized component runtime metadata --- scripts/create-component-release.ts | 6 ++-- .../package/component-release.test.ts | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/contract/package/component-release.test.ts diff --git a/scripts/create-component-release.ts b/scripts/create-component-release.ts index a11c1360..73a1923e 100644 --- a/scripts/create-component-release.ts +++ b/scripts/create-component-release.ts @@ -1,7 +1,7 @@ import { createHash } from 'node:crypto'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { componentReleaseSchema, deploymentDigest } from '@treeseed/sdk/deployment'; +import { componentReleaseSchema, deploymentDigest, packageRuntimeSchema } from '@treeseed/sdk/deployment'; const release = process.env.TREESEED_RELEASE, sourceCommit = process.env.TREESEED_SOURCE_COMMIT; const adminDigest = process.env.TREESEED_ADMIN_DIGEST; @@ -13,7 +13,7 @@ const debianRelease = `${release.replace(/-rc\.(\d+)$/u, '~rc$1')}-${revision}`; const compose = readFileSync(resolve('deploy/compose.template.yml'), 'utf8').replace('@ADMIN_IMAGE@', `treeseed/admin@${adminDigest}`); if (/\bbuild\s*:/u.test(compose) || /@ADMIN_IMAGE@/u.test(compose)) throw new Error('Production Compose bundle is not fully materialized.'); const composeDigest = `sha256:${createHash('sha256').update(compose).digest('hex')}`; -const runtime = { +const runtime = packageRuntimeSchema.parse({ schemaVersion: 'treeseed.package-runtime/v1' as const, componentId: 'admin', version: debianRelease, compose: { projectName: 'treeseed-admin', files: [{ path: 'compose.yml', digest: composeDigest }] }, services: [{ id: 'admin', composeService: 'admin', endpoints: [{ id: 'http', protocol: 'http' as const, port: 4322, @@ -21,7 +21,7 @@ const runtime = { authentication: 'application' as const, healthGate: { protocol: 'http' as const, path: '/healthz', timeoutSeconds: 120 } }] }], stateVolumes: [], migrations: [], requiredCapabilities: ['docker-compose'], dependencies: [{ id: 'api', capability: 'control-plane-api', locality: 'either' as const, optional: false }], -}; +}); const tagUrl = `https://hub.docker.com/r/treeseed/admin/tags?name=${encodeURIComponent(release)}`; const bundle = componentReleaseSchema.parse({ schemaVersion: 'treeseed.component-release/v1', componentId: 'admin', release: debianRelease, applicationVersion: release, diff --git a/tests/contract/package/component-release.test.ts b/tests/contract/package/component-release.test.ts new file mode 100644 index 00000000..7cfcf684 --- /dev/null +++ b/tests/contract/package/component-release.test.ts @@ -0,0 +1,29 @@ +import { execFileSync } from 'node:child_process'; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { resolve } from 'node:path'; +import { expect, it } from 'vitest'; +import { componentReleaseSchema, deploymentDigest } from '@treeseed/sdk/deployment'; + +it('publishes a digest of the emitted schema-normalized component runtime', () => { + const root = mkdtempSync(resolve(tmpdir(), 'treeseed-component-proof-')); + try { + mkdirSync(resolve(root, 'deploy')); + copyFileSync(resolve('deploy/compose.template.yml'), resolve(root, 'deploy/compose.template.yml')); + const digest = `sha256:${'a'.repeat(64)}`; + execFileSync(process.execPath, ['--import', resolve('node_modules/tsx/dist/loader.mjs'), + resolve('scripts/create-component-release.ts')], { + cwd: root, stdio: 'pipe', + env: { PATH: process.env.PATH, TREESEED_RELEASE: '0.13.0-rc.1', + TREESEED_SOURCE_COMMIT: 'a'.repeat(40), TREESEED_ADMIN_DIGEST: digest, + TREESEED_MANAGER_DIGEST: digest, TREESEED_RUNNER_DIGEST: digest, + TREESEED_SANDBOX_BASE_DIGEST: digest, TREESEED_GUEST_DIGEST: digest }, + }); + const raw = JSON.parse(readFileSync(resolve(root, 'release-assets/component-release.json'), 'utf8')); + const parsed = componentReleaseSchema.parse(raw); + expect(raw.runtimeDigest).toBe(deploymentDigest(raw.runtime)); + expect(parsed.runtimeDigest).toBe(deploymentDigest(parsed.runtime)); + expect(componentReleaseSchema.parse(JSON.parse(JSON.stringify(parsed)))).toEqual(parsed); + expect(parsed.runtime.configuration).toBeDefined(); + } finally { rmSync(root, { recursive: true, force: true }); } +});