fix: write reference sources before fingerprints.json, atomic on failure

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 02:59:27 +02:00
co-authored by Claude Opus 5.5
parent ed806a1cc8
commit 4b619d12e9
2 changed files with 45 additions and 10 deletions
+11 -3
View File
@@ -172,13 +172,21 @@ final class DiviCheckCommand
return 1; return 1;
} }
$sourceDir = ($this->sourceDir ?? DiviReference::sourceDir(...))();
try {
// Sources first, fingerprints.json last: the fingerprints decide the release gate, so a write that
// fails before this last one leaves the gate exactly as it was.
foreach ($sourceDir === null ? [] : $prints as $id => $print) {
DiviReference::writeSource($id, $print->source, $sourceDir);
}
DiviReference::writeExpected( DiviReference::writeExpected(
array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints), array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints),
$this->expectedFile $this->expectedFile
); );
$sourceDir = ($this->sourceDir ?? DiviReference::sourceDir(...))(); } catch (RuntimeException $e) {
foreach ($sourceDir === null ? [] : $prints as $id => $print) { ($this->out)('Referenz nicht vollständig aktualisiert: ' . $e->getMessage());
DiviReference::writeSource($id, $print->source, $sourceDir);
return 1;
} }
delete_transient(ReleaseResolver::CACHE); delete_transient(ReleaseResolver::CACHE);
ReleaseGate::forget(); ReleaseGate::forget();
+30 -3
View File
@@ -44,8 +44,12 @@ final class DiviCheckCommandTest extends TestCase
parent::tearDown(); parent::tearDown();
} }
/** @param array<string, list<string>> $problems Step key => problems. */ /**
private function command(array $problems = []): DiviCheckCommand * @param array<string, list<string>> $problems Step key => problems.
* @param bool $expectSave True asserts save() is called exactly once (the green happy path); otherwise it is
* merely allowed, since some tests (e.g. no admin) never reach the save() call.
*/
private function command(array $problems = [], bool $expectSave = false): DiviCheckCommand
{ {
$steps = array_map( $steps = array_map(
static fn (string $key): CheckStep => new CheckStep($key, $problems[$key] ?? []), static fn (string $key): CheckStep => new CheckStep($key, $problems[$key] ?? []),
@@ -54,7 +58,11 @@ final class DiviCheckCommandTest extends TestCase
$check = Mockery::mock(CompatibilityCheck::class); $check = Mockery::mock(CompatibilityCheck::class);
$check->allows('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', $steps)); $check->allows('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', $steps));
$reports = Mockery::mock(CheckReportStore::class); $reports = Mockery::mock(CheckReportStore::class);
if ($expectSave) {
$reports->expects('save')->once();
} else {
$reports->allows('save'); $reports->allows('save');
}
return new DiviCheckCommand( return new DiviCheckCommand(
static fn () => $check, static fn () => $check,
@@ -78,7 +86,7 @@ final class DiviCheckCommandTest extends TestCase
public function testPrintsTheStepsAndSucceedsWhenGreen(): void public function testPrintsTheStepsAndSucceedsWhenGreen(): void
{ {
self::assertSame(0, $this->command()->run([])); self::assertSame(0, $this->command(expectSave: true)->run([]));
self::assertContains('✓ Schreibtest auf einer Wegwerfseite', $this->lines); self::assertContains('✓ Schreibtest auf einer Wegwerfseite', $this->lines);
} }
@@ -134,6 +142,25 @@ final class DiviCheckCommandTest extends TestCase
self::assertFileDoesNotExist($this->expectedFile, 'all or nothing'); self::assertFileDoesNotExist($this->expectedFile, 'all or nothing');
} }
public function testUpdateReferenceWritesNothingWhenASourceIsUnwritable(): void
{
$this->writeDivi();
chmod($this->sourceDir, 0500);
try {
$code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]);
self::assertSame(1, $code);
self::assertStringContainsString(
'Referenz nicht vollständig aktualisiert:',
implode("\n", $this->lines)
);
self::assertFileDoesNotExist($this->expectedFile, 'sources are written before fingerprints.json');
} finally {
chmod($this->sourceDir, 0777);
}
}
public function testUpdateReferenceWritesFingerprintsAndSources(): void public function testUpdateReferenceWritesFingerprintsAndSources(): void
{ {
$this->writeDivi(); $this->writeDivi();