diff --git a/CHANGELOG.md b/CHANGELOG.md index 9110d70..d227892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,11 @@ See [keep a changelog] for information about writing changes to this log. - [PR-27](https://github.com/itk-dev/event-database-api/pull/27) Add Claude Code project setup (CLAUDE.md, agents, skills) +## [1.2.5] - 2026-10-07 + +- [PR-67](https://github.com/itk-dev/event-database-api/pull/67) + Fix `app:apikeys:add` failing when `.env.local` is a bind-mounted file + ## [1.2.4] - 2026-10-07 - [PR-64](https://github.com/itk-dev/event-database-api/pull/64) @@ -165,7 +170,8 @@ See [keep a changelog] for information about writing changes to this log. - Added multi-value filtering for Lactions and Organizations [keep a changelog]: https://keepachangelog.com/en/1.1.0/ -[Unreleased]: https://github.com/itk-dev/event-database-api/compare/1.2.4...HEAD +[Unreleased]: https://github.com/itk-dev/event-database-api/compare/1.2.5...HEAD +[1.2.5]: https://github.com/itk-dev/event-database-api/compare/1.2.4...1.2.5 [1.2.4]: https://github.com/itk-dev/event-database-api/compare/1.2.3...1.2.4 [1.2.3]: https://github.com/itk-dev/event-database-api/compare/1.2.2...1.2.3 [1.2.2]: https://github.com/itk-dev/event-database-api/compare/1.2.1...1.2.2 diff --git a/src/Command/ApiKeysAddCommand.php b/src/Command/ApiKeysAddCommand.php index 04d8262..a343f19 100644 --- a/src/Command/ApiKeysAddCommand.php +++ b/src/Command/ApiKeysAddCommand.php @@ -137,7 +137,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $hasBackup = file_exists($envFile); - if ($hasBackup && (!chmod($tempFile, fileperms($envFile) & 0777) || !copy($envFile, $backupFile))) { + if ($hasBackup && !copy($envFile, $backupFile)) { @unlink($tempFile); @unlink($backupFile); $io->error(sprintf('Failed to back up %s, the file is unchanged.', $envFile)); @@ -145,11 +145,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - // Atomic replace, .env.local never goes missing. - if (!rename($tempFile, $envFile)) { - @unlink($tempFile); - @unlink($backupFile); - $io->error(sprintf('Failed to replace %s, the file is unchanged.', $envFile)); + // Write in place instead of rename(): on prod .env.local is a single-file + // bind mount, which rename() cannot replace (EBUSY). Keeps owner and mode too. + $replaced = @copy($tempFile, $envFile) && file_get_contents($envFile) === $contents; + unlink($tempFile); + + if (!$replaced) { + $restored = $hasBackup ? @copy($backupFile, $envFile) : @unlink($envFile); + $io->error($restored + ? sprintf('Failed to write %s, the change has been reverted.', $envFile) + : sprintf('Failed to write %s and could not restore it. The original is in %s.', $envFile, $backupFile)); return Command::FAILURE; } diff --git a/tests/Command/ApiKeysAddCommandTest.php b/tests/Command/ApiKeysAddCommandTest.php index 6f8bdec..b3389ec 100644 --- a/tests/Command/ApiKeysAddCommandTest.php +++ b/tests/Command/ApiKeysAddCommandTest.php @@ -50,9 +50,13 @@ public function testReplacesMultiLineValueAndKeepsOtherLines(): void OTHER=value ENV); chmod($this->dir.'/.env.local', 0600); + // Same inode afterwards means .env.local was written in place, not renamed over, + // which is required when it is a single-file bind mount (prod). + $inode = fileinode($this->dir.'/.env.local'); $this->assertSame(Command::SUCCESS, $this->runCommand("o'brien")->getStatusCode()); $this->assertSame(0600, fileperms($this->dir.'/.env.local') & 0777); + $this->assertSame($inode, fileinode($this->dir.'/.env.local')); $this->assertFileDoesNotExist($this->dir.'/.env.local.backup'); $this->assertFileDoesNotExist($this->dir.'/.env.local.temp');