Skip to content

Commit 8ca20e0

Browse files
committed
fix(release): support aube-only checkouts without package-lock.json
After the aube migration in #91 the repo no longer ships a `package-lock.json`, but `scripts/release-helpers.mjs` and `scripts/release-prep.mjs` still hardcoded it as a required file: - `readPackageVersions` did `readJsonFile('package-lock.json')` unconditionally and failed with ENOENT on the current main. - `assertPackageVersionsMatch` then asserted the lockfile's `version` and `packages[""].version` matched `package.json.version`. - `release-prep.mjs` froze `VERSION_FILE_PATHS` as `['package.json', 'package-lock.json']` at module scope and staged both, so even if the version-match assertion were relaxed, the staging step would still fail because `package-lock.json` does not exist. The net effect was that `npm run release:prep` (and, by inheritance, `npm run release:finalize`) failed end-to-end on `main`, blocking the documented release flow. This change makes the lockfile path optional: - `readPackageVersions` checks `existsSync('package-lock.json')`. When present it still parses + returns the lockfile version fields; otherwise it returns `hasPackageLock: false` with null lockfile versions and only validates `package.json`. - `assertPackageVersionsMatch` only runs the lockfile-coherence assertions when `hasPackageLock` is true. - `release-prep.mjs` computes the version-file allowlist at runtime via `resolveVersionFilePaths(root)`. On an aube-only checkout this resolves to `['package.json']`; on a checkout that still carries `package-lock.json` it stays at `['package.json', 'package-lock.json']`, preserving the prior behavior for downstream consumers that re-introduce an npm lockfile. `release-finalize.mjs` consumes `assertPackageVersionsMatch` and inherits the fix without changes. Tests ----- `createTempRepo` / `writePackageFiles` gain an `includePackageLock` option (default `true`), and two new integration test cases cover the aube-only path: - `prepares a release on an aube-only checkout (no package-lock.json)` asserts the prep commit's diff is exactly `['package.json']` and that `package.json.version` matches the requested release version. - `finalizes on an aube-only checkout (no package-lock.json)` asserts the annotated tag is created and pushed. The pre-existing npm-lockfile test cases are unchanged and continue to assert the two-file diff + readVersions triple. All 27 release-scripts tests pass. Format, lint, and typecheck are clean. Docs ---- `docs/RELEASE-PROCESS.md` is updated so the changelog-mode prose and the manual-prep fallback no longer claim that `package-lock.json` is always part of the prep commit; the fallback snippets now stage `package-lock.json` only when it actually exists. Change-Id: Idfb4bbdde5d222c4f0f9096d650bbbc2f6d5f9c9 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 9148c6a commit 8ca20e0

5 files changed

Lines changed: 151 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
- 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)).
2828
- `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)).
29+
- `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).
2930

3031
### Notes
3132

docs/RELEASE-PROCESS.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Versions containing a hyphen, such as `-beta.0` or `-rc.0`, are published by the
131131

132132
### Changelog mode
133133

134-
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.
134+
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.
135135

