diff --git a/.github/workflows/build-artifacts.yml b/.github/workflows/build-artifacts.yml index 4d288b92..76bd9cdc 100644 --- a/.github/workflows/build-artifacts.yml +++ b/.github/workflows/build-artifacts.yml @@ -40,6 +40,7 @@ on: - "pyproject.toml" - "uv.lock" - ".github/workflows/build-artifacts.yml" + - ".github/scripts/**" workflow_dispatch: concurrency: diff --git a/deploy/ami/user-data.sh b/deploy/ami/user-data.sh index d67a4a5f..82bc503f 100755 --- a/deploy/ami/user-data.sh +++ b/deploy/ami/user-data.sh @@ -22,10 +22,14 @@ exec > >(tee -a /var/log/open-swe-user-data.log) 2>&1 echo "==> open-swe user-data start $(date -u +%FT%TZ)" # --- CDK-rendered values ----------------------------------------------------- -OPENSWE_ENV="@@OPENSWE_ENV@@" # dev | prod -ASSETS_BUCKET="@@ASSETS_BUCKET@@" # open-swe--assets -SERVER_NAME="@@SERVER_NAME@@" # openswe[-dev].seahaven.com -ARTIFACT_PREFIX="@@ARTIFACT_PREFIX@@" # e.g. releases/latest +# NOTE: CDK-substituted tokens use %%...%% (rendered by app-service.ts), DISTINCT +# from the @@...@@ tokens this script seds into the baked systemd/nginx templates. +# The two MUST NOT share a delimiter: a shared @@OPENSWE_ENV@@ / @@SERVER_NAME@@ +# let CDK clobber the sed PATTERN, leaving the unit's token unsubstituted. +OPENSWE_ENV="%%OPENSWE_ENV%%" # dev | prod +ASSETS_BUCKET="%%ASSETS_BUCKET%%" # open-swe--assets +SERVER_NAME="%%SERVER_NAME%%" # openswe[-dev].seahaven.com +ARTIFACT_PREFIX="%%ARTIFACT_PREFIX%%" # e.g. releases/latest # --- fixed layout (must match provision.sh + templates) ---------------------- SERVICE_USER="openswe" @@ -69,13 +73,13 @@ mountpoint -q /run/open-swe || mount /run/open-swe || mount -t tmpfs \ # --- install the deploy script (single source of the app-deploy procedure) --- # deploy.sh (deploy/ami/deploy.sh) pulls the release from S3, builds the venv with # `uv sync`, and restarts the service. CDK base64-renders the file into the -# @@DEPLOY_SH_B64@@ token below so it is a normal reviewable repo file, not an +# %%DEPLOY_SH_B64%% token below so it is a normal reviewable repo file, not an # inline heredoc. The `open-swe--deploy` SSM document runs this same script # for every subsequent release. echo "==> install /opt/open-swe/bin/deploy.sh" install -d -o root -g root -m 0755 /opt/open-swe/bin base64 -d >/opt/open-swe/bin/deploy.sh <<'DEPLOY_SH_B64' -@@DEPLOY_SH_B64@@ +%%DEPLOY_SH_B64%% DEPLOY_SH_B64 chmod 0755 /opt/open-swe/bin/deploy.sh diff --git a/infra/lib/constructs/app-service.ts b/infra/lib/constructs/app-service.ts index c49fec23..fd724dbe 100644 --- a/infra/lib/constructs/app-service.ts +++ b/infra/lib/constructs/app-service.ts @@ -183,11 +183,15 @@ export class AppService extends Construct { const userData = ec2.UserData.custom( fs .readFileSync(userDataPath, "utf8") - .replace(/@@OPENSWE_ENV@@/g, env) - .replace(/@@ASSETS_BUCKET@@/g, `${p}-assets`) - .replace(/@@SERVER_NAME@@/g, net.dashboardHost) - .replace(/@@ARTIFACT_PREFIX@@/g, artifactPrefix) - .replace(/@@DEPLOY_SH_B64@@/g, deployShB64), + // %%...%% tokens are CDK-substituted here; they are DELIBERATELY a + // different delimiter from the @@...@@ tokens user-data.sh seds into the + // baked systemd/nginx templates, so CDK can never clobber a sed pattern + // (a shared @@OPENSWE_ENV@@/@@SERVER_NAME@@ left the unit unsubstituted). + .replace(/%%OPENSWE_ENV%%/g, env) + .replace(/%%ASSETS_BUCKET%%/g, `${p}-assets`) + .replace(/%%SERVER_NAME%%/g, net.dashboardHost) + .replace(/%%ARTIFACT_PREFIX%%/g, artifactPrefix) + .replace(/%%DEPLOY_SH_B64%%/g, deployShB64), ); this.instance = new ec2.Instance(this, "Instance", { diff --git a/infra/test/ascii-aws-fields.test.ts b/infra/test/ascii-aws-fields.test.ts index 89fb56bc..5de3a93d 100644 --- a/infra/test/ascii-aws-fields.test.ts +++ b/infra/test/ascii-aws-fields.test.ts @@ -71,4 +71,17 @@ describe("ASCII-only EC2 description fields", () => { expect(`${id}: ${encoded} bytes`).toBe(encoded < 25600 ? `${id}: ${encoded} bytes` : "OVER 25600"); } }); + + // CDK substitutes %%...%% tokens in user-data at synth. Any %%TOKEN%% left in the + // rendered script means a token wasn't wired in app-service.ts (the @@...@@ tokens + // are intentional — user-data seds those into the baked templates at boot). + it("user-data has no unresolved %%CDK%% tokens", () => { + for (const [id, r] of Object.entries(resources)) { + if (r.Type !== "AWS::EC2::Instance") continue; + const ud = (r.Properties?.UserData as { "Fn::Base64"?: string }) ?? {}; + const script = typeof ud["Fn::Base64"] === "string" ? ud["Fn::Base64"] : ""; + const leftover = script.match(/%%[A-Z0-9_]+%%/g) ?? []; + expect(`${id}: ${leftover.join(",")}`).toBe(`${id}: `); + } + }); });