Skip to content

Commit dab5b78

Browse files
feat: Build for release
1 parent 891964c commit dab5b78

File tree

11 files changed

+223
-81
lines changed

11 files changed

+223
-81
lines changed

README.md

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,89 @@
1-
# Release GitHub Actions
2-
3-
[![Build Status](https://github.com/technote-space/release-github-actions/workflows/Build/badge.svg)](https://github.com/technote-space/release-github-actions/actions)
4-
[![Coverage Status](https://coveralls.io/repos/github/technote-space/release-github-actions/badge.svg?branch=master)](https://coveralls.io/github/technote-space/release-github-actions?branch=master)
5-
[![CodeFactor](https://www.codefactor.io/repository/github/technote-space/release-github-actions/badge)](https://www.codefactor.io/repository/github/technote-space/release-github-actions)
6-
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/technote-space/release-github-actions/blob/master/LICENSE)
7-
8-
GitHub actions to auto release.
9-
Once you publish the release, this action will automatically
10-
1. Run build
11-
1. Create branch for release
12-
1. Change tag to release branch
13-
1. Change release tag
14-
15-
## Installation
16-
.github/workflows/release.yml
17-
```yaml
18-
on: release
19-
name: Release
20-
jobs:
21-
release:
22-
name: Release GitHub Actions
23-
runs-on: ubuntu-latest
24-
steps:
25-
- name: Release GitHub Actions
26-
uses: technote-space/release-github-actions@v1
27-
with:
28-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29-
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
30-
```
31-
32-
## ACCESS_TOKEN
33-
1. Generate a [personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with the public_repo or repo scope.
34-
(repo is required for private repositories).
35-
1. [Save as secrets](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables)
36-
37-
## Details
38-
### Target event
39-
- release
40-
### Target action
41-
- published
42-
### Branch name
43-
- Tag name
44-
45-
## Options
46-
### BUILD_COMMAND
47-
Build command.
48-
default: `''`
49-
- If package.json includes build or production or prod in scripts, the command is used for build.
50-
- If command does not have install command like `npm run install` or `yarn install`, install commands are added.
51-
- If command is not provided, `rm -rdf .github` command is added.
52-
53-
so if `BUILD_COMMAND` is not provided and package.json has `build` script,
54-
the following commands are executed.
55-
```shell
56-
yarn install
57-
yarn build
58-
yarn install --production
59-
rm -rdf .github
60-
```
61-
62-
### COMMIT_MESSAGE
63-
Commit message.
64-
default: `'feat: Build for release'`
65-
66-
### COMMIT_NAME
67-
Commit name.
68-
default: `'GitHub Actions'`
69-
70-
### COMMIT_EMAIL
71-
Commit email.
72-
default: `'[email protected]'`
1+
# Release GitHub Actions
2+
3+
[![Build Status](https://github.com/technote-space/release-github-actions/workflows/Build/badge.svg)](https://github.com/technote-space/release-github-actions/actions)
4+
[![Coverage Status](https://coveralls.io/repos/github/technote-space/release-github-actions/badge.svg?branch=master)](https://coveralls.io/github/technote-space/release-github-actions?branch=master)
5+
[![CodeFactor](https://www.codefactor.io/repository/github/technote-space/release-github-actions/badge)](https://www.codefactor.io/repository/github/technote-space/release-github-actions)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/technote-space/release-github-actions/blob/master/LICENSE)
7+
8+
GitHub actions to auto release.
9+
Once you publish the release, this action will automatically
10+
1. Run build
11+
1. Create branch for release
12+
1. Change [tags](#tags) to release branch
13+
1. Change release tag
14+
15+
## Installation
16+
.github/workflows/release.yml
17+
```yaml
18+
on: release
19+
name: Release
20+
jobs:
21+
release:
22+
name: Release GitHub Actions
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Release GitHub Actions
26+
uses: technote-space/release-github-actions@v1
27+
with:
28+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
30+
```
31+
32+
## ACCESS_TOKEN
33+
1. Generate a [personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with the public_repo or repo scope.
34+
(repo is required for private repositories).
35+
1. [Save as secrets](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables)
36+
37+
## Details
38+
### Target event
39+
- release
40+
### Target action
41+
- published
42+
### Branch name
43+
- Tag name
44+
45+
## Options
46+
### BUILD_COMMAND
47+
Build command.
48+
default: `''`
49+
- If package.json includes build or production or prod in scripts, the command is used for build.
50+
- If command does not have install command like `npm run install` or `yarn install`, install commands are added.
51+
- If command is not provided, some files are deleted (see [CLEAN_TARGETS](#clean_targets)).
52+
53+
so if `BUILD_COMMAND` is not provided and package.json has `build` script,
54+
the following commands are executed.
55+
```shell
56+
yarn install
57+
yarn build
58+
yarn install --production
59+
rm -rdf .github
60+
...
61+
rm -rdf *.lock
62+
```
63+
64+
### COMMIT_MESSAGE
65+
Commit message.
66+
default: `'feat: Build for release'`
67+
68+
### COMMIT_NAME
69+
Commit name.
70+
default: `'GitHub Actions'`
71+
72+
### COMMIT_EMAIL
73+
Commit email.
74+
default: `'[email protected]'`
75+
76+
### CLEAN_TARGETS
77+
Files or directories to delete before release (Comma separated).
78+
default: `.github,__tests__,src,.gitignore,*.js,*.json,*.lock`
79+
Absolute path and `..` are not permitted to use.
80+
81+
## Addition
82+
### tags
83+
Tag name format must be [Semantic Versioning](https://semver.org/).
84+
The following tags will be created or attached to latest release.
85+
- tag name
86+
- major tag name (generated by tag name)
87+
- e.g. `v1`
88+
- minor tag name (generated by tag name)
89+
- e.g. `v1.2`

__tests__/utils/misc.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import {
1212
getGitUrl,
1313
detectBuildCommand,
1414
getRepository,
15+
isValidTagName,
16+
getMajorTag,
17+
getMinorTag,
18+
uniqueArray,
1519
} from '../../src/utils/misc';
1620
import {DEFAULT_COMMIT_MESSAGE, DEFAULT_COMMIT_NAME, DEFAULT_COMMIT_EMAIL, DEFAULT_BRANCH_NAME} from '../../src/constant';
21+
import {describe} from 'jest-circus';
1722

1823
const testEnv = () => {
1924
const OLD_ENV = process.env;
@@ -193,6 +198,12 @@ describe('getBuildCommands', () => {
193198
'yarn build', // build command of package.json
194199
'yarn install --production',
195200
'rm -rdf .github',
201+
'rm -rdf __tests__',
202+
'rm -rdf src',
203+
'rm -rdf .gitignore',
204+
'rm -rdf *.js',
205+
'rm -rdf *.json',
206+
'rm -rdf *.lock',
196207
]);
197208
});
198209

@@ -226,6 +237,20 @@ describe('getBuildCommands', () => {
226237
expect(getBuildCommands(path.resolve(__dirname, '..', 'fixtures', 'test1'))).toEqual([
227238
'yarn install --production',
228239
'rm -rdf .github',
240+
'rm -rdf __tests__',
241+
'rm -rdf src',
242+
'rm -rdf .gitignore',
243+
'rm -rdf *.js',
244+
'rm -rdf *.json',
245+
'rm -rdf *.lock',
246+
]);
247+
});
248+
249+
it('should get build commands 7', () => {
250+
process.env.INPUT_CLEAN_TARGETS = 'test';
251+
expect(getBuildCommands(path.resolve(__dirname, '..', 'fixtures', 'test1'))).toEqual([
252+
'yarn install --production',
253+
'rm -rdf test',
229254
]);
230255
});
231256
});
@@ -306,3 +331,49 @@ describe('getRepository', () => {
306331
})).toBe('Hello/World');
307332
});
308333
});
334+
335+
describe('isValidTagName', () => {
336+
it('should return true', () => {
337+
expect(isValidTagName('0')).toBeTruthy();
338+
expect(isValidTagName('v12')).toBeTruthy();
339+
expect(isValidTagName('1.2')).toBeTruthy();
340+
expect(isValidTagName('V1.2.3')).toBeTruthy();
341+
expect(isValidTagName('v12.23.34.45')).toBeTruthy();
342+
});
343+
344+
it('should return false', () => {
345+
expect(isValidTagName('')).toBeFalsy();
346+
expect(isValidTagName('abc')).toBeFalsy();
347+
expect(isValidTagName('v1.')).toBeFalsy();
348+
expect(isValidTagName('v.9')).toBeFalsy();
349+
});
350+
});
351+
352+
describe('getMajorTag', () => {
353+
it('should get major tag', () => {
354+
expect(getMajorTag('0')).toBe('v0');
355+
expect(getMajorTag('v12')).toBe('v12');
356+
expect(getMajorTag('1.2')).toBe('v1');
357+
expect(getMajorTag('V1.2.3')).toBe('v1');
358+
expect(getMajorTag('v12.23.34.45')).toBe('v12');
359+
});
360+
});
361+
362+
describe('getMinorTag', () => {
363+
it('should get minor tag', () => {
364+
expect(getMinorTag('0')).toBe('v0.0');
365+
expect(getMinorTag('v12')).toBe('v12.0');
366+
expect(getMinorTag('1.2')).toBe('v1.2');
367+
expect(getMinorTag('V1.2.3')).toBe('v1.2');
368+
expect(getMinorTag('v12.23.34.45')).toBe('v12.23');
369+
});
370+
});
371+
372+
describe('uniqueArray', () => {
373+
it('should return unique array', () => {
374+
expect(uniqueArray([])).toEqual([]);
375+
expect(uniqueArray<number>([1, 2, 2, 3, 4, 3])).toEqual([1, 2, 3, 4]);
376+
expect(uniqueArray<string>(['1', '2', '2', '3', '4', '3'])).toEqual(['1', '2', '3', '4']);
377+
expect(uniqueArray<string>(['v1.2', 'v1', 'v1.2'])).toEqual(['v1.2', 'v1']);
378+
});
379+
});

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ inputs:
2323
BRANCH_NAME:
2424
description: Branch name.
2525
default: 'gh-actions'
26+
CLEAN_TARGETS:
27+
description: Files or directories to delete before release. (Comma separated)
28+
default: '.github,__tests__,src,.gitignore,*.js,*.json,*.lock'
2629
runs:
2730
using: node12
2831
main: lib/main.js

lib/constant.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ exports.DEFAULT_COMMIT_MESSAGE = 'feat: Build for release';
44
exports.DEFAULT_COMMIT_NAME = 'GitHub Actions';
55
exports.DEFAULT_COMMIT_EMAIL = '[email protected]';
66
exports.DEFAULT_BRANCH_NAME = 'gh-actions';
7+
exports.DEFAULT_CLEAN_TARGETS = '.github,__tests__,src,.gitignore,*.js,*.json,*.lock';
78
exports.TARGET_EVENT_NAME = 'release';
89
exports.TARGET_EVENT_ACTION = 'published';
910
exports.SEARCH_BUILD_COMMAND_TARGETS = [

lib/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ function run() {
2626
return;
2727
}
2828
signale_1.default.info(`Tag name: ${github_1.context.payload.release.tag_name}`);
29+
if (!misc_1.isValidTagName(github_1.context.payload.release.tag_name)) {
30+
signale_1.default.info('This tag name is invalid.');
31+
return;
32+
}
2933
yield command_1.deploy(github_1.context.payload.release.tag_name, new github_1.GitHub(core_1.getInput('GITHUB_TOKEN', { required: true })), github_1.context);
3034
}
3135
catch (error) {

lib/utils/command.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,15 @@ const commit = (pushDir) => __awaiter(this, void 0, void 0, function* () {
9595
const push = (pushDir, tagName, branchName, context) => __awaiter(this, void 0, void 0, function* () {
9696
signale_1.default.info('Pushing to %s@%s (tag: %s)', misc_1.getRepository(context), branchName, tagName);
9797
const url = misc_1.getGitUrl(context);
98-
yield execAsync(`git -C ${pushDir} push --delete "${url}" tag ${tagName}`, true, 'git push --delete origin tag');
98+
const tagNames = misc_1.uniqueArray([tagName, misc_1.getMajorTag(tagName), misc_1.getMinorTag(tagName)]);
99+
for (const tagName of tagNames) {
100+
yield execAsync(`git -C ${pushDir} push --delete "${url}" tag ${tagName}`, true, 'git push --delete origin tag', true);
101+
}
99102
yield execAsync(`git -C ${pushDir} tag -l | xargs git -C ${pushDir} tag -d`);
100103
yield execAsync(`git -C ${pushDir} fetch "${url}" --tags`, true, 'git fetch origin --tags');
101-
yield execAsync(`git -C ${pushDir} tag ${tagName}`);
104+
for (const tagName of tagNames) {
105+
yield execAsync(`git -C ${pushDir} tag ${tagName}`);
106+
}
102107
yield execAsync(`git -C ${pushDir} push --quiet --tags "${url}" "${branchName}":"refs/heads/${branchName}"`, true, `git push --tags "${branchName}":"refs/heads/${branchName}"`);
103108
return true;
104109
});

lib/utils/misc.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ exports.getBuildCommands = (dir) => {
3131
commands.push('yarn install --production');
3232
}
3333
if ('' === command) {
34-
commands.push('rm -rdf .github');
34+
commands.push(...getCleanTargets().map(target => `rm -rdf ${target}`));
3535
}
3636
return commands;
3737
};
@@ -57,4 +57,10 @@ exports.detectBuildCommand = (dir) => {
5757
}
5858
return false;
5959
};
60+
exports.uniqueArray = (array) => [...new Set(array)];
61+
exports.isValidTagName = (tagName) => /^v?\d+(\.\d+)*$/i.test(tagName);
62+
exports.getMajorTag = (tagName) => 'v' + getVersionFragments(tagName).slice(0, 1).join('.');
63+
exports.getMinorTag = (tagName) => 'v' + getVersionFragments(tagName).concat(['0']).slice(0, 2).join('.');
64+
const getVersionFragments = (tagName) => tagName.trim().replace(/^v?/gi, '').split('.');
6065
const normalizeCommand = (command) => command.trim().replace(/\s{2,}/g, ' ');
66+
const getCleanTargets = () => [...new Set((core_1.getInput('CLEAN_TARGETS') || constant_1.DEFAULT_CLEAN_TARGETS).split(',').map(target => target.trim()).filter(target => target && !target.startsWith('/') && !target.includes('..')))];

src/constant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const DEFAULT_COMMIT_MESSAGE = 'feat: Build for release';
22
export const DEFAULT_COMMIT_NAME = 'GitHub Actions';
33
export const DEFAULT_COMMIT_EMAIL = '[email protected]';
44
export const DEFAULT_BRANCH_NAME = 'gh-actions';
5+
export const DEFAULT_CLEAN_TARGETS = '.github,__tests__,src,.gitignore,*.js,*.json,*.lock';
56
export const TARGET_EVENT_NAME = 'release';
67
export const TARGET_EVENT_ACTION = 'published';
78
export const SEARCH_BUILD_COMMAND_TARGETS = [

src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {setFailed, getInput} from '@actions/core';
22
import {context, GitHub} from '@actions/github';
33
import signale from 'signale';
44
import {deploy} from './utils/command';
5-
import {isTargetEvent} from './utils/misc';
5+
import {isTargetEvent, isValidTagName} from './utils/misc';
66

77
async function run() {
88
try {
@@ -14,6 +14,11 @@ async function run() {
1414
}
1515

1616
signale.info(`Tag name: ${context.payload.release.tag_name}`);
17+
if (!isValidTagName(context.payload.release.tag_name)) {
18+
signale.info('This tag name is invalid.');
19+
return;
20+
}
21+
1722
await deploy(context.payload.release.tag_name, new GitHub(getInput('GITHUB_TOKEN', {required: true})), context);
1823
} catch (error) {
1924
setFailed(error.message);

src/utils/command.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ import signale from 'signale';
44
import {exec} from 'child_process';
55
import {GitHub} from '@actions/github/lib/github';
66
import {Context} from '@actions/github/lib/context';
7-
import {getGitUrl, getRepository, getBuildCommands, getWorkspace, getCommitMessage, getCommitName, getCommitEmail, getBranchName} from './misc';
7+
import {
8+
getGitUrl,
9+
getRepository,
10+
getBuildCommands,
11+
getWorkspace,
12+
getCommitMessage,
13+
getCommitName,
14+
getCommitEmail,
15+
getBranchName,
16+
getMajorTag,
17+
getMinorTag,
18+
uniqueArray,
19+
} from './misc';
820

921
export const deploy = async (tagName: string, octokit: GitHub, context: Context) => {
1022
const workDir = path.resolve(getWorkspace(), '.work');
@@ -96,10 +108,15 @@ const push = async (pushDir: string, tagName: string, branchName: string, contex
96108
signale.info('Pushing to %s@%s (tag: %s)', getRepository(context), branchName, tagName);
97109

98110
const url = getGitUrl(context);
99-
await execAsync(`git -C ${pushDir} push --delete "${url}" tag ${tagName}`, true, 'git push --delete origin tag');
111+
const tagNames = uniqueArray([tagName, getMajorTag(tagName), getMinorTag(tagName)]);
112+
for (const tagName of tagNames) {
113+
await execAsync(`git -C ${pushDir} push --delete "${url}" tag ${tagName}`, true, 'git push --delete origin tag', true);
114+
}
100115
await execAsync(`git -C ${pushDir} tag -l | xargs git -C ${pushDir} tag -d`);
101116
await execAsync(`git -C ${pushDir} fetch "${url}" --tags`, true, 'git fetch origin --tags');
102-
await execAsync(`git -C ${pushDir} tag ${tagName}`);
117+
for (const tagName of tagNames) {
118+
await execAsync(`git -C ${pushDir} tag ${tagName}`);
119+
}
103120
await execAsync(`git -C ${pushDir} push --quiet --tags "${url}" "${branchName}":"refs/heads/${branchName}"`, true, `git push --tags "${branchName}":"refs/heads/${branchName}"`);
104121
return true;
105122
};

0 commit comments

Comments
 (0)