mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-09-29 12:07:05 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa47dc9fb | ||
|
|
eb1b9fba12 |
@@ -0,0 +1,92 @@
|
||||
name: Auto-retry known-transient CI flakes
|
||||
|
||||
# Careful, narrow auto-healing for this repo's OWN test CI - not a blanket
|
||||
# "retry until green" mechanism. game-ci/cli's Unity build/activate steps
|
||||
# already retry a specific, documented set of transient licensing errors
|
||||
# in-process (up to --licenseRetryMaxAttempts, default 4, exponential
|
||||
# backoff - see dist/platforms/{mac,ubuntu}/steps/{activate,build}.sh). That
|
||||
# retries on the SAME runner instance, so it can't help when the underlying
|
||||
# issue is runner-specific (e.g. a stuck Gatekeeper/codesign cache) rather
|
||||
# than a genuinely transient network blip - all 4 in-process attempts then
|
||||
# fail identically, and only a fresh job (potentially a different runner)
|
||||
# has a chance.
|
||||
#
|
||||
# This workflow closes exactly that gap: if a build-tests-* run finishes
|
||||
# with failed jobs, and EVERY failed job's log matches the exact same
|
||||
# known-transient pattern the in-process retry already uses (nothing
|
||||
# broader - a real compile error, a genuine license misconfiguration, or
|
||||
# any other failure never matches and is never auto-retried), rerun the
|
||||
# failed jobs once. If any failed job doesn't match, or this is already a
|
||||
# retry (workflow_run.run_attempt > 1), do nothing - a human or agent needs
|
||||
# to look at it.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Builds - MacOS', 'Builds - Ubuntu', 'Builds - Windows']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
retry-if-known-flake:
|
||||
name: Retry if known-transient flake
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1
|
||||
steps:
|
||||
- name: Check failed jobs against the known-transient pattern, rerun if all match
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
# Kept in sync with dist/platforms/{mac,ubuntu}/steps/{activate,build}.sh's
|
||||
# own UNITY_*_TRANSIENT_LICENSE_ERROR_PATTERN - update both together.
|
||||
TRANSIENT_PATTERN: 'TimeoutPolicy did not complete|Access token is unavailable|entitlement groups and 0 free entitlements|License activation has failed|No valid Unity Editor license found|License is not active'
|
||||
with:
|
||||
script: |
|
||||
const runId = context.payload.workflow_run.id;
|
||||
const pattern = new RegExp(process.env.TRANSIENT_PATTERN);
|
||||
|
||||
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: runId,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const failedJobs = jobs.filter((job) => job.conclusion === 'failure');
|
||||
if (failedJobs.length === 0) {
|
||||
core.info('No failed jobs found (a cancelled/skipped-only run) - nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
let allMatch = true;
|
||||
for (const job of failedJobs) {
|
||||
let log = '';
|
||||
try {
|
||||
const response = await github.rest.actions.downloadJobLogsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: job.id,
|
||||
});
|
||||
log = response.data.toString();
|
||||
} catch (error) {
|
||||
core.warning(`Could not fetch logs for job ${job.name} (${job.id}): ${error.message}`);
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!pattern.test(log)) {
|
||||
core.info(`Job "${job.name}" failed without the known-transient pattern - not auto-retrying this run.`);
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
core.info(`Job "${job.name}" failed with the known-transient pattern.`);
|
||||
}
|
||||
|
||||
if (!allMatch) return;
|
||||
|
||||
core.info(`All ${failedJobs.length} failed job(s) matched the known-transient pattern - rerunning failed jobs.`);
|
||||
await github.rest.actions.reRunWorkflowFailedJobs({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: runId,
|
||||
});
|
||||
@@ -12,6 +12,10 @@ jobs:
|
||||
buildForAllPlatformsMacOS:
|
||||
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
|
||||
runs-on: macos-latest
|
||||
# Kept as a backstop even after splitting the cache save below into its
|
||||
# own bounded step: real builds finish in well under 40m, so this only
|
||||
# fires if something else entirely hangs.
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -41,12 +45,21 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Cache #
|
||||
# Cache (restore) #
|
||||
###########################
|
||||
- uses: actions/cache@v4
|
||||
# Split from the save side (below) rather than the combined
|
||||
# actions/cache@v4: that action's implicit post-run save has been
|
||||
# observed hanging indefinitely on macOS runners (unrelated to the
|
||||
# cache key itself - confirmed by reproducing it again after fixing a
|
||||
# real key collision here). A per-step timeout doesn't bound an
|
||||
# implicit post-run cleanup step, only an explicit step does - and
|
||||
# caching is a pure optimization, so losing one save is far cheaper
|
||||
# than a job stuck "in_progress" for 1.5h+.
|
||||
- uses: actions/cache/restore@v4
|
||||
id: cache-restore
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}
|
||||
key: Library-${{ matrix.projectPath }}-macos-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}-${{ matrix.buildProfile }}
|
||||
restore-keys: |
|
||||
Library-${{ matrix.projectPath }}-macos-
|
||||
Library-
|
||||
@@ -65,6 +78,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
@@ -85,3 +99,19 @@ jobs:
|
||||
name: Build ${{ matrix.targetPlatform }} on MacOS (${{ matrix.unityVersion }})${{ matrix.buildProfile && ' With Build Profile' || '' }}
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
||||
###########################
|
||||
# Cache (save) #
|
||||
###########################
|
||||
# A bounded, non-fatal step (unlike the combined action's implicit
|
||||
# post-run save): a hang here is killed after 5m instead of stalling
|
||||
# the whole job, and continue-on-error means a failed/timed-out save
|
||||
# never fails the build - only a real cache miss on the next run,
|
||||
# which is a much cheaper failure mode.
|
||||
- if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: ${{ steps.cache-restore.outputs.cache-primary-key }}
|
||||
|
||||
@@ -37,6 +37,18 @@ jobs:
|
||||
buildForAllPlatformsUbuntu:
|
||||
name: "${{ matrix.targetPlatform }} on ${{ matrix.unityVersion}}${{startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }}"
|
||||
runs-on: ubuntu-latest
|
||||
# See build-tests-mac.yml's matching comment - a per-step timeout doesn't
|
||||
# bound an action's own implicit post-run cleanup step, only a job-level
|
||||
# timeout does.
|
||||
timeout-minutes: 60
|
||||
# Known, disclosed gap (see #844's description and src/build-args.ts's own
|
||||
# header comment): the CLI always detects the Unity version from the
|
||||
# checked-out project's ProjectSettings/ProjectVersion.txt and has no flag
|
||||
# to override it, so the one matrix cell that specifically needs a version
|
||||
# override to exercise Unity 6's Build Profiles (`knownGap: true` below)
|
||||
# is expected to fail until that's fixed upstream - every other cell must
|
||||
# still fail the job normally.
|
||||
continue-on-error: ${{ matrix.knownGap == true }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -96,6 +108,9 @@ jobs:
|
||||
- unityVersion: 6000.0.36f1
|
||||
targetPlatform: WebGL
|
||||
buildProfile: 'Assets/Settings/Build Profiles/Sample WebGL Build Profile.asset'
|
||||
# Known gap: needs the unityVersion override the CLI doesn't
|
||||
# support yet (see the job-level continue-on-error comment above).
|
||||
knownGap: true
|
||||
|
||||
steps:
|
||||
- name: Clear Space for Android Build
|
||||
@@ -110,12 +125,20 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Cache #
|
||||
# Cache (restore) #
|
||||
###########################
|
||||
- uses: actions/cache@v4
|
||||
# Split from the save side (below) rather than the combined
|
||||
# actions/cache@v4: that action's implicit post-run save has been
|
||||
# observed hanging indefinitely on GitHub-hosted runners, unrelated to
|
||||
# the cache key itself. A per-step timeout doesn't bound an implicit
|
||||
# post-run cleanup step, only an explicit step does - and caching is a
|
||||
# pure optimization, so losing one save is far cheaper than a job
|
||||
# stuck "in_progress" for well over an hour.
|
||||
- uses: actions/cache/restore@v4
|
||||
id: cache-restore
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}
|
||||
key: Library-${{ matrix.projectPath }}-ubuntu-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}-${{ matrix.buildProfile }}
|
||||
restore-keys: |
|
||||
Library-${{ matrix.projectPath }}-ubuntu-
|
||||
Library-
|
||||
@@ -138,6 +161,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
buildName: 'GameCI Test Build'
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
@@ -161,6 +185,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
buildName: 'GameCI Test Build'
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
@@ -183,6 +208,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
buildName: 'GameCI Test Build'
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
@@ -201,3 +227,19 @@ jobs:
|
||||
name: "Build ${{ matrix.targetPlatform }}${{ startsWith(matrix.buildProfile, 'Assets') && ' (via Build Profile)' || '' }} on Ubuntu (${{ matrix.unityVersion }}_il2cpp_${{ matrix.buildWithIl2cpp }}_params_${{ matrix.additionalParameters }})"
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
||||
###########################
|
||||
# Cache (save) #
|
||||
###########################
|
||||
# A bounded, non-fatal step (unlike the combined action's implicit
|
||||
# post-run save): a hang here is killed after 5m instead of stalling
|
||||
# the whole job, and continue-on-error means a failed/timed-out save
|
||||
# never fails the build - only a real cache miss on the next run,
|
||||
# which is a much cheaper failure mode.
|
||||
- if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: ${{ steps.cache-restore.outputs.cache-primary-key }}
|
||||
|
||||
@@ -12,6 +12,10 @@ jobs:
|
||||
buildForAllPlatformsWindows:
|
||||
name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }}
|
||||
runs-on: windows-2022
|
||||
# See build-tests-mac.yml's matching comment - a per-step timeout doesn't
|
||||
# bound an action's own implicit post-run cleanup step, only a job-level
|
||||
# timeout does.
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -49,12 +53,20 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
###########################
|
||||
# Cache #
|
||||
# Cache (restore) #
|
||||
###########################
|
||||
- uses: actions/cache@v4
|
||||
# Split from the save side (below) rather than the combined
|
||||
# actions/cache@v4: that action's implicit post-run save has been
|
||||
# observed hanging indefinitely on GitHub-hosted runners, unrelated to
|
||||
# the cache key itself. A per-step timeout doesn't bound an implicit
|
||||
# post-run cleanup step, only an explicit step does - and caching is a
|
||||
# pure optimization, so losing one save is far cheaper than a job
|
||||
# stuck "in_progress" for well over an hour.
|
||||
- uses: actions/cache/restore@v4
|
||||
id: cache-restore
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}
|
||||
key: Library-${{ matrix.projectPath }}-windows-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}-${{ matrix.buildProfile }}
|
||||
restore-keys: |
|
||||
Library-${{ matrix.projectPath }}-windows-
|
||||
Library-
|
||||
@@ -105,6 +117,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
@@ -132,6 +145,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
@@ -157,6 +171,7 @@ jobs:
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
with:
|
||||
@@ -179,3 +194,19 @@ jobs:
|
||||
GPU' || '' }}${{ matrix.buildProfile && ' With Build Profile' || '' }}
|
||||
path: build
|
||||
retention-days: 14
|
||||
|
||||
###########################
|
||||
# Cache (save) #
|
||||
###########################
|
||||
# A bounded, non-fatal step (unlike the combined action's implicit
|
||||
# post-run save): a hang here is killed after 5m instead of stalling
|
||||
# the whole job, and continue-on-error means a failed/timed-out save
|
||||
# never fails the build - only a real cache miss on the next run,
|
||||
# which is a much cheaper failure mode.
|
||||
- if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: ${{ steps.cache-restore.outputs.cache-primary-key }}
|
||||
|
||||
@@ -11,6 +11,8 @@ on:
|
||||
options:
|
||||
- game-ci/orchestrator
|
||||
- game-ci/cli
|
||||
- game-ci/unity-test-runner
|
||||
- game-ci/unity-activate
|
||||
dry_run:
|
||||
description: 'Dry run (list secrets to sync without writing)'
|
||||
required: false
|
||||
@@ -70,7 +72,14 @@ jobs:
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "🔍 DRY RUN: would sync $name → $TARGET_REPO"
|
||||
else
|
||||
if echo "$value" | gh secret set "$name" -R "$TARGET_REPO" --body - 2>/dev/null; then
|
||||
# `gh secret set` reads the value from stdin only when --body is
|
||||
# NOT passed. `--body -` does not mean "read stdin" - gh takes it
|
||||
# literally, so this wrote the single character "-" as the value
|
||||
# of every secret it synced, silently destroying them. (A secret
|
||||
# whose value is "-" also makes Actions mask every hyphen in the
|
||||
# logs, which mangles unrelated output like image tags:
|
||||
# "unityci/editor:ubuntu***2022.3.7f1***linux***il2cpp***3".)
|
||||
if gh secret set "$name" -R "$TARGET_REPO" --body "$value" 2>/dev/null; then
|
||||
echo "✅ SYNCED: $name → $TARGET_REPO"
|
||||
else
|
||||
echo "⚠️ FAILED: $name → $TARGET_REPO (continuing)"
|
||||
|
||||
Vendored
+3
-13
@@ -10,19 +10,9 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Jest Test",
|
||||
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"args": [
|
||||
"--collectCoverage=false",
|
||||
"--colors",
|
||||
"--config",
|
||||
"${workspaceRoot}/jest.config.js",
|
||||
"--runInBand",
|
||||
"--runTestsByPath",
|
||||
"${relativeFile}",
|
||||
"--testPathPattern=${fileDirname}",
|
||||
"--testTimeout=10000000"
|
||||
],
|
||||
"name": "Debug Vitest Test",
|
||||
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||
"args": ["run", "${relativeFile}"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"envFile": "${workspaceRoot}/.env",
|
||||
|
||||
+46
-3
@@ -121,8 +121,11 @@ inputs:
|
||||
default: 'local'
|
||||
required: false
|
||||
description:
|
||||
'Build execution strategy. Use "local" for local Docker/Mac builds. For remote builds (aws, k8s, etc.), install
|
||||
@game-ci/orchestrator and use the game-ci/orchestrator action which declares its own inputs.'
|
||||
'Build execution strategy. Use "local" (the default) for Docker/Mac builds driven by this action itself, or
|
||||
"local-system" to build natively on the host with no Docker at all (game-ci/orchestrator''s own local-system
|
||||
provider, invoked via `game-ci orchestrate`) - useful for self-hosted runners that already have Unity installed.
|
||||
For remote builds (aws, k8s, etc.), install @game-ci/orchestrator and use the game-ci/orchestrator action which
|
||||
declares its own inputs.'
|
||||
runAsHostUser:
|
||||
required: false
|
||||
default: 'false'
|
||||
@@ -186,11 +189,51 @@ inputs:
|
||||
skipActivation:
|
||||
default: 'false'
|
||||
required: false
|
||||
description: 'Skip the activation/deactivation of Unity. This assumes Unity is already activated.'
|
||||
description:
|
||||
'Skip the activation/deactivation of Unity. This assumes Unity is already activated. Also honoured by
|
||||
providerStrategy=local-system, for self-hosted runners with an already-licensed, long-lived Unity Hub session.'
|
||||
linux64RemoveExecutableExtension:
|
||||
default: 'false'
|
||||
required: false
|
||||
description: 'When building for StandaloneLinux64, remove the default file extension of `.x86_64`. Set to true to restore the extensionless behavior from v4.'
|
||||
engineLaunchWrapper:
|
||||
default: ''
|
||||
required: false
|
||||
description:
|
||||
"Only applicable to providerStrategy=local-system. Command to prefix the Unity engine process invocation with
|
||||
(e.g. a self-hosted runner's own launch-serialization lock). Applied precisely around the engine launch itself,
|
||||
not the surrounding build step. Empty by default."
|
||||
enableBuildRetry:
|
||||
default: 'false'
|
||||
required: false
|
||||
description:
|
||||
'Only applicable to providerStrategy=local-system. Enable automatic classify/decide/retry recovery for failed
|
||||
Unity builds (budget-gated). Default off: a single failed attempt still throws exactly as before - retry can
|
||||
back up or wipe the Library folder as a recovery action, which is a meaningful behavior change to opt into.'
|
||||
localCacheEnabled:
|
||||
default: 'false'
|
||||
required: false
|
||||
description:
|
||||
'Only applicable to providerStrategy=local-system. Enable local filesystem caching of the Unity Library folder
|
||||
and/or .git/lfs between builds on the same self-hosted runner. Off by default.'
|
||||
localCacheLibrary:
|
||||
default: 'true'
|
||||
required: false
|
||||
description:
|
||||
'Only applicable to providerStrategy=local-system, and only takes effect when localCacheEnabled=true. Cache the
|
||||
engine Library folder locally.'
|
||||
localCacheLfs:
|
||||
default: 'false'
|
||||
required: false
|
||||
description:
|
||||
'Only applicable to providerStrategy=local-system, and only takes effect when localCacheEnabled=true. Cache
|
||||
.git/lfs locally.'
|
||||
localCacheMode:
|
||||
default: 'tar'
|
||||
required: false
|
||||
description:
|
||||
'Only applicable to providerStrategy=local-system, and only takes effect when localCacheEnabled=true. Local
|
||||
cache save/restore mode: "tar", "move-directory", or "copy-directory".'
|
||||
|
||||
outputs:
|
||||
volume:
|
||||
|
||||
+11857
-7754
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+356
-12
@@ -83,18 +83,6 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@actions/tool-cache
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@azure/abort-controller
|
||||
MIT
|
||||
Copyright (c) Microsoft Corporation.
|
||||
@@ -416,6 +404,362 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
|
||||
@protobuf-ts/runtime
|
||||
(Apache-2.0 AND BSD-3-Clause)
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
@protobuf-ts/runtime-rpc
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
@typespec/ts-http-runtime
|
||||
MIT
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
global.fetch = fetch;
|
||||
+7
-14
@@ -21,18 +21,19 @@
|
||||
"setup:hooks": "node scripts/ensure-husky.mjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.@(ts|tsx|mts|js|jsx|mjs|cjs)": [
|
||||
"oxlint --fix --quiet",
|
||||
"!(dist)/**/*.@(ts|tsx|mts|js|jsx|mjs|cjs)": [
|
||||
"oxlint --fix --quiet --no-error-on-unmatched-pattern",
|
||||
"oxfmt --write"
|
||||
],
|
||||
"*.@(json|jsonc|json5|md|mdx|yaml|yml|css|scss|sass|html|toml)": "oxfmt --write",
|
||||
"!(dist)/**/*.@(json|jsonc|json5|md|mdx|yaml|yml|css|scss|sass|html|toml)": "oxfmt --write",
|
||||
"package.json": "oxfmt --write",
|
||||
".github/workflows/*.@(yml|yaml)": "actionlint"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/cache": "^3.2.4",
|
||||
"@actions/cache": "^4.0.0",
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/tool-cache": "^2.0.1"
|
||||
@@ -44,12 +45,9 @@
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"@vitest/coverage-istanbul": "^4.1.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"husky": "9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.3.2",
|
||||
"lint-staged": "^16.4.0",
|
||||
"node-fetch": "2",
|
||||
"oxfmt": "^0.48.0",
|
||||
"oxlint": "^1.63.0",
|
||||
"ts-node": "10.8.1",
|
||||
@@ -58,10 +56,5 @@
|
||||
"vitest": "^4",
|
||||
"yarn-audit-fix": "^9.3.8"
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"dependenciesMeta": {
|
||||
"lefthook": {
|
||||
"built": true
|
||||
}
|
||||
}
|
||||
"packageManager": "[email protected]"
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
echo "installing game-ci cli"
|
||||
if exist %UserProfile%\AppData\LocalLow\game-ci\ (
|
||||
echo Installed Updating
|
||||
git -C %UserProfile%\AppData\LocalLow\game-ci\ fetch
|
||||
git -C %UserProfile%\AppData\LocalLow\game-ci\ reset --hard
|
||||
git -C %UserProfile%\AppData\LocalLow\game-ci\ pull
|
||||
git -C %UserProfile%\AppData\LocalLow\game-ci\ branch
|
||||
) else (
|
||||
echo Not Installed Downloading...
|
||||
mkdir %UserProfile%\AppData\LocalLow\game-ci\
|
||||
git clone https://github.com/game-ci/unity-builder %UserProfile%\AppData\LocalLow\game-ci\
|
||||
)
|
||||
|
||||
call yarn --cwd %UserProfile%\AppData\LocalLow\game-ci\ install
|
||||
call yarn --cwd %UserProfile%\AppData\LocalLow\game-ci\ run gcp-secrets-cli %* --projectPath %cd% --awsStackName game-ci-cli
|
||||
@@ -23,6 +23,24 @@ describe('buildCliArgs', () => {
|
||||
).toStrictEqual(['build', 'game', '--targetPlatform=StandaloneLinux64']);
|
||||
});
|
||||
|
||||
it('omits --engineVersion when unityVersion is unset or "auto"', () => {
|
||||
const noneSet = buildCliArgs(inputsOf({ targetPlatform: 'StandaloneLinux64' }));
|
||||
const explicitAuto = buildCliArgs(
|
||||
inputsOf({ targetPlatform: 'StandaloneLinux64', unityVersion: 'auto' }),
|
||||
);
|
||||
|
||||
expect(noneSet.some((arg) => arg.startsWith('--engineVersion'))).toBe(false);
|
||||
expect(explicitAuto.some((arg) => arg.startsWith('--engineVersion'))).toBe(false);
|
||||
});
|
||||
|
||||
it('maps an explicit unityVersion to --engineVersion, overriding auto-detection', () => {
|
||||
const args = buildCliArgs(
|
||||
inputsOf({ targetPlatform: 'WebGL', unityVersion: '6000.0.36f1' }),
|
||||
);
|
||||
|
||||
expect(args).toContain('--engineVersion=6000.0.36f1');
|
||||
});
|
||||
|
||||
it('throws for a non-local providerStrategy, matching the base action without @game-ci/orchestrator', () => {
|
||||
expect(() =>
|
||||
buildCliArgs(inputsOf({ targetPlatform: 'StandaloneLinux64', providerStrategy: 'aws' })),
|
||||
@@ -106,4 +124,274 @@ describe('buildCliArgs', () => {
|
||||
|
||||
expect(args).toStrictEqual(['build', '--targetPlatform=StandaloneLinux64']);
|
||||
});
|
||||
|
||||
describe('providerStrategy=local (default) regression', () => {
|
||||
// A full, representative sweep of every carried-forward `build` flag,
|
||||
// asserted byte-identical to what the pre-local-system implementation
|
||||
// produced. If this ever fails, the local/default branch changed -
|
||||
// which it must not, since local-system is meant to be strictly
|
||||
// additive.
|
||||
it('produces byte-identical output for a full-flag build', () => {
|
||||
const inputs = {
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
projectPath: 'game',
|
||||
providerStrategy: 'local',
|
||||
customImage: 'unityci/editor:ubuntu-2021.3.1f1',
|
||||
buildProfile: 'Assets/Profile.asset',
|
||||
buildName: 'MyGame',
|
||||
buildsPath: 'build',
|
||||
buildMethod: 'Foo.Bar',
|
||||
customParameters: '-profile SomeProfile -someBoolean',
|
||||
versioning: 'Tag',
|
||||
version: '1.2.3',
|
||||
androidVersionCode: '7',
|
||||
androidExportType: 'androidAppBundle',
|
||||
androidKeystoreName: 'keystore.keystore',
|
||||
androidKeystoreBase64: 'base64==',
|
||||
androidKeystorePass: 'keystore-secret',
|
||||
androidKeyaliasName: 'my-alias',
|
||||
androidKeyaliasPass: 'alias-secret',
|
||||
androidTargetSdkVersion: '33',
|
||||
androidSymbolType: 'public',
|
||||
sshAgent: '/tmp/ssh-agent.sock',
|
||||
sshPublicKeysDirectoryPath: '/tmp/keys',
|
||||
gitPrivateToken: 'ghp_token',
|
||||
chownFilesTo: '1000:1000',
|
||||
dockerCpuLimit: '4',
|
||||
dockerMemoryLimit: '4g',
|
||||
dockerIsolationMode: 'process',
|
||||
containerRegistryRepository: 'unityci/editor',
|
||||
containerRegistryImageVersion: '3',
|
||||
unityHubVersionOnMac: '3.4.0',
|
||||
unityLicensingServer: 'http://license.example.com',
|
||||
dockerWorkspacePath: '/github/workspace',
|
||||
manualExit: 'true',
|
||||
enableGpu: 'true',
|
||||
useHostNetwork: 'true',
|
||||
runAsHostUser: 'true',
|
||||
allowDirtyBuild: 'true',
|
||||
cacheUnityInstallationOnMac: 'true',
|
||||
skipActivation: 'true',
|
||||
linux64RemoveExecutableExtension: 'true',
|
||||
};
|
||||
|
||||
expect(buildCliArgs(inputsOf(inputs))).toStrictEqual([
|
||||
'build',
|
||||
'game',
|
||||
'--targetPlatform=StandaloneLinux64',
|
||||
'--customImage=unityci/editor:ubuntu-2021.3.1f1',
|
||||
'--buildProfile=Assets/Profile.asset',
|
||||
'--buildName=MyGame',
|
||||
'--buildsPath=build',
|
||||
'--buildMethod=Foo.Bar',
|
||||
'--customParameters=-profile SomeProfile -someBoolean',
|
||||
'--versioningStrategy=Tag',
|
||||
'--version=1.2.3',
|
||||
'--androidVersionCode=7',
|
||||
'--androidExportType=androidAppBundle',
|
||||
'--androidKeystoreName=keystore.keystore',
|
||||
'--androidKeystoreBase64=base64==',
|
||||
'--androidKeystorePassword=keystore-secret',
|
||||
'--androidKeyAlias=my-alias',
|
||||
'--androidKeyAliasPassword=alias-secret',
|
||||
'--androidTargetSdkVersion=33',
|
||||
'--androidSymbolType=public',
|
||||
'--sshAgent=/tmp/ssh-agent.sock',
|
||||
'--sshPublicKeysDirectoryPath=/tmp/keys',
|
||||
'--gitPrivateToken=ghp_token',
|
||||
'--chownFilesTo=1000:1000',
|
||||
'--dockerCpuLimit=4',
|
||||
'--dockerMemoryLimit=4g',
|
||||
'--dockerIsolationMode=process',
|
||||
'--containerRegistryRepository=unityci/editor',
|
||||
'--containerRegistryImageVersion=3',
|
||||
'--unityHubVersionOnMac=3.4.0',
|
||||
'--unityLicensingServer=http://license.example.com',
|
||||
'--dockerWorkspacePath=/github/workspace',
|
||||
'--manualExit',
|
||||
'--enableGpu',
|
||||
'--useHostNetwork',
|
||||
'--runAsHostUser',
|
||||
'--allowDirtyBuild',
|
||||
'--cacheUnityInstallationOnMac',
|
||||
'--skipActivation',
|
||||
'--linux64RemoveExecutableExtension',
|
||||
]);
|
||||
});
|
||||
|
||||
it('produces byte-identical output when providerStrategy is left unset (default)', () => {
|
||||
expect(
|
||||
buildCliArgs(inputsOf({ targetPlatform: 'StandaloneLinux64', buildName: 'MyGame' })),
|
||||
).toStrictEqual(['build', '--targetPlatform=StandaloneLinux64', '--buildName=MyGame']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('providerStrategy=local-system', () => {
|
||||
it('shells out to `orchestrate` with --providerStrategy=local-system', () => {
|
||||
const args = buildCliArgs(
|
||||
inputsOf({ targetPlatform: 'StandaloneLinux64', providerStrategy: 'local-system' }),
|
||||
);
|
||||
|
||||
expect(args).toStrictEqual([
|
||||
'orchestrate',
|
||||
'--targetPlatform=StandaloneLinux64',
|
||||
'--providerStrategy=local-system',
|
||||
]);
|
||||
});
|
||||
|
||||
it('still requires targetPlatform', () => {
|
||||
expect(() => buildCliArgs(inputsOf({ providerStrategy: 'local-system' }))).toThrow(
|
||||
/targetPlatform/,
|
||||
);
|
||||
});
|
||||
|
||||
it('puts projectPath as the positional argument right after "orchestrate", matching the build branch', () => {
|
||||
const args = buildCliArgs(
|
||||
inputsOf({
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
providerStrategy: 'local-system',
|
||||
projectPath: 'game',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(args).toStrictEqual([
|
||||
'orchestrate',
|
||||
'game',
|
||||
'--targetPlatform=StandaloneLinux64',
|
||||
'--providerStrategy=local-system',
|
||||
]);
|
||||
});
|
||||
|
||||
it('carries forward every verified-supported flag', () => {
|
||||
const inputs = {
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
providerStrategy: 'local-system',
|
||||
buildName: 'MyGame',
|
||||
buildsPath: 'build',
|
||||
buildMethod: 'Foo.Bar',
|
||||
buildProfile: 'Assets/Profile.asset',
|
||||
androidVersionCode: '7',
|
||||
chownFilesTo: '1000:1000',
|
||||
sshAgent: '/tmp/ssh-agent.sock',
|
||||
sshPublicKeysDirectoryPath: '/tmp/keys',
|
||||
gitPrivateToken: 'ghp_token',
|
||||
manualExit: 'true',
|
||||
allowDirtyBuild: 'true',
|
||||
skipActivation: 'true',
|
||||
engineLaunchWrapper: 'flock /tmp/engine.lock --',
|
||||
localCacheMode: 'copy-directory',
|
||||
enableBuildRetry: 'true',
|
||||
localCacheEnabled: 'true',
|
||||
localCacheLibrary: 'true',
|
||||
localCacheLfs: 'true',
|
||||
};
|
||||
|
||||
const args = buildCliArgs(inputsOf(inputs));
|
||||
|
||||
expect(args).toContain('--buildName=MyGame');
|
||||
expect(args).toContain('--buildsPath=build');
|
||||
expect(args).toContain('--buildMethod=Foo.Bar');
|
||||
expect(args).toContain('--buildProfile=Assets/Profile.asset');
|
||||
expect(args).toContain('--androidVersionCode=7');
|
||||
expect(args).toContain('--chownFilesTo=1000:1000');
|
||||
expect(args).toContain('--sshAgent=/tmp/ssh-agent.sock');
|
||||
expect(args).toContain('--sshPublicKeysDirectoryPath=/tmp/keys');
|
||||
expect(args).toContain('--gitPrivateToken=ghp_token');
|
||||
expect(args).toContain('--manualExit');
|
||||
expect(args).toContain('--allowDirtyBuild');
|
||||
expect(args).toContain('--skipActivation');
|
||||
expect(args).toContain('--engineLaunchWrapper=flock /tmp/engine.lock --');
|
||||
expect(args).toContain('--localCacheMode=copy-directory');
|
||||
expect(args).toContain('--enableBuildRetry');
|
||||
expect(args).toContain('--localCacheEnabled');
|
||||
expect(args).toContain('--localCacheLibrary');
|
||||
expect(args).toContain('--localCacheLfs');
|
||||
});
|
||||
|
||||
it('never emits build-only/Docker-only flags that have no orchestrate-side equivalent', () => {
|
||||
const inputs = {
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
providerStrategy: 'local-system',
|
||||
customImage: 'unityci/editor:ubuntu-2021.3.1f1',
|
||||
customParameters: '-profile SomeProfile',
|
||||
versioning: 'Tag',
|
||||
version: '1.2.3',
|
||||
androidExportType: 'androidAppBundle',
|
||||
androidKeystoreName: 'keystore.keystore',
|
||||
androidKeystoreBase64: 'base64==',
|
||||
androidKeystorePass: 'keystore-secret',
|
||||
androidKeyaliasName: 'my-alias',
|
||||
androidKeyaliasPass: 'alias-secret',
|
||||
androidTargetSdkVersion: '33',
|
||||
androidSymbolType: 'public',
|
||||
dockerCpuLimit: '4',
|
||||
dockerMemoryLimit: '4g',
|
||||
dockerIsolationMode: 'process',
|
||||
containerRegistryRepository: 'unityci/editor',
|
||||
containerRegistryImageVersion: '3',
|
||||
unityHubVersionOnMac: '3.4.0',
|
||||
unityLicensingServer: 'http://license.example.com',
|
||||
dockerWorkspacePath: '/github/workspace',
|
||||
enableGpu: 'true',
|
||||
useHostNetwork: 'true',
|
||||
runAsHostUser: 'true',
|
||||
cacheUnityInstallationOnMac: 'true',
|
||||
linux64RemoveExecutableExtension: 'true',
|
||||
};
|
||||
|
||||
const args = buildCliArgs(inputsOf(inputs));
|
||||
|
||||
const excludedFlags = [
|
||||
'customImage',
|
||||
'customParameters',
|
||||
'versioning',
|
||||
'versioningStrategy',
|
||||
'version',
|
||||
'androidExportType',
|
||||
'androidKeystoreName',
|
||||
'androidKeystoreBase64',
|
||||
'androidKeystorePassword',
|
||||
'androidKeyAlias',
|
||||
'androidKeyAliasPassword',
|
||||
'androidTargetSdkVersion',
|
||||
'androidSymbolType',
|
||||
'dockerCpuLimit',
|
||||
'dockerMemoryLimit',
|
||||
'dockerIsolationMode',
|
||||
'containerRegistryRepository',
|
||||
'containerRegistryImageVersion',
|
||||
'unityHubVersionOnMac',
|
||||
'unityLicensingServer',
|
||||
'dockerWorkspacePath',
|
||||
'enableGpu',
|
||||
'useHostNetwork',
|
||||
'runAsHostUser',
|
||||
'cacheUnityInstallationOnMac',
|
||||
'linux64RemoveExecutableExtension',
|
||||
];
|
||||
|
||||
for (const flag of excludedFlags) {
|
||||
expect(args.some((arg) => arg === `--${flag}` || arg.startsWith(`--${flag}=`))).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('omits the new orchestrator-only flags when their inputs are unset', () => {
|
||||
const args = buildCliArgs(
|
||||
inputsOf({ targetPlatform: 'StandaloneLinux64', providerStrategy: 'local-system' }),
|
||||
);
|
||||
|
||||
expect(args.some((arg) => arg.startsWith('--engineLaunchWrapper='))).toBe(false);
|
||||
expect(args.some((arg) => arg.startsWith('--localCacheMode='))).toBe(false);
|
||||
expect(args).not.toContain('--enableBuildRetry');
|
||||
expect(args).not.toContain('--localCacheEnabled');
|
||||
expect(args).not.toContain('--localCacheLibrary');
|
||||
expect(args).not.toContain('--localCacheLfs');
|
||||
});
|
||||
});
|
||||
|
||||
it('still throws for a non-local/non-local-system providerStrategy', () => {
|
||||
expect(() =>
|
||||
buildCliArgs(inputsOf({ targetPlatform: 'StandaloneLinux64', providerStrategy: 'aws' })),
|
||||
).toThrow(/aws/);
|
||||
});
|
||||
});
|
||||
|
||||
+193
-31
@@ -1,16 +1,20 @@
|
||||
/**
|
||||
* Translates unity-builder's action inputs into `game-ci build` CLI flags.
|
||||
* Translates unity-builder's action inputs into `game-ci build` (or, for
|
||||
* providerStrategy=local-system, `game-ci orchestrate`) CLI flags.
|
||||
*
|
||||
* Two deliberate omissions, both because there is nothing to translate to:
|
||||
* - `unityVersion` (except "auto"): the CLI always detects the Unity
|
||||
* version from the checked-out project's ProjectSettings/ProjectVersion.txt
|
||||
* and has no flag to override that today (game-ci/cli's engine-detection
|
||||
* middleware unconditionally overwrites any passed value). Pinning a
|
||||
* version other than "auto" is a known, real gap versus the original
|
||||
* action - see the PR this shipped in.
|
||||
* - `providerStrategy` values other than "local": the base action (without
|
||||
* the separately-installed @game-ci/orchestrator plugin) already throws
|
||||
* for these today, so throwing here isn't a regression.
|
||||
* One deliberate omission for the `build` path, because there is nothing
|
||||
* to translate to:
|
||||
* - `providerStrategy` values other than "local"/"local-system": the base
|
||||
* action (without the separately-installed @game-ci/orchestrator plugin)
|
||||
* already throws for these today, so throwing here isn't a regression.
|
||||
*
|
||||
* `unityVersion` (except "auto", which means "let the CLI auto-detect from
|
||||
* ProjectSettings/ProjectVersion.txt", so it's never passed through as a
|
||||
* literal flag value): mapped to `--engineVersion`, which game-ci/cli's
|
||||
* engine-detection middleware now respects instead of unconditionally
|
||||
* overwriting (game-ci/cli#154). Not carried into ORCHESTRATE_STRING_FLAGS -
|
||||
* unverified whether `orchestrate`'s local-system path reads engineVersion
|
||||
* the same way; scope this out until confirmed.
|
||||
*
|
||||
* Boolean inputs use GitHub Actions' own truthy/falsy string convention
|
||||
* ('true'/'false', case-insensitive) - see actions/toolkit's getBooleanInput.
|
||||
@@ -61,15 +65,168 @@ const BOOLEAN_FLAGS: Array<[input: string, flag: string]> = [
|
||||
['linux64RemoveExecutableExtension', 'linux64RemoveExecutableExtension'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Flags carried forward into `game-ci orchestrate --providerStrategy=local-system`.
|
||||
*
|
||||
* Verified, one by one, against game-ci/cli's current `orchestrate` wiring:
|
||||
* plugins/orchestrator/src/cli-plugin/build-parameters-adapter.ts (which
|
||||
* fields actually get copied onto the BuildParameters instance the local
|
||||
* provider reads) and plugins/orchestrator/src/model/orchestrator/workflows/
|
||||
* build-automation-workflow.ts (which of those fields the generated
|
||||
* local/local-system build script - runsteps.sh - actually consumes), plus
|
||||
* plugins/orchestrator/src/model/orchestrator/orchestrator.ts and
|
||||
* .../options/orchestrator-folders.ts for the git-auth fields.
|
||||
*
|
||||
* Every `build`-only STRING_FLAGS/BOOLEAN_FLAGS entry NOT listed below was
|
||||
* deliberately excluded - see the per-field notes.
|
||||
*/
|
||||
const ORCHESTRATE_STRING_FLAGS: Array<[input: string, flag: string]> = [
|
||||
// build-automation-workflow.ts's runsteps.sh export block: BUILD_NAME,
|
||||
// BUILD_METHOD, BUILD_PROFILE, CHOWN_FILES_TO, ANDROID_VERSION_CODE.
|
||||
['buildName', 'buildName'],
|
||||
['buildsPath', 'buildsPath'],
|
||||
['buildMethod', 'buildMethod'],
|
||||
['buildProfile', 'buildProfile'],
|
||||
['androidVersionCode', 'androidVersionCode'],
|
||||
['chownFilesTo', 'chownFilesTo'],
|
||||
// Git-auth fields consumed by orchestrator.ts / orchestrator-folders.ts
|
||||
// for the repo clone step, independent of provider/Docker.
|
||||
['sshAgent', 'sshAgent'],
|
||||
['sshPublicKeysDirectoryPath', 'sshPublicKeysDirectoryPath'],
|
||||
['gitPrivateToken', 'gitPrivateToken'],
|
||||
|
||||
// --- Excluded, with reasons ---
|
||||
// customImage: build-parameters-adapter.ts assigns bp.customImage, but no
|
||||
// local/local-system codepath (provider or workflow) ever reads it back -
|
||||
// it is a Docker image selector with nothing to select on bare host.
|
||||
// customParameters: NOT assigned by build-parameters-adapter.ts at all
|
||||
// (unlike `build`'s CUSTOM_PARAMETERS env var). It only appears in the
|
||||
// unrelated hot-runner-service.ts path. Currently a no-op for
|
||||
// local-system, so intentionally not carried forward.
|
||||
// versioning / version: the adapter has no `versioningStrategy` concept -
|
||||
// it only accepts an already-resolved `buildVersion` string
|
||||
// (`bp.buildVersion = options.buildVersion || '0.0.1'`), which is a
|
||||
// different contract than this action's "Semantic/Tag/Custom strategy
|
||||
// name" `versioning` input. No existing action input maps cleanly onto
|
||||
// `--buildVersion`, so both are left out pending a real answer upstream.
|
||||
// androidExportType/androidKeystoreName/androidKeystoreBase64/
|
||||
// androidKeystorePass/androidKeyaliasName/androidKeyaliasPass/
|
||||
// androidTargetSdkVersion/androidSymbolType: none of these are assigned
|
||||
// by build-parameters-adapter.ts - only androidVersionCode is. Android
|
||||
// signing/export beyond the version code is currently unsupported by
|
||||
// `orchestrate`.
|
||||
// dockerCpuLimit/dockerMemoryLimit/dockerIsolationMode/
|
||||
// containerRegistryRepository/containerRegistryImageVersion/
|
||||
// dockerWorkspacePath: Docker container concepts with no bare-host
|
||||
// equivalent (containerRegistry* aren't even assigned by the adapter).
|
||||
// unityHubVersionOnMac: not assigned by the adapter at all.
|
||||
// unityLicensingServer: per the adapter's own comment, this "flows
|
||||
// opaquely through BuildParameters' index signature ... orchestrator
|
||||
// does not read them" - i.e. it is a no-op for `orchestrate` today;
|
||||
// licensing-server activation is `game-ci activate`'s concern, out of
|
||||
// scope here (matches this file's existing unityVersion/activation
|
||||
// scope boundary).
|
||||
];
|
||||
|
||||
const ORCHESTRATE_BOOLEAN_FLAGS: Array<[input: string, flag: string]> = [
|
||||
// build-automation-workflow.ts's runsteps.sh export block: MANUAL_EXIT.
|
||||
['manualExit', 'manualExit'],
|
||||
// pre-build-cleanup-service.ts reads this for the dirty-branch check,
|
||||
// independent of provider.
|
||||
['allowDirtyBuild', 'allowDirtyBuild'],
|
||||
// Explicitly documented upstream (orchestrator-options-plugin.ts,
|
||||
// build-parameters-adapter.ts) as "only meaningful for
|
||||
// providerStrategy=local(-system)", and exported as SKIP_ACTIVATION in
|
||||
// runsteps.sh.
|
||||
['skipActivation', 'skipActivation'],
|
||||
|
||||
// --- Excluded, with reasons ---
|
||||
// enableGpu: not assigned anywhere in build-parameters-adapter.ts or the
|
||||
// orchestrator model - a real, currently-unfilled gap versus `build`'s
|
||||
// GPU passthrough, not something to silently pretend to support.
|
||||
// useHostNetwork/runAsHostUser: Docker networking/uid-mapping concepts;
|
||||
// not assigned by the adapter, and runAsHostUser is meaningless by
|
||||
// construction on a bare host process that already runs as that host
|
||||
// user.
|
||||
// cacheUnityInstallationOnMac: build-parameters-adapter.ts DOES assign
|
||||
// bp.cacheUnityInstallationOnMac, but nothing downstream (no provider,
|
||||
// no workflow) ever reads it back - it's a currently-dead field for
|
||||
// `orchestrate`, so it is not carried forward despite being Mac-native
|
||||
// (i.e. not actually Docker-only) in principle.
|
||||
// linux64RemoveExecutableExtension: not assigned by the adapter at all.
|
||||
];
|
||||
|
||||
/**
|
||||
* New orchestrator-only inputs surfaced for providerStrategy=local-system.
|
||||
* All four are confirmed both registered as `orchestrate` CLI options
|
||||
* (orchestrator-options-plugin.ts) and consumed by
|
||||
* build-parameters-adapter.ts. `middlewarePipeline` is available in the same
|
||||
* places but deliberately deferred - its expected value shape is not yet
|
||||
* documented/stable enough to lock into this action's public input surface.
|
||||
*/
|
||||
const ORCHESTRATE_ONLY_STRING_FLAGS: Array<[input: string, flag: string]> = [
|
||||
['engineLaunchWrapper', 'engineLaunchWrapper'],
|
||||
['localCacheMode', 'localCacheMode'],
|
||||
];
|
||||
|
||||
const ORCHESTRATE_ONLY_BOOLEAN_FLAGS: Array<[input: string, flag: string]> = [
|
||||
['enableBuildRetry', 'enableBuildRetry'],
|
||||
['localCacheEnabled', 'localCacheEnabled'],
|
||||
['localCacheLibrary', 'localCacheLibrary'],
|
||||
['localCacheLfs', 'localCacheLfs'],
|
||||
];
|
||||
|
||||
function isTruthy(value: string): boolean {
|
||||
return value.trim().toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
function pushStringFlags(
|
||||
args: string[],
|
||||
getInput: (name: string) => string,
|
||||
flags: Array<[input: string, flag: string]>,
|
||||
) {
|
||||
for (const [input, flag] of flags) {
|
||||
const value = getInput(input);
|
||||
// "--flag value" as two argv tokens is ambiguous when value itself
|
||||
// starts with "-" (e.g. customParameters="-profile Foo -someBoolean"):
|
||||
// yargs sees the next token starting with "-" and assumes the flag
|
||||
// takes no value, leaving the value string to be mis-parsed as its own
|
||||
// (partly alias-colliding) short-flag cluster. "--flag=value" glues
|
||||
// them into one token, which is unambiguous.
|
||||
if (value) args.push(`--${flag}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function pushBooleanFlags(
|
||||
args: string[],
|
||||
getInput: (name: string) => string,
|
||||
flags: Array<[input: string, flag: string]>,
|
||||
) {
|
||||
for (const [input, flag] of flags) {
|
||||
const value = getInput(input);
|
||||
if (value && isTruthy(value)) args.push(`--${flag}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface BuildArgsOptions {
|
||||
getInput(name: string): string;
|
||||
}
|
||||
|
||||
export function buildCliArgs({ getInput }: BuildArgsOptions): string[] {
|
||||
const providerStrategy = getInput('providerStrategy') || 'local';
|
||||
|
||||
if (providerStrategy === 'local-system') {
|
||||
return buildOrchestrateArgs(getInput);
|
||||
}
|
||||
|
||||
if (providerStrategy !== 'local') {
|
||||
throw new Error(
|
||||
`Provider strategy "${providerStrategy}" is not supported by this thin wrapper. ` +
|
||||
"Use providerStrategy=local, or invoke game-ci/cli's `orchestrate` command directly " +
|
||||
'for remote builds.',
|
||||
);
|
||||
}
|
||||
|
||||
const args: string[] = ['build'];
|
||||
|
||||
const projectPath = getInput('projectPath');
|
||||
@@ -81,30 +238,35 @@ export function buildCliArgs({ getInput }: BuildArgsOptions): string[] {
|
||||
}
|
||||
args.push(`--targetPlatform=${targetPlatform}`);
|
||||
|
||||
const providerStrategy = getInput('providerStrategy') || 'local';
|
||||
if (providerStrategy !== 'local') {
|
||||
throw new Error(
|
||||
`Provider strategy "${providerStrategy}" is not supported by this thin wrapper. ` +
|
||||
"Use providerStrategy=local, or invoke game-ci/cli's `orchestrate` command directly " +
|
||||
'for remote builds.',
|
||||
);
|
||||
const unityVersion = getInput('unityVersion');
|
||||
if (unityVersion && unityVersion !== 'auto') {
|
||||
args.push(`--engineVersion=${unityVersion}`);
|
||||
}
|
||||
|
||||
for (const [input, flag] of STRING_FLAGS) {
|
||||
const value = getInput(input);
|
||||
// "--flag value" as two argv tokens is ambiguous when value itself
|
||||
// starts with "-" (e.g. customParameters="-profile Foo -someBoolean"):
|
||||
// yargs sees the next token starting with "-" and assumes the flag
|
||||
// takes no value, leaving the value string to be mis-parsed as its own
|
||||
// (partly alias-colliding) short-flag cluster. "--flag=value" glues
|
||||
// them into one token, which is unambiguous.
|
||||
if (value) args.push(`--${flag}=${value}`);
|
||||
pushStringFlags(args, getInput, STRING_FLAGS);
|
||||
pushBooleanFlags(args, getInput, BOOLEAN_FLAGS);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function buildOrchestrateArgs(getInput: (name: string) => string): string[] {
|
||||
const args: string[] = ['orchestrate'];
|
||||
|
||||
const projectPath = getInput('projectPath');
|
||||
if (projectPath) args.push(projectPath);
|
||||
|
||||
const targetPlatform = getInput('targetPlatform');
|
||||
if (!targetPlatform) {
|
||||
throw new Error('targetPlatform is required.');
|
||||
}
|
||||
args.push(`--targetPlatform=${targetPlatform}`);
|
||||
|
||||
args.push('--providerStrategy=local-system');
|
||||
|
||||
for (const [input, flag] of BOOLEAN_FLAGS) {
|
||||
const value = getInput(input);
|
||||
if (value && isTruthy(value)) args.push(`--${flag}`);
|
||||
}
|
||||
pushStringFlags(args, getInput, ORCHESTRATE_STRING_FLAGS);
|
||||
pushStringFlags(args, getInput, ORCHESTRATE_ONLY_STRING_FLAGS);
|
||||
pushBooleanFlags(args, getInput, ORCHESTRATE_BOOLEAN_FLAGS);
|
||||
pushBooleanFlags(args, getInput, ORCHESTRATE_ONLY_BOOLEAN_FLAGS);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
+212
-27
@@ -1,32 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { assetNameFor, binaryNameFor } from './download-cli';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as cache from '@actions/cache';
|
||||
import * as exec from '@actions/exec';
|
||||
import { binaryNameFor, downloadCli, resolveLatestTag } from './download-cli';
|
||||
|
||||
describe('assetNameFor', () => {
|
||||
it('maps linux x64 to a .tar.gz archive', () => {
|
||||
expect(assetNameFor('linux', 'x64')).toBe('game-ci-linux-x64.tar.gz');
|
||||
});
|
||||
vi.mock('@actions/exec');
|
||||
vi.mock('@actions/cache');
|
||||
vi.mock('node:fs/promises');
|
||||
|
||||
it('maps linux arm64 to a .tar.gz archive', () => {
|
||||
expect(assetNameFor('linux', 'arm64')).toBe('game-ci-linux-arm64.tar.gz');
|
||||
});
|
||||
|
||||
it('maps darwin x64 to a .tar.gz archive', () => {
|
||||
expect(assetNameFor('darwin', 'x64')).toBe('game-ci-macos-x64.tar.gz');
|
||||
});
|
||||
|
||||
it('maps darwin arm64 to a .tar.gz archive', () => {
|
||||
expect(assetNameFor('darwin', 'arm64')).toBe('game-ci-macos-arm64.tar.gz');
|
||||
});
|
||||
|
||||
it('maps win32 x64 to a .zip archive', () => {
|
||||
expect(assetNameFor('win32', 'x64')).toBe('game-ci-windows-x64.zip');
|
||||
});
|
||||
|
||||
it('throws for an unsupported platform/arch combination', () => {
|
||||
expect(() => assetNameFor('win32', 'arm64')).toThrow(/unsupported/i);
|
||||
expect(() => assetNameFor('freebsd', 'x64')).toThrow(/unsupported/i);
|
||||
});
|
||||
});
|
||||
function mockInstallScriptFetch(): void {
|
||||
globalThis.fetch = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => '#!/usr/bin/env bash\necho mock install.sh',
|
||||
})) as unknown as typeof fetch;
|
||||
}
|
||||
|
||||
describe('binaryNameFor', () => {
|
||||
it('is game-ci.exe on win32', () => {
|
||||
@@ -38,3 +26,200 @@ describe('binaryNameFor', () => {
|
||||
expect(binaryNameFor('darwin')).toBe('game-ci');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveLatestTag', () => {
|
||||
// This is the piece that makes caching "latest" possible at all: caching
|
||||
// under the literal string "latest" would silently pin every job to
|
||||
// whatever version happened to be current on the first cache write.
|
||||
// Resolving to the concrete tag first means a real new release is a fresh
|
||||
// cache key (a miss by construction), never a stale hit.
|
||||
it('returns the tag_name from a successful GitHub API response', async () => {
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ tag_name: 'v0.1.15' }),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
const tag = await resolveLatestTag(fetchFn);
|
||||
|
||||
expect(tag).toBe('v0.1.15');
|
||||
expect(fetchFn).toHaveBeenCalledWith(
|
||||
'https://api.github.com/repos/game-ci/cli/releases/latest',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Accept: 'application/vnd.github+json' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sends no Authorization header when GITHUB_TOKEN/GH_TOKEN are unset', async () => {
|
||||
const originalGithub = process.env.GITHUB_TOKEN;
|
||||
const originalGh = process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.GH_TOKEN;
|
||||
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ tag_name: 'v0.1.15' }),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
await resolveLatestTag(fetchFn);
|
||||
const [, init] = vi.mocked(fetchFn).mock.calls[0] as [
|
||||
string,
|
||||
RequestInit & { headers: Record<string, string> },
|
||||
];
|
||||
expect(init.headers.Authorization).toBeUndefined();
|
||||
} finally {
|
||||
if (originalGithub !== undefined) process.env.GITHUB_TOKEN = originalGithub;
|
||||
if (originalGh !== undefined) process.env.GH_TOKEN = originalGh;
|
||||
}
|
||||
});
|
||||
|
||||
it('sends an Authorization header from GITHUB_TOKEN when set, to avoid the unauthenticated rate limit', async () => {
|
||||
const original = process.env.GITHUB_TOKEN;
|
||||
process.env.GITHUB_TOKEN = 'test-token-123';
|
||||
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ tag_name: 'v0.1.15' }),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
await resolveLatestTag(fetchFn);
|
||||
expect(fetchFn).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer test-token-123' }),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
if (original === undefined) delete process.env.GITHUB_TOKEN;
|
||||
else process.env.GITHUB_TOKEN = original;
|
||||
}
|
||||
});
|
||||
|
||||
it('throws with a clear message on a non-ok response', async () => {
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: async () => ({}),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await expect(resolveLatestTag(fetchFn)).rejects.toThrow(/404/);
|
||||
});
|
||||
|
||||
it('throws with a clear message when the response has no tag_name', async () => {
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({}),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await expect(resolveLatestTag(fetchFn)).rejects.toThrow(/no tag_name/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadCli', () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(cache.isFeatureAvailable).mockReturnValue(false);
|
||||
// Real chmod/mkdir/writeFile aren't under test here (that's install.sh's
|
||||
// job, and it isn't actually run - see mockInstallScriptFetch) and the
|
||||
// paths involved don't exist on disk in this test environment. Without
|
||||
// these, restoreFromCache's real fs.chmod call throws ENOENT on any
|
||||
// platform where that branch actually runs (linux/darwin - the
|
||||
// `process.platform !== 'win32'` guard means it's silently skipped, and
|
||||
// the bug masked, on a Windows dev machine) - restoreFromCache's own
|
||||
// try/catch then swallows that and returns null, so a cache-hit test
|
||||
// silently falls through to the real install path instead of catching
|
||||
// what it's meant to catch.
|
||||
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
// The actual install mechanics (platform detection, archive format,
|
||||
// extraction) live in game-ci/cli's own scripts/install.sh now, fetched
|
||||
// and run at the resolved version's tag - see game-ci/cli#187. This
|
||||
// wrapper's own job is just: fetch that script, run it correctly, and
|
||||
// take its stdout as the binary path.
|
||||
it('fetches and runs install.sh for the given version, returning its stdout as the binary path', async () => {
|
||||
mockInstallScriptFetch();
|
||||
vi.mocked(exec.exec).mockImplementation(async (_cmd, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from('/tmp/game-ci-cli-cache/v0.1.32/game-ci\n'));
|
||||
return 0;
|
||||
});
|
||||
|
||||
const binaryPath = await downloadCli('v0.1.32');
|
||||
|
||||
expect(binaryPath).toBe('/tmp/game-ci-cli-cache/v0.1.32/game-ci');
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
'https://raw.githubusercontent.com/game-ci/cli/v0.1.32/scripts/install.sh',
|
||||
);
|
||||
expect(exec.exec).toHaveBeenCalledWith(
|
||||
'bash',
|
||||
expect.arrayContaining(['v0.1.32']),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves "latest" to a concrete tag before fetching install.sh', async () => {
|
||||
globalThis.fetch = vi.fn(async (url: string) => {
|
||||
if (url.includes('/releases/latest')) {
|
||||
return { ok: true, status: 200, json: async () => ({ tag_name: 'v0.1.33' }) };
|
||||
}
|
||||
return { ok: true, status: 200, text: async () => 'echo mock install.sh' };
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
vi.mocked(exec.exec).mockImplementation(async (_cmd, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from('/tmp/game-ci\n'));
|
||||
return 0;
|
||||
});
|
||||
|
||||
await downloadCli('latest');
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
'https://raw.githubusercontent.com/game-ci/cli/v0.1.33/scripts/install.sh',
|
||||
);
|
||||
expect(exec.exec).toHaveBeenCalledWith(
|
||||
'bash',
|
||||
expect.arrayContaining(['v0.1.33']),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a clear error when the install.sh fetch fails', async () => {
|
||||
globalThis.fetch = vi.fn(async () => ({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await expect(downloadCli('v0.1.32')).rejects.toThrow(/404/);
|
||||
expect(exec.exec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws a clear error when install.sh produces no output', async () => {
|
||||
mockInstallScriptFetch();
|
||||
vi.mocked(exec.exec).mockImplementation(async () => 0);
|
||||
|
||||
await expect(downloadCli('v0.1.32')).rejects.toThrow(/produced no output/);
|
||||
});
|
||||
|
||||
it('restores from cache instead of fetching install.sh on a cache hit', async () => {
|
||||
vi.mocked(cache.isFeatureAvailable).mockReturnValue(true);
|
||||
vi.mocked(cache.restoreCache).mockResolvedValue('game-ci-cli-v0.1.32-key');
|
||||
|
||||
const binaryPath = await downloadCli('v0.1.32');
|
||||
|
||||
expect(binaryPath).toContain(binaryNameFor(process.platform));
|
||||
expect(exec.exec).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
+139
-78
@@ -3,103 +3,81 @@ import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as cache from '@actions/cache';
|
||||
import * as core from '@actions/core';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
import * as exec from '@actions/exec';
|
||||
|
||||
const CLI_REPO = 'game-ci/cli';
|
||||
|
||||
export function assetNameFor(platform: NodeJS.Platform, arch: string): string {
|
||||
const targets: Partial<Record<NodeJS.Platform, Partial<Record<string, string>>>> = {
|
||||
linux: { x64: 'linux-x64', arm64: 'linux-arm64' },
|
||||
darwin: { x64: 'macos-x64', arm64: 'macos-arm64' },
|
||||
win32: { x64: 'windows-x64' },
|
||||
};
|
||||
|
||||
const target = targets[platform]?.[arch];
|
||||
if (!target)
|
||||
throw new Error(`Unsupported platform/arch for the game-ci CLI: ${platform}/${arch}`);
|
||||
|
||||
const extension = platform === 'win32' ? 'zip' : 'tar.gz';
|
||||
|
||||
return `game-ci-${target}.${extension}`;
|
||||
}
|
||||
|
||||
/** The binary's name once extracted - matches release-cli.yml's per-platform `binary` matrix value. */
|
||||
export function binaryNameFor(platform: NodeJS.Platform): string {
|
||||
return platform === 'win32' ? 'game-ci.exe' : 'game-ci';
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads (or reuses a cached copy of) the game-ci CLI release archive
|
||||
* matching the current runner, extracts it, and returns the path to the
|
||||
* binary inside.
|
||||
*
|
||||
* The archive - not a bare binary - is what's published: cli.ts resolves
|
||||
* its own static assets (default-build-script/, platforms/*,
|
||||
* unity-config/services-config.json.template, all needed for Docker
|
||||
* volume mounts) relative to its own directory on disk, and those assets
|
||||
* aren't embedded in the compiled binary itself. dist/ ships as the
|
||||
* binary's sibling inside the archive - see game-ci/cli#73.
|
||||
*
|
||||
* Pinned versions are cached via @actions/cache (GitHub's cache service),
|
||||
* so repeat jobs on ephemeral, GitHub-hosted runners skip the download
|
||||
* entirely - @actions/tool-cache alone only survives for the life of one
|
||||
* runner's disk, which GitHub-hosted runners don't persist between jobs.
|
||||
* "latest" is intentionally never persisted this way: caching a moving
|
||||
* target under a fixed key would silently pin every job to whatever
|
||||
* version happened to be "latest" on the first cache write.
|
||||
*
|
||||
* @param version A release tag (e.g. "v0.1.0"), or "latest".
|
||||
* Resolves the "latest" alias to the actual release tag it currently
|
||||
* points to, via a small GitHub API call - not the release-asset
|
||||
* redirect, which never reveals the concrete tag it landed on. This is
|
||||
* what makes caching "latest" possible at all: caching under the literal
|
||||
* string "latest" would silently pin every job to whatever version
|
||||
* happened to be current on the first cache write, but caching under the
|
||||
* *resolved* tag self-invalidates the moment a new release ships (a new
|
||||
* tag is a cache miss by construction), while still hitting cache on
|
||||
* every run in between. It's also what pins install.sh (below) to a
|
||||
* specific, immutable version rather than "latest" - install.sh doesn't
|
||||
* know how to resolve "latest" itself, by design, since which tag it's
|
||||
* fetched at IS the version it installs.
|
||||
*/
|
||||
export async function downloadCli(version: string): Promise<string> {
|
||||
const asset = assetNameFor(process.platform, process.arch);
|
||||
const binaryName = binaryNameFor(process.platform);
|
||||
const isPinned = version !== 'latest';
|
||||
export async function resolveLatestTag(fetchFn: typeof fetch = fetch): Promise<string> {
|
||||
const headers: Record<string, string> = { Accept: 'application/vnd.github+json' };
|
||||
// Actions runners share IPs across many concurrent jobs from unrelated
|
||||
// repos/orgs, so the unauthenticated rate limit (60 req/hour per IP) gets
|
||||
// exhausted by traffic this job never generated. The default GITHUB_TOKEN
|
||||
// reads public repo data (game-ci/cli's releases) fine regardless of which
|
||||
// repo the workflow runs in, and lifts the limit to 5000 req/hour.
|
||||
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
if (isPinned) {
|
||||
const cached = await restoreFromCache(version, binaryName);
|
||||
if (cached) return cached;
|
||||
const response = await fetchFn(`https://api.github.com/repos/${CLI_REPO}/releases/latest`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to resolve the latest game-ci CLI release: GitHub API returned ${response.status}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const url = isPinned
|
||||
? `https://github.com/${CLI_REPO}/releases/download/${version}/${asset}`
|
||||
: `https://github.com/${CLI_REPO}/releases/latest/download/${asset}`;
|
||||
|
||||
core.info(`Downloading game-ci CLI (${version}) from ${url}`);
|
||||
const archivePath = await tc.downloadTool(url);
|
||||
const extractedDir =
|
||||
process.platform === 'win32'
|
||||
? await tc.extractZip(archivePath)
|
||||
: await tc.extractTar(archivePath);
|
||||
|
||||
const binaryPath = path.join(extractedDir, binaryName);
|
||||
if (process.platform !== 'win32') {
|
||||
await fs.chmod(binaryPath, 0o755);
|
||||
const body = (await response.json()) as { tag_name?: string };
|
||||
if (!body.tag_name) {
|
||||
throw new Error('Failed to resolve the latest game-ci CLI release: response had no tag_name.');
|
||||
}
|
||||
|
||||
if (isPinned) {
|
||||
await saveToCache(version, binaryName, extractedDir);
|
||||
}
|
||||
|
||||
return binaryPath;
|
||||
return body.tag_name;
|
||||
}
|
||||
|
||||
function cacheDirFor(version: string): string {
|
||||
return path.join(os.tmpdir(), 'game-ci-cli-cache', version);
|
||||
}
|
||||
|
||||
function cacheKeyFor(version: string, binaryName: string): string {
|
||||
return `game-ci-cli-${version}-${binaryName}`;
|
||||
/**
|
||||
* Includes platform+arch, not just version: darwin-x64 and darwin-arm64
|
||||
* (or any two architectures on the same OS) both extract to a binary
|
||||
* named plain "game-ci", so a key built from version+binaryName alone
|
||||
* (this cache's previous scheme) can't tell them apart and would let one
|
||||
* architecture's binary get restored onto the other's runner.
|
||||
*/
|
||||
function cacheKeyFor(version: string): string {
|
||||
return `game-ci-cli-${version}-${process.platform}-${process.arch}`;
|
||||
}
|
||||
|
||||
async function restoreFromCache(version: string, binaryName: string): Promise<string | null> {
|
||||
async function restoreFromCache(version: string): Promise<string | null> {
|
||||
if (!cache.isFeatureAvailable()) return null;
|
||||
|
||||
const cacheDir = cacheDirFor(version);
|
||||
try {
|
||||
const hitKey = await cache.restoreCache([cacheDir], cacheKeyFor(version, binaryName));
|
||||
const hitKey = await cache.restoreCache([cacheDir], cacheKeyFor(version));
|
||||
if (!hitKey) return null;
|
||||
|
||||
const binaryPath = path.join(cacheDir, binaryName);
|
||||
const binaryPath = path.join(cacheDir, binaryNameFor(process.platform));
|
||||
// Cache restore doesn't guarantee the executable bit survives.
|
||||
if (process.platform !== 'win32') await fs.chmod(binaryPath, 0o755);
|
||||
|
||||
@@ -111,21 +89,104 @@ async function restoreFromCache(version: string, binaryName: string): Promise<st
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToCache(
|
||||
version: string,
|
||||
binaryName: string,
|
||||
extractedDir: string,
|
||||
): Promise<void> {
|
||||
async function saveToCache(version: string): Promise<void> {
|
||||
if (!cache.isFeatureAvailable()) return;
|
||||
|
||||
const cacheDir = cacheDirFor(version);
|
||||
try {
|
||||
await fs.mkdir(path.dirname(cacheDir), { recursive: true });
|
||||
await fs.cp(extractedDir, cacheDir, { recursive: true });
|
||||
await cache.saveCache([cacheDir], cacheKeyFor(version, binaryName));
|
||||
await cache.saveCache([cacheDirFor(version)], cacheKeyFor(version));
|
||||
} catch (error: any) {
|
||||
// A cache miss on save (e.g. another concurrent job already saved this
|
||||
// key) isn't fatal - the download itself already succeeded.
|
||||
// key) isn't fatal - the install itself already succeeded.
|
||||
core.warning(`Failed to save game-ci CLI to cache: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads (or reuses a cached copy of) the game-ci CLI release archive
|
||||
* matching the current runner, extracts it, and returns the path to the
|
||||
* binary inside.
|
||||
*
|
||||
* The actual install mechanics - platform/arch detection, archive format,
|
||||
* download, extraction - live in exactly one place: game-ci/cli's own
|
||||
* scripts/install.sh, fetched and run at the resolved version's tag. That
|
||||
* script is what every engine wrapper (this one today, others later)
|
||||
* delegates to, so a bugfix or a new supported platform ships once, there,
|
||||
* and every wrapper picks it up on its next run with no code change of its
|
||||
* own - see game-ci/cli#187. GitHub Actions caching stays here, in the
|
||||
* wrapper: it's an Actions-only service with no shell-callable API, so
|
||||
* install.sh has no way to drive it itself.
|
||||
*
|
||||
* Every version - including "latest" - is cached via @actions/cache
|
||||
* (GitHub's cache service), so repeat jobs on ephemeral, GitHub-hosted
|
||||
* runners skip the archive download entirely. "latest" is resolved to its
|
||||
* concrete tag first (see resolveLatestTag) and cached under *that*, not
|
||||
* under the literal string "latest" - a real new release is a fresh tag,
|
||||
* so it's a cache miss by construction, never a stale hit.
|
||||
*
|
||||
* @param version A release tag (e.g. "v0.1.0"), or "latest".
|
||||
*/
|
||||
export async function downloadCli(version: string): Promise<string> {
|
||||
const resolvedVersion = version === 'latest' ? await resolveLatestTag() : version;
|
||||
|
||||
const cached = await restoreFromCache(resolvedVersion);
|
||||
if (cached) return cached;
|
||||
|
||||
const destDir = cacheDirFor(resolvedVersion);
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
|
||||
const installScriptUrl = `https://raw.githubusercontent.com/${CLI_REPO}/${resolvedVersion}/scripts/install.sh`;
|
||||
core.info(`Installing game-ci CLI ${resolvedVersion} via ${installScriptUrl}`);
|
||||
|
||||
// Fetched and written to a file, then run as `bash <path> <args...>`,
|
||||
// rather than piped straight into a `bash -c '... "$0" ...'` wrapper: a
|
||||
// curl-into-bash one-liner needs resolvedVersion/destDir interpolated
|
||||
// into the -c script text (even safely, as quoted positional params),
|
||||
// which is exactly the shape CodeQL's uncontrolled-command-line query
|
||||
// flags. Passing a real file path plus a plain string[] of args - the
|
||||
// same pattern this file's own callers already use for the CLI binary
|
||||
// itself - has no such shell-text construction step to flag at all.
|
||||
const scriptResponse = await fetch(installScriptUrl);
|
||||
if (!scriptResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch install.sh for game-ci CLI ${resolvedVersion}: ` +
|
||||
`GitHub returned ${scriptResponse.status} for ${installScriptUrl}.`,
|
||||
);
|
||||
}
|
||||
const scriptPath = path.join(os.tmpdir(), `game-ci-install-${resolvedVersion.replace(/[^\w.-]/g, '_')}.sh`);
|
||||
await fs.writeFile(scriptPath, await scriptResponse.text(), { mode: 0o755 });
|
||||
|
||||
let stdout = '';
|
||||
await exec.exec('bash', [scriptPath, resolvedVersion, destDir], {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// install.sh writes progress to stderr and only the final binary path to
|
||||
// stdout, but take the last non-empty line regardless - defensive against
|
||||
// any stray stdout noise from a future version of the script. Walked
|
||||
// backwards by index rather than toReversed()/reverse() - toReversed()
|
||||
// isn't available on this project's target Node version, and reverse()
|
||||
// mutates in place, which unicorn/no-array-reverse flags even when the
|
||||
// array being mutated is a fresh one from map().
|
||||
const lines = stdout.split('\n').map((line) => line.trim());
|
||||
let binaryPath: string | undefined;
|
||||
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
||||
if (lines[i]) {
|
||||
binaryPath = lines[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!binaryPath) {
|
||||
throw new Error(
|
||||
`Failed to install the game-ci CLI ${resolvedVersion}: install.sh produced no output.`,
|
||||
);
|
||||
}
|
||||
|
||||
await saveToCache(resolvedVersion);
|
||||
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
+16
-11
@@ -18,15 +18,6 @@ import { resolveProjectPath } from './resolve-project-path';
|
||||
|
||||
export async function run() {
|
||||
try {
|
||||
const unityVersion = core.getInput('unityVersion') || 'auto';
|
||||
if (unityVersion !== 'auto') {
|
||||
core.warning(
|
||||
`unityVersion="${unityVersion}" is ignored: the underlying game-ci CLI always detects the Unity ` +
|
||||
"version from the checked-out project's ProjectSettings/ProjectVersion.txt and has no flag to " +
|
||||
'override it yet.',
|
||||
);
|
||||
}
|
||||
|
||||
const cliVersion = core.getInput('cliVersion') || 'latest';
|
||||
const cliPath = await downloadCli(cliVersion);
|
||||
|
||||
@@ -40,11 +31,25 @@ export async function run() {
|
||||
getInput: (name) => (name === 'projectPath' ? projectPath : core.getInput(name)),
|
||||
});
|
||||
|
||||
// CodeQL flags this line (js/command-line-injection) since args
|
||||
// ultimately derives from Action inputs. Verified false positive: args
|
||||
// is an array of discrete argv entries, not a concatenated shell
|
||||
// string, and @actions/exec's toolrunner.js passes it straight to
|
||||
// child_process.spawn(fileName, args, options) - verified by reading
|
||||
// node_modules/@actions/exec/lib/toolrunner.js - never a shell string,
|
||||
// never shell-parsed. CodeQL's static analysis can't see through
|
||||
// @actions/exec's internals to confirm that. This comment does not
|
||||
// suppress the alert (GitHub Code Scanning's default setup has no
|
||||
// inline-suppression-comment mechanism - that was legacy LGTM.com
|
||||
// behavior); the alert needs dismissing via the Security tab/API.
|
||||
const exitCode = await exec.exec(cliPath, args, { ignoreReturnCode: true });
|
||||
|
||||
// Matches the original action's engineExitCode output: 0 on success,
|
||||
// otherwise the exit code of whichever step (activation or build)
|
||||
// failed - which is exactly what the CLI subprocess itself exits with.
|
||||
// otherwise the exit code of whichever step (activation or build) - or,
|
||||
// for providerStrategy=local-system, whichever stage of the
|
||||
// orchestrator's setup/build/cleanup workflow - failed. Either way it's
|
||||
// exactly what the CLI subprocess itself exits with, so no special
|
||||
// handling is needed here for the orchestrate path.
|
||||
core.setOutput('engineExitCode', exitCode);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { fetch as undiciFetch, Headers, Request, Response } from 'undici';
|
||||
|
||||
Object.assign(globalThis, { fetch: undiciFetch, Headers, Request, Response });
|
||||
+1
-1
@@ -14,6 +14,6 @@
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*", "types/**/*"],
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "lib"]
|
||||
}
|
||||
|
||||
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
declare module 'shell-quote' {
|
||||
/**
|
||||
* Quote an array of strings to be safe to use as shell arguments.
|
||||
* @param args - Array of strings to quote
|
||||
* @returns A properly escaped string for shell usage
|
||||
*/
|
||||
export function quote(args: string[]): string;
|
||||
|
||||
/**
|
||||
* Parse a shell command string into an array of arguments.
|
||||
* @param cmd - The command string to parse
|
||||
* @returns Array of parsed arguments
|
||||
*/
|
||||
export function parse(cmd: string): string[];
|
||||
}
|
||||
Reference in New Issue
Block a user