diff --git a/docs/production_deploy.md b/docs/production_deploy.md index 1d4172d..0a6a14d 100644 --- a/docs/production_deploy.md +++ b/docs/production_deploy.md @@ -37,6 +37,10 @@ Install the Python dependency if required: python -m pip install boto3 ``` +The PowerShell entry point calls `tool/upload_launcher_release.py` as a regular +Python file. Keep this helper beside the deployment script; it performs the S3 +upload and verifies the uploaded object's byte length. + 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. diff --git a/tool/deploy_launcher.ps1 b/tool/deploy_launcher.ps1 index 5d0a450..cbee24d 100644 --- a/tool/deploy_launcher.ps1 +++ b/tool/deploy_launcher.ps1 @@ -617,45 +617,15 @@ try { $env:MOONWELL_RELEASE_KEY = $S3Key $env:MOONWELL_RELEASE_VERSION = $version $env:MOONWELL_RELEASE_SHA256 = $installerSha256 - $uploadScript = @' -import os -import boto3 -from botocore.config import Config + $uploadHelper = Join-Path ` + $repositoryRoot ` + 'tool\upload_launcher_release.py' + if (-not (Test-Path -LiteralPath $uploadHelper -PathType Leaf)) { + throw "S3 upload helper was not found: $uploadHelper" + } -path_style = os.getenv("AWS_USE_PATH_STYLE_ENDPOINT", "true").lower() -addressing_style = "path" if path_style in ("1", "true", "yes") else "virtual" -s3 = boto3.client( - "s3", - endpoint_url=os.environ["MOONWELL_RELEASE_ENDPOINT"], - region_name=os.getenv("AWS_DEFAULT_REGION", "ru-central1"), - config=Config(s3={"addressing_style": addressing_style}), -) -s3.upload_file( - os.environ["MOONWELL_RELEASE_FILE"], - os.environ["MOONWELL_RELEASE_BUCKET"], - os.environ["MOONWELL_RELEASE_KEY"], - ExtraArgs={ - "ACL": "public-read", - "ContentType": "application/x-msdownload", - "CacheControl": "no-cache, max-age=0", - "Metadata": { - "launcher-version": os.environ["MOONWELL_RELEASE_VERSION"], - "sha256": os.environ["MOONWELL_RELEASE_SHA256"], - }, - }, -) -response = s3.head_object( - Bucket=os.environ["MOONWELL_RELEASE_BUCKET"], - Key=os.environ["MOONWELL_RELEASE_KEY"], -) -print("S3_VERSION_ID=" + str(response.get("VersionId", ""))) -print("S3_CONTENT_LENGTH=" + str(response["ContentLength"])) -'@ try { - Invoke-RequiredCommand -Command $python -Arguments @( - '-c', - $uploadScript - ) + Invoke-RequiredCommand -Command $python -Arguments @($uploadHelper) } finally { Remove-Item Env:MOONWELL_RELEASE_FILE -ErrorAction SilentlyContinue diff --git a/tool/upload_launcher_release.py b/tool/upload_launcher_release.py new file mode 100644 index 0000000..463c14c --- /dev/null +++ b/tool/upload_launcher_release.py @@ -0,0 +1,70 @@ +"""Upload a MoonWell Launcher installer to S3-compatible storage. + +Release metadata is supplied by ``deploy_launcher.ps1`` through environment +variables. A real Python file avoids Windows PowerShell quoting problems that +occur when a multiline program is passed to ``python -c``. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import boto3 +from botocore.config import Config + + +def required_environment_value(name: str) -> str: + value = os.environ.get(name, "").strip() + if not value: + raise RuntimeError(f"Required environment variable is missing: {name}") + return value + + +release_file = Path(required_environment_value("MOONWELL_RELEASE_FILE")) +endpoint = required_environment_value("MOONWELL_RELEASE_ENDPOINT") +bucket = required_environment_value("MOONWELL_RELEASE_BUCKET") +object_key = required_environment_value("MOONWELL_RELEASE_KEY") +version = required_environment_value("MOONWELL_RELEASE_VERSION") +sha256 = required_environment_value("MOONWELL_RELEASE_SHA256") + +if not release_file.is_file(): + raise FileNotFoundError(f"Installer does not exist: {release_file}") + +path_style = os.getenv("AWS_USE_PATH_STYLE_ENDPOINT", "true").lower() +addressing_style = ( + "path" if path_style in ("1", "true", "yes") else "virtual" +) +s3 = boto3.client( + "s3", + endpoint_url=endpoint, + region_name=os.getenv("AWS_DEFAULT_REGION", "ru-central1"), + config=Config(s3={"addressing_style": addressing_style}), +) + +s3.upload_file( + str(release_file), + bucket, + object_key, + ExtraArgs={ + "ACL": "public-read", + "ContentType": "application/x-msdownload", + "CacheControl": "no-cache, max-age=0", + "Metadata": { + "launcher-version": version, + "sha256": sha256, + }, + }, +) + +response = s3.head_object(Bucket=bucket, Key=object_key) +local_size = release_file.stat().st_size +remote_size = int(response["ContentLength"]) +if remote_size != local_size: + raise RuntimeError( + f"Uploaded object size mismatch: local={local_size}, " + f"remote={remote_size}" + ) + +print("S3_VERSION_ID=" + str(response.get("VersionId", ""))) +print("S3_CONTENT_LENGTH=" + str(remote_size))