Skip to content

Commit

Permalink
feat(core): Support Windows
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Support Windows, now that CodeClimate has released a Windows binary of the reporter – see codeclimate.com/changelog/7dd79ee1cf1af7141b2bd18b

Fixes #665.
  • Loading branch information
paambaati committed May 7, 2023
2 parents daa1013 + c08aaec commit f0efca8
Show file tree
Hide file tree
Showing 23 changed files with 546 additions and 302 deletions.
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# REFER: https://www.aleksandrhovhannisyan.com/blog/crlf-vs-lf-normalizing-line-endings-in-git/

# All files are checked into the repo with LF
* text=auto

# These files are checked out using CRLF locally
*.bat eol=crlf
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: "PR checks"
on: [pull_request, push]
on:
push:
branches:
- main
pull_request:

jobs:
check_pr:
Expand Down Expand Up @@ -46,10 +50,10 @@ jobs:
os:
- { index: 1, os-name: 'ubuntu-latest', os-label: 'Linux' }
- { index: 2, os-name: 'macos-latest', os-label: 'macOS' }
# NOTE: Windows is not supported because CodeClimate does not publish a Windows build of the reporter (yet).
- { index: 3, os-name: 'windows-latest', os-label: 'Windows' }
runs-on: ${{ matrix.os.os-name }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os.os-name }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
steps:
- name: checkout code
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@

