Compare commits

...

2 Commits

Author SHA1 Message Date
Matheus Costa 81ed299e10 Feat/migrate aws sdk v3 (#698)
* chore(cloud-runner): migrate/replace deps aws-sdk v2 to v3

* chore(aws): refactor aws services to support SDK v3

* chore(aws): refactor aws runner to support SDK v3

* chore(aws): update dist

* fix(aws): error handling wrap try/catch to avoid unhandled promise rejections.

* fix(aws): keeping the syntax simpler for arrays
2025-04-10 22:48:14 +02:00
Michael Buhler 9d6bdcbdc5 feat: add buildProfile parameter (#685)
* feat: add `buildProfile` parameter

add new `buildProfile` action param, which will be passed into
Unity as the `-activeBuildProfile ...` CLI param.

closes https://github.com/game-ci/unity-builder/issues/674

* ci: add tests for Unity 6 and build profiles
2025-02-17 11:41:38 -06:00
28 changed files with 79315 additions and 40467 deletions
+12 -3
View File
@@ -36,7 +36,8 @@ env:
jobs: jobs:
buildForAllPlatformsUbuntu: buildForAllPlatformsUbuntu:
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }} name:
"${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -91,6 +92,12 @@ jobs:
- targetPlatform: StandaloneWindows64 - targetPlatform: StandaloneWindows64
additionalParameters: -standaloneBuildSubtarget Server additionalParameters: -standaloneBuildSubtarget Server
buildWithIl2cpp: true buildWithIl2cpp: true
include:
- unityVersion: 6000.0.36f1
targetPlatform: WebGL
- unityVersion: 6000.0.36f1
targetPlatform: WebGL
buildProfile: 'Assets/Settings/Build Profiles/Sample WebGL Build Profile.asset'
steps: steps:
- name: Clear Space for Android Build - name: Clear Space for Android Build
@@ -136,6 +143,7 @@ jobs:
with: with:
buildName: 'GameCI Test Build' buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }} projectPath: ${{ matrix.projectPath }}
buildProfile: ${{ matrix.buildProfile }}
unityVersion: ${{ matrix.unityVersion }} unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }} targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
@@ -158,6 +166,7 @@ jobs:
with: with:
buildName: 'GameCI Test Build' buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }} projectPath: ${{ matrix.projectPath }}
buildProfile: ${{ matrix.buildProfile }}
unityVersion: ${{ matrix.unityVersion }} unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }} targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
@@ -179,6 +188,7 @@ jobs:
with: with:
buildName: 'GameCI Test Build' buildName: 'GameCI Test Build'
projectPath: ${{ matrix.projectPath }} projectPath: ${{ matrix.projectPath }}
buildProfile: ${{ matrix.buildProfile }}
unityVersion: ${{ matrix.unityVersion }} unityVersion: ${{ matrix.unityVersion }}
targetPlatform: ${{ matrix.targetPlatform }} targetPlatform: ${{ matrix.targetPlatform }}
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }} customParameters: -profile SomeProfile -someBoolean -someValue exampleValue ${{ matrix.additionalParameters }}
@@ -191,7 +201,6 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: name:
'Build ${{ matrix.targetPlatform }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp "Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
}}_params_${{ matrix.additionalParameters }})'
path: build path: build
retention-days: 14 retention-days: 14
+5 -1
View File
@@ -18,7 +18,11 @@ inputs:
projectPath: projectPath:
required: false required: false
default: '' default: ''
description: 'Relative path to the project to be built.' description: 'Path to the project to be built, relative to the repository root.'
buildProfile:
required: false
default: ''
description: 'Path to the build profile to activate, relative to the project root.'
buildName: buildName:
required: false required: false
default: '' default: ''
@@ -6,6 +6,9 @@ using UnityBuilderAction.Reporting;
using UnityBuilderAction.Versioning; using UnityBuilderAction.Versioning;
using UnityEditor; using UnityEditor;
using UnityEditor.Build.Reporting; using UnityEditor.Build.Reporting;
#if UNITY_6000_0_OR_NEWER
using UnityEditor.Build.Profile;
#endif
using UnityEngine; using UnityEngine;
namespace UnityBuilderAction namespace UnityBuilderAction
@@ -17,47 +20,9 @@ namespace UnityBuilderAction
// Gather values from args // Gather values from args
var options = ArgumentsParser.GetValidatedOptions(); var options = ArgumentsParser.GetValidatedOptions();
// Gather values from project
var scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(s => s.path).ToArray();
// Get all buildOptions from options
BuildOptions buildOptions = BuildOptions.None;
foreach (string buildOptionString in Enum.GetNames(typeof(BuildOptions))) {
if (options.ContainsKey(buildOptionString)) {
BuildOptions buildOptionEnum = (BuildOptions) Enum.Parse(typeof(BuildOptions), buildOptionString);
buildOptions |= buildOptionEnum;
}
}
#if UNITY_2021_2_OR_NEWER
// Determine subtarget
StandaloneBuildSubtarget buildSubtarget;
if (!options.TryGetValue("standaloneBuildSubtarget", out var subtargetValue) || !Enum.TryParse(subtargetValue, out buildSubtarget)) {
buildSubtarget = default;
}
#endif
// Define BuildPlayer Options
var buildPlayerOptions = new BuildPlayerOptions {
scenes = scenes,
locationPathName = options["customBuildPath"],
target = (BuildTarget) Enum.Parse(typeof(BuildTarget), options["buildTarget"]),
options = buildOptions,
#if UNITY_2021_2_OR_NEWER
subtarget = (int) buildSubtarget
#endif
};
// Set version for this build // Set version for this build
VersionApplicator.SetVersion(options["buildVersion"]); VersionApplicator.SetVersion(options["buildVersion"]);
// Apply Android settings
if (buildPlayerOptions.target == BuildTarget.Android)
{
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
AndroidSettings.Apply(options);
}
// Execute default AddressableAsset content build, if the package is installed. // Execute default AddressableAsset content build, if the package is installed.
// Version defines would be the best solution here, but Unity 2018 doesn't support that, // Version defines would be the best solution here, but Unity 2018 doesn't support that,
// so we fall back to using reflection instead. // so we fall back to using reflection instead.
@@ -78,6 +43,72 @@ namespace UnityBuilderAction
} }
} }
// Get all buildOptions from options
BuildOptions buildOptions = BuildOptions.None;
foreach (string buildOptionString in Enum.GetNames(typeof(BuildOptions))) {
if (options.ContainsKey(buildOptionString)) {
BuildOptions buildOptionEnum = (BuildOptions) Enum.Parse(typeof(BuildOptions), buildOptionString);
buildOptions |= buildOptionEnum;
}
}
// Depending on whether the build is using a build profile, `buildPlayerOptions` will an instance
// of either `UnityEditor.BuildPlayerOptions` or `UnityEditor.BuildPlayerWithProfileOptions`
dynamic buildPlayerOptions;
if (options["customBuildProfile"] != "") {
#if UNITY_6000_0_OR_NEWER
// Load build profile from Assets folder
BuildProfile buildProfile = AssetDatabase.LoadAssetAtPath<BuildProfile>(options["customBuildProfile"]);
// Set it as active
BuildProfile.SetActiveBuildProfile(buildProfile);
// Define BuildPlayerWithProfileOptions
buildPlayerOptions = new BuildPlayerWithProfileOptions {
buildProfile = buildProfile,
locationPathName = options["customBuildPath"],
options = buildOptions,
};
#else
throw new Exception("Build profiles are not supported by this version of Unity (" + Application.unityVersion +")");
#endif
} else {
// Gather values from project
var scenes = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(s => s.path).ToArray();
#if UNITY_2021_2_OR_NEWER
// Determine subtarget
StandaloneBuildSubtarget buildSubtarget;
if (!options.TryGetValue("standaloneBuildSubtarget", out var subtargetValue) || !Enum.TryParse(subtargetValue, out buildSubtarget)) {
buildSubtarget = default;
}
#endif
BuildTarget buildTarget = (BuildTarget) Enum.Parse(typeof(BuildTarget), options["buildTarget"]);
// Define BuildPlayerOptions
buildPlayerOptions = new BuildPlayerOptions {
scenes = scenes,
locationPathName = options["customBuildPath"],
target = buildTarget,
options = buildOptions,
#if UNITY_2021_2_OR_NEWER
subtarget = (int) buildSubtarget
#endif
};
// Apply Android settings
if (buildTarget == BuildTarget.Android) {
VersionApplicator.SetAndroidVersionCode(options["androidVersionCode"]);
AndroidSettings.Apply(options);
}
}
// Perform build // Perform build
BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions); BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions);
Generated Vendored
+63889 -40109
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+14938 -187
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -19,6 +19,23 @@ echo "Using build name \"$BUILD_NAME\"."
echo "Using build target \"$BUILD_TARGET\"." echo "Using build target \"$BUILD_TARGET\"."
#
# Display the build profile
#
if [ -z "$BUILD_PROFILE" ]; then
# User has not provided a build profile
#
echo "Doing a default \"$BUILD_TARGET\" platform build."
#
else
# User has provided a path to a build profile `.asset` file
#
echo "Using build profile \"$BUILD_PROFILE\" relative to \"$UNITY_PROJECT_PATH\"."
#
fi
# #
# Display build path and file # Display build path and file
# #
@@ -139,6 +156,7 @@ echo ""
-buildTarget "$BUILD_TARGET" \ -buildTarget "$BUILD_TARGET" \
-customBuildTarget "$BUILD_TARGET" \ -customBuildTarget "$BUILD_TARGET" \
-customBuildPath "$CUSTOM_BUILD_PATH" \ -customBuildPath "$CUSTOM_BUILD_PATH" \
-customBuildProfile "$BUILD_PROFILE" \
-executeMethod "$BUILD_METHOD" \ -executeMethod "$BUILD_METHOD" \
-buildVersion "$VERSION" \ -buildVersion "$VERSION" \
-androidVersionCode "$ANDROID_VERSION_CODE" \ -androidVersionCode "$ANDROID_VERSION_CODE" \
+17
View File
@@ -19,6 +19,22 @@ echo "Using build name \"$BUILD_NAME\"."
echo "Using build target \"$BUILD_TARGET\"." echo "Using build target \"$BUILD_TARGET\"."
#
# Display the build profile
#
if [ -z "$BUILD_PROFILE" ]; then
# User has not provided a build profile
#
echo "Doing a default \"$BUILD_TARGET\" platform build."
#
else
# User has provided a path to a build profile `.asset` file
#
echo "Using build profile \"$BUILD_PROFILE\" relative to \"$UNITY_PROJECT_PATH\"."
#
fi
# #
# Display build path and file # Display build path and file
# #
@@ -112,6 +128,7 @@ unity-editor \
-buildTarget "$BUILD_TARGET" \ -buildTarget "$BUILD_TARGET" \
-customBuildTarget "$BUILD_TARGET" \ -customBuildTarget "$BUILD_TARGET" \
-customBuildPath "$CUSTOM_BUILD_PATH" \ -customBuildPath "$CUSTOM_BUILD_PATH" \
-customBuildProfile "$BUILD_PROFILE" \
-executeMethod "$BUILD_METHOD" \ -executeMethod "$BUILD_METHOD" \
-buildVersion "$VERSION" \ -buildVersion "$VERSION" \
-androidVersionCode "$ANDROID_VERSION_CODE" \ -androidVersionCode "$ANDROID_VERSION_CODE" \
+20
View File
@@ -16,6 +16,25 @@ Write-Output "$('Using build name "')$($Env:BUILD_NAME)$('".')"
Write-Output "$('Using build target "')$($Env:BUILD_TARGET)$('".')" Write-Output "$('Using build target "')$($Env:BUILD_TARGET)$('".')"
#
# Display the build profile
#
if ($Env:BUILD_PROFILE)
{
# User has provided a path to a build profile `.asset` file
#
Write-Output "$('Using build profile "')$($Env:BUILD_PROFILE)$('" relative to "')$($Env:UNITY_PROJECT_PATH)$('".')"
#
}
else
{
# User has not provided a build profile
#
Write-Output "$('Doing a default "')$($Env:BUILD_TARGET)$('" platform build.')"
#
}
# #
# Display build path and file # Display build path and file
# #
@@ -143,6 +162,7 @@ $unityArgs = @(
"-buildTarget", "`"$Env:BUILD_TARGET`"", "-buildTarget", "`"$Env:BUILD_TARGET`"",
"-customBuildTarget", "`"$Env:BUILD_TARGET`"", "-customBuildTarget", "`"$Env:BUILD_TARGET`"",
"-customBuildPath", "`"$Env:CUSTOM_BUILD_PATH`"", "-customBuildPath", "`"$Env:CUSTOM_BUILD_PATH`"",
"-customBuildProfile", "`"$Env:BUILD_PROFILE`"",
"-buildVersion", "`"$Env:VERSION`"", "-buildVersion", "`"$Env:VERSION`"",
"-androidVersionCode", "`"$Env:ANDROID_VERSION_CODE`"", "-androidVersionCode", "`"$Env:ANDROID_VERSION_CODE`"",
"-androidKeystorePass", "`"$Env:ANDROID_KEYSTORE_PASS`"", "-androidKeystorePass", "`"$Env:ANDROID_KEYSTORE_PASS`"",
+5
View File
@@ -32,6 +32,11 @@
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@aws-sdk/client-cloudformation": "^3.777.0",
"@aws-sdk/client-cloudwatch-logs": "^3.777.0",
"@aws-sdk/client-ecs": "^3.778.0",
"@aws-sdk/client-kinesis": "^3.777.0",
"@aws-sdk/client-s3": "^3.779.0",
"@kubernetes/client-node": "^0.16.3", "@kubernetes/client-node": "^0.16.3",
"@octokit/core": "^5.1.0", "@octokit/core": "^5.1.0",
"async-wait-until": "^2.0.12", "async-wait-until": "^2.0.12",
+6
View File
@@ -71,6 +71,12 @@ describe('BuildParameters', () => {
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue })); await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue }));
}); });
it('returns the build profile', async () => {
const mockValue = 'path/to/build_profile.asset';
jest.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildProfile: mockValue }));
});
it('returns the build name', async () => { it('returns the build name', async () => {
const mockValue = 'someBuildName'; const mockValue = 'someBuildName';
jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
+2
View File
@@ -26,6 +26,7 @@ class BuildParameters {
public runnerTempPath!: string; public runnerTempPath!: string;
public targetPlatform!: string; public targetPlatform!: string;
public projectPath!: string; public projectPath!: string;
public buildProfile!: string;
public buildName!: string; public buildName!: string;
public buildPath!: string; public buildPath!: string;
public buildFile!: string; public buildFile!: string;
@@ -152,6 +153,7 @@ class BuildParameters {
runnerTempPath: Input.runnerTempPath, runnerTempPath: Input.runnerTempPath,
targetPlatform: Input.targetPlatform, targetPlatform: Input.targetPlatform,
projectPath: Input.projectPath, projectPath: Input.projectPath,
buildProfile: Input.buildProfile,
buildName: Input.buildName, buildName: Input.buildName,
buildPath: `${Input.buildsPath}/${Input.targetPlatform}`, buildPath: `${Input.buildsPath}/${Input.targetPlatform}`,
buildFile, buildFile,
@@ -1,6 +1,18 @@
import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as SDK from 'aws-sdk'; import {
CloudFormation,
CreateStackCommand,
CreateStackCommandInput,
DescribeStacksCommand,
DescribeStacksCommandInput,
ListStacksCommand,
Parameter,
UpdateStackCommand,
UpdateStackCommandInput,
waitUntilStackCreateComplete,
waitUntilStackUpdateComplete,
} from '@aws-sdk/client-cloudformation';
import { BaseStackFormation } from './cloud-formations/base-stack-formation'; import { BaseStackFormation } from './cloud-formations/base-stack-formation';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
@@ -10,51 +22,49 @@ export class AWSBaseStack {
} }
private baseStackName: string; private baseStackName: string;
async setupBaseStack(CF: SDK.CloudFormation) { async setupBaseStack(CF: CloudFormation) {
const baseStackName = this.baseStackName; const baseStackName = this.baseStackName;
const baseStack = BaseStackFormation.formation; const baseStack = BaseStackFormation.formation;
// Cloud Formation Input // Cloud Formation Input
const describeStackInput: SDK.CloudFormation.DescribeStacksInput = { const describeStackInput: DescribeStacksCommandInput = {
StackName: baseStackName, StackName: baseStackName,
}; };
const parametersWithoutHash: SDK.CloudFormation.Parameter[] = [ const parametersWithoutHash: Parameter[] = [{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName }];
{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName },
];
const parametersHash = crypto const parametersHash = crypto
.createHash('md5') .createHash('md5')
.update(baseStack + JSON.stringify(parametersWithoutHash)) .update(baseStack + JSON.stringify(parametersWithoutHash))
.digest('hex'); .digest('hex');
const parameters: SDK.CloudFormation.Parameter[] = [ const parameters: Parameter[] = [
...parametersWithoutHash, ...parametersWithoutHash,
...[{ ParameterKey: 'Version', ParameterValue: parametersHash }], ...[{ ParameterKey: 'Version', ParameterValue: parametersHash }],
]; ];
const updateInput: SDK.CloudFormation.UpdateStackInput = { const updateInput: UpdateStackCommandInput = {
StackName: baseStackName, StackName: baseStackName,
TemplateBody: baseStack, TemplateBody: baseStack,
Parameters: parameters, Parameters: parameters,
Capabilities: ['CAPABILITY_IAM'], Capabilities: ['CAPABILITY_IAM'],
}; };
const createStackInput: SDK.CloudFormation.CreateStackInput = { const createStackInput: CreateStackCommandInput = {
StackName: baseStackName, StackName: baseStackName,
TemplateBody: baseStack, TemplateBody: baseStack,
Parameters: parameters, Parameters: parameters,
Capabilities: ['CAPABILITY_IAM'], Capabilities: ['CAPABILITY_IAM'],
}; };
const stacks = await CF.listStacks({ const stacks = await CF.send(
StackStatusFilter: ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE'], new ListStacksCommand({ StackStatusFilter: ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE'] }),
}).promise(); );
const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || []; const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || [];
const stackExists: Boolean = stackNames.includes(baseStackName) || false; const stackExists: Boolean = stackNames.includes(baseStackName) || false;
const describeStack = async () => { const describeStack = async () => {
return await CF.describeStacks(describeStackInput).promise(); return await CF.send(new DescribeStacksCommand(describeStackInput));
}; };
try { try {
if (!stackExists) { if (!stackExists) {
CloudRunnerLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`); CloudRunnerLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`);
await CF.createStack(createStackInput).promise(); await CF.send(new CreateStackCommand(createStackInput));
CloudRunnerLogger.log(`created stack (version: ${parametersHash})`); CloudRunnerLogger.log(`created stack (version: ${parametersHash})`);
} }
const CFState = await describeStack(); const CFState = await describeStack();
@@ -65,7 +75,13 @@ export class AWSBaseStack {
const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue; const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue;
if (stack.StackStatus === 'CREATE_IN_PROGRESS') { if (stack.StackStatus === 'CREATE_IN_PROGRESS') {
await CF.waitFor('stackCreateComplete', describeStackInput).promise(); await waitUntilStackCreateComplete(
{
client: CF,
maxWaitTime: 200,
},
describeStackInput,
);
} }
if (stackExists) { if (stackExists) {
@@ -73,7 +89,7 @@ export class AWSBaseStack {
if (parametersHash !== stackVersion) { if (parametersHash !== stackVersion) {
CloudRunnerLogger.log(`Attempting update of base stack`); CloudRunnerLogger.log(`Attempting update of base stack`);
try { try {
await CF.updateStack(updateInput).promise(); await CF.send(new UpdateStackCommand(updateInput));
} catch (error: any) { } catch (error: any) {
if (error['message'].includes('No updates are to be performed')) { if (error['message'].includes('No updates are to be performed')) {
CloudRunnerLogger.log(`No updates are to be performed`); CloudRunnerLogger.log(`No updates are to be performed`);
@@ -93,7 +109,13 @@ export class AWSBaseStack {
); );
} }
if (stack.StackStatus === 'UPDATE_IN_PROGRESS') { if (stack.StackStatus === 'UPDATE_IN_PROGRESS') {
await CF.waitFor('stackUpdateComplete', describeStackInput).promise(); await waitUntilStackUpdateComplete(
{
client: CF,
maxWaitTime: 200,
},
describeStackInput,
);
} }
} }
CloudRunnerLogger.log('base stack is now ready'); CloudRunnerLogger.log('base stack is now ready');
@@ -1,15 +1,15 @@
import CloudRunnerLogger from '../../services/core/cloud-runner-logger'; import CloudRunnerLogger from '../../services/core/cloud-runner-logger';
import * as SDK from 'aws-sdk'; import { CloudFormation, DescribeStackEventsCommand } from '@aws-sdk/client-cloudformation';
import * as core from '@actions/core'; import * as core from '@actions/core';
import CloudRunner from '../../cloud-runner'; import CloudRunner from '../../cloud-runner';
export class AWSError { export class AWSError {
static async handleStackCreationFailure(error: any, CF: SDK.CloudFormation, taskDefStackName: string) { static async handleStackCreationFailure(error: any, CF: CloudFormation, taskDefStackName: string) {
CloudRunnerLogger.log('aws error: '); CloudRunnerLogger.log('aws error: ');
core.error(JSON.stringify(error, undefined, 4)); core.error(JSON.stringify(error, undefined, 4));
if (CloudRunner.buildParameters.cloudRunnerDebug) { if (CloudRunner.buildParameters.cloudRunnerDebug) {
CloudRunnerLogger.log('Getting events and resources for task stack'); CloudRunnerLogger.log('Getting events and resources for task stack');
const events = (await CF.describeStackEvents({ StackName: taskDefStackName }).promise()).StackEvents; const events = (await CF.send(new DescribeStackEventsCommand({ StackName: taskDefStackName }))).StackEvents;
CloudRunnerLogger.log(JSON.stringify(events, undefined, 4)); CloudRunnerLogger.log(JSON.stringify(events, undefined, 4));
} }
} }
@@ -1,4 +1,12 @@
import * as SDK from 'aws-sdk'; import {
CloudFormation,
CreateStackCommand,
CreateStackCommandInput,
DescribeStackResourcesCommand,
DescribeStacksCommand,
ListStacksCommand,
waitUntilStackCreateComplete,
} from '@aws-sdk/client-cloudformation';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
import CloudRunnerSecret from '../../options/cloud-runner-secret'; import CloudRunnerSecret from '../../options/cloud-runner-secret';
import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates'; import { AWSCloudFormationTemplates } from './aws-cloud-formation-templates';
@@ -16,7 +24,7 @@ export class AWSJobStack {
} }
public async setupCloudFormations( public async setupCloudFormations(
CF: SDK.CloudFormation, CF: CloudFormation,
buildGuid: string, buildGuid: string,
image: string, image: string,
entrypoint: string[], entrypoint: string[],
@@ -119,7 +127,7 @@ export class AWSJobStack {
let previousStackExists = true; let previousStackExists = true;
while (previousStackExists) { while (previousStackExists) {
previousStackExists = false; previousStackExists = false;
const stacks = await CF.listStacks().promise(); const stacks = await CF.send(new ListStacksCommand({}));
if (!stacks.StackSummaries) { if (!stacks.StackSummaries) {
throw new Error('Faild to get stacks'); throw new Error('Faild to get stacks');
} }
@@ -132,7 +140,7 @@ export class AWSJobStack {
} }
} }
} }
const createStackInput: SDK.CloudFormation.CreateStackInput = { const createStackInput: CreateStackCommandInput = {
StackName: taskDefStackName, StackName: taskDefStackName,
TemplateBody: taskDefCloudFormation, TemplateBody: taskDefCloudFormation,
Capabilities: ['CAPABILITY_IAM'], Capabilities: ['CAPABILITY_IAM'],
@@ -140,9 +148,15 @@ export class AWSJobStack {
}; };
try { try {
CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`); CloudRunnerLogger.log(`Creating job aws formation ${taskDefStackName}`);
await CF.createStack(createStackInput).promise(); await CF.send(new CreateStackCommand(createStackInput));
await CF.waitFor('stackCreateComplete', { StackName: taskDefStackName }).promise(); await waitUntilStackCreateComplete(
const describeStack = await CF.describeStacks({ StackName: taskDefStackName }).promise(); {
client: CF,
maxWaitTime: 200,
},
{ StackName: taskDefStackName },
);
const describeStack = await CF.send(new DescribeStacksCommand({ StackName: taskDefStackName }));
for (const parameter of parameters) { for (const parameter of parameters) {
if (!describeStack.Stacks?.[0].Parameters?.some((x) => x.ParameterKey === parameter.ParameterKey)) { if (!describeStack.Stacks?.[0].Parameters?.some((x) => x.ParameterKey === parameter.ParameterKey)) {
throw new Error(`Parameter ${parameter.ParameterKey} not found in stack`); throw new Error(`Parameter ${parameter.ParameterKey} not found in stack`);
@@ -153,7 +167,7 @@ export class AWSJobStack {
throw error; throw error;
} }
const createCleanupStackInput: SDK.CloudFormation.CreateStackInput = { const createCleanupStackInput: CreateStackCommandInput = {
StackName: `${taskDefStackName}-cleanup`, StackName: `${taskDefStackName}-cleanup`,
TemplateBody: CleanupCronFormation.formation, TemplateBody: CleanupCronFormation.formation,
Capabilities: ['CAPABILITY_IAM'], Capabilities: ['CAPABILITY_IAM'],
@@ -183,7 +197,7 @@ export class AWSJobStack {
if (CloudRunnerOptions.useCleanupCron) { if (CloudRunnerOptions.useCleanupCron) {
try { try {
CloudRunnerLogger.log(`Creating job cleanup formation`); CloudRunnerLogger.log(`Creating job cleanup formation`);
await CF.createStack(createCleanupStackInput).promise(); await CF.send(new CreateStackCommand(createCleanupStackInput));
// await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise(); // await CF.waitFor('stackCreateComplete', { StackName: createCleanupStackInput.StackName }).promise();
} catch (error) { } catch (error) {
@@ -193,12 +207,15 @@ export class AWSJobStack {
} }
const taskDefResources = ( const taskDefResources = (
await CF.describeStackResources({ await CF.send(
StackName: taskDefStackName, new DescribeStackResourcesCommand({
}).promise() StackName: taskDefStackName,
}),
)
).StackResources; ).StackResources;
const baseResources = (await CF.describeStackResources({ StackName: this.baseStackName }).promise()).StackResources; const baseResources = (await CF.send(new DescribeStackResourcesCommand({ StackName: this.baseStackName })))
.StackResources;
return { return {
taskDefStackName, taskDefStackName,
@@ -1,4 +1,19 @@
import * as AWS from 'aws-sdk'; import {
DescribeTasksCommand,
ECS,
RunTaskCommand,
RunTaskCommandInput,
Task,
waitUntilTasksRunning,
} from '@aws-sdk/client-ecs';
import {
DescribeStreamCommand,
DescribeStreamCommandOutput,
GetRecordsCommand,
GetRecordsCommandOutput,
GetShardIteratorCommand,
Kinesis,
} from '@aws-sdk/client-kinesis';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import * as core from '@actions/core'; import * as core from '@actions/core';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
@@ -12,8 +27,8 @@ import CloudRunnerOptions from '../../options/cloud-runner-options';
import GitHub from '../../../github'; import GitHub from '../../../github';
class AWSTaskRunner { class AWSTaskRunner {
public static ECS: AWS.ECS; public static ECS: ECS;
public static Kinesis: AWS.Kinesis; public static Kinesis: Kinesis;
private static readonly encodedUnderscore = `$252F`; private static readonly encodedUnderscore = `$252F`;
static async runTask( static async runTask(
taskDef: CloudRunnerAWSTaskDef, taskDef: CloudRunnerAWSTaskDef,
@@ -60,7 +75,7 @@ class AWSTaskRunner {
throw new Error(`Container Overrides length must be at most 8192`); throw new Error(`Container Overrides length must be at most 8192`);
} }
const task = await AWSTaskRunner.ECS.runTask(runParameters).promise(); const task = await AWSTaskRunner.ECS.send(new RunTaskCommand(runParameters as RunTaskCommandInput));
const taskArn = task.tasks?.[0].taskArn || ''; const taskArn = task.tasks?.[0].taskArn || '';
CloudRunnerLogger.log('Cloud runner job is starting'); CloudRunnerLogger.log('Cloud runner job is starting');
await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster); await AWSTaskRunner.waitUntilTaskRunning(taskArn, cluster);
@@ -108,7 +123,13 @@ class AWSTaskRunner {
private static async waitUntilTaskRunning(taskArn: string, cluster: string) { private static async waitUntilTaskRunning(taskArn: string, cluster: string) {
try { try {
await AWSTaskRunner.ECS.waitFor('tasksRunning', { tasks: [taskArn], cluster }).promise(); await waitUntilTasksRunning(
{
client: AWSTaskRunner.ECS,
maxWaitTime: 120,
},
{ tasks: [taskArn], cluster },
);
} catch (error_) { } catch (error_) {
const error = error_ as Error; const error = error_ as Error;
await new Promise((resolve) => setTimeout(resolve, 3000)); await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -124,10 +145,7 @@ class AWSTaskRunner {
} }
static async describeTasks(clusterName: string, taskArn: string) { static async describeTasks(clusterName: string, taskArn: string) {
const tasks = await AWSTaskRunner.ECS.describeTasks({ const tasks = await AWSTaskRunner.ECS.send(new DescribeTasksCommand({ cluster: clusterName, tasks: [taskArn] }));
cluster: clusterName,
tasks: [taskArn],
}).promise();
if (tasks.tasks?.[0]) { if (tasks.tasks?.[0]) {
return tasks.tasks?.[0]; return tasks.tasks?.[0];
} else { } else {
@@ -169,9 +187,7 @@ class AWSTaskRunner {
output: string, output: string,
shouldCleanup: boolean, shouldCleanup: boolean,
) { ) {
const records = await AWSTaskRunner.Kinesis.getRecords({ const records = await AWSTaskRunner.Kinesis.send(new GetRecordsCommand({ ShardIterator: iterator }));
ShardIterator: iterator,
}).promise();
iterator = records.NextShardIterator || ''; iterator = records.NextShardIterator || '';
({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords( ({ shouldReadLogs, output, shouldCleanup } = AWSTaskRunner.logRecords(
records, records,
@@ -184,7 +200,7 @@ class AWSTaskRunner {
return { iterator, shouldReadLogs, output, shouldCleanup }; return { iterator, shouldReadLogs, output, shouldCleanup };
} }
private static checkStreamingShouldContinue(taskData: AWS.ECS.Task, timestamp: number, shouldReadLogs: boolean) { private static checkStreamingShouldContinue(taskData: Task, timestamp: number, shouldReadLogs: boolean) {
if (taskData?.lastStatus === 'UNKNOWN') { if (taskData?.lastStatus === 'UNKNOWN') {
CloudRunnerLogger.log('## Cloud runner job unknwon'); CloudRunnerLogger.log('## Cloud runner job unknwon');
} }
@@ -204,15 +220,17 @@ class AWSTaskRunner {
} }
private static logRecords( private static logRecords(
records: AWS.Kinesis.GetRecordsOutput, records: GetRecordsCommandOutput,
iterator: string, iterator: string,
shouldReadLogs: boolean, shouldReadLogs: boolean,
output: string, output: string,
shouldCleanup: boolean, shouldCleanup: boolean,
) { ) {
if (records.Records.length > 0 && iterator) { if ((records.Records ?? []).length > 0 && iterator) {
for (const record of records.Records) { for (const record of records.Records ?? []) {
const json = JSON.parse(zlib.gunzipSync(Buffer.from(record.Data as string, 'base64')).toString('utf8')); const json = JSON.parse(
zlib.gunzipSync(Buffer.from(record.Data as unknown as string, 'base64')).toString('utf8'),
);
if (json.messageType === 'DATA_MESSAGE') { if (json.messageType === 'DATA_MESSAGE') {
for (const logEvent of json.logEvents) { for (const logEvent of json.logEvents) {
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration( ({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
@@ -230,19 +248,19 @@ class AWSTaskRunner {
} }
private static async getLogStream(kinesisStreamName: string) { private static async getLogStream(kinesisStreamName: string) {
return await AWSTaskRunner.Kinesis.describeStream({ return await AWSTaskRunner.Kinesis.send(new DescribeStreamCommand({ StreamName: kinesisStreamName }));
StreamName: kinesisStreamName,
}).promise();
} }
private static async getLogIterator(stream: AWS.Kinesis.DescribeStreamOutput) { private static async getLogIterator(stream: DescribeStreamCommandOutput) {
return ( return (
( (
await AWSTaskRunner.Kinesis.getShardIterator({ await AWSTaskRunner.Kinesis.send(
ShardIteratorType: 'TRIM_HORIZON', new GetShardIteratorCommand({
StreamName: stream.StreamDescription.StreamName, ShardIteratorType: 'TRIM_HORIZON',
ShardId: stream.StreamDescription.Shards[0].ShardId, StreamName: stream.StreamDescription?.StreamName ?? '',
}).promise() ShardId: stream.StreamDescription?.Shards?.[0]?.ShardId || '',
}),
)
).ShardIterator || '' ).ShardIterator || ''
); );
} }
@@ -1,9 +1,9 @@
import * as AWS from 'aws-sdk'; import { StackResource } from '@aws-sdk/client-cloudformation';
class CloudRunnerAWSTaskDef { class CloudRunnerAWSTaskDef {
public taskDefStackName!: string; public taskDefStackName!: string;
public taskDefCloudFormation!: string; public taskDefCloudFormation!: string;
public taskDefResources: AWS.CloudFormation.StackResources | undefined; public taskDefResources: StackResource[] | undefined;
public baseResources: AWS.CloudFormation.StackResources | undefined; public baseResources: StackResource[] | undefined;
} }
export default CloudRunnerAWSTaskDef; export default CloudRunnerAWSTaskDef;
+28 -18
View File
@@ -1,4 +1,6 @@
import * as SDK from 'aws-sdk'; import { CloudFormation, DeleteStackCommand, waitUntilStackDeleteComplete } from '@aws-sdk/client-cloudformation';
import { ECS as ECSClient } from '@aws-sdk/client-ecs';
import { Kinesis } from '@aws-sdk/client-kinesis';
import CloudRunnerSecret from '../../options/cloud-runner-secret'; import CloudRunnerSecret from '../../options/cloud-runner-secret';
import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable'; import CloudRunnerEnvironmentVariable from '../../options/cloud-runner-environment-variable';
import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def'; import CloudRunnerAWSTaskDef from './cloud-runner-aws-task-def';
@@ -75,7 +77,7 @@ class AWSBuildEnvironment implements ProviderInterface {
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[], defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
) { ) {
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const CF = new SDK.CloudFormation(); const CF = new CloudFormation({ region: Input.region });
await new AwsBaseStack(this.baseStackName).setupBaseStack(CF); await new AwsBaseStack(this.baseStackName).setupBaseStack(CF);
} }
@@ -89,10 +91,10 @@ class AWSBuildEnvironment implements ProviderInterface {
secrets: CloudRunnerSecret[], secrets: CloudRunnerSecret[],
): Promise<string> { ): Promise<string> {
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const ECS = new SDK.ECS(); const ECS = new ECSClient({ region: Input.region });
const CF = new SDK.CloudFormation(); const CF = new CloudFormation({ region: Input.region });
AwsTaskRunner.ECS = ECS; AwsTaskRunner.ECS = ECS;
AwsTaskRunner.Kinesis = new SDK.Kinesis(); AwsTaskRunner.Kinesis = new Kinesis({ region: Input.region });
CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`); CloudRunnerLogger.log(`AWS Region: ${CF.config.region}`);
const entrypoint = ['/bin/sh']; const entrypoint = ['/bin/sh'];
const startTimeMs = Date.now(); const startTimeMs = Date.now();
@@ -129,23 +131,31 @@ class AWSBuildEnvironment implements ProviderInterface {
} }
} }
async cleanupResources(CF: SDK.CloudFormation, taskDef: CloudRunnerAWSTaskDef) { async cleanupResources(CF: CloudFormation, taskDef: CloudRunnerAWSTaskDef) {
CloudRunnerLogger.log('Cleanup starting'); CloudRunnerLogger.log('Cleanup starting');
await CF.deleteStack({ await CF.send(new DeleteStackCommand({ StackName: taskDef.taskDefStackName }));
StackName: taskDef.taskDefStackName,
}).promise();
if (CloudRunnerOptions.useCleanupCron) { if (CloudRunnerOptions.useCleanupCron) {
await CF.deleteStack({ await CF.send(new DeleteStackCommand({ StackName: `${taskDef.taskDefStackName}-cleanup` }));
StackName: `${taskDef.taskDefStackName}-cleanup`,
}).promise();
} }
await CF.waitFor('stackDeleteComplete', { await waitUntilStackDeleteComplete(
StackName: taskDef.taskDefStackName, {
}).promise(); client: CF,
await CF.waitFor('stackDeleteComplete', { maxWaitTime: 200,
StackName: `${taskDef.taskDefStackName}-cleanup`, },
}).promise(); {
StackName: taskDef.taskDefStackName,
},
);
await waitUntilStackDeleteComplete(
{
client: CF,
maxWaitTime: 200,
},
{
StackName: `${taskDef.taskDefStackName}-cleanup`,
},
);
CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`); CloudRunnerLogger.log(`Deleted Stack: ${taskDef.taskDefStackName}`);
CloudRunnerLogger.log('Cleanup complete'); CloudRunnerLogger.log('Cleanup complete');
} }
@@ -1,4 +1,11 @@
import AWS from 'aws-sdk'; import {
CloudFormation,
DeleteStackCommand,
DeleteStackCommandInput,
DescribeStackResourcesCommand,
} from '@aws-sdk/client-cloudformation';
import { CloudWatchLogs, DeleteLogGroupCommand } from '@aws-sdk/client-cloudwatch-logs';
import { ECS, StopTaskCommand } from '@aws-sdk/client-ecs';
import Input from '../../../../input'; import Input from '../../../../input';
import CloudRunnerLogger from '../../../services/core/cloud-runner-logger'; import CloudRunnerLogger from '../../../services/core/cloud-runner-logger';
import { TaskService } from './task-service'; import { TaskService } from './task-service';
@@ -12,9 +19,9 @@ export class GarbageCollectionService {
public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) { public static async cleanup(deleteResources = false, OneDayOlderOnly: boolean = false) {
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const CF = new AWS.CloudFormation(); const CF = new CloudFormation({ region: Input.region });
const ecs = new AWS.ECS(); const ecs = new ECS({ region: Input.region });
const cwl = new AWS.CloudWatchLogs(); const cwl = new CloudWatchLogs({ region: Input.region });
const taskDefinitionsInUse = new Array(); const taskDefinitionsInUse = new Array();
const tasks = await TaskService.getTasks(); const tasks = await TaskService.getTasks();
@@ -23,14 +30,14 @@ export class GarbageCollectionService {
taskDefinitionsInUse.push(taskElement.taskDefinitionArn); taskDefinitionsInUse.push(taskElement.taskDefinitionArn);
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.createdAt!))) { if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(taskElement.createdAt!))) {
CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`); CloudRunnerLogger.log(`Stopping task ${taskElement.containers?.[0].name}`);
await ecs.stopTask({ task: taskElement.taskArn || '', cluster: element }).promise(); await ecs.send(new StopTaskCommand({ task: taskElement.taskArn || '', cluster: element }));
} }
} }
const jobStacks = await TaskService.getCloudFormationJobStacks(); const jobStacks = await TaskService.getCloudFormationJobStacks();
for (const element of jobStacks) { for (const element of jobStacks) {
if ( if (
(await CF.describeStackResources({ StackName: element.StackName }).promise()).StackResources?.some( (await CF.send(new DescribeStackResourcesCommand({ StackName: element.StackName }))).StackResources?.some(
(x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId), (x) => x.ResourceType === 'AWS::ECS::TaskDefinition' && taskDefinitionsInUse.includes(x.PhysicalResourceId),
) )
) { ) {
@@ -39,7 +46,10 @@ export class GarbageCollectionService {
return; return;
} }
if (deleteResources && (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(element.CreationTime))) { if (
deleteResources &&
(!OneDayOlderOnly || (element.CreationTime && GarbageCollectionService.isOlderThan1day(element.CreationTime)))
) {
if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') { if (element.StackName === 'game-ci' || element.TemplateDescription === 'Game-CI base stack') {
CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`); CloudRunnerLogger.log(`Skipping ${element.StackName} ignore list`);
@@ -47,8 +57,8 @@ export class GarbageCollectionService {
} }
CloudRunnerLogger.log(`Deleting ${element.StackName}`); CloudRunnerLogger.log(`Deleting ${element.StackName}`);
const deleteStackInput: AWS.CloudFormation.DeleteStackInput = { StackName: element.StackName }; const deleteStackInput: DeleteStackCommandInput = { StackName: element.StackName };
await CF.deleteStack(deleteStackInput).promise(); await CF.send(new DeleteStackCommand(deleteStackInput));
} }
} }
const logGroups = await TaskService.getLogGroups(); const logGroups = await TaskService.getLogGroups();
@@ -58,7 +68,7 @@ export class GarbageCollectionService {
(!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.creationTime!))) (!OneDayOlderOnly || GarbageCollectionService.isOlderThan1day(new Date(element.creationTime!)))
) { ) {
CloudRunnerLogger.log(`Deleting ${element.logGroupName}`); CloudRunnerLogger.log(`Deleting ${element.logGroupName}`);
await cwl.deleteLogGroup({ logGroupName: element.logGroupName || '' }).promise(); await cwl.send(new DeleteLogGroupCommand({ logGroupName: element.logGroupName || '' }));
} }
} }
@@ -1,12 +1,31 @@
import AWS from 'aws-sdk'; import {
CloudFormation,
DescribeStackResourcesCommand,
DescribeStacksCommand,
ListStacksCommand,
StackSummary,
} from '@aws-sdk/client-cloudformation';
import {
CloudWatchLogs,
DescribeLogGroupsCommand,
DescribeLogGroupsCommandInput,
LogGroup,
} from '@aws-sdk/client-cloudwatch-logs';
import {
DescribeTasksCommand,
DescribeTasksCommandInput,
ECS,
ListClustersCommand,
ListTasksCommand,
ListTasksCommandInput,
Task,
} from '@aws-sdk/client-ecs';
import { ListObjectsCommand, ListObjectsCommandInput, S3 } from '@aws-sdk/client-s3';
import Input from '../../../../input'; import Input from '../../../../input';
import CloudRunnerLogger from '../../../services/core/cloud-runner-logger'; import CloudRunnerLogger from '../../../services/core/cloud-runner-logger';
import { BaseStackFormation } from '../cloud-formations/base-stack-formation'; import { BaseStackFormation } from '../cloud-formations/base-stack-formation';
import AwsTaskRunner from '../aws-task-runner'; import AwsTaskRunner from '../aws-task-runner';
import { ListObjectsRequest } from 'aws-sdk/clients/s3';
import CloudRunner from '../../../cloud-runner'; import CloudRunner from '../../../cloud-runner';
import { StackSummaries } from 'aws-sdk/clients/cloudformation';
import { LogGroups } from 'aws-sdk/clients/cloudwatchlogs';
export class TaskService { export class TaskService {
static async watch() { static async watch() {
@@ -20,20 +39,24 @@ export class TaskService {
return output; return output;
} }
public static async getCloudFormationJobStacks() { public static async getCloudFormationJobStacks() {
const result: StackSummaries = []; const result: StackSummary[] = [];
CloudRunnerLogger.log(``); CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`List Cloud Formation Stacks`); CloudRunnerLogger.log(`List Cloud Formation Stacks`);
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const CF = new AWS.CloudFormation(); const CF = new CloudFormation({ region: Input.region });
const stacks = const stacks =
(await CF.listStacks().promise()).StackSummaries?.filter( (await CF.send(new ListStacksCommand({}))).StackSummaries?.filter(
(_x) => (_x) =>
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription !== BaseStackFormation.baseStackDecription, _x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription !== BaseStackFormation.baseStackDecription,
) || []; ) || [];
CloudRunnerLogger.log(``); CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`Cloud Formation Stacks ${stacks.length}`); CloudRunnerLogger.log(`Cloud Formation Stacks ${stacks.length}`);
for (const element of stacks) { for (const element of stacks) {
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime()); if (!element.CreationTime) {
CloudRunnerLogger.log(`${element.StackName} due to undefined CreationTime`);
}
const ageDate: Date = new Date(Date.now() - (element.CreationTime?.getTime() ?? 0));
CloudRunnerLogger.log( CloudRunnerLogger.log(
`Task Stack ${element.StackName} - Age D${Math.floor( `Task Stack ${element.StackName} - Age D${Math.floor(
@@ -43,14 +66,18 @@ export class TaskService {
result.push(element); result.push(element);
} }
const baseStacks = const baseStacks =
(await CF.listStacks().promise()).StackSummaries?.filter( (await CF.send(new ListStacksCommand({}))).StackSummaries?.filter(
(_x) => (_x) =>
_x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription, _x.StackStatus !== 'DELETE_COMPLETE' && _x.TemplateDescription === BaseStackFormation.baseStackDecription,
) || []; ) || [];
CloudRunnerLogger.log(``); CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`); CloudRunnerLogger.log(`Base Stacks ${baseStacks.length}`);
for (const element of baseStacks) { for (const element of baseStacks) {
const ageDate: Date = new Date(Date.now() - element.CreationTime.getTime()); if (!element.CreationTime) {
CloudRunnerLogger.log(`${element.StackName} due to undefined CreationTime`);
}
const ageDate: Date = new Date(Date.now() - (element.CreationTime?.getTime() ?? 0));
CloudRunnerLogger.log( CloudRunnerLogger.log(
`Task Stack ${element.StackName} - Age D${Math.floor( `Task Stack ${element.StackName} - Age D${Math.floor(
@@ -64,22 +91,22 @@ export class TaskService {
return result; return result;
} }
public static async getTasks() { public static async getTasks() {
const result: { taskElement: AWS.ECS.Task; element: string }[] = []; const result: { taskElement: Task; element: string }[] = [];
CloudRunnerLogger.log(``); CloudRunnerLogger.log(``);
CloudRunnerLogger.log(`List Tasks`); CloudRunnerLogger.log(`List Tasks`);
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const ecs = new AWS.ECS(); const ecs = new ECS({ region: Input.region });
const clusters = (await ecs.listClusters().promise()).clusterArns || []; const clusters = (await ecs.send(new ListClustersCommand({}))).clusterArns || [];
CloudRunnerLogger.log(`Task Clusters ${clusters.length}`); CloudRunnerLogger.log(`Task Clusters ${clusters.length}`);
for (const element of clusters) { for (const element of clusters) {
const input: AWS.ECS.ListTasksRequest = { const input: ListTasksCommandInput = {
cluster: element, cluster: element,
}; };
const list = (await ecs.listTasks(input).promise()).taskArns || []; const list = (await ecs.send(new ListTasksCommand(input))).taskArns || [];
if (list.length > 0) { if (list.length > 0) {
const describeInput: AWS.ECS.DescribeTasksRequest = { tasks: list, cluster: element }; const describeInput: DescribeTasksCommandInput = { tasks: list, cluster: element };
const describeList = (await ecs.describeTasks(describeInput).promise()).tasks || []; const describeList = (await ecs.send(new DescribeTasksCommand(describeInput))).tasks || [];
if (describeList.length === 0) { if (describeList.length === 0) {
CloudRunnerLogger.log(`No Tasks`); CloudRunnerLogger.log(`No Tasks`);
continue; continue;
@@ -105,37 +132,48 @@ export class TaskService {
} }
public static async awsDescribeJob(job: string) { public static async awsDescribeJob(job: string) {
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const CF = new AWS.CloudFormation(); const CF = new CloudFormation({ region: Input.region });
const stack = (await CF.listStacks().promise()).StackSummaries?.find((_x) => _x.StackName === job) || undefined; try {
const stackInfo = (await CF.describeStackResources({ StackName: job }).promise()) || undefined; const stack =
const stackInfo2 = (await CF.describeStacks({ StackName: job }).promise()) || undefined; (await CF.send(new ListStacksCommand({}))).StackSummaries?.find((_x) => _x.StackName === job) || undefined;
if (stack === undefined) { const stackInfo = (await CF.send(new DescribeStackResourcesCommand({ StackName: job }))) || undefined;
throw new Error('stack not defined'); const stackInfo2 = (await CF.send(new DescribeStacksCommand({ StackName: job }))) || undefined;
} if (stack === undefined) {
const ageDate: Date = new Date(Date.now() - stack.CreationTime.getTime()); throw new Error('stack not defined');
const message = ` }
if (!stack.CreationTime) {
CloudRunnerLogger.log(`${stack.StackName} due to undefined CreationTime`);
}
const ageDate: Date = new Date(Date.now() - (stack.CreationTime?.getTime() ?? 0));
const message = `
Task Stack ${stack.StackName} Task Stack ${stack.StackName}
Age D${Math.floor(ageDate.getHours() / 24)} H${ageDate.getHours()} M${ageDate.getMinutes()} Age D${Math.floor(ageDate.getHours() / 24)} H${ageDate.getHours()} M${ageDate.getMinutes()}
${JSON.stringify(stack, undefined, 4)} ${JSON.stringify(stack, undefined, 4)}
${JSON.stringify(stackInfo, undefined, 4)} ${JSON.stringify(stackInfo, undefined, 4)}
${JSON.stringify(stackInfo2, undefined, 4)} ${JSON.stringify(stackInfo2, undefined, 4)}
`; `;
CloudRunnerLogger.log(message); CloudRunnerLogger.log(message);
return message; return message;
} catch (error) {
CloudRunnerLogger.error(
`Failed to describe job ${job}: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
}
} }
public static async getLogGroups() { public static async getLogGroups() {
const result: LogGroups = []; const result: Array<LogGroup> = [];
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const ecs = new AWS.CloudWatchLogs(); const ecs = new CloudWatchLogs();
let logStreamInput: AWS.CloudWatchLogs.DescribeLogGroupsRequest = { let logStreamInput: DescribeLogGroupsCommandInput = {
/* logGroupNamePrefix: 'game-ci' */ /* logGroupNamePrefix: 'game-ci' */
}; };
let logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise(); let logGroupsDescribe = await ecs.send(new DescribeLogGroupsCommand(logStreamInput));
const logGroups = logGroupsDescribe.logGroups || []; const logGroups = logGroupsDescribe.logGroups || [];
while (logGroupsDescribe.nextToken) { while (logGroupsDescribe.nextToken) {
logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken }; logStreamInput = { /* logGroupNamePrefix: 'game-ci',*/ nextToken: logGroupsDescribe.nextToken };
logGroupsDescribe = await ecs.describeLogGroups(logStreamInput).promise(); logGroupsDescribe = await ecs.send(new DescribeLogGroupsCommand(logStreamInput));
logGroups.push(...(logGroupsDescribe?.logGroups || [])); logGroups.push(...(logGroupsDescribe?.logGroups || []));
} }
@@ -159,11 +197,12 @@ export class TaskService {
} }
public static async getLocks() { public static async getLocks() {
process.env.AWS_REGION = Input.region; process.env.AWS_REGION = Input.region;
const s3 = new AWS.S3(); const s3 = new S3({ region: Input.region });
const listRequest: ListObjectsRequest = { const listRequest: ListObjectsCommandInput = {
Bucket: CloudRunner.buildParameters.awsStackName, Bucket: CloudRunner.buildParameters.awsStackName,
}; };
const results = await s3.listObjects(listRequest).promise();
const results = await s3.send(new ListObjectsCommand(listRequest));
return results.Contents || []; return results.Contents || [];
} }
+1
View File
@@ -36,6 +36,7 @@ class ImageEnvironmentFactory {
value: process.env.USYM_UPLOAD_AUTH_TOKEN, value: process.env.USYM_UPLOAD_AUTH_TOKEN,
}, },
{ name: 'PROJECT_PATH', value: parameters.projectPath }, { name: 'PROJECT_PATH', value: parameters.projectPath },
{ name: 'BUILD_PROFILE', value: parameters.buildProfile },
{ name: 'BUILD_TARGET', value: parameters.targetPlatform }, { name: 'BUILD_TARGET', value: parameters.targetPlatform },
{ name: 'BUILD_NAME', value: parameters.buildName }, { name: 'BUILD_NAME', value: parameters.buildName },
{ name: 'BUILD_PATH', value: parameters.buildPath }, { name: 'BUILD_PATH', value: parameters.buildPath },
+13
View File
@@ -59,6 +59,19 @@ describe('Input', () => {
}); });
}); });
describe('buildProfile', () => {
it('returns the default value', () => {
expect(Input.buildProfile).toStrictEqual('');
});
it('takes input from the users workflow', () => {
const mockValue = 'path/to/build_profile.asset';
const spy = jest.spyOn(core, 'getInput').mockReturnValue(mockValue);
expect(Input.buildProfile).toStrictEqual(mockValue);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('buildName', () => { describe('buildName', () => {
it('returns the default value', () => { it('returns the default value', () => {
expect(Input.buildName).toStrictEqual(Input.targetPlatform); expect(Input.buildName).toStrictEqual(Input.targetPlatform);
+4
View File
@@ -107,6 +107,10 @@ class Input {
return rawProjectPath.replace(/\/$/, ''); return rawProjectPath.replace(/\/$/, '');
} }
static get buildProfile(): string {
return Input.getInput('buildProfile') ?? '';
}
static get runnerTempPath(): string { static get runnerTempPath(): string {
return Input.getInput('RUNNER_TEMP') ?? ''; return Input.getInput('RUNNER_TEMP') ?? '';
} }
+1
View File
@@ -170,6 +170,7 @@ class SetupMac {
process.env.UNITY_LICENSING_SERVER = buildParameters.unityLicensingServer; process.env.UNITY_LICENSING_SERVER = buildParameters.unityLicensingServer;
process.env.SKIP_ACTIVATION = buildParameters.skipActivation; process.env.SKIP_ACTIVATION = buildParameters.skipActivation;
process.env.PROJECT_PATH = buildParameters.projectPath; process.env.PROJECT_PATH = buildParameters.projectPath;
process.env.BUILD_PROFILE = buildParameters.buildProfile;
process.env.BUILD_TARGET = buildParameters.targetPlatform; process.env.BUILD_TARGET = buildParameters.targetPlatform;
process.env.BUILD_NAME = buildParameters.buildName; process.env.BUILD_NAME = buildParameters.buildName;
process.env.BUILD_PATH = buildParameters.buildPath; process.env.BUILD_PATH = buildParameters.buildPath;
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 28bfc999a135648538355bfcb6a23aee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cd91492ed9aca40c49d42156a4a8f387
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
m_Name: Sample WebGL Build Profile
m_EditorClassIdentifier:
m_AssetVersion: 1
m_BuildTarget: 20
m_Subtarget: 0
m_PlatformId: 84a3bb9e7420477f885e98145999eb20
m_PlatformBuildProfile:
rid: 200022742090383361
m_OverrideGlobalSceneList: 0
m_Scenes: []
m_ScriptingDefines: []
m_PlayerSettingsYaml:
m_Settings: []
references:
version: 2
RefIds:
- rid: 200022742090383361
type: {class: WebGLPlatformSettings, ns: UnityEditor.WebGL, asm: UnityEditor.WebGL.Extensions}
data:
m_Development: 0
m_ConnectProfiler: 0
m_BuildWithDeepProfilingSupport: 0
m_AllowDebugging: 0
m_WaitForManagedDebugger: 0
m_ManagedDebuggerFixedPort: 0
m_ExplicitNullChecks: 0
m_ExplicitDivideByZeroChecks: 0
m_ExplicitArrayBoundsChecks: 0
m_CompressionType: -1
m_InstallInBuildFolder: 0
m_CodeOptimization: 0
m_WebGLClientBrowserPath:
m_WebGLClientBrowserType: 0
m_WebGLTextureSubtarget: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b9aac23ad2add4b439decb0cf65b0d68
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant: