Skip to content

Commit

Permalink
define errors and update usage
Browse files Browse the repository at this point in the history
  • Loading branch information
smac89 committed Nov 23, 2023
1 parent 1a24580 commit e5f85d4
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 38 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/versioning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ on:
types:
- released
- edited
push: #(1)
push:
tags:
- 'v[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v?[0-9]+.[0-9]+.[0-9]+'
branches-ignore:
- '**'
paths-ignore:
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,9 @@ on:
types:
- released
- edited
push: #(1)
push: # (1)
tags:
- 'v[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v?[0-9]+.[0-9]+.[0-9]+'
branches-ignore:
- '**'
paths-ignore:
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
"type": "git",
"url": "git+https://github.com/Actions-R-Us/actions-tagger.git"
},
"engines": {
"node": ">=20"
},
"main": "./lib/index.js",
"bin": {
"actionstagger": "./lib/index.js"
},
"scripts": {
"watch": "ncc build src/index.ts --watch",
"build": "ncc build src/index.ts --license LICENSE --minify --out lib",
"build": "ncc build src/index.ts --license LICENSE --minify --no-cache --out lib",
"lint": "prettier --check . && tsc && tsc -p ./tests",
"format": "prettier --write .",
"test": "jest"
},
"engines": {
"node": ">=20"
},
"author": {
"name": "Actions-R-Us",
"url": "https://github.com/Actions-R-Us"
Expand Down
20 changes: 20 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
enum ActionError {
ACTION_CONTEXT_ERROR = 'This action should only be used in a release context or when creating a new tag or branch',
ACTION_SEMVER_ERROR = 'This action can only operate on semantically versioned tags\nSee: https://semver.org/',
ACTION_OLDREF_ERROR = 'Nothing to do because ref id is earlier than major tag commit',
}

type ActionErrorObj = { [key in keyof typeof ActionError]: (typeof ActionError)[key] };
type ActionErrorRevObj = {
[key in keyof typeof ActionError as ActionErrorObj[key]]: key;
};

export const ActionErrorMap = {
...ActionError,
...Object.keys(ActionError).reduce((acc, key: keyof typeof ActionError) => {
(acc as any)[ActionError[key]] = key;
return acc;
}, {} as ActionErrorRevObj),
} as const;

export default ActionError;
15 changes: 10 additions & 5 deletions src/functions/private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import type { GitHub, GraphQlQueryRepository } from './types';

/* eslint-disable @typescript-eslint/no-namespace */
namespace Functions.Private {
function isSemVerPrelease(semv: SemVer | null): boolean {
return (semv?.prerelease.length ?? 0) + (semv?.build.length ?? 0) > 0;
}
/**
* Checks if the event that triggered this action was a release
* See: https://docs.github.com/en/webhooks/webhook-events-and-payloads#release
Expand Down Expand Up @@ -43,7 +46,7 @@ namespace Functions.Private {
* @returns true if the tag is a prerelease
*/
export function isPreReleaseRef(): boolean {
return (Private.getPushRefVersion()?.prerelease.length ?? 0) > 0;
return isSemVerPrelease(Private.getPushRefVersion());
}

/**
Expand All @@ -65,14 +68,14 @@ namespace Functions.Private {
* @returns true if the event is a branch push
*/
export function isBranchPush(): boolean {
return Private.isNewRefPush() && context.payload.ref.startsWith('refs/heads/');
return Private.isNewRefPush() && context.payload.ref?.startsWith('refs/heads/');
}

/**
* @returns true if the event is a tag push
*/
export function isTagPush(): boolean {
return Private.isNewRefPush() && context.payload.ref.startsWith('refs/tags/');
return Private.isNewRefPush() && context.payload.ref?.startsWith('refs/tags/');
}

/**
Expand Down Expand Up @@ -128,7 +131,9 @@ namespace Functions.Private {
*
* @param github The github client
*/
export async function* listAllRefs(github: GitHub): AsyncGenerator<readonly [SemVer, string]> {
export async function* listAllPublicRefs(
github: GitHub
): AsyncGenerator<readonly [SemVer, string]> {
for (let nextPage = ''; true; ) {
const { repository }: { repository: GraphQlQueryRepository } = await github.graphql(
queryAllRefs,
Expand All @@ -142,7 +147,7 @@ namespace Functions.Private {

for (const { ref } of repository.refs.refsList) {
const semverRef = semverParse(ref.name);
if (semverRef !== null && semverRef.prerelease?.length === 0) {
if (semverRef !== null && !isSemVerPrelease(semverRef)) {
if (core.isDebug()) {
core.debug(`checking ${ref.name}`);
}
Expand Down
10 changes: 5 additions & 5 deletions src/functions/public.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import semverGt from 'semver/functions/gt';
import major from 'semver/functions/major';
import valid from 'semver/functions/valid';
import semverMajor from 'semver/functions/major';
import semverValid from 'semver/functions/valid';

import { context } from '@actions/github';
import Private from '@actionstagger/functions/private';
Expand Down Expand Up @@ -47,14 +47,14 @@ namespace Functions {
* Checks if the tag version of the pushed tag/release has valid version
*/
export function isSemVersionedRef(): boolean {
return valid(Functions.getPublishRefVersion()) !== null;
return semverValid(Functions.getPublishRefVersion()) !== null;
}

/**
* Get the major number of the release tag
*/
export function majorVersion(): number {
return major(Functions.getPublishRefVersion()!);
return semverMajor(Functions.getPublishRefVersion()!);
}

/**
Expand All @@ -76,7 +76,7 @@ namespace Functions {
let [repoLatest, repoSha] = [majorLatest, majorSha];

const major = majorLatest.major;
for await (const [semverRef, shaId] of Private.listAllRefs(github)) {
for await (const [semverRef, shaId] of Private.listAllPublicRefs(github)) {
if (semverRef.major === major && semverGt(semverRef, majorLatest)) {
[majorLatest, majorSha] = [semverRef, shaId];
}
Expand Down
21 changes: 11 additions & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import semverGte from 'semver/functions/gte';

import * as core from '@actions/core';
import { getOctokit } from '@actions/github';
import ActionError, { ActionErrorMap } from '@actionstagger/errors';
import {
createRequiredRefs,
findLatestRef,
Expand All @@ -16,17 +17,12 @@ import { preferences } from '@actionstagger/util';

export default async function main(): Promise<void> {
if (!(isPublicRefPush() || isPublishedRelease() || isEditedRelease())) {
core.info(
'This action should only be used in a release context or when creating a new tag or branch'
);
ifErrorSubmitBug();
presentError(ActionError.ACTION_CONTEXT_ERROR);
return;
}

if (!isSemVersionedRef()) {
core.info('This action can only operate on semantically versioned tags');
core.info('See: https://semver.org/');
ifErrorSubmitBug();
presentError(ActionError.ACTION_SEMVER_ERROR);
return;
}

Expand All @@ -41,8 +37,7 @@ export default async function main(): Promise<void> {
outputTagName(ref);
outputLatest(latest);
} else if (majorLatest.shaId !== process.env.GITHUB_SHA) {
core.info('Nothing to do because release commit is earlier than major tag commit');
ifErrorSubmitBug();
presentError(ActionError.ACTION_OLDREF_ERROR);
}
}

Expand Down Expand Up @@ -79,7 +74,13 @@ function createOctoKit(): GitHub {
return getOctokit(token);
}

function ifErrorSubmitBug(): void {
function presentError(actionError: ActionError): void {
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID != null) {
const error = new Error(actionError);
error.name = ActionErrorMap[actionError];
throw error;
}
core.info(actionError);
core.info('If you believe this to be an error, please submit a bug report');
core.info(
`https://github.com/${
Expand Down
101 changes: 95 additions & 6 deletions tests/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { getOctokit } from '@actions/github';
import type { GraphQlQueryRepository } from '@actionstagger/functions/types';

describe('Functions', () => {
beforeEach(() => jest.resetModules());
afterEach(() => jest.restoreAllMocks());

describe.each([
{ preferbranchReleases: 'false', expected: 'tags' },
{ preferbranchReleases: 'true', expected: 'heads' },
Expand Down Expand Up @@ -70,6 +67,75 @@ describe('Functions', () => {
});
});

describe.each([
{
tagName: '10.20.30',
expected: true,
},
{
tagName: '1.1.2-prerelease+meta',
expected: false,
},
{
tagName: '1.0.0-alpha.1',
expected: false,
},
{
tagName: 'v1.1.7',
expected: true,
},
{
tagName: '2023.01.01',
expected: true,
},
{
tagName: '2.0.0-rc.1+build.123',
expected: false,
},
{
// although not valid semver, it does not contain prerelease or build fields
tagName: '1.2',
expected: true,
},
{
// although not valid semver, it does not contain prerelease or build fields
tagName: 'v1',
expected: true,
},
])('#isPublicRefPush()', ({ tagName, expected }) => {
const dir = fs.mkdtempSync(os.tmpdir() + '/jest-push');

beforeEach(() => {
const pushEvent = {
ref: `refs/tags/${tagName}`,
created: true,
};
fs.writeFileSync(`${dir}/event.json`, JSON.stringify(pushEvent));
jest.replaceProperty(process, 'env', {
GITHUB_EVENT_PATH: `${dir}/event.json`,
INPUT_PREFER_BRANCH_RELEASES: 'false',
GITHUB_EVENT_NAME: 'push',
GITHUB_REF_NAME: tagName,
GITHUB_REF: `refs/tags/${tagName}`,
});
});

afterEach(() => {
fs.readdirSync(dir, { recursive: true }).forEach(file => {
fs.rmSync(`${dir}/${file}`);
});
});

afterAll(() => {
fs.rmSync(dir, { recursive: true });
});

test(`when pushed ref=${tagName}, returns ${expected}`, async () =>
await import('@actionstagger/functions').then(({ isPublicRefPush }) =>
expect(isPublicRefPush()).toBe(expected)
));
});

describe.each([
{
eventName: 'release',
Expand Down Expand Up @@ -176,14 +242,14 @@ describe('Functions', () => {
// to create semver objects such as SemVer is not instanceof SemVer...🙄
// In short see https://backend.cafe/should-you-use-jest-as-a-testing-library
const { default: semverParse } = await import('semver/functions/parse');
const semverTag = semverParse(pushedRef)!;
const semverTag = semverParse(pushedRef);

await import('@actionstagger/functions').then(functions => {
jest.spyOn(functions.default, 'getPublishRefVersion').mockReturnValue(semverTag);
});

await import('@actionstagger/functions/private').then(functions => {
jest.spyOn(functions.default, 'listAllRefs').mockImplementation(async function* () {
jest.spyOn(functions.default, 'listAllPublicRefs').mockImplementation(async function* () {
for (const ref of refsList.map(
({ ref }) => [semverParse(ref.name)!, ref.object.shaId] as const
)) {
Expand All @@ -205,6 +271,29 @@ describe('Functions', () => {
});
});

describe.each([
{ ref: 'v1.0.0.alpha', valid: false },
{ ref: 'v1.2', valid: false },
{ ref: '1.2.3-0123', valid: false },
{ ref: '10.20.30', valid: true },
{ ref: '10.20.30-rc1', valid: true },
{ ref: '2022.01.01', valid: false },
{ ref: 'v3.2.2', valid: true },
{ ref: '3.2.2-alpha', valid: true },
])('#isSemVersionedRef()', ({ ref, valid }) => {
beforeEach(async () => {
const { default: semverParse } = await import('semver/functions/parse');
await import('@actionstagger/functions/public').then(({ default: functions }) => {
jest.spyOn(functions, 'getPublishRefVersion').mockReturnValue(semverParse(ref));
});
});

test(`ref ${ref} is ${valid ? 'valid' : 'invalid'} semver`, async () => {
const { isSemVersionedRef } = await import('@actionstagger/functions');
expect(isSemVersionedRef()).toBe(valid);
});
});

describe.each([
{
refToCreate: 'v3.3.7',
Expand All @@ -220,7 +309,7 @@ describe('Functions', () => {
'#createRequiredRefs(github, publishLatest)',
({ refToCreate, publishLatest, expectedRef }) => {
beforeEach(async () => {
const semverTag = (await import('semver/functions/parse')).default(refToCreate)!;
const semverTag = (await import('semver/functions/parse')).default(refToCreate);
await import('@actionstagger/functions/private').then(functions =>
jest.spyOn(functions.default, 'createRef').mockResolvedValue()
);
Expand Down
24 changes: 22 additions & 2 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
test('placeholder', () => {
expect(true).toBeTruthy();
import ActionError from '@actionstagger/errors';

test('Action does not run if event is not public push or release', async () => {
await import('@actionstagger/functions/public').then(({ default: functions }) => {
jest.spyOn(functions, 'isPublicRefPush').mockReturnValue(false);
jest.spyOn(functions, 'isPublishedRelease').mockReturnValue(false);
jest.spyOn(functions, 'isEditedRelease').mockReturnValue(false);
});

await import('@actionstagger/main').then(async ({ default: main }) => {
await expect(main()).rejects.toThrow(ActionError.ACTION_CONTEXT_ERROR);
});
});

test('Action does not run if ref does not match semver', async () => {
await import('@actionstagger/functions/public').then(({ default: functions }) => {
jest.spyOn(functions, 'isPublicRefPush').mockReturnValue(true);
jest.spyOn(functions, 'isSemVersionedRef').mockReturnValue(false);
});
await import('@actionstagger/main').then(async ({ default: main }) => {
await expect(main()).rejects.toThrow(ActionError.ACTION_SEMVER_ERROR);
});
});
3 changes: 3 additions & 0 deletions tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ beforeAll(() => {
process.env.GITHUB_REPOSITORY ??= 'test/test';
process.env.GITHUB_ACTION_REPOSITORY ??= 'Actions-R-Us/actions-tagger';
});

beforeEach(() => jest.resetModules());
afterEach(() => jest.restoreAllMocks());

0 comments on commit e5f85d4

Please sign in to comment.