A GitHub action that publishes your code coverage to [Code Climate](http://codeclimate.com/).

> **Note**
>
> Please use a _specific_ version of this action – for example, `v4.0.0`, instead of using only major versions like `v4` or `v4.0` – these will **not** work!
> **Warning**
>
> Please upgrade to v3.1.1 (or higher) immediately. v3.1.0 was recently broken inadverdently, and the only fix is to upgrade your action to v3.1.1 or higher. Please see [#626](https://github.com/paambaati/codeclimate-action/issues/626) for more details.
Expand Down Expand Up @@ -129,6 +125,6 @@ steps:

Example projects

1. [paambaati/websight](https://github.com/paambaati/websight/blob/89f03007680531587dd5ff5c673e6d813a298d8c/.github/workflows/ci.yml#L33-L50)
1. [paambaati/websight](https://github.com/paambaati/websight/blob/5ab56bcc365ee73dd7937e87267db30f6357c4cd/.github/workflows/ci.yml#L33-L50)

2. [MartinNuc/coverage-ga-test](https://github.com/MartinNuc/coverage-ga-test/blob/master/.github/workflows/ci.yaml)
16 changes: 10 additions & 6 deletions package-lock.json

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

27 changes: 10 additions & 17 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ import * as glob from '@actions/glob';
import {
downloadToFile,
getOptionalString,
parsePathAndFormat,
verifyChecksum,
verifySignature,
} from './utils';
import type { ExecOptions } from '@actions/exec/lib/interfaces';

const PLATFORM = 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()}-${
arch() === 'arm64' ? 'arm64' : 'amd64'
}`;
export const DOWNLOAD_URL = `https://codeclimate.com/downloads/test-reporter/test-reporter-latest-${
PLATFORM === 'win32' ? 'windows' : PLATFORM
}-${arch() === 'arm64' ? 'arm64' : 'amd64'}`;
/** Local file name of the CodeClimate reporter. */
export const EXECUTABLE = './cc-reporter';
export const CODECLIMATE_GPG_PUBLIC_KEY_ID =
Expand Down Expand Up @@ -143,12 +145,8 @@ async function getLocationLines(
.filter((pat) => pat)
.map((pat) => pat.trim());

const patternsAndFormats = coverageLocationPatternsLines.map((line) => {
const lineParts = line.split(':');
const format = lineParts.slice(-1)[0];
const pattern = lineParts.slice(0, -1)[0];
return { format, pattern };
});
const patternsAndFormats =
coverageLocationPatternsLines.map(parsePathAndFormat);

const pathsWithFormat = await Promise.all(
patternsAndFormats.map(async ({ format, pattern }) => {
Expand Down Expand Up @@ -179,13 +177,6 @@ export function run(
verifyDownload: string = DEFAULT_VERIFY_DOWNLOAD
): Promise<void> {
return new Promise(async (resolve, reject) => {
if (platform() === 'win32') {
const err = new Error('CC Reporter is not supported on Windows!');
error(err.message);
setFailed('🚨 CodeClimate Reporter will not run on Windows!');
return reject(err);
}

let lastExitCode = 1;
if (workingDirectory) {
debug(`Changing working directory to ${workingDirectory}`);
Expand Down Expand Up @@ -267,7 +258,9 @@ export function run(
// Run format-coverage on each location.
const parts: Array<string> = [];
for (const i in coverageLocations) {
const [location, type] = coverageLocations[i].split(':');
const { format: type, pattern: location } = parsePathAndFormat(
coverageLocations[i]
);
if (!type) {
const err = new Error(`Invalid formatter type ${type}`);
debug(
Expand Down
62 changes: 59 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHash, timingSafeEqual } from 'crypto';
import { readFile, createWriteStream } from 'fs';
import { platform } from 'os';
import { promisify } from 'util';
import { getInput } from '@actions/core';
import fetch from 'node-fetch';
Expand Down Expand Up @@ -44,7 +45,16 @@ export function downloadToFile(
): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const response = await fetch(url, { timeout: 2 * 60 * 1000 }); // Timeout in 2 minutes.
const response = await fetch(url, {
redirect: 'follow',
follow: 5,
timeout: 2 * 60 * 1000, // Timeout in 2 minutes.
});
if (response.status < 200 || response.status > 299) {
throw new Error(
`Download of '${url}' failed with response status code ${response.status}`
);
}
const writer = createWriteStream(file, { mode });
response.body.pipe(writer);
writer.on('close', () => {
Expand Down Expand Up @@ -106,7 +116,7 @@ export async function getFileChecksum(
* Note that the checksum file is of the format `<checksum> <filename>`.
*
* @param originalFile Original file for which the checksum was generated.
* @param checksumFile Checksum file.
* @param checksumFile Checksum file. Note that the checksum file has to be of the format <filename> <checksum>
* @param algorithm (Optional) Hashing algorithm. @default `sha256`
* @returns Returns `true` if checksums match, `false` if they don't.
*/
Expand All @@ -120,7 +130,7 @@ export async function verifyChecksum(
const declaredChecksum = declaredChecksumFileContents
.toString()
.trim()
.split(' ')[0];
.split(/\s+/)[0];
try {
return timingSafeEqual(
Buffer.from(binaryChecksum),
Expand Down Expand Up @@ -171,3 +181,49 @@ export async function verifySignature(
return false;
}
}

/**
* Parses a given coverage config line that looks like this –
*
* ```
* /Users/gp/projects/cc/*.lcov:lcov
* ```
*
* or –
*
* ```
* D:\Users\gp\projects\cc\*.lcov:lcov
* ```
*
* into –
*
* ```json
* { "format": "lcov", "pattern": "/Users/gp/projects/cc/*.lcov" }
* ```
*
* or –
*
* ```json
* { "format": "lcov", "pattern": "D:\Users\gp\projects\cc\*.lcov" }
* ```
* @param coverageConfigLine
* @returns
*/
export function parsePathAndFormat(coverageConfigLine: string): {
format: string;
pattern: string;
} {
let lineParts = coverageConfigLine.split(':');
// On Windows, if the glob received an absolute path, the path will
// include the Drive letter and the path – for example, `C:\Users\gp\projects\cc\*.lcov:lcov`
// which leads to 2 colons. So we handle this special case.
if (
platform() === 'win32' &&
(coverageConfigLine.match(/:/g) || []).length > 1
) {
lineParts = [lineParts.slice(0, -1).join(':'), lineParts.slice(-1)[0]];
}
const format = lineParts.slice(-1)[0];
const pattern = lineParts.slice(0, -1)[0];
return { format, pattern };
}
19 changes: 19 additions & 0 deletions test/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Notes on how to create fixtures

Currently, this is cumbersome, but it is recommended to check checksums on target platforms as there's minor differences in line endings, and as such, checksums vary for the same file, based on platform.

### Windows

1. To create the SHA256 checksum of a file –

```
certUtil -hashfile test/fixtures/filename.bat SHA256
```

### macOS

1. To create the SHA256 checksum of a file –

```
shasum -a 256 test/fixtures/filename.sh
```
8 changes: 8 additions & 0 deletions test/fixtures/dummy-cc-reporter-after-build-error.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
:: Dummy shell script that exits with a non-zero code when the argument 'after-build' is given.
@echo off
IF "%*" == "after-build --exit-code 0" (
EXIT /b 69
) ELSE (
:: `CALL` is a no-op.
CALL
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dc9edd7421a4cda22b95f340a5ccc34bbeedf8a7376e9b5819d01537eee4bfef test/fixtures/dummy-cc-reporter-after-build-error.bat
2 changes: 1 addition & 1 deletion test/fixtures/dummy-cc-reporter-after-build-error.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash
# Dummy shell script that with a non-zero code when the argument 'after-build' is given.
# Dummy shell script that exits with a non-zero code when the argument 'after-build' is given.
if [[ "$*" == "after-build --exit-code 0" ]]
then exit 69
else
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
036653ec3894054dfc655d66b2724156ec2020dce6b92ee724614cda9196d3b0 test/fixtures/dummy-cc-reporter-after-build-error.sh
1 change: 0 additions & 1 deletion test/fixtures/dummy-cc-reporter-after-build-error.sha256

This file was deleted.

3 changes: 3 additions & 0 deletions test/fixtures/dummy-cc-reporter-before-build-error.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:: Dummy shell script exits with a non-zero code.
@echo off
EXIT /b 69
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
04c7a007b4a43067639bb4015c28ee44ad0aef59021b59dcfcca188a41072b19 test/fixtures/dummy-cc-reporter-before-build-error.bat
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7bbab1e443418dc4d00671c369725a3c46da409f57303825bf1a554bfc4db2a9 test/fixtures/dummy-cc-reporter-before-build-error.sh
7bbab1e443418dc4d00671c369725a3c46da409f57303825bf1a554bfc4db2a9 test/fixtures/dummy-cc-reporter-before-build-error.sh
3 changes: 3 additions & 0 deletions test/fixtures/dummy-cc-reporter.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:: Dummy shell script that just echoes back all arguments.
@echo off
echo %*
1 change: 1 addition & 0 deletions test/fixtures/dummy-cc-reporter.bat.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
d737110ee3d94adfe87c39b7da8c7b2a6e3394e2a1674deeb7205677c88edcb8 dummy-cc-reporter
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f6ee1f4ce8ed9da602f03b6193950938cccdc8f72fb73212223bf56b90465046 dummy-cc-reporter
f6ee1f4ce8ed9da602f03b6193950938cccdc8f72fb73212223bf56b90465046 dummy-cc-reporter
Binary file added test/fixtures/dummy-cc-reporter.sh.sha256.sig
Binary file not shown.
71 changes: 40 additions & 31 deletions test/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from 'tape';
import { unlinkSync } from 'node:fs';
import { EOL, arch, platform } from 'node:os';
import { default as hookStd } from 'hook-std';
import {
downloadAndRecord,
Expand All @@ -9,37 +10,45 @@ import {
FILE_ARTIFACTS,
} from '../src/main';

test('🧪 verifyChecksumAndSignature() should download the CC reporter and pass all validations (happy path).', async (t) => {
t.plan(1);
let capturedOutput = '';
const stdHook = hookStd((text: string) => {
capturedOutput += text;
});
test.skip(
'🧪 verifyChecksumAndSignature() should download the CC reporter and pass all validations (happy path).',
{
// NOTE: Skipping integration test because the CC reporter is not available on macOS Apple Silicon!
skip: platform() === 'darwin' && arch() === 'arm64',
},
async (t) => {
t.plan(1);
let capturedOutput = '';
const stdHook = hookStd((text: string) => {
capturedOutput += text;
});

try {
await downloadAndRecord(DOWNLOAD_URL, EXECUTABLE);
await verifyChecksumAndSignature(DOWNLOAD_URL, EXECUTABLE);
stdHook.unhook();
} catch (err) {
stdHook.unhook();
t.fail(err);
} finally {
for (const artifact of FILE_ARTIFACTS) {
try {
unlinkSync(artifact);
} catch {}
try {
await downloadAndRecord(DOWNLOAD_URL, EXECUTABLE);
await verifyChecksumAndSignature(DOWNLOAD_URL, EXECUTABLE);
stdHook.unhook();
} catch (err) {
stdHook.unhook();
t.fail(err);
} finally {
for (const artifact of FILE_ARTIFACTS) {
try {
unlinkSync(artifact);
} catch {}
}
}
}

t.equal(
capturedOutput,
// prettier-ignore
`::debug::ℹ️ Verifying CC Reporter checksum...
::debug::✅ CC Reported checksum verification completed...
::debug::ℹ️ Verifying CC Reporter GPG signature...
::debug::✅ CC Reported GPG signature verification completed...
`,
'should download the reporter and correctly pass checksum and signature verification steps.'
);
t.end();
});
t.equal(
capturedOutput,
[
`::debug::ℹ️ Verifying CC Reporter checksum...`,
`::debug::✅ CC Reported checksum verification completed...`,
`::debug::ℹ️ Verifying CC Reporter GPG signature...`,
`::debug::✅ CC Reported GPG signature verification completed...`,
``,
].join(EOL),
'should download the reporter and correctly pass checksum and signature verification steps.'
);
t.end();
}
);
Loading

0 comments on commit f0efca8

Please sign in to comment.