Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
cc26bbb
unity-xcode-builder@v1.2.1
StephenHodgson Apr 19, 2025
2e9828f
revert some stuff
StephenHodgson Apr 19, 2025
1b9dcf8
refactor app id
StephenHodgson Apr 19, 2025
fd8da19
better logging
StephenHodgson Apr 19, 2025
a55c90e
reorder
StephenHodgson Apr 19, 2025
8c63c51
check file count
StephenHodgson Apr 19, 2025
638d772
truncate CFBundleShortVersionString if it doesn't match major.minor.r…
StephenHodgson Apr 22, 2025
80abe40
reassign cFBundleShortVersionString
StephenHodgson Apr 22, 2025
45b9338
update info.plist with updated CFBundleShortVersionString
StephenHodgson Apr 22, 2025
8401fe5
throw
StephenHodgson Apr 22, 2025
40173a3
force to string
StephenHodgson Apr 22, 2025
3a45359
more loggin
StephenHodgson Apr 22, 2025
e1dcc6e
specify version
StephenHodgson Apr 25, 2025
c7fd5f8
change how we select xcode version
StephenHodgson Apr 25, 2025
725e512
list files
StephenHodgson Apr 25, 2025
3334025
change how we check installed version string w/one provided
StephenHodgson Apr 25, 2025
ec49ae3
convert bundle version number to string
StephenHodgson Apr 28, 2025
b7977b0
set PR number in what's new
StephenHodgson Apr 28, 2025
6b3c8df
always read from latest
StephenHodgson Apr 28, 2025
29bb7cc
change permissions
StephenHodgson Apr 28, 2025
1f763bb
fix app id
StephenHodgson Apr 29, 2025
69d0c12
updated logging info
StephenHodgson Apr 29, 2025
c3968a3
more tweaks
StephenHodgson Apr 29, 2025
0386a38
set projectRef.bundleVersion after auto increment
StephenHodgson Apr 29, 2025
ee43808
make sure to set auto increment before writing export options
StephenHodgson Apr 29, 2025
97d11f0
update polling states
StephenHodgson Apr 29, 2025
d2f82fb
tweak polling algo
StephenHodgson Apr 29, 2025
6f83676
log processing state
StephenHodgson Apr 29, 2025
b197c0e
update build poll alog x2
StephenHodgson Apr 29, 2025
0525f8a
cleanup
StephenHodgson Apr 29, 2025
b9eddda
don't stringify error
StephenHodgson Apr 29, 2025
4834b18
refactor auto increment version for semver patterns
StephenHodgson Apr 29, 2025
55b83ad
cleaned up what's new
StephenHodgson Apr 29, 2025
4f4de8b
remove newline in PR details of what's new
StephenHodgson Apr 29, 2025
04aa365
fix duplicate call to update after we create
StephenHodgson Apr 29, 2025
da29620
idex
StephenHodgson Apr 29, 2025
06bd759
check version before returning valid build
StephenHodgson Apr 29, 2025
cfb0eef
break
StephenHodgson Apr 29, 2025
0ba2788
add some additional logging
StephenHodgson Apr 29, 2025
42db137
fix leading 0s
StephenHodgson Apr 29, 2025
f159f46
updated some logging
StephenHodgson Apr 29, 2025
26474ea
update validation
StephenHodgson Apr 29, 2025
4790120
refactor build validation poll
StephenHodgson Apr 29, 2025
256a8ec
use v1
StephenHodgson Apr 29, 2025
c8f07c5
log everything
StephenHodgson Apr 29, 2025
5f044aa
list all app ids if multiple found
StephenHodgson Apr 30, 2025
40aed68
enable more logging
StephenHodgson Apr 30, 2025
9e354d2
fetch all just in case the sha isn't in the local db
StephenHodgson Apr 30, 2025
1c9c09e
fetch head depth 1
StephenHodgson Apr 30, 2025
fe412c9
disable some logging
StephenHodgson Apr 30, 2025
3fc4cce
use more explicit bundle id matching
StephenHodgson May 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ jobs:
version-file: 'None'
build-targets: ${{ matrix.build-target }}
unity-version: ${{ matrix.unity-version }}
architecture: 'arm64'
- name: Find Unity Template Path and Version
run: |
$rootPath = $env:UNITY_EDITOR_PATH -replace "Editor.*", ""
Expand Down
350 changes: 221 additions & 129 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unity-xcode-builder",
"version": "1.2.0",
"version": "1.2.1",
"description": "A GitHub Action to build, archive, and upload Unity exported xcode projects.",
"author": "buildalon",
"license": "MIT",
Expand All @@ -25,18 +25,18 @@
"uuid": "^10.0.0"
},
"devDependencies": {
"@types/node": "^22.13.14",
"@types/node": "^22.15.3",
"@types/plist": "^3.0.5",
"@types/semver": "^7.7.0",
"@types/uuid": "^10.0.0",
"@vercel/ncc": "^0.34.0",
"shx": "^0.3.4",
"typescript": "^5.8.2"
"typescript": "^5.8.3"
},
"scripts": {
"build": "npm run clean && npm run bundle",
"bundle": "ncc build src/index.ts -o dist --source-map --license licenses.txt",
"watch": "ncc build src/index.ts -o dist --source-map --license licenses.txt --watch",
"clean": "npm install && shx rm -rf dist/ out/ node_modules/ && npm ci"
}
}
}
114 changes: 64 additions & 50 deletions src/AppStoreConnectClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ function checkAuthError(error: any) {
}
}