136136
```bash
137137
npm run release:prep -- --version <version> --changelog ci
@@ -174,12 +174,13 @@ and commits the resulting `CHANGELOG.md` update back to the release branch. When
174174

175175
### Manual prep fallback
176176

177-
If the scripted prep path is blocked, use the manual fallback only from a clean, up-to-date `main` checkout:
177+
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):
178178

179179
```bash
180180
git switch -c release/<version>
181181
npm version <version> --no-git-tag-version
182-
git add package.json package-lock.json
182+
git add package.json
183+
[[ -f package-lock.json ]] && git add package-lock.json
183184
git commit -m "chore(release): <version>"
184185
git push -u origin release/<version>
185186
gh pr create --base main --head release/<version> --title "chore(release): <version>"
@@ -189,7 +190,8 @@ For the local changelog variant, run Communique after `npm version ... --no-git-
189190

190191
```bash
191192
communique generate "v<version>" --changelog --repo coder/agent-tty
192-
git add package.json package-lock.json CHANGELOG.md
193+
git add package.json CHANGELOG.md
194+
[[ -f package-lock.json ]] && git add package-lock.json
193195
git commit -m "chore(release): <version>"
194196
```
195197

scripts/release-helpers.mjs

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -242,35 +242,48 @@ export function readPackageVersions(root = process.cwd()) {
242242
readJsonFile(join(resolvedRoot, 'package.json'), 'package.json'),
243243
'package.json',
244244
);
245-
const packageLock = assertPackageLike(
246-
readJsonFile(join(resolvedRoot, 'package-lock.json'), 'package-lock.json'),
247-
'package-lock.json',
248-
);
249-
250245
const packageVersion = assertString(
251246
packageJson.version,
252247
'package.json version',
253248
);
254-
const lockfileVersion = assertString(
255-
packageLock.version,
256-
'package-lock.json version',
257-
);
258-
const packages = assertPackageLike(
259-
packageLock.packages,
260-
'package-lock.json packages',
261-
);
262-
const rootPackage = assertPackageLike(
263-
packages[''],
264-
'package-lock.json packages[""]',
265-
);
266-
const lockRootVersion = assertString(
267-
rootPackage.version,
268-
'package-lock.json packages[""].version',
269-
);
249+
250+
// `package-lock.json` is optional: after the aube migration (PR #91) this
251+
// repo no longer carries one. When present, its version fields are asserted
252+
// for coherence with `package.json`; when absent, only `package.json` is
253+
// validated and dependency pinning is delegated to `aube-lock.yaml`.
254+
const packageLockPath = join(resolvedRoot, 'package-lock.json');
255+
const hasPackageLock = existsSync(packageLockPath);
256+
257+
let lockfileVersion = null;
258+
let lockRootVersion = null;
259+
260+
if (hasPackageLock) {
261+
const packageLock = assertPackageLike(
262+
readJsonFile(packageLockPath, 'package-lock.json'),
263+
'package-lock.json',
264+
);
265+
lockfileVersion = assertString(
266+
packageLock.version,
267+
'package-lock.json version',
268+
);
269+
const packages = assertPackageLike(
270+
packageLock.packages,
271+
'package-lock.json packages',
272+
);
273+
const rootPackage = assertPackageLike(
274+
packages[''],
275+
'package-lock.json packages[""]',
276+
);
277+
lockRootVersion = assertString(
278+
rootPackage.version,
279+
'package-lock.json packages[""].version',
280+
);
281+
}
270282

271283
return {
272284
packageName: assertString(packageJson.name, 'package.json name'),
273285
packageVersion,
286+
hasPackageLock,
274287
lockfileVersion,
275288
lockRootVersion,
276289
};
@@ -290,16 +303,18 @@ export function assertPackageVersionsMatch(
290303
'agent-tty',
291304
'package.json name must be agent-tty',
292305
);
293-
assert.equal(
294-
versions.lockfileVersion,
295-
versions.packageVersion,
296-
'package-lock.json version must match package.json version',
297-
);
298-
assert.equal(
299-
versions.lockRootVersion,
300-
versions.packageVersion,
301-
'package-lock.json packages[""].version must match package.json version',
302-
);
306+
if (versions.hasPackageLock) {
307+
assert.equal(
308+
versions.lockfileVersion,
309+
versions.packageVersion,
310+
'package-lock.json version must match package.json version',
311+
);
312+
assert.equal(
313+
versions.lockRootVersion,
314+
versions.packageVersion,
315+
'package-lock.json packages[""].version must match package.json version',
316+
);
317+
}
303318
if (expectedVersion !== null) {
304319
assert.equal(
305320
versions.packageVersion,

scripts/release-prep.mjs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
2-
import { resolve } from 'node:path';
2+
import { existsSync } from 'node:fs';
3+
import { join, resolve } from 'node:path';
34
import process from 'node:process';
45
import { fileURLToPath } from 'node:url';
56

@@ -25,14 +26,23 @@ import {
2526
stageFiles,
2627
} from './release-helpers.mjs';
2728

28-
const VERSION_FILE_PATHS = Object.freeze(['package.json', 'package-lock.json']);
29+
// `package-lock.json` is included in the version-file allowlist only when it
30+
// actually exists. After the aube migration (PR #91) the repo no longer ships
31+
// one, but the npm-lockfile path is still supported for downstream consumers
32+
// that re-introduce `package-lock.json`.
33+
function resolveVersionFilePaths(root) {
34+
return existsSync(join(root, 'package-lock.json'))
35+
? Object.freeze(['package.json', 'package-lock.json'])
36+
: Object.freeze(['package.json']);
37+
}
2938

3039
// The env override is intentionally scoped to external release tools
3140
// (release-it, Communique, and verification). Git operations use process.env
3241
// because the supported entrypoint is spawning this script with the desired env.
3342
export function releasePrep(argv = process.argv.slice(2), env = process.env) {
3443
const options = parsePrepArgs(argv);
3544
const root = assertRepoRoot(process.cwd());
45+
const versionFilePaths = resolveVersionFilePaths(root);
3646
const { packageVersion } = assertPackageVersionsMatch(root);
3747
assertTargetVersionIsGreater(packageVersion, options.version);
3848

@@ -53,24 +63,24 @@ export function releasePrep(argv = process.argv.slice(2), env = process.env) {
5363
if (options.changelog === 'local') {
5464
runCommunique(root, options.version, env);
5565
const changedFiles = assertAllowedChangedFiles(root, [
56-
...VERSION_FILE_PATHS,
66+
...versionFilePaths,
5767
'CHANGELOG.md',
5868
]);
5969
assertExpectedFilesChanged(changedFiles, [
60-
...VERSION_FILE_PATHS,
70+
...versionFilePaths,
6171
'CHANGELOG.md',
6272
]);
63-
stageFiles(root, [...VERSION_FILE_PATHS, 'CHANGELOG.md']);
73+
stageFiles(root, [...versionFilePaths, 'CHANGELOG.md']);
6474
} else {
6575
const changedFiles = assertAllowedChangedFiles(root, [
66-
...VERSION_FILE_PATHS,
76+
...versionFilePaths,
6777
'CHANGELOG.md',
6878
]);
6979
if (changedFiles.includes('CHANGELOG.md')) {
7080
throw new Error('CHANGELOG.md must not change when using --changelog ci');
7181
}
72-
assertExpectedFilesChanged(changedFiles, VERSION_FILE_PATHS);
73-
stageFiles(root, VERSION_FILE_PATHS);
82+
assertExpectedFilesChanged(changedFiles, versionFilePaths);
83+
stageFiles(root, versionFilePaths);
7484
}
7585

7686
createCommit(root, `chore(release): ${options.version}`);

test/integration/release-scripts.test.ts

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,36 +68,47 @@ function runGit(repo: string, args: string[]): string {
6868
return run('git', args, { cwd: repo, expectedStatus: 0 }).stdout.trim();
6969
}
7070

71-
function writePackageFiles(repo: string, version: string): void {
71+
function writePackageFiles(
72+
repo: string,
73+
version: string,
74+
{ includePackageLock = true }: { includePackageLock?: boolean } = {},
75+
): void {
7276
const packageJson = {
7377
name: 'agent-tty',
7478
version,
7579
type: 'module',
7680
private: true,
7781
};
78-
const packageLock = {
79-
name: 'agent-tty',
80-
version,
81-
lockfileVersion: 3,
82-
requires: true,
83-
packages: {
84-
'': {
85-
name: 'agent-tty',
86-
version,
87-
license: 'Apache-2.0',
88-
},
89-
},
90-
};
9182

9283
writeFileSync(join(repo, 'package.json'), `${JSON.stringify(packageJson)}\n`);
93-
writeFileSync(
94-
join(repo, 'package-lock.json'),
95-
`${JSON.stringify(packageLock)}\n`,
96-
);
84+
85+
if (includePackageLock) {
86+
const packageLock = {
87+
name: 'agent-tty',
88+
version,
89+
lockfileVersion: 3,
90+
requires: true,
91+
packages: {
92+
'': {
93+
name: 'agent-tty',
94+
version,
95+
license: 'Apache-2.0',
96+
},
97+
},
98+
};
99+
writeFileSync(
100+
join(repo, 'package-lock.json'),
101+
`${JSON.stringify(packageLock)}\n`,
102+
);
103+
}
104+
97105
writeFileSync(join(repo, 'CHANGELOG.md'), '# Changelog\n');
98106
}
99107

100-
function createTempRepo(version = '0.1.1-beta.4'): TempRepo {
108+
function createTempRepo(
109+
version = '0.1.1-beta.4',
110+
{ includePackageLock = true }: { includePackageLock?: boolean } = {},
111+
): TempRepo {
101112
const root = mkdtempSync(join(tmpdir(), 'agent-tty-release-scripts-'));
102113
tempRoots.push(root);
103114
const origin = join(root, 'origin.git');
@@ -108,8 +119,11 @@ function createTempRepo(version = '0.1.1-beta.4'): TempRepo {
108119
runGit(repo, ['remote', 'add', 'origin', origin]);
109120
runGit(repo, ['config', 'user.name', 'Agent TTY Test']);
110121
runGit(repo, ['config', 'user.email', 'agent-tty-test@example.invalid']);
111-
writePackageFiles(repo, version);
112-
runGit(repo, ['add', 'package.json', 'package-lock.json', 'CHANGELOG.md']);
122+
writePackageFiles(repo, version, { includePackageLock });
123+
const initialFiles = includePackageLock
124+
? ['package.json', 'package-lock.json', 'CHANGELOG.md']
125+
: ['package.json', 'CHANGELOG.md'];
126+
runGit(repo, ['add', ...initialFiles]);
113127
runGit(repo, ['commit', '-q', '-m', 'init']);
114128
runGit(repo, ['push', '-q', '-u', 'origin', 'main']);
115129

@@ -252,6 +266,39 @@ describe('release scripts', () => {
252266
]);
253267
});
254268

269+
it('prepares a release on an aube-only checkout (no package-lock.json)', () => {
270+
const { repo } = createTempRepo(undefined, { includePackageLock: false });
271+
272+
const result = runReleasePrep(repo, [
273+
'--version',
274+
'0.1.1-beta.5',
275+
'--changelog',
276+
'ci',
277+
]);
278+
279+
expect(result.status).toBe(0);
280+
expect(result.stdout).toContain(
281+
'Release prep commit created on release/0.1.1-beta.5.',
282+
);
283+
expect(runGit(repo, ['branch', '--show-current'])).toBe(
284+
'release/0.1.1-beta.5',
285+
);
286+
expect(runGit(repo, ['status', '--short'])).toBe('');
287+
expect(runGit(repo, ['rev-list', '--count', 'origin/main..HEAD'])).toBe(
288+
'1',
289+
);
290+
expect(runGit(repo, ['show', '-s', '--format=%s', 'HEAD'])).toBe(
291+
'chore(release): 0.1.1-beta.5',
292+
);
293+
expect(
294+
runGit(repo, ['diff', '--name-only', 'HEAD^..HEAD']).split('\n'),
295+
).toEqual(['package.json']);
296+
const packageJson = JSON.parse(
297+
readFileSync(join(repo, 'package.json'), 'utf8'),
298+
) as { version: string };
299+
expect(packageJson.version).toBe('0.1.1-beta.5');
300+
});
301+
255302
it('prepares a release from a repo root reached through a symlink', () => {
256303
const { root, repo } = createTempRepo();
257304
const linkedRepo = join(root, 'repo-link');
@@ -622,6 +669,18 @@ if (changedFiles.includes('CHANGELOG.md')) {
622669
).toContain('refs/tags/v0.1.1-beta.5');
623670
});
624671

672+
it('finalizes on an aube-only checkout (no package-lock.json)', () => {
673+
const { repo } = createTempRepo('0.1.1-beta.5', {
674+
includePackageLock: false,
675+
});
676+
677+
const result = runReleaseFinalize(repo);
678+
679+
expect(result.status).toBe(0);
680+
expect(result.stdout).toContain('Release tag v0.1.1-beta.5 pushed.');
681+
expect(runGit(repo, ['tag', '--list'])).toBe('v0.1.1-beta.5');
682+
});
683+
625684
it('refuses to finalize from a dirty tree', () => {
626685
const { repo } = createTempRepo('0.1.1-beta.5');
627686
writeFileSync(join(repo, 'CHANGELOG.md'), '# Changelog\n\nDirty work\n');

0 commit comments

Comments
 (0)