Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
17 changes: 11 additions & 6 deletions src/Command/ApiKeysAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,24 @@ 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));

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;
}
Expand Down
4 changes: 4 additions & 0 deletions tests/Command/ApiKeysAddCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
Loading