diff --git a/src/model/output.test.ts b/src/model/output.test.ts index e49e6bbc..ba82bfd6 100644 --- a/src/model/output.test.ts +++ b/src/model/output.test.ts @@ -1,9 +1,48 @@ +import { afterEach, describe, expect, it } from 'bun:test'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { Output } from './output.ts'; +const originalGitHubOutput = process.env['GITHUB_OUTPUT']; + +let directory: string | undefined; + +afterEach(() => { + if (originalGitHubOutput === undefined) { + delete process.env['GITHUB_OUTPUT']; + } else { + process.env['GITHUB_OUTPUT'] = originalGitHubOutput; + } + + if (directory) { + rmSync(directory, { recursive: true, force: true }); + directory = undefined; + } +}); + describe('Output', () => { describe('setBuildVersion', () => { it('does not throw', () => { expect(() => Output.setBuildVersion('1.0.0')).not.toThrow(); }); + + // The end of the chain unity-builder reads: `game-ci build` publishes a + // version, and a wrapping action gets it out of $GITHUB_OUTPUT. + it('reaches $GITHUB_OUTPUT', () => { + directory = mkdtempSync(join(tmpdir(), 'game-ci-outputs-')); + const filePath = join(directory, 'outputs'); + writeFileSync(filePath, ''); + process.env['GITHUB_OUTPUT'] = filePath; + + Output.setBuildVersion('1.166.0'); + Output.setAndroidVersionCode('1166000'); + + const written = readFileSync(filePath, 'utf8'); + expect(written).toContain('buildVersion<<'); + expect(written).toContain('1.166.0'); + expect(written).toContain('androidVersionCode<<'); + expect(written).toContain('1166000'); + }); }); }); diff --git a/src/module/actions/core.test.ts b/src/module/actions/core.test.ts new file mode 100644 index 00000000..c9922456 --- /dev/null +++ b/src/module/actions/core.test.ts @@ -0,0 +1,84 @@ +import { afterEach, describe, expect, it } from 'bun:test'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { EOL, tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { core } from './index.ts'; + +const originalGitHubOutput = process.env['GITHUB_OUTPUT']; +const originalConsoleLog = console.log; + +let directory: string | undefined; + +/** An empty $GITHUB_OUTPUT file, as a runner would hand one to a step. */ +function useOutputFile(): string { + directory = mkdtempSync(join(tmpdir(), 'game-ci-outputs-')); + const filePath = join(directory, 'outputs'); + writeFileSync(filePath, ''); + process.env['GITHUB_OUTPUT'] = filePath; + return filePath; +} + +afterEach(() => { + if (originalGitHubOutput === undefined) { + delete process.env['GITHUB_OUTPUT']; + } else { + process.env['GITHUB_OUTPUT'] = originalGitHubOutput; + } + + console.log = originalConsoleLog; + + if (directory) { + rmSync(directory, { recursive: true, force: true }); + directory = undefined; + } +}); + +describe('core.setOutput', () => { + // The regression unity-builder v6 shipped with: the CLI printed a version + // and never wrote the file, so buildVersion/androidVersionCode arrived + // empty on every `unity-builder` run that used the CLI. + it('writes a real record to $GITHUB_OUTPUT when a runner provides one', () => { + const filePath = useOutputFile(); + + core.setOutput('buildVersion', '1.166.0'); + + const written = readFileSync(filePath, 'utf8'); + expect(written).toMatch( + /^buildVersion< { + const filePath = useOutputFile(); + + core.setOutput('buildVersion', `1.166.0${EOL}second-line`); + + expect(readFileSync(filePath, 'utf8')).toContain(`1.166.0${EOL}second-line`); + }); + + it('appends instead of overwriting, so several outputs survive', () => { + const filePath = useOutputFile(); + + core.setOutput('buildVersion', '1.166.0'); + core.setOutput('androidVersionCode', '1166000'); + + const written = readFileSync(filePath, 'utf8'); + expect(written).toContain('buildVersion<<'); + expect(written).toContain('androidVersionCode<<'); + expect(written).toContain('1166000'); + }); + + it('falls back to printing when there is no $GITHUB_OUTPUT', () => { + delete process.env['GITHUB_OUTPUT']; + const printed: string[] = []; + console.log = (message?: unknown) => { + printed.push(String(message)); + }; + + core.setOutput('buildVersion', '1.166.0'); + + expect(printed).toEqual(['(mock) Output "buildVersion" is set to "1.166.0"']); + }); +}); diff --git a/src/module/actions/core.ts b/src/module/actions/core.ts index 46146c14..17142e7b 100644 --- a/src/module/actions/core.ts +++ b/src/module/actions/core.ts @@ -1,5 +1,9 @@ /* eslint-disable no-console */ +import { appendFileSync } from 'node:fs'; +import { randomUUID } from 'node:crypto'; +import { EOL } from 'node:os'; + // Adapted from: https://github.com/actions/toolkit/blob/9b7bcb1567c9b7f134eb3c2d6bbf409a5106a956/packages/core/src/core.ts#L13 /** * Interface for getInput options @@ -22,6 +26,20 @@ export const core = { }, setOutput: (key: string, value: string) => { + // Same split as the real @actions/core: write the file GitHub hands us, + // and only fall back to stdout when there isn't one. Printing was + // previously the only branch, so this never reached $GITHUB_OUTPUT at all + // - which silently dropped the buildVersion/androidVersionCode that + // `game-ci build` publishes for unity-builder to consume. + const filePath = process.env['GITHUB_OUTPUT']; + if (filePath) { + // The delimiter form, not `key=value`: the value is user-supplied (a + // version string), and `key=value` cannot carry a newline. + const delimiter = `ghadelimiter_${randomUUID()}`; + appendFileSync(filePath, `${key}<<${delimiter}${EOL}${value}${EOL}${delimiter}${EOL}`); + return; + } + console.log(`(mock) Output "${key}" is set to "${value}"`); },