Compare commits

...

2 Commits

Author SHA1 Message Date
frostebite
71a0700bfa fix: stop uploading empty placeholder cache tars to S3
The build workflow created empty tar files (via `tar --files-from /dev/null`)
as placeholders before remote-cli-post-build runs. When the container is
OOM-killed, only these ~10KB empty tars survive and get uploaded to S3.
On the next build, the pull-cache hook downloads them and extracts an empty
Library, providing zero caching benefit.

Changes:
- Remove empty placeholder tar creation in build-automation-workflow.ts
- Keep mkdir -p for cache directories (hooks need them)
- Add size check in aws-s3-upload-cache hook to delete tar files < 1KB
  before uploading, as a safety net against stale stubs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:55:38 +00:00
Frostebite
ce7ce7a416 fix: pass Unity license secrets to AWS ECS container via RunTask overrides (#821)
* fix: pass Unity license secrets to AWS ECS container via RunTask overrides

The AWS provider was not passing UNITY_EMAIL, UNITY_PASSWORD, and
UNITY_SERIAL to the ECS container as environment variables. These
secrets were only sent to CloudFormation Secrets Manager, but the
template generation produced duplicate YAML Secrets keys (one per
secret), causing only the last secret to survive. The activate.sh
script requires all three to be present simultaneously.

This fix merges secrets into the ECS RunTask containerOverrides
environment array, matching how the docker and k8s providers already
handle secrets. The CloudFormation Secrets Manager path is preserved
as a secondary mechanism.

Fixes license activation failure when using providerStrategy: aws.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Pin LocalStack to 4.4.0 (pre-auth-token requirement)

As of 2026-03-23, localstack/localstack:latest requires an auth token
even for community features. Pin to 4.4.0 (last community release
before the single-image migration) to restore CI.

See: https://blog.localstack.cloud/localstack-single-image-next-steps/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 19:49:23 +00:00
7 changed files with 42 additions and 19 deletions

View File

@@ -91,7 +91,7 @@ jobs:
-e SERVICES=s3,cloudformation,ecs,kinesis,cloudwatch,logs,efs,ec2,iam,elasticfilesystem,secretsmanager,lambda,events,sts \
-e DEBUG=0 \
-e HOSTNAME_EXTERNAL=localstack-main \
localstack/localstack:latest || true
localstack/localstack:4.4.0 || true
# Wait for LocalStack to be ready - check both health endpoint and S3 service
echo "Waiting for LocalStack to be ready..."
MAX_ATTEMPTS=60

28
dist/index.js generated vendored
View File

@@ -3398,7 +3398,7 @@ class AWSTaskRunner {
return { name: x.name, value };
});
}
static async runTask(taskDef, environment, commands) {
static async runTask(taskDef, environment, secrets, commands) {
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
const taskDefinition = taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'TaskDefinition')?.PhysicalResourceId || '';
const SubnetOne = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'PublicSubnetOne')?.PhysicalResourceId || '';
@@ -3407,6 +3407,11 @@ class AWSTaskRunner {
const streamName = taskDef.taskDefResources?.find((x) => x.LogicalResourceId === 'KinesisStream')?.PhysicalResourceId || '';
// Transform localhost endpoints for container environment
const transformedEnvironment = AWSTaskRunner.transformEndpointsForContainer(environment);
// Merge secrets into environment as plain env vars, matching docker and k8s provider behavior.
// This ensures UNITY_EMAIL, UNITY_PASSWORD, UNITY_SERIAL reach the container reliably
// without depending on CloudFormation Secrets Manager resolution.
const secretsAsEnvironment = secrets.map((s) => ({ name: s.EnvironmentVariable, value: s.ParameterValue }));
const mergedEnvironment = [...transformedEnvironment, ...secretsAsEnvironment];
const runParameters = {
cluster,
taskDefinition,
@@ -3415,7 +3420,7 @@ class AWSTaskRunner {
containerOverrides: [
{
name: taskDef.taskDefStackName,
environment: transformedEnvironment,
environment: mergedEnvironment,
command: ['-c', command_hook_service_1.CommandHookService.ApplyHooksToCommands(commands, orchestrator_1.default.buildParameters)],
},
],
@@ -4449,7 +4454,7 @@ class AWSBuildEnvironment {
try {
const postSetupStacksTimeMs = Date.now();
orchestrator_logger_1.default.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`);
const { output, shouldCleanup } = await aws_task_runner_1.default.runTask(taskDef, environment, commands);
const { output, shouldCleanup } = await aws_task_runner_1.default.runTask(taskDef, environment, secrets, commands);
postRunTaskTimeMs = Date.now();
orchestrator_logger_1.default.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`);
if (shouldCleanup) {
@@ -9414,6 +9419,10 @@ class ContainerHookService {
fi
ENDPOINT_ARGS=""
if [ -n "$AWS_S3_ENDPOINT" ]; then ENDPOINT_ARGS="--endpoint-url $AWS_S3_ENDPOINT"; fi
# Skip uploading empty or near-empty tar files (< 1KB) these are leftover
# stubs with no real cache data and would poison the cache for the next build.
find /data/cache/$CACHE_KEY/lfs -name "*.tar*" -size -1k -delete 2>/dev/null || true
find /data/cache/$CACHE_KEY/Library -name "*.tar*" -size -1k -delete 2>/dev/null || true
aws $ENDPOINT_ARGS s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://${orchestrator_1.default.buildParameters.awsStackName}/orchestrator-cache/$CACHE_KEY/lfs || true
rm -r /data/cache/$CACHE_KEY/lfs || true
aws $ENDPOINT_ARGS s3 cp --recursive /data/cache/$CACHE_KEY/Library s3://${orchestrator_1.default.buildParameters.awsStackName}/orchestrator-cache/$CACHE_KEY/Library || true
@@ -9907,13 +9916,14 @@ echo "CACHE_KEY=$CACHE_KEY"`;
if ! command -v yarn > /dev/null 2>&1; then printf '#!/bin/sh\nexit 0\n' > /usr/local/bin/yarn && chmod +x /usr/local/bin/yarn; fi
# Pipe entrypoint.sh output through log stream to capture Unity build output (including "Build succeeded")
{ echo "game ci start"; echo "game ci start" >> /home/job-log.txt; echo "CACHE_KEY=$CACHE_KEY"; echo "$CACHE_KEY"; if [ -n "$LOCKED_WORKSPACE" ]; then echo "Retained Workspace: true"; fi; if [ -n "$LOCKED_WORKSPACE" ] && [ -d "$GITHUB_WORKSPACE/.git" ]; then echo "Retained Workspace Already Exists!"; fi; /entrypoint.sh; } | node ${builderPath} -m remote-cli-log-stream --logFile /home/job-log.txt
# Ensure cache directories exist for post-build and S3 upload hooks.
# Do NOT create empty placeholder tars they waste S3 storage and on next
# build the pull-cache hook downloads them, giving Unity an empty Library
# (no caching benefit). The real tars are created by remote-cli-post-build
# via Caching.PushToCache(), and the S3 upload hooks use || true so missing
# files are handled gracefully.
mkdir -p "/data/cache/$CACHE_KEY/Library"
if [ ! -f "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar" ] && [ ! -f "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar.lz4" ]; then
tar -cf "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar" --files-from /dev/null || touch "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar"
fi
if [ ! -f "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar" ] && [ ! -f "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4" ]; then
tar -cf "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar" --files-from /dev/null || touch "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar"
fi
mkdir -p "/data/cache/$CACHE_KEY/build"
# Run post-build tasks and capture output
# Note: Post-build may clean up the builder directory, so we write output directly to log file
# Use set +e to allow the command to fail without exiting the script

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
import { DescribeTasksCommand, RunTaskCommand, waitUntilTasksRunning } from '@aws-sdk/client-ecs';
import { DescribeStreamCommand, GetRecordsCommand, GetShardIteratorCommand } from '@aws-sdk/client-kinesis';
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
import OrchestratorSecret from '../../options/orchestrator-secret';
import * as core from '@actions/core';
import OrchestratorAWSTaskDef from './orchestrator-aws-task-def';
import * as zlib from 'node:zlib';
@@ -56,6 +57,7 @@ class AWSTaskRunner {
static async runTask(
taskDef: OrchestratorAWSTaskDef,
environment: OrchestratorEnvironmentVariable[],
secrets: OrchestratorSecret[],
commands: string,
): Promise<{ output: string; shouldCleanup: boolean }> {
const cluster = taskDef.baseResources?.find((x) => x.LogicalResourceId === 'ECSCluster')?.PhysicalResourceId || '';
@@ -73,6 +75,12 @@ class AWSTaskRunner {
// Transform localhost endpoints for container environment
const transformedEnvironment = AWSTaskRunner.transformEndpointsForContainer(environment);
// Merge secrets into environment as plain env vars, matching docker and k8s provider behavior.
// This ensures UNITY_EMAIL, UNITY_PASSWORD, UNITY_SERIAL reach the container reliably
// without depending on CloudFormation Secrets Manager resolution.
const secretsAsEnvironment = secrets.map((s) => ({ name: s.EnvironmentVariable, value: s.ParameterValue }));
const mergedEnvironment = [...transformedEnvironment, ...secretsAsEnvironment];
const runParameters = {
cluster,
taskDefinition,
@@ -81,7 +89,7 @@ class AWSTaskRunner {
containerOverrides: [
{
name: taskDef.taskDefStackName,
environment: transformedEnvironment,
environment: mergedEnvironment,
command: ['-c', CommandHookService.ApplyHooksToCommands(commands, Orchestrator.buildParameters)],
},
],

View File

@@ -125,7 +125,7 @@ class AWSBuildEnvironment implements ProviderInterface {
try {
const postSetupStacksTimeMs = Date.now();
OrchestratorLogger.log(`Setup job time: ${Math.floor((postSetupStacksTimeMs - startTimeMs) / 1000)}s`);
const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, commands);
const { output, shouldCleanup } = await AwsTaskRunner.runTask(taskDef, environment, secrets, commands);
postRunTaskTimeMs = Date.now();
OrchestratorLogger.log(`Run job time: ${Math.floor((postRunTaskTimeMs - postSetupStacksTimeMs) / 1000)}s`);
if (shouldCleanup) {

View File

@@ -155,6 +155,10 @@ export class ContainerHookService {
fi
ENDPOINT_ARGS=""
if [ -n "$AWS_S3_ENDPOINT" ]; then ENDPOINT_ARGS="--endpoint-url $AWS_S3_ENDPOINT"; fi
# Skip uploading empty or near-empty tar files (< 1KB) — these are leftover
# stubs with no real cache data and would poison the cache for the next build.
find /data/cache/$CACHE_KEY/lfs -name "*.tar*" -size -1k -delete 2>/dev/null || true
find /data/cache/$CACHE_KEY/Library -name "*.tar*" -size -1k -delete 2>/dev/null || true
aws $ENDPOINT_ARGS s3 cp --recursive /data/cache/$CACHE_KEY/lfs s3://${
Orchestrator.buildParameters.awsStackName
}/orchestrator-cache/$CACHE_KEY/lfs || true

View File

@@ -170,13 +170,14 @@ echo "CACHE_KEY=$CACHE_KEY"`;
if ! command -v yarn > /dev/null 2>&1; then printf '#!/bin/sh\nexit 0\n' > /usr/local/bin/yarn && chmod +x /usr/local/bin/yarn; fi
# Pipe entrypoint.sh output through log stream to capture Unity build output (including "Build succeeded")
{ echo "game ci start"; echo "game ci start" >> /home/job-log.txt; echo "CACHE_KEY=$CACHE_KEY"; echo "$CACHE_KEY"; if [ -n "$LOCKED_WORKSPACE" ]; then echo "Retained Workspace: true"; fi; if [ -n "$LOCKED_WORKSPACE" ] && [ -d "$GITHUB_WORKSPACE/.git" ]; then echo "Retained Workspace Already Exists!"; fi; /entrypoint.sh; } | node ${builderPath} -m remote-cli-log-stream --logFile /home/job-log.txt
# Ensure cache directories exist for post-build and S3 upload hooks.
# Do NOT create empty placeholder tars — they waste S3 storage and on next
# build the pull-cache hook downloads them, giving Unity an empty Library
# (no caching benefit). The real tars are created by remote-cli-post-build
# via Caching.PushToCache(), and the S3 upload hooks use || true so missing
# files are handled gracefully.
mkdir -p "/data/cache/$CACHE_KEY/Library"
if [ ! -f "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar" ] && [ ! -f "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar.lz4" ]; then
tar -cf "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar" --files-from /dev/null || touch "/data/cache/$CACHE_KEY/Library/lib-$BUILD_GUID.tar"
fi
if [ ! -f "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar" ] && [ ! -f "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar.lz4" ]; then
tar -cf "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar" --files-from /dev/null || touch "/data/cache/$CACHE_KEY/build/build-$BUILD_GUID.tar"
fi
mkdir -p "/data/cache/$CACHE_KEY/build"
# Run post-build tasks and capture output
# Note: Post-build may clean up the builder directory, so we write output directly to log file
# Use set +e to allow the command to fail without exiting the script