export async function GetAppId(project: XcodeProject): Promise<XcodeProject> {
if (project.appId) { return project; }
export async function GetAppId(project: XcodeProject): Promise<string> {
await getOrCreateClient(project);
const { data: response, error } = await appStoreConnectClient.api.AppsService.appsGetCollection({
query: { 'filter[bundleId]': [project.bundleId] }
Expand All @@ -58,27 +57,32 @@ export async function GetAppId(project: XcodeProject): Promise<XcodeProject> {
checkAuthError(error);
throw new Error(`Error fetching apps: ${JSON.stringify(error)}`);
}
log(`GET /appsGetCollection\n${JSON.stringify(response, null, 2)}`);
if (!response) {
throw new Error(`No apps found for bundle id ${project.bundleId}`);
}
if (response.data.length === 0) {
throw new Error(`No apps found for bundle id ${project.bundleId}`);
}
project.appId = response.data[0].id;
return project;
if (response.data.length > 1) {
log(`Multiple apps found for bundle id ${project.bundleId}!`);
for (const app of response.data) {
log(`[${app.id}] ${app.attributes?.bundleId}`);
if (project.bundleId === app.attributes?.bundleId) {
return app.id;
}
}
}
return response.data[0].id;
}

export async function GetLatestBundleVersion(project: XcodeProject): Promise<number> {
export async function GetLatestBundleVersion(project: XcodeProject): Promise<string | null> {
await getOrCreateClient(project);
let { preReleaseVersion, build } = await getLastPreReleaseVersionAndBuild(project);
if (!build) {
build = await getLastPrereleaseBuild(preReleaseVersion);
}
const buildVersion = build.attributes.version;
if (!buildVersion) {
throw new Error(`No build version found!\n${JSON.stringify(build, null, 2)}`);
}
return Number(buildVersion);
return build?.attributes?.version;
}

function reMapPlatform(project: XcodeProject): ('IOS' | 'MAC_OS' | 'TV_OS' | 'VISION_OS') {
Expand All @@ -97,7 +101,7 @@ function reMapPlatform(project: XcodeProject): ('IOS' | 'MAC_OS' | 'TV_OS' | 'VI
}

async function getLastPreReleaseVersionAndBuild(project: XcodeProject): Promise<PreReleaseVersionWithBuild> {
if (!project.appId) { project = await GetAppId(project); }
if (!project.appId) { project.appId = await GetAppId(project); }
const preReleaseVersionRequest: PreReleaseVersionsGetCollectionData = {
query: {
'filter[app]': [project.appId],
Expand All @@ -109,7 +113,7 @@ async function getLastPreReleaseVersionAndBuild(project: XcodeProject): Promise<
limit: 1,
}
};
log(`/preReleaseVersions?${JSON.stringify(preReleaseVersionRequest.query)}`);
log(`GET /preReleaseVersions?${JSON.stringify(preReleaseVersionRequest.query)}`);
const { data: preReleaseResponse, error: preReleaseError } = await appStoreConnectClient.api.PreReleaseVersionsService.preReleaseVersionsGetCollection(preReleaseVersionRequest);
const responseJson = JSON.stringify(preReleaseResponse, null, 2);
if (preReleaseError) {
Expand Down Expand Up @@ -151,7 +155,7 @@ async function getLastPrereleaseBuild(prereleaseVersion: PrereleaseVersion): Pro
limit: 1
}
};
log(`/builds?${JSON.stringify(buildsRequest.query)}`);
log(`GET /builds?${JSON.stringify(buildsRequest.query)}`);
const { data: buildsResponse, error: buildsError } = await appStoreConnectClient.api.BuildsService.buildsGetCollection(buildsRequest);
const responseJson = JSON.stringify(buildsResponse, null, 2);
if (buildsError) {
Expand All @@ -173,7 +177,7 @@ async function getBetaBuildLocalization(build: Build): Promise<BetaBuildLocaliza
'fields[betaBuildLocalizations]': ['whatsNew']
}
};
log(`/betaBuildLocalizations?${JSON.stringify(betaBuildLocalizationRequest.query)}`);
log(`GET /betaBuildLocalizations?${JSON.stringify(betaBuildLocalizationRequest.query)}`);
const { data: betaBuildLocalizationResponse, error: betaBuildLocalizationError } = await appStoreConnectClient.api.BetaBuildLocalizationsService.betaBuildLocalizationsGetCollection(betaBuildLocalizationRequest);
const responseJson = JSON.stringify(betaBuildLocalizationResponse, null, 2);
if (betaBuildLocalizationError) {
Expand Down Expand Up @@ -205,7 +209,7 @@ async function createBetaBuildLocalization(build: Build, whatsNew: string): Prom
}
}
}
log(`/betaBuildLocalizations\n${JSON.stringify(betaBuildLocalizationRequest, null, 2)}`);
log(`POST /betaBuildLocalizations\n${JSON.stringify(betaBuildLocalizationRequest, null, 2)}`);
const { data: response, error: responseError } = await appStoreConnectClient.api.BetaBuildLocalizationsService.betaBuildLocalizationsCreateInstance({
body: betaBuildLocalizationRequest
});
Expand All @@ -228,63 +232,73 @@ async function updateBetaBuildLocalization(betaBuildLocalization: BetaBuildLocal
}
}
};
log(`/betaBuildLocalizations/${betaBuildLocalization.id}\n${JSON.stringify(updateBuildLocalization, null, 2)}`);
log(`POST /betaBuildLocalizations/${betaBuildLocalization.id}\n${JSON.stringify(updateBuildLocalization, null, 2)}`);
const { error: updateError } = await appStoreConnectClient.api.BetaBuildLocalizationsService.betaBuildLocalizationsUpdateInstance({
path: { id: betaBuildLocalization.id },
body: updateBuildLocalization
});
const responseJson = JSON.stringify(updateBuildLocalization, null, 2);
if (updateError) {
checkAuthError(updateError);
throw new Error(`Error updating beta build localization: ${JSON.stringify(updateError, null, 2)}`);
}
log(responseJson);
return betaBuildLocalization;
}

