diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..29f8539 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +AWS_ACCESS_KEY_ID=replace_with_access_key_id +AWS_SECRET_ACCESS_KEY=replace_with_secret_access_key +AWS_DEFAULT_REGION=ru-central1 +AWS_BUCKET=warcraft-client +AWS_ENDPOINT=https://storage.yandexcloud.net +AWS_USE_PATH_STYLE_ENDPOINT=true +LAUNCHER_AUTH_KEY=replace_with_launcher_auth_key diff --git a/.gitignore b/.gitignore index b1636b2..d9b8843 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,8 @@ app.*.map.json # Local launcher update signing material .moonwell_signing/ + +# Local production deployment credentials +.env +.env.* +!.env.example diff --git a/README.md b/README.md index b5b9da9..99fb5bd 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,10 @@ Remove `-DryRun` only after reviewing the generated installer and AppCast. See `docs/production_deploy.md` for required environment variables, production defaults, safety checks, and failure recovery. +Local deploy credentials are read from the ignored root `.env`; use +`.env.example` as the safe template. Process and CI environment variables +override values from the file. + ## Architecture The UI lives in `lib/app`. Presentation-only design-system components live in diff --git a/docs/production_deploy.md b/docs/production_deploy.md index d67e0e0..3558103 100644 --- a/docs/production_deploy.md +++ b/docs/production_deploy.md @@ -41,10 +41,21 @@ Back up `.moonwell_signing\dsa_priv.pem` in the project secret store. Never commit it, upload it to Object Storage, or send it through chat. Losing this key prevents released launchers from accepting future updates. -## Required Environment +## Local `.env` -The script reads secrets and storage configuration only from environment -variables: +For local releases, copy `.env.example` to `.env` and fill in the real values. +The repository ignores `.env` and every `.env.*` variant except the safe +`.env.example` template. + +```powershell +Copy-Item .env.example .env +``` + +The deploy script loads `.env` automatically. Variables already present in the +current process or CI environment take precedence over the file. Use +`-EnvFile C:\secure\moonwell.env` to select a different local file. + +The following values are supported: | Variable | Required | Purpose | | --- | --- | --- | @@ -56,8 +67,8 @@ variables: | `AWS_BUCKET` | optional | Defaults to `warcraft-client` | | `AWS_USE_PATH_STYLE_ENDPOINT` | optional | Defaults to `true` | -Load secrets from the team secret manager into the current PowerShell process. -Do not put real values in a tracked `.env` file or in the script. +Production CI should load these values from the team secret manager instead of +creating `.env`. Never commit the real file or put its contents into logs. ## Prepare a Release diff --git a/tool/deploy_launcher.ps1 b/tool/deploy_launcher.ps1 index 7f127f5..75fece0 100644 --- a/tool/deploy_launcher.ps1 +++ b/tool/deploy_launcher.ps1 @@ -14,6 +14,9 @@ Fails unless the X.Y.Z part of pubspec.yaml matches this value. .PARAMETER ReleaseNotes Text written to the AppCast item description. +.PARAMETER EnvFile +Local dotenv file used to fill environment variables that are not already set. + .PARAMETER DryRun Builds, signs, and verifies local artifacts without changing production. @@ -42,6 +45,7 @@ param( [string]$S3Endpoint, [string]$S3Bucket, [string]$S3Key = 'moonwell_launcher_setup.exe', + [string]$EnvFile = '.env', [string]$PrivateKeyPath = '.moonwell_signing\dsa_priv.pem', [string]$PublicKeyPath = 'windows\runner\resources\dsa_pub.pem', [string]$FlutterCommand = 'flutter', @@ -101,6 +105,64 @@ function Get-FileSha256 { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +function Import-DotEnv { + param([Parameter(Mandatory = $true)][string]$Path) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return + } + + $allowedNames = @( + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_DEFAULT_REGION', + 'AWS_BUCKET', + 'AWS_ENDPOINT', + 'AWS_USE_PATH_STYLE_ENDPOINT', + 'LAUNCHER_AUTH_KEY' + ) + + foreach ($line in Get-Content -LiteralPath $Path) { + $trimmed = $line.Trim() + if ( + [string]::IsNullOrWhiteSpace($trimmed) -or + $trimmed.StartsWith('#') + ) { + continue + } + + $separator = $trimmed.IndexOf('=') + if ($separator -le 0) { + throw "Invalid dotenv line in ${Path}: $line" + } + + $name = $trimmed.Substring(0, $separator).Trim() + if ($allowedNames -notcontains $name) { + continue + } + + $value = $trimmed.Substring($separator + 1).Trim() + if ( + $value.Length -ge 2 -and + ( + ($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'")) + ) + ) { + $value = $value.Substring(1, $value.Length - 2) + } + + $existing = [Environment]::GetEnvironmentVariable($name) + if ([string]::IsNullOrWhiteSpace($existing)) { + [Environment]::SetEnvironmentVariable( + $name, + $value, + [EnvironmentVariableTarget]::Process + ) + } + } +} + function Get-AppcastVersion { param([Parameter(Mandatory = $true)][string]$Path) @@ -282,6 +344,14 @@ $previousLocation = Get-Location try { Set-Location $repositoryRoot + $resolvedEnvFile = if ([IO.Path]::IsPathRooted($EnvFile)) { + $EnvFile + } + else { + Join-Path $repositoryRoot $EnvFile + } + Import-DotEnv -Path $resolvedEnvFile + if ([string]::IsNullOrWhiteSpace($S3Endpoint)) { $S3Endpoint = $env:AWS_ENDPOINT }