b760c74f453b

check release output paths before any build runs, and pin the duplicate platform, the missing --notes value and the unchanged migration in tests (3.0.84)

time
2026-09-27 10:37 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
deepseek/deepseek-flash
session
koh 3 phases 6-8
parent
1278e421b5fc
7 files VERSION+1 −1src/build/build_test.v+1 −0src/migrate/migrate_test.v+16 −0src/release/release.v+11 −8src/release/release_test.v+30 −0tests/cli_test.v+4 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.831+3.0.84

view file

src/build/build_test.v+1 −0
@@ -192,6 +192,7 @@192192 		['output = a\nplatforms = macos-aarch64\n', 'has no command'],193193 		['command = true\noutput = a\n', 'names no platforms'],194194 		['command = true\noutput = a\nplatforms = macos-aarch64, amiga\n', 'amiga'],195+		['command = true\noutput = a\nplatforms = macos-aarch64, macos-aarch64\n', 'twice'],195196 		['command = true\noutput = a\nplatforms = macos-aarch64\nlang = cobol\n', 'cobol'],196197 	]197198 	for case in cases {

view file

src/migrate/migrate_test.v+16 −0
@@ -227,6 +227,22 @@227227 	assert !report.build_renamed228228 	assert !os.exists(os.join_path(fixture.dir, 'koh.build'))229229 	assert os.is_dir(os.join_path(fixture.dir, '.koh-v2'))230+	store := storage.open(fixture.dir)!231+	defer {232+		store.close()233+	}234+	head := history.head(store) or { panic('no head after migrating') }235+	// The migration carries Koh 2's ignore rules to a root .kohignore, so the236+	// tree differs from the imported head by exactly that file — which is what237+	// the report tells the operator to save.238+	saved := history.files_of(store, head.id)!239+	mut unsaved := []string{}240+	for file in snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))! {241+		if !saved.any(it.path == file.path) {242+			unsaved << file.path243+		}244+	}245+	assert unsaved == [snapshot.ignore_file]230246 }231247 232248 fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {

view file

src/release/release.v+11 −8
@@ -68,17 +68,20 @@6868 pub fn publish(store &storage.Store, mut backend remote.Backend, repo string,6969 	config build.Config, version string, notes string) !Published {7070 	head := history.head(store) or { return error(exchange.no_saves) }71-	mut built := []build.Built{}71+	// Every build runs before the first upload, so two platforms landing on one72+	// path would publish the second build's bytes for both. This is checked73+	// before anything runs, so a collision builds nothing at all.7274 	mut paths := map[string]bool{}7375 	for platform in config.platforms {74-		artifact := build.build_one(store.root, config, platform, version)!75-		// Every build happens before the first upload, so two platforms landing76-		// on one path would publish the second build's bytes for both.77-		if artifact.path in paths {78-			return error('two platforms build to the same file, ${artifact.path} — name the platform in ${build.config_name}\'s output, as "out/demo-{platform}" does')76+		path := build.output_path(config, platform, version)77+		if path in paths {78+			return error('two platforms build to the same file, ${path} — name the platform in ${build.config_name}\'s output, as "out/demo-{platform}" does')7979 		}80-		paths[artifact.path] = true81-		built << artifact80+		paths[path] = true81+	}82+	mut built := []build.Built{}83+	for platform in config.platforms {84+		built << build.build_one(store.root, config, platform, version)!8285 	}8386 	mut files := []File{}8487 	for artifact in built {

view file

src/release/release_test.v+30 −0
@@ -124,6 +124,36 @@124124 	assert backend.get(file_key('proj', '1.2.3', 'linux-aarch64', 'demo-1.2.3-linux-aarch64'))!.found125125 }126126 127+// Two platforms that build to one file are refused before anything runs: no128+// build, no upload, and the bucket is untouched.129+fn test_two_platforms_building_to_one_file_are_refused() {130+	base := scratch('collision')!131+	defer {132+		os.rmdir_all(base) or {}133+	}134+	mut store := open_repo('collision')!135+	defer {136+		store.close()137+	}138+	write(store.root, 'VERSION', '1.2.3\n')!139+	// No {platform}, so both platforms would write the same file.140+	write(store.root, 'koh.build', 'command = mkdir -p out && printf koh > out/demo141+output = out/demo142+platforms = macos-aarch64, linux-aarch64143+')!144+	config := build.read_config(store.root)!145+	make_save(store, 'first')!146+	mut backend := remote.new_folder(os.join_path(base, 'remote'))!147+148+	if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {149+		assert false, 'two platforms building to one file must be refused'150+	} else {151+		assert err.msg().contains('two platforms build to the same file')152+	}153+	assert !os.exists(os.join_path(store.root, 'out'))154+	assert backend.list('proj/releases')!.len == 0155+}156+127157 // A published version never changes: a second build with different bytes for128158 // the same platform is refused before anything is uploaded.129159 fn test_a_different_file_for_a_published_version_is_refused() {

view file

tests/cli_test.v+4 −0
@@ -812,6 +812,10 @@812812 	notes := 'a release note long enough to clear forty characters and the twelve word rule easily'813813 	assert run(dir, 'offer', '--notes', notes).code == 2814814 	assert run(dir, 'offer', '--notes', '').code == 2815+	// A value-taking option with nothing after it is a usage error too.816+	missing := run(dir, 'offer', '--notes')817+	assert missing.code == 2818+	assert missing.err.contains('needs a value')815819 	assert run(dir, 'offer', 'extra').code == 2816820 817821 	write(dir, 'VERSION', '1.2.3\n')

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.0.83'3+	version: '3.0.84'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file