Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 action-src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"@cspell/cspell-bundled-dicts"
],
"devDependencies": {
"@actions/core": "^2.0.2",
"@actions/github": "^8.0.0",
"@cspell/cspell-types": "^9.6.2",
"@octokit/webhooks-types": "^7.6.1",
Expand Down
2 changes: 1 addition & 1 deletion action-src/src/action.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from 'node:path';

import { debug, error, info, setFailed, setOutput, warning } from '@actions/core';
import type { Context as GitHubContext } from '@actions/github/lib/context.js';
import type { RunResult } from 'cspell';

import { validateActionParams } from './ActionParams.js';
import { debug, error, info, setFailed, setOutput, warning } from './actions/core/index.js';
import { checkDotMap } from './checkDotMap.js';
import { checkSpellingForContext, type Context } from './checkSpelling.js';
import { getActionParams } from './getActionParams.js';
Expand Down
9 changes: 9 additions & 0 deletions action-src/src/actions/core/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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.
3 changes: 3 additions & 0 deletions action-src/src/actions/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @action/core

This is a pared down version of GitHubs [@action/core](https://github.com/actions/toolkit/tree/dfc20ac/packages/core)
29 changes: 29 additions & 0 deletions action-src/src/actions/core/command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test, vi } from 'vitest';

import { formatCommand, issueCommand } from './command.js';

describe('command', () => {
test.each`
command | properties | message | expected
${'warning'} | ${{}} | ${'This is a warning message'} | ${'::warning::This is a warning message'}
${'name'} | ${{ key: 'value', key2: 'value2' }} | ${'Test message'} | ${'::name key=value,key2=value2::Test message'}
${'set-env'} | ${{ name: 'MY_VAR' }} | ${'some value'} | ${'::set-env name=MY_VAR::some value'}
${'add-mask'} | ${{}} | ${'secretValue123'} | ${'::add-mask::secretValue123'}
${'set-env'} | ${{ name: 'MY_VAR', empty: 0 }} | ${'some value without empty 0'} | ${'::set-env name=MY_VAR::some value without empty 0'}
${'set-env'} | ${{ name: 'MY_VAR', empty: '' }} | ${'some value without empty'} | ${'::set-env name=MY_VAR::some value without empty'}
${''} | ${{ name: 'MY_VAR' }} | ${'no command'} | ${'::missing.command name=MY_VAR::no command'}
`('formatCommand $command, $properties, $message', ({ command, properties, message, expected }) => {
const r = formatCommand(command, properties, message);
expect(r).toBe(expected);
});

test.each`
command | properties | message | expected
${'warning'} | ${{}} | ${'This is a warning message'} | ${'::warning::This is a warning message'}
${'name'} | ${{ key: 'value', key2: 'value2' }} | ${'Test message'} | ${'::name key=value,key2=value2::Test message'}
`('formatCommand $command, $properties, $message', ({ command, properties, message, expected }) => {
const w = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
issueCommand(command, properties, message);
expect(w).toHaveBeenLastCalledWith(expected + '\n');
});
});
100 changes: 100 additions & 0 deletions action-src/src/actions/core/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as os from 'os';

import type { CommandProperties } from './coreTypes.js';
import { toCommandValue } from './utils.js';

/**
* Issues a command to the GitHub Actions runner
*
* @param command - The command name to issue
* @param properties - Additional properties for the command (key-value pairs)
* @param message - The message to include with the command
* @remarks
* This function outputs a specially formatted string to stdout that the Actions
* runner interprets as a command. These commands can control workflow behavior,
* set outputs, create annotations, mask values, and more.
*
* Command Format:
* ::name key=value,key=value::message
*
* @example
* ```typescript
* // Issue a warning annotation
* issueCommand('warning', {}, 'This is a warning message');
* // Output: ::warning::This is a warning message
*
* // Set an environment variable
* issueCommand('set-env', { name: 'MY_VAR' }, 'some value');
* // Output: ::set-env name=MY_VAR::some value
*
* // Add a secret mask
* issueCommand('add-mask', {}, 'secretValue123');
* // Output: ::add-mask::secretValue123
* ```
*
* @internal
* This is an internal utility function that powers the public API functions
* such as setSecret, warning, error, and exportVariable.
*/
export function issueCommand(command: string, properties: CommandProperties, message: string): void {
const cmd = formatCommand(command, properties, message);
process.stdout.write(cmd + os.EOL);
}

export function formatCommand(command: string, properties: CommandProperties, message: string): string {
const cmd = new Command(command, properties, message);
return cmd.toString();
}

const CMD_STRING = '::';

class Command {
private readonly command: string;
private readonly message: string;
private readonly properties: CommandProperties;

constructor(command: string, properties: CommandProperties, message: string) {
if (!command) {
command = 'missing.command';
}

this.command = command;
this.properties = properties;
this.message = message;
}

toString(): string {
let cmdStr = CMD_STRING + this.command;

if (this.properties && Object.keys(this.properties).length > 0) {
cmdStr += ' ';
let first = true;
for (const [key, val] of Object.entries(this.properties)) {
if (val) {
if (first) {
first = false;
} else {
cmdStr += ',';
}
cmdStr += `${key}=${escapeProperty(val)}`;
}
}
}

cmdStr += `${CMD_STRING}${escapeData(this.message)}`;
return cmdStr;
}
}

function escapeData(s: unknown): string {
return toCommandValue(s).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
}

function escapeProperty(s: unknown): string {
return toCommandValue(s)
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A')
.replace(/:/g, '%3A')
.replace(/,/g, '%2C');
}
125 changes: 125 additions & 0 deletions action-src/src/actions/core/core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { afterEach, describe, expect, test, vi } from 'vitest';

import { issueCommand } from './command.js';
import { debug, error, getInput, info, setFailed, setOutput, warning } from './core.js';
import { CommandProperties } from './coreTypes.js';
import { issueFileCommand } from './file-command.js';

vi.mock(import('./file-command.js'), async (importActual) => {
const actual = await importActual();
return {
...actual,
issueFileCommand: vi.fn(),
};
});

vi.mock('./command.js', async (_importActual) => {
return {
formatCommand: vi.fn(),
issueCommand: vi.fn(),
};
});

const env_GITHUB_OUTPUT = process.env['GITHUB_OUTPUT'];

describe('core', () => {
afterEach(() => {
vi.resetAllMocks();
process.env['GITHUB_OUTPUT'] = env_GITHUB_OUTPUT;
});

const empty: CommandProperties = {
col: undefined,
endColumn: undefined,
endLine: undefined,
file: undefined,
line: undefined,
title: undefined,
};

test('getInput no value', () => {
const r = getInput('test input');
expect(r).toBe('');
});

test('getInput required', () => {
expect(() => getInput('test input', { required: true })).toThrow('Input required and not supplied: test input');
});

test('getInput', () => {
process.env['INPUT_MY_TEST_INPUT'] = ' some value\n';
const r = getInput('my test input');
expect(r).toBe('some value');
});

test('getInput no trim', () => {
process.env['INPUT_MY_TEST_INPUT'] = ' some value\n';
const r = getInput('my test input', { trimWhitespace: false });
expect(r).toBe(' some value\n');
});

test('setOutput no env', () => {
const mock = vi.mocked(issueCommand);
process.env['GITHUB_OUTPUT'] = '';
setOutput('my_output', 'some value');
expect(mock).toHaveBeenCalledWith('set-output', { name: 'my_output' }, 'some value');
});

test('setOutput with env', () => {
const mock = vi.mocked(issueFileCommand);
process.env['GITHUB_OUTPUT'] = './some/path/to/output/file.txt';
setOutput('my_output', 'some value');
expect(mock).toHaveBeenCalledWith(
'OUTPUT',
expect.stringMatching(/^my_output<<[\w-]+[\r\n]+some value[\r\n]+[\w-]+$/),
);
});

test('setFailed', () => {
const mock = vi.mocked(issueCommand);
setFailed('failure message');
expect(process.exitCode).toBe(1);
process.exitCode = 0; // reset for other tests
expect(mock).toHaveBeenCalledWith('error', {}, 'failure message');
});

test('setFailed with Error', () => {
const mock = vi.mocked(issueCommand);
setFailed(new Error('failure message'));
expect(process.exitCode).toBe(1);
process.exitCode = 0; // reset for other tests
expect(mock).toHaveBeenCalledWith('error', {}, 'Error: failure message');
});

test('debug', () => {
const mock = vi.mocked(issueCommand);
debug('failure message');
expect(mock).toHaveBeenCalledWith('debug', {}, 'failure message');
});

test('error', () => {
const mock = vi.mocked(issueCommand);
error('failure message', { title: 'My Error' });
expect(mock).toHaveBeenCalledWith('error', { ...empty, title: 'My Error' }, 'failure message');
});

test('warning', () => {
const mock = vi.mocked(issueCommand);
warning('warning message', { title: 'Watch Out!' });
expect(mock).toHaveBeenCalledWith('warning', { ...empty, title: 'Watch Out!' }, 'warning message');
});

test('warning with Error', () => {
const mock = vi.mocked(issueCommand);
warning(new Error('warning message'), { title: 'Watch Out!' });
expect(mock).toHaveBeenCalledWith('warning', { ...empty, title: 'Watch Out!' }, 'Error: warning message');
});

test('info', () => {
const mock = vi.mocked(issueCommand);
const stdout = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
info('info message');
expect(mock).toHaveBeenCalledTimes(0);
expect(stdout).toHaveBeenCalledWith('info message\n');
});
});
98 changes: 98 additions & 0 deletions action-src/src/actions/core/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as os from 'os';

import { issueCommand } from './command.js';
import type { AnnotationProperties, InputOptions } from './coreTypes.js';
import { ExitCode } from './coreTypes.js';
import { issueFileCommand, prepareKeyValueMessage } from './file-command.js';
import { toCommandProperties, toCommandValue } from './utils.js';

/**
* Gets the value of an input.
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
* Returns an empty string if the value is not defined.
*
* @param name name of the input to get
* @param options optional. See InputOptions.
* @returns string
*/
export function getInput(name: string, options?: InputOptions): string {
const val: string = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
if (options?.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}

if (options?.trimWhitespace === false) {
return val;
}

return val.trim();
}

/**
* Sets the value of an output.
*
* @param name name of the output to set
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
*/
export function setOutput(name: string, value: unknown): void {
const filePath = process.env['GITHUB_OUTPUT'] || '';
if (filePath) {
return issueFileCommand('OUTPUT', prepareKeyValueMessage(name, value));
}

process.stdout.write(os.EOL);
issueCommand('set-output', { name }, toCommandValue(value));
}

//-----------------------------------------------------------------------
// Results
//-----------------------------------------------------------------------

/**
* Sets the action status to failed.
* When the action exits it will be with an exit code of 1
* @param message add error issue message
*/
export function setFailed(message: string | Error): void {
process.exitCode = ExitCode.Failure;

error(message);
}

//-----------------------------------------------------------------------
// Logging Commands
//-----------------------------------------------------------------------

/**
* Writes debug message to user log
* @param message debug message
*/
export function debug(message: string): void {
issueCommand('debug', {}, message);
}

/**
* Adds an error issue
* @param message error issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function error(message: string | Error, properties: AnnotationProperties = {}): void {
issueCommand('error', toCommandProperties(properties), message instanceof Error ? message.toString() : message);
}

/**
* Adds a warning issue
* @param message warning issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function warning(message: string | Error, properties: AnnotationProperties = {}): void {
issueCommand('warning', toCommandProperties(properties), message instanceof Error ? message.toString() : message);
}

/**
* Writes info to log with console.log.
* @param message info message
*/
export function info(message: string): void {
process.stdout.write(message + os.EOL);
}
Loading
Loading