diff --git a/src/Commands/ConfigInit.php b/src/Commands/ConfigInit.php index 3b445b7..91148ff 100644 --- a/src/Commands/ConfigInit.php +++ b/src/Commands/ConfigInit.php @@ -49,6 +49,7 @@ use Gitcd\Helpers\GitHub; use Gitcd\Helpers\GitHubApp; use Gitcd\Utils\Json; +use Gitcd\Helpers\DeploymentState; use Gitcd\Commands\Init\DotMenuTrait; Class ConfigInit extends Command { @@ -269,8 +270,7 @@ protected function flowEncrypt( ' Continue to encrypt files? [Y/n] ', true ); if (!$helper->ask($input, $output, $question)) { - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); return Command::SUCCESS; } } @@ -284,8 +284,7 @@ protected function flowEncrypt( $output->writeln(" No unencrypted .env files found in the config repo."); $output->writeln(" All secrets appear to be encrypted already."); $output->writeln(''); - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); $this->showCompletion($output, $foldername, Config::read('env', 'unknown')); return Command::SUCCESS; } @@ -316,8 +315,7 @@ protected function flowEncrypt( } } - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); $this->showCompletion($output, $foldername, Config::read('env', 'unknown')); return Command::SUCCESS; @@ -668,8 +666,7 @@ protected function stepSecrets( $output->writeln(" protocol secrets:setup {$hexKey}"); $output->writeln(''); - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); $this->offerEncryptFiles($input, $output, $helper, $repo_dir, $configrepo); } else { @@ -704,8 +701,7 @@ protected function offerEncryptFiles( Shell::run("git -C '$configrepo' add -A"); Shell::run("git -C '$configrepo' commit -m 'encrypt secrets' 2>/dev/null"); - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); } } diff --git a/src/Commands/ConfigSave.php b/src/Commands/ConfigSave.php index 56a51ab..2d0e997 100644 --- a/src/Commands/ConfigSave.php +++ b/src/Commands/ConfigSave.php @@ -49,6 +49,7 @@ use Gitcd\Helpers\FileEncryption; use Gitcd\Helpers\Secrets; use Gitcd\Utils\Json; +use Gitcd\Helpers\DeploymentState; Class ConfigSave extends Command { @@ -99,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Re-encrypt .env files if encryption is configured - $secretsMode = Json::read('deployment.secrets', 'file', $repo_dir); + $secretsMode = DeploymentState::secretsMode($repo_dir); if ($secretsMode === 'encrypted' && Secrets::hasKey()) { $unencrypted = FileEncryption::findUnencryptedEnvFiles($configrepo); if (!empty($unencrypted)) { diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index b17450f..dc273be 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -44,6 +44,7 @@ use Gitcd\Helpers\Secrets; use Gitcd\Helpers\GitHub; use Gitcd\Utils\Json; +use Gitcd\Helpers\DeploymentState; Class Migrate extends Command { @@ -93,7 +94,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Step 1: Check current state $currentStrategy = Json::read('deployment.strategy', null, $repo_dir); - $currentSecrets = Json::read('deployment.secrets', null, $repo_dir); + $currentSecrets = DeploymentState::secretsMode($repo_dir); if ($currentStrategy === 'release' && $currentSecrets === 'encrypted' && !$secretsOnly) { $output->writeln('This repo is already using release-based deployment with encrypted secrets.'); @@ -148,8 +149,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($secretsOnly) { // Update just the secrets setting in protocol.json - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); $output->writeln(''); $output->writeln('Secrets migration complete. protocol.json updated with secrets: "encrypted"'); return Command::SUCCESS; @@ -162,8 +162,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int Json::write('deployment.strategy', 'release', $repo_dir); Json::write('deployment.pointer', 'github_variable', $repo_dir); Json::write('deployment.pointer_name', 'PROTOCOL_ACTIVE_RELEASE', $repo_dir); - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); $output->writeln(' - Updated deployment strategy to "release"'); $output->writeln(' - Updated secrets mode to "encrypted"'); diff --git a/src/Commands/ProtocolInit.php b/src/Commands/ProtocolInit.php index 9d7a0af..6f71651 100644 --- a/src/Commands/ProtocolInit.php +++ b/src/Commands/ProtocolInit.php @@ -51,6 +51,7 @@ use Gitcd\Helpers\GitHubApp; use Gitcd\Utils\Json; use Gitcd\Utils\Yaml; +use Gitcd\Helpers\DeploymentState; use Gitcd\Commands\Init\ProjectType; use Gitcd\Helpers\BlueGreen; use Gitcd\Helpers\BlueGreen\ReleaseBuilder; @@ -1621,8 +1622,7 @@ protected function configureSecrets( if ($helper->ask($input, $output, $question)) { $command = $this->getApplication()->find('secrets:setup'); $command->run(new ArrayInput([]), $output); - Json::write('deployment.secrets', 'encrypted', $repo_dir); - Json::save($repo_dir); + DeploymentState::setSecretsMode($repo_dir, 'encrypted'); } else { $this->writeInfo($output, 'Skipped. Run protocol secrets:setup later.'); } diff --git a/src/Commands/ProtocolStatus.php b/src/Commands/ProtocolStatus.php index ab630cf..8765145 100644 --- a/src/Commands/ProtocolStatus.php +++ b/src/Commands/ProtocolStatus.php @@ -153,7 +153,7 @@ private function buildContext(InputInterface $input): array $currentBranch = null; $awaitingRelease = false; $dockerImage = Json::read('docker.image', null, $repo_dir); - $secretsMode = Json::read('deployment.secrets', 'file', $repo_dir); + $secretsMode = DeploymentState::secretsMode($repo_dir); $gitRemote = null; $activeDir = $repo_dir; } diff --git a/src/Helpers/DeploymentState.php b/src/Helpers/DeploymentState.php index b2774b3..f72f784 100644 --- a/src/Helpers/DeploymentState.php +++ b/src/Helpers/DeploymentState.php @@ -33,6 +33,9 @@ class DeploymentState /** * Get the current deployment strategy. + * + * Strategy is node-specific (stored in NodeConfig), never from protocol.json. + * No node config means local dev = "none". */ public static function strategy(string $repoDir): string { @@ -44,7 +47,7 @@ public static function strategy(string $repoDir): string } } - return Json::read('deployment.strategy', 'none', $repoDir); + return 'none'; } /** @@ -200,6 +203,30 @@ public static function target(string $repoDir): ?string return NodeConfig::read($project, 'release.target'); } + /** + * Get the secrets mode for this deployment. + * + * Production nodes (those with a NodeConfig entry) read exclusively + * from NodeConfig (~/.protocol/.node/nodes/.json) and never + * fall back to the repo-level protocol.json. + * + * Development/staging (no NodeConfig) reads from protocol.json. + * + * @return string "file", "encrypted", or "aws" + */ + public static function secretsMode(string $repoDir): string + { + $project = self::resolveProjectName($repoDir); + if ($project) { + $mode = NodeConfig::read($project, 'deployment.secrets'); + if ($mode) { + return $mode; + } + } + + return Json::read('deployment.secrets', 'file', $repoDir); + } + // ─── Write ─────────────────────────────────────────────────────── /** @@ -398,6 +425,30 @@ public static function setStrategy(string $repoDir, string $strategy): void Json::save($repoDir); } + /** + * Set the secrets mode for this deployment. + * + * Writes to NodeConfig for production nodes, and always to + * the repo-level protocol.json for dev/staging. + * + * @param string $mode "file", "encrypted", or "aws" + */ + public static function setSecretsMode(string $repoDir, string $mode): void + { + Log::info('deployment', "setSecretsMode: mode={$mode}"); + + $project = self::resolveProjectName($repoDir); + if ($project) { + NodeConfig::modify($project, function (array $nodeData) use ($mode) { + $nodeData['deployment']['secrets'] = $mode; + return $nodeData; + }); + } + + Json::write('deployment.secrets', $mode, $repoDir); + Json::save($repoDir); + } + // ─── Helpers ───────────────────────────────────────────────────── /** diff --git a/src/Helpers/SecretsProvider.php b/src/Helpers/SecretsProvider.php index d6d23ba..670fa2e 100644 --- a/src/Helpers/SecretsProvider.php +++ b/src/Helpers/SecretsProvider.php @@ -33,6 +33,7 @@ namespace Gitcd\Helpers; use Gitcd\Utils\Json; +use Gitcd\Helpers\DeploymentState; use Symfony\Component\Yaml\Yaml; class SecretsProvider @@ -53,7 +54,7 @@ class SecretsProvider */ public static function resolveToTempFile(string $repoDir): ?string { - $mode = Json::read('deployment.secrets', 'file', $repoDir); + $mode = DeploymentState::secretsMode($repoDir); self::log("resolveToTempFile: mode={$mode} repoDir={$repoDir}"); if ($mode === 'encrypted') { diff --git a/src/Helpers/Soc2Check.php b/src/Helpers/Soc2Check.php index b719067..bc33ba3 100644 --- a/src/Helpers/Soc2Check.php +++ b/src/Helpers/Soc2Check.php @@ -6,6 +6,7 @@ use Gitcd\Utils\Json; use Gitcd\Utils\NodeConfig; +use Gitcd\Helpers\DeploymentState; class Soc2Check extends BaseAuditChecker { @@ -30,7 +31,7 @@ public function runAll(): array */ protected function checkSecretsEncrypted(): void { - $mode = Json::read('deployment.secrets', 'file', $this->repoDir); + $mode = DeploymentState::secretsMode($this->repoDir); $hasKey = Secrets::hasKey(); if ($mode === 'aws') { diff --git a/src/Plugins/awssecrets/AwsSecretsHelper.php b/src/Plugins/awssecrets/AwsSecretsHelper.php index 24515bc..d0ff97e 100644 --- a/src/Plugins/awssecrets/AwsSecretsHelper.php +++ b/src/Plugins/awssecrets/AwsSecretsHelper.php @@ -27,11 +27,11 @@ public static function region($repoDir = false): string /** * Get the secret name in AWS Secrets Manager. + * Always derived dynamically as protocol/{projectName}/{environment}. */ public static function secretName($repoDir = false): string { - $default = self::defaultSecretName($repoDir); - return self::config('secret_name', $default, $repoDir); + return self::defaultSecretName($repoDir); } /** @@ -72,9 +72,9 @@ public static function getClient($repoDir = false): SecretsManagerClient */ public static function log(string $message): void { - $dir = NODE_DATA_DIR; + $dir = '/var/log/protocol/'; if (!is_dir($dir)) { - mkdir($dir, 0700, true); + mkdir($dir, 0755, true); } $logFile = $dir . 'aws-secrets.log'; $entry = date('Y-m-d\TH:i:sP') . ' ' . $message . "\n"; diff --git a/src/Plugins/awssecrets/Commands/AwsSecretsInit.php b/src/Plugins/awssecrets/Commands/AwsSecretsInit.php index d93ca92..235da4c 100644 --- a/src/Plugins/awssecrets/Commands/AwsSecretsInit.php +++ b/src/Plugins/awssecrets/Commands/AwsSecretsInit.php @@ -13,6 +13,7 @@ use Gitcd\Helpers\Shell; use Gitcd\Helpers\Config; use Gitcd\Utils\Json; +use Gitcd\Helpers\DeploymentState; class AwsSecretsInit extends Command { @@ -201,7 +202,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln(" Secrets Manager access confirmed"); $output->writeln(''); - // ── Step 2: Configure Region & Secret Name ─────────────────── + // ── Step 2: Configure Region ────────────────────────────────── $output->writeln(' Step 2/3: Configuration'); $output->writeln(''); @@ -213,12 +214,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $region = $helper->ask($input, $output, $question); $output->writeln(''); - // Secret name - $defaultName = AwsSecretsHelper::config('secret_name', null, $repoDir) - ?: AwsSecretsHelper::defaultSecretName($repoDir); - - $question = new Question(" Secret name [{$defaultName}]: ", $defaultName); - $secretName = $helper->ask($input, $output, $question); + // Secret name is always derived: protocol/{name}/{env} + $secretName = AwsSecretsHelper::defaultSecretName($repoDir); + $output->writeln(" Secret name: {$secretName} (protocol/{name}/{env})"); $output->writeln(''); // ── Step 3: Check Secret ───────────────────────────────────── @@ -243,14 +241,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int // ── Save Configuration ─────────────────────────────────────── Json::write('aws.region', $region, $repoDir); - Json::write('aws.secret_name', $secretName, $repoDir); - Json::write('deployment.secrets', 'aws', $repoDir); - Json::save($repoDir); + DeploymentState::setSecretsMode($repoDir, 'aws'); $output->writeln(' Configuration saved to protocol.json:'); - $output->writeln(" aws.region = {$region}"); - $output->writeln(" aws.secret_name = {$secretName}"); + $output->writeln(" aws.region = {$region}"); $output->writeln(" deployment.secrets = aws"); + $output->writeln(" secret name = {$secretName} (derived)"); $output->writeln(''); $output->writeln(' Next steps:'); $output->writeln(' protocol aws:push Push your .env secrets to AWS');