async function pollForValidBuild(project: XcodeProject, buildVersion: number, whatsNew: string, maxRetries: number = 60, interval: number = 30): Promise<BetaBuildLocalization> {
async function pollForValidBuild(project: XcodeProject, maxRetries: number = 60, interval: number = 30): Promise<Build> {
log(`Polling build validation...`);
await new Promise(resolve => setTimeout(resolve, interval * 1000));
let retries = 0;
while (retries < maxRetries) {
if (core.isDebug()) {
core.startGroup(`Polling for build... Attempt ${++retries}/${maxRetries}`);
}
try {
let { preReleaseVersion, build } = await getLastPreReleaseVersionAndBuild(project);
if (!preReleaseVersion) {
throw new Error('No pre-release version found!');
}
while (++retries < maxRetries) {
core.info(`Polling for build... Attempt ${retries}/${maxRetries}`);
let { preReleaseVersion, build } = await getLastPreReleaseVersionAndBuild(project);
if (preReleaseVersion) {
if (!build) {
build = await getLastPrereleaseBuild(preReleaseVersion);
}
if (build.attributes?.version !== buildVersion.toString()) {
throw new Error(`Build version ${build.attributes?.version} does not match expected version ${buildVersion}`);
}
if (build.attributes?.processingState !== 'VALID') {
throw new Error(`Build ${buildVersion} is not valid yet!`);
}
const betaBuildLocalization = await getBetaBuildLocalization(build);
try {
if (!betaBuildLocalization) {
return await createBetaBuildLocalization(build, whatsNew);
if (build) {
const normalizedBuildVersion = normalizeVersion(build.attributes?.version);
const normalizedProjectVersion = normalizeVersion(project.bundleVersion);
switch (build.attributes?.processingState) {
case 'VALID':
if (normalizedBuildVersion === normalizedProjectVersion) {
core.info(`Build ${build.attributes.version} is VALID`);
return build;
} else {
core.info(`Waiting for ${project.bundleVersion}...`);
}
break;
case 'FAILED':
case 'INVALID':
throw new Error(`Build ${build.attributes.version} === ${build.attributes.processingState}!`);
default:
core.info(`Build ${build.attributes.version} is ${build.attributes.processingState}...`);
break;
}
} catch (error) {
log(error, core.isDebug() ? 'warning' : 'info');
}
return await updateBetaBuildLocalization(betaBuildLocalization, whatsNew);
} catch (error) {
log(error, core.isDebug() ? 'error' : 'info');
}
finally {
if (core.isDebug()) {
core.endGroup();
} else {
core.info(`Waiting for build ${preReleaseVersion.attributes?.version}...`);
}
} else {
core.info(`Waiting for pre-release build ${project.versionString}...`);
}
await new Promise(resolve => setTimeout(resolve, interval * 1000));
}
throw new Error('Timed out waiting for valid build!');
}

export async function UpdateTestDetails(project: XcodeProject, buildVersion: number, whatsNew: string): Promise<void> {
export async function UpdateTestDetails(project: XcodeProject, whatsNew: string): Promise<void> {
core.info(`Updating test details...`);
await getOrCreateClient(project);
await pollForValidBuild(project, buildVersion, whatsNew);
const build = await pollForValidBuild(project);
const betaBuildLocalization = await getBetaBuildLocalization(build);
if (!betaBuildLocalization) {
core.info(`Creating beta build localization...`);
await createBetaBuildLocalization(build, whatsNew);
} else {
core.info(`Updating beta build localization...`);
await updateBetaBuildLocalization(betaBuildLocalization, whatsNew);
}
}

function normalizeVersion(version: string): string {
return version.split('.').map(part => parseInt(part, 10).toString()).join('.');
}
1 change: 0 additions & 1 deletion src/AppleCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class AppleCredential {
signingIdentity?: string;
provisioningProfileUUID?: string;
bearerToken?: string;
appleId?: string;
ascPublicId?: string;
}

Expand Down
7 changes: 4 additions & 3 deletions src/XcodeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class XcodeProject {
bundleId: string,
projectDirectory: string,
versionString: string,
bundleVersion: number,
bundleVersion: string,
scheme: string,
credential: AppleCredential,
xcodeVersion: SemVer
Expand All @@ -30,6 +30,7 @@ export class XcodeProject {
projectPath: string;
projectName: string;
bundleId: string;
appId: string;
projectDirectory: string;
credential: AppleCredential;
platform: string;
Expand All @@ -40,11 +41,11 @@ export class XcodeProject {
exportOption: string;
exportOptionsPath: string;
entitlementsPath: string;
appId: string;
versionString: string;
bundleVersion: number;
bundleVersion: string;
scheme: string;
xcodeVersion: SemVer;
autoIncrementBuildNumber: boolean;
isAppStoreUpload(): boolean {
return this.exportOption === 'app-store' || this.exportOption === 'app-store-connect';
}
Expand Down
Loading
Loading