From 696b56b75bf98ecc08711cf9e55081d877bd296a Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 25 Jun 2026 06:10:25 +0000 Subject: [PATCH] fix(unit-only): include harness Dockerfile content in deploy hash A content-only edit to a harness-referenced Dockerfile did not change the project deploy hash, so deploy/dev reported 'No changes detected' and the cloud Harness kept running the previously-built container image. computeProjectDeployHash now parses each harness.json, and when a dockerfile field is set, reads and hashes the referenced Dockerfile's content (under try/catch so a missing file degrades gracefully). Adds a unit test asserting a content-only Dockerfile change produces a different hash. --- .../deploy/__tests__/change-detection.test.ts | 22 +++++++++++++++++-- src/cli/operations/deploy/change-detection.ts | 10 +++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/cli/operations/deploy/__tests__/change-detection.test.ts b/src/cli/operations/deploy/__tests__/change-detection.test.ts index 028411d11..6bdd0c232 100644 --- a/src/cli/operations/deploy/__tests__/change-detection.test.ts +++ b/src/cli/operations/deploy/__tests__/change-detection.test.ts @@ -1,11 +1,14 @@ import { canSkipDeploy, computeProjectDeployHash } from '../change-detection'; import { describe, expect, it, vi } from 'vitest'; +let harnessJsonContent = '{"name":"h1","model":{"provider":"bedrock","modelId":"anthropic.claude-3"}}'; +let dockerfileContent = 'FROM python:3.12'; + vi.mock('node:fs/promises', () => ({ readFile: vi.fn().mockImplementation((path: string) => { - if (path.includes('harness.json')) - return Promise.resolve('{"name":"h1","model":{"provider":"bedrock","modelId":"anthropic.claude-3"}}'); + if (path.includes('harness.json')) return Promise.resolve(harnessJsonContent); if (path.includes('system-prompt.md')) return Promise.resolve('You are a helpful assistant.'); + if (path.includes('Dockerfile')) return Promise.resolve(dockerfileContent); return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })); }), })); @@ -52,6 +55,21 @@ describe('computeProjectDeployHash', () => { const hash2 = await computeProjectDeployHash(io2); expect(hash1).not.toBe(hash2); }); + + it('returns different hash when only the referenced Dockerfile content changes', async () => { + harnessJsonContent = '{"name":"h1","dockerfile":"Dockerfile"}'; + try { + const io = mockConfigIO({}); + dockerfileContent = 'FROM python:3.12'; + const hash1 = await computeProjectDeployHash(io); + dockerfileContent = 'FROM python:3.13'; + const hash2 = await computeProjectDeployHash(io); + expect(hash1).not.toBe(hash2); + } finally { + harnessJsonContent = '{"name":"h1","model":{"provider":"bedrock","modelId":"anthropic.claude-3"}}'; + dockerfileContent = 'FROM python:3.12'; + } + }); }); describe('canSkipDeploy', () => { diff --git a/src/cli/operations/deploy/change-detection.ts b/src/cli/operations/deploy/change-detection.ts index a3e520d0a..7638c635b 100644 --- a/src/cli/operations/deploy/change-detection.ts +++ b/src/cli/operations/deploy/change-detection.ts @@ -23,9 +23,11 @@ export async function computeProjectDeployHash(configIO: ConfigIO): Promise