diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a02dbd..7cd739d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Default-location screenshot PNGs, snapshot JSON files, and `record export` artifacts are now rolled back when the subsequent artifact-manifest append fails, so a manifest-validation failure no longer leaves an orphaned, unmanifested file under the session's `artifacts/` directory. Explicit `--out` paths supplied by the caller are preserved on failure because they belong to the user, not the session manifest ([#95](https://github.com/coder/agent-tty/pull/95), fixes [#79](https://github.com/coder/agent-tty/issues/79)). - `EventLog.open` now closes the underlying file handle when validation (size-limit check or existing-content parsing) fails, preventing a file-descriptor leak on rejected session host startup ([#51](https://github.com/coder/agent-tty/pull/51)). +- `npm run release:prep` and `npm run release:finalize` now work on aube-only checkouts where `package-lock.json` does not exist. `readPackageVersions` / `assertPackageVersionsMatch` skip the lockfile-coherence assertions when `package-lock.json` is absent, and `release-prep.mjs` stages only `package.json` in that case. The npm-lockfile path is still fully supported when a `package-lock.json` is present. Without this fix, the documented release flow was broken after the `aube` migration in [#91](https://github.com/coder/agent-tty/pull/91). ### Notes diff --git a/docs/RELEASE-PROCESS.md b/docs/RELEASE-PROCESS.md index a1d87aa..2908176 100644 --- a/docs/RELEASE-PROCESS.md +++ b/docs/RELEASE-PROCESS.md @@ -131,7 +131,7 @@ Versions containing a hyphen, such as `-beta.0` or `-rc.0`, are published by the ### Changelog mode -Use `--changelog ci` for the default maintainer path. The prep commit will contain only `package.json` and `package-lock.json`; the `Release Changelog` workflow will update `CHANGELOG.md` on the release branch when needed. +Use `--changelog ci` for the default maintainer path. The prep commit will contain only the version files — `package.json` plus `package-lock.json` when present (after PR #91 this repo uses `aube-lock.yaml` instead, so the prep commit on the default branch contains only `package.json`). The `Release Changelog` workflow will update `CHANGELOG.md` on the release branch when needed. ```bash npm run release:prep -- --version --changelog ci @@ -174,12 +174,13 @@ and commits the resulting `CHANGELOG.md` update back to the release branch. When ### Manual prep fallback -If the scripted prep path is blocked, use the manual fallback only from a clean, up-to-date `main` checkout: +If the scripted prep path is blocked, use the manual fallback only from a clean, up-to-date `main` checkout. Stage `package-lock.json` only if your checkout still has one (post-PR #91 the repo is aube-only and the file is absent): ```bash git switch -c release/ npm version --no-git-tag-version -git add package.json package-lock.json +git add package.json +[[ -f package-lock.json ]] && git add package-lock.json git commit -m "chore(release): " git push -u origin release/ gh pr create --base main --head release/ --title "chore(release): " @@ -189,7 +190,8 @@ For the local changelog variant, run Communique after `npm version ... --no-git- ```bash communique generate "v" --changelog --repo coder/agent-tty -git add package.json package-lock.json CHANGELOG.md +git add package.json CHANGELOG.md +[[ -f package-lock.json ]] && git add package-lock.json git commit -m "chore(release): " ``` diff --git a/scripts/release-helpers.mjs b/scripts/release-helpers.mjs index 50337d8..1225940 100644 --- a/scripts/release-helpers.mjs +++ b/scripts/release-helpers.mjs @@ -242,35 +242,48 @@ export function readPackageVersions(root = process.cwd()) { readJsonFile(join(resolvedRoot, 'package.json'), 'package.json'), 'package.json', ); - const packageLock = assertPackageLike( - readJsonFile(join(resolvedRoot, 'package-lock.json'), 'package-lock.json'), - 'package-lock.json', - ); - const packageVersion = assertString( packageJson.version, 'package.json version', ); - const lockfileVersion = assertString( - packageLock.version, - 'package-lock.json version', - ); - const packages = assertPackageLike( - packageLock.packages, - 'package-lock.json packages', - ); - const rootPackage = assertPackageLike( - packages[''], - 'package-lock.json packages[""]', - ); - const lockRootVersion = assertString( - rootPackage.version, - 'package-lock.json packages[""].version', - ); + + // `package-lock.json` is optional: after the aube migration (PR #91) this + // repo no longer carries one. When present, its version fields are asserted + // for coherence with `package.json`; when absent, only `package.json` is + // validated and dependency pinning is delegated to `aube-lock.yaml`. + const packageLockPath = join(resolvedRoot, 'package-lock.json'); + const hasPackageLock = existsSync(packageLockPath); + + let lockfileVersion = null; + let lockRootVersion = null; + + if (hasPackageLock) { + const packageLock = assertPackageLike( + readJsonFile(packageLockPath, 'package-lock.json'), + 'package-lock.json', + ); + lockfileVersion = assertString( + packageLock.version, + 'package-lock.json version', + ); + const packages = assertPackageLike( + packageLock.packages, + 'package-lock.json packages', + ); + const rootPackage = assertPackageLike( + packages[''], + 'package-lock.json packages[""]', + ); + lockRootVersion = assertString( + rootPackage.version, + 'package-lock.json packages[""].version', + ); + } return { packageName: assertString(packageJson.name, 'package.json name'), packageVersion, + hasPackageLock, lockfileVersion, lockRootVersion, }; @@ -290,16 +303,18 @@ export function assertPackageVersionsMatch( 'agent-tty', 'package.json name must be agent-tty', ); - assert.equal( - versions.lockfileVersion, - versions.packageVersion, - 'package-lock.json version must match package.json version', - ); - assert.equal( - versions.lockRootVersion, - versions.packageVersion, - 'package-lock.json packages[""].version must match package.json version', - ); + if (versions.hasPackageLock) { + assert.equal( + versions.lockfileVersion, + versions.packageVersion, + 'package-lock.json version must match package.json version', + ); + assert.equal( + versions.lockRootVersion, + versions.packageVersion, + 'package-lock.json packages[""].version must match package.json version', + ); + } if (expectedVersion !== null) { assert.equal( versions.packageVersion, diff --git a/scripts/release-prep.mjs b/scripts/release-prep.mjs index abfdacb..b255882 100755 --- a/scripts/release-prep.mjs +++ b/scripts/release-prep.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node -import { resolve } from 'node:path'; +import { existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -25,7 +26,15 @@ import { stageFiles, } from './release-helpers.mjs'; -const VERSION_FILE_PATHS = Object.freeze(['package.json', 'package-lock.json']); +// `package-lock.json` is included in the version-file allowlist only when it +// actually exists. After the aube migration (PR #91) the repo no longer ships +// one, but the npm-lockfile path is still supported for downstream consumers +// that re-introduce `package-lock.json`. +function resolveVersionFilePaths(root) { + return existsSync(join(root, 'package-lock.json')) + ? Object.freeze(['package.json', 'package-lock.json']) + : Object.freeze(['package.json']); +} // The env override is intentionally scoped to external release tools // (release-it, Communique, and verification). Git operations use process.env @@ -33,6 +42,7 @@ const VERSION_FILE_PATHS = Object.freeze(['package.json', 'package-lock.json']); export function releasePrep(argv = process.argv.slice(2), env = process.env) { const options = parsePrepArgs(argv); const root = assertRepoRoot(process.cwd()); + const versionFilePaths = resolveVersionFilePaths(root); const { packageVersion } = assertPackageVersionsMatch(root); assertTargetVersionIsGreater(packageVersion, options.version); @@ -53,24 +63,24 @@ export function releasePrep(argv = process.argv.slice(2), env = process.env) { if (options.changelog === 'local') { runCommunique(root, options.version, env); const changedFiles = assertAllowedChangedFiles(root, [ - ...VERSION_FILE_PATHS, + ...versionFilePaths, 'CHANGELOG.md', ]); assertExpectedFilesChanged(changedFiles, [ - ...VERSION_FILE_PATHS, + ...versionFilePaths, 'CHANGELOG.md', ]); - stageFiles(root, [...VERSION_FILE_PATHS, 'CHANGELOG.md']); + stageFiles(root, [...versionFilePaths, 'CHANGELOG.md']); } else { const changedFiles = assertAllowedChangedFiles(root, [ - ...VERSION_FILE_PATHS, + ...versionFilePaths, 'CHANGELOG.md', ]); if (changedFiles.includes('CHANGELOG.md')) { throw new Error('CHANGELOG.md must not change when using --changelog ci'); } - assertExpectedFilesChanged(changedFiles, VERSION_FILE_PATHS); - stageFiles(root, VERSION_FILE_PATHS); + assertExpectedFilesChanged(changedFiles, versionFilePaths); + stageFiles(root, versionFilePaths); } createCommit(root, `chore(release): ${options.version}`); diff --git a/test/integration/release-scripts.test.ts b/test/integration/release-scripts.test.ts index 802b592..3b1253c 100644 --- a/test/integration/release-scripts.test.ts +++ b/test/integration/release-scripts.test.ts @@ -68,36 +68,47 @@ function runGit(repo: string, args: string[]): string { return run('git', args, { cwd: repo, expectedStatus: 0 }).stdout.trim(); } -function writePackageFiles(repo: string, version: string): void { +function writePackageFiles( + repo: string, + version: string, + { includePackageLock = true }: { includePackageLock?: boolean } = {}, +): void { const packageJson = { name: 'agent-tty', version, type: 'module', private: true, }; - const packageLock = { - name: 'agent-tty', - version, - lockfileVersion: 3, - requires: true, - packages: { - '': { - name: 'agent-tty', - version, - license: 'Apache-2.0', - }, - }, - }; writeFileSync(join(repo, 'package.json'), `${JSON.stringify(packageJson)}\n`); - writeFileSync( - join(repo, 'package-lock.json'), - `${JSON.stringify(packageLock)}\n`, - ); + + if (includePackageLock) { + const packageLock = { + name: 'agent-tty', + version, + lockfileVersion: 3, + requires: true, + packages: { + '': { + name: 'agent-tty', + version, + license: 'Apache-2.0', + }, + }, + }; + writeFileSync( + join(repo, 'package-lock.json'), + `${JSON.stringify(packageLock)}\n`, + ); + } + writeFileSync(join(repo, 'CHANGELOG.md'), '# Changelog\n'); } -function createTempRepo(version = '0.1.1-beta.4'): TempRepo { +function createTempRepo( + version = '0.1.1-beta.4', + { includePackageLock = true }: { includePackageLock?: boolean } = {}, +): TempRepo { const root = mkdtempSync(join(tmpdir(), 'agent-tty-release-scripts-')); tempRoots.push(root); const origin = join(root, 'origin.git'); @@ -108,8 +119,11 @@ function createTempRepo(version = '0.1.1-beta.4'): TempRepo { runGit(repo, ['remote', 'add', 'origin', origin]); runGit(repo, ['config', 'user.name', 'Agent TTY Test']); runGit(repo, ['config', 'user.email', 'agent-tty-test@example.invalid']); - writePackageFiles(repo, version); - runGit(repo, ['add', 'package.json', 'package-lock.json', 'CHANGELOG.md']); + writePackageFiles(repo, version, { includePackageLock }); + const initialFiles = includePackageLock + ? ['package.json', 'package-lock.json', 'CHANGELOG.md'] + : ['package.json', 'CHANGELOG.md']; + runGit(repo, ['add', ...initialFiles]); runGit(repo, ['commit', '-q', '-m', 'init']); runGit(repo, ['push', '-q', '-u', 'origin', 'main']); @@ -252,6 +266,39 @@ describe('release scripts', () => { ]); }); + it('prepares a release on an aube-only checkout (no package-lock.json)', () => { + const { repo } = createTempRepo(undefined, { includePackageLock: false }); + + const result = runReleasePrep(repo, [ + '--version', + '0.1.1-beta.5', + '--changelog', + 'ci', + ]); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + 'Release prep commit created on release/0.1.1-beta.5.', + ); + expect(runGit(repo, ['branch', '--show-current'])).toBe( + 'release/0.1.1-beta.5', + ); + expect(runGit(repo, ['status', '--short'])).toBe(''); + expect(runGit(repo, ['rev-list', '--count', 'origin/main..HEAD'])).toBe( + '1', + ); + expect(runGit(repo, ['show', '-s', '--format=%s', 'HEAD'])).toBe( + 'chore(release): 0.1.1-beta.5', + ); + expect( + runGit(repo, ['diff', '--name-only', 'HEAD^..HEAD']).split('\n'), + ).toEqual(['package.json']); + const packageJson = JSON.parse( + readFileSync(join(repo, 'package.json'), 'utf8'), + ) as { version: string }; + expect(packageJson.version).toBe('0.1.1-beta.5'); + }); + it('prepares a release from a repo root reached through a symlink', () => { const { root, repo } = createTempRepo(); const linkedRepo = join(root, 'repo-link'); @@ -622,6 +669,18 @@ if (changedFiles.includes('CHANGELOG.md')) { ).toContain('refs/tags/v0.1.1-beta.5'); }); + it('finalizes on an aube-only checkout (no package-lock.json)', () => { + const { repo } = createTempRepo('0.1.1-beta.5', { + includePackageLock: false, + }); + + const result = runReleaseFinalize(repo); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('Release tag v0.1.1-beta.5 pushed.'); + expect(runGit(repo, ['tag', '--list'])).toBe('v0.1.1-beta.5'); + }); + it('refuses to finalize from a dirty tree', () => { const { repo } = createTempRepo('0.1.1-beta.5'); writeFileSync(join(repo, 'CHANGELOG.md'), '# Changelog\n\nDirty work\n');