diff --git a/README.md b/README.md index aa630a28..7aa8628e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This action requires that you set the [`CC_TEST_REPORTER_ID`](https://docs.codec | `coverageLocations` | | Locations to find code coverage as a multiline string.
Each line should be of the form `:`.
`type` can be any one of `clover, cobertura, coverage.py, excoveralls, gcov, gocov, jacoco, lcov, lcov-json, simplecov, xccov`. See examples below. | | `prefix` | `undefined` | See [`--prefix`](https://docs.codeclimate.com/docs/configuring-test-coverage) | | `verifyDownload` | `true` | Verifies the downloaded Code Climate reporter binary's checksum and GPG signature. See [Verifying binaries](https://github.com/codeclimate/test-reporter#verifying-binaries) | +| `verifyEnvironment` | `true` | Verifies the current runtime environment (operating system and CPU architecture) is supported by the Code Climate reporter. See [list of supported platforms](https://github.com/codeclimate/test-reporter#binaries) | > **Note** > If you are a Ruby developer using [SimpleCov](https://github.com/simplecov-ruby/simplecov), other users have recommended installing an additional gem – `gem "simplecov_json_formatter"` – this gem fixes `json` error from the default `coverage/.resultset.json` output from SimpleCov. diff --git a/package.json b/package.json index 648826ee..a7992747 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@actions/exec": "1.1.1", "@actions/github": "6.0.0", "@actions/glob": "0.4.0", + "arch": "3.0.0", "hook-std": "3.0.0", "node-fetch": "3.3.2", "openpgp": "5.11.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a14142da..fbc1ea5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@actions/glob': specifier: 0.4.0 version: 0.4.0 + arch: + specifier: 3.0.0 + version: 3.0.0 hook-std: specifier: 3.0.0 version: 3.0.0 @@ -723,6 +726,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arch@3.0.0: + resolution: {integrity: sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2493,6 +2499,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arch@3.0.0: {} + arg@4.1.3: {} asn1.js@5.4.1: diff --git a/src/main.ts b/src/main.ts index aa8a8348..4e21012f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,4 @@ import { unlinkSync } from 'node:fs'; -import { arch, platform } from 'node:os'; import { resolve } from 'node:path'; import { chdir } from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -11,6 +10,7 @@ import * as glob from '@actions/glob'; import { downloadToFile, getOptionalString, + getSupportedEnvironmentInfo, parsePathAndFormat, verifyChecksum, verifySignature, @@ -36,14 +36,17 @@ export interface ActionArguments { coveragePrefix?: string; /** Verifies the downloaded binary with a Code Climate-provided SHA 256 checksum and GPG sinature. @default true */ verifyDownload?: string; + /** Verifies if the current OS and CPU architecture is supported by CodeClimate test reporter. */ + verifyEnvironment?: string; } -const PLATFORM = platform(); +const CURRENT_ENVIRONMENT = getSupportedEnvironmentInfo(); +const PLATFORM = CURRENT_ENVIRONMENT.platform; // REFER: https://docs.codeclimate.com/docs/configuring-test-coverage#locations-of-pre-built-binaries /** Canonical download URL for the official CodeClimate reporter. */ export const DOWNLOAD_URL = `https://codeclimate.com/downloads/test-reporter/test-reporter-latest-${ PLATFORM === 'win32' ? 'windows' : PLATFORM -}-${arch() === 'arm64' ? 'arm64' : 'amd64'}`; +}-${CURRENT_ENVIRONMENT.architecture === 'arm64' ? 'arm64' : 'amd64'}`; /** Local file name of the CodeClimate reporter. */ export const EXECUTABLE = './cc-reporter'; export const CODECLIMATE_GPG_PUBLIC_KEY_ID = @@ -55,6 +58,7 @@ const DEFAULT_WORKING_DIRECTORY = ''; const DEFAULT_CODECLIMATE_DEBUG = 'false'; const DEFAULT_COVERAGE_LOCATIONS = ''; const DEFAULT_VERIFY_DOWNLOAD = 'true'; +const DEFAULT_VERIFY_ENVIRONMENT = 'true'; const SUPPORTED_GITHUB_EVENTS = [ // Regular PRs. @@ -204,10 +208,24 @@ export async function run({ coverageLocationsParam = DEFAULT_COVERAGE_LOCATIONS, coveragePrefix, verifyDownload = DEFAULT_VERIFY_DOWNLOAD, + verifyEnvironment = DEFAULT_VERIFY_ENVIRONMENT, }: ActionArguments = {}): Promise { let lastExitCode = 1; + if (verifyEnvironment === 'true') { + debug('ℹī¸ Verifying environment...'); + const { supported, platform, architecture } = getSupportedEnvironmentInfo(); + if (!supported) { + const errorMessage = `Unsupported platform and architecture! CodeClimate Test Reporter currently is not available for ${architecture} on ${platform} OS`; + error(errorMessage); + setFailed('🚨 Environment verification failed!'); + throw new Error(errorMessage); + } + lastExitCode = 0; + debug('✅ Environment verification completed...'); + } + if (workingDirectory) { - debug(`Changing working directory to ${workingDirectory}`); + debug(`ℹī¸ Changing working directory to ${workingDirectory}`); try { chdir(workingDirectory); lastExitCode = 0; @@ -410,6 +428,11 @@ if (isThisFileBeingRunViaCLI) { 'verifyDownload', DEFAULT_VERIFY_DOWNLOAD, ); + const verifyEnvironment = getOptionalString( + 'verifyEnvironment', + DEFAULT_VERIFY_ENVIRONMENT, + ); + try { run({ downloadUrl: DOWNLOAD_URL, @@ -420,6 +443,7 @@ if (isThisFileBeingRunViaCLI) { coverageLocationsParam: coverageLocations, coveragePrefix, verifyDownload, + verifyEnvironment, }); } finally { // Finally clean up all artifacts that we downloaded. diff --git a/src/utils.ts b/src/utils.ts index be952d00..014d554b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import { createWriteStream, readFile } from 'node:fs'; import { platform } from 'node:os'; import { promisify } from 'node:util'; import { getInput } from '@actions/core'; +import arch from 'arch'; import fetch from 'node-fetch'; import { type VerificationResult, @@ -15,6 +16,18 @@ import { const readFileAsync = promisify(readFile); type ReadFileAsyncOptions = Omit[1], 'string'>; +/** List of environments not supported by the CodeClimate test reporter. */ +// REFER: https://github.com/codeclimate/test-reporter#download +export const UNSUPPORTED_ENVIRONMENTS: Array<{ + platform: ReturnType; + architecture: ReturnType; +}> = [ + { + platform: 'darwin', + architecture: 'arm64', + }, +]; + /** * Parses GitHub Action input and returns the optional value as a string. * @@ -249,3 +262,25 @@ export function parsePathAndFormat(coverageConfigLine: string): { const pattern = lineParts.slice(0, -1)[0] as string; return { format, pattern }; } + +/** + * Reads information about the current operating system and the CPU architecture + * and tells if the CodeClimate test reporter supports it. + */ +export function getSupportedEnvironmentInfo() { + const currentEnvironment = { + platform: platform(), + architecture: arch(), + }; + + return { + supported: !UNSUPPORTED_ENVIRONMENTS.some((e) => { + return ( + e.architecture === currentEnvironment.architecture && + e.platform === currentEnvironment.platform + ); + }), + platform: currentEnvironment.platform, + architecture: currentEnvironment.architecture, + }; +} diff --git a/test/integration.test.ts b/test/integration.test.ts index 52c80c95..2472f308 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -1,5 +1,6 @@ import { unlinkSync } from 'node:fs'; -import { EOL, arch, platform } from 'node:os'; +import { EOL, platform } from 'node:os'; +import arch from 'arch'; import { hookStd } from 'hook-std'; import t from 'tap'; import { diff --git a/test/main.test.ts b/test/main.test.ts index 71c5d56b..c3a1974f 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -18,6 +18,7 @@ import t from 'tap'; import which from 'which'; import { CODECLIMATE_GPG_PUBLIC_KEY_ID, prepareEnv, run } from '../src/main.js'; import * as utils from '../src/utils.js'; +import { getSupportedEnvironmentInfo } from '../src/utils.js'; /** * Dev Notes @@ -181,6 +182,7 @@ t.test('đŸ§Ē run() should run the CC reporter (happy path).', async (t) => { executable: filePath, coverageCommand: `${ECHO_CMD} 'coverage ok'`, verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -246,6 +248,7 @@ t.test( executable: filePath, coverageCommand: `${ECHO_CMD} 'coverage ok'`, verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -283,6 +286,122 @@ t.test( }, ); +t.test( + 'đŸ§Ē run() should run environment verification if configured and fail on unsupported platforms.', + { + skip: getSupportedEnvironmentInfo().supported + ? 'Skipping as test is targeted only for unsupported platforms.' + : false, + }, + async (t) => { + t.plan(1); + t.teardown(() => sandbox.restore()); + + let capturedOutput = ''; + const stdHook = hookStd((text: string) => { + capturedOutput += text; + }); + + try { + await run({ + downloadUrl: 'http://localhost.test/dummy-cc-reporter', + verifyDownload: 'false', + verifyEnvironment: 'true', + }); + t.fail({ error: 'should have thrown an error on unsupported platforms' }); + stdHook.unhook(); + } catch (err) { + stdHook.unhook(); + t.equal( + capturedOutput, + [ + '::debug::ℹī¸ Verifying environment...', + `::error::Unsupported platform and architecture! CodeClimate Test Reporter currently is not available for ${getSupportedEnvironmentInfo().architecture} on ${getSupportedEnvironmentInfo().platform} OS`, + '::error::🚨 Environment verification failed!', + '', + ].join(EOL), + 'should execute all steps (including environment verification).', + ); + } finally { + nock.cleanAll(); + } + t.end(); + }, +); + +t.test( + 'đŸ§Ē run() should run environment verification if configured.', + { + skip: getSupportedEnvironmentInfo().supported + ? false + : 'Skipping as test is targeted only for supported platforms.', + }, + async (t) => { + t.plan(1); + t.teardown(() => sandbox.restore()); + const filePath = `./test.${EXE_EXT}`; + nock('http://localhost.test') + .get('/dummy-cc-reporter') + .reply(200, async () => { + const dummyReporterFile = joinPath( + THIS_MODULE_DIRNAME, + `../test/fixtures/dummy-cc-reporter.${EXE_EXT}`, + ); + const dummyReporter = await readFileAsync(dummyReporterFile); + return intoStream(dummyReporter); + }); + + let capturedOutput = ''; + const stdHook = hookStd((text: string) => { + capturedOutput += text; + }); + + try { + await run({ + downloadUrl: 'http://localhost.test/dummy-cc-reporter', + executable: filePath, + coverageCommand: `${ECHO_CMD} 'coverage ok'`, + verifyDownload: 'false', + verifyEnvironment: 'true', + }); + stdHook.unhook(); + } catch (err) { + stdHook.unhook(); + t.fail({ error: err }); + } finally { + nock.cleanAll(); + } + + t.equal( + capturedOutput, + [ + '::debug::ℹī¸ Verifying environment...', + '::debug::✅ Environment verification completed...', + '::debug::ℹī¸ Downloading CC Reporter from http://localhost.test/dummy-cc-reporter ...', + '::debug::✅ CC Reporter downloaded...', + PLATFORM === 'win32' + ? `[command]${EXE_PATH_PREFIX} "${DEFAULT_WORKDIR}\\test.${EXE_EXT} before-build"` + : `[command]${DEFAULT_WORKDIR}/test.${EXE_EXT} before-build`, + 'before-build', + '::debug::✅ CC Reporter before-build checkin completed...', + `[command]${ECHO_CMD} 'coverage ok'`, + `'coverage ok'`, + '::debug::✅ Coverage run completed...', + PLATFORM === 'win32' + ? `[command]${EXE_PATH_PREFIX} "${DEFAULT_WORKDIR}\\test.${EXE_EXT} after-build --exit-code 0"` + : `[command]${DEFAULT_WORKDIR}/test.${EXE_EXT} after-build --exit-code 0`, + 'after-build --exit-code 0', + '::debug::✅ CC Reporter after-build checkin completed!', + '', + ].join(EOL), + 'should execute all steps (including environment verification).', + ); + unlinkSync(filePath); + nock.cleanAll(); + t.end(); + }, +); + t.test( 'đŸ§Ē run() should run the CC reporter without a coverage command.', async (t) => { @@ -311,6 +430,7 @@ t.test( executable: filePath, coverageCommand: '', verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -435,6 +555,7 @@ t.test( coverageCommand: '', coverageLocationsParam: filePattern, codeClimateDebug: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -538,6 +659,7 @@ t.test( coverageCommand: `${ECHO_CMD} 'coverage ok'`, workingDirectory: CUSTOM_WORKDIR, verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -550,7 +672,7 @@ t.test( t.equal( capturedOutput, [ - `::debug::Changing working directory to ${CUSTOM_WORKDIR}`, + `::debug::ℹī¸ Changing working directory to ${CUSTOM_WORKDIR}`, '::debug::✅ Changing working directory completed...', '::debug::ℹī¸ Downloading CC Reporter from http://localhost.test/dummy-cc-reporter ...', '::debug::✅ CC Reporter downloaded...', @@ -613,6 +735,7 @@ t.test( executable: filePath, coverageCommand: `${ECHO_CMD} 'coverage ok'`, verifyDownload: 'true', + verifyEnvironment: 'false', }); t.fail('should have thrown an error'); stdHook.unhook(); @@ -708,6 +831,7 @@ t.test( downloadUrl: 'http://localhost.test/dummy-cc-reporter', executable: filePath, coverageCommand: `${ECHO_CMD} 'coverage ok'`, + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -776,6 +900,7 @@ t.test( executable: filePath, coverageCommand: `${ECHO_CMD} 'coverage ok'`, verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -835,6 +960,7 @@ t.test( executable: filePath, coverageCommand: `${ECHO_CMD} 'coverage ok'`, verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); } catch (err) { @@ -902,6 +1028,7 @@ t.test( executable: filePath, coverageCommand: COVERAGE_COMMAND, verifyDownload: 'false', + verifyEnvironment: 'false', }); stdHook.unhook(); t.fail('Should throw an error.');