Skip to content

Commit 4759bd9

Browse files
author
childish-sambino
authored
feat: add GitHub release action (#129)
1 parent d615c0a commit 4759bd9

15 files changed

+10178
-1
lines changed

.eslintrc.yml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
env:
2+
browser: true
3+
es2021: true
4+
extends:
5+
- eslint:recommended
6+
- plugin:@typescript-eslint/recommended
7+
parser: '@typescript-eslint/parser'
8+
parserOptions:
9+
ecmaVersion: latest
10+
sourceType: module
11+
plugins:
12+
- '@typescript-eslint'
13+
rules:
14+
indent:
15+
- error
16+
- 2
17+
linebreak-style:
18+
- error
19+
- unix
20+
quotes:
21+
- error
22+
- double
23+
semi:
24+
- error
25+
- always

.github/workflows/test.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Test
2+
on:
3+
pull_request:
4+
branches: [ main ]
5+
6+
jobs:
7+
test:
8+
name: Test
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 20
11+
steps:
12+
- name: Checkout dx-automator
13+
uses: actions/checkout@v2
14+
15+
- name: Set up Node
16+
uses: actions/setup-node@v2
17+
with:
18+
node-version: 12
19+
20+
- name: Install Dependencies
21+
run: npm install
22+
23+
- name: Build & Test
24+
run: npm run all
25+
26+
- name: Show git status and fail on diff
27+
run: |
28+
git status
29+
git diff
30+
test -z "$(git status --porcelain)"

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ __pycache__/
1111
env/
1212
build/
1313
develop-eggs/
14-
dist/
1514
eggs/
1615
.eggs/
1716
lib/
@@ -43,6 +42,7 @@ nosetests.xml
4342
coverage.xml
4443
*,cover
4544
.hypothesis/
45+
coverage/
4646

4747
# Translations
4848
*.mo

actions/README.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Actions
2+
3+
## GitHubRelease
4+
5+
A GitHub action to create or update a Release in GitHub.
6+
7+
### Usage
8+
9+
```yml
10+
- name: Create a release
11+
uses: sendgrid/dx-automator/actions/release@main
12+
with:
13+
changelog-filename: CHANGES.md
14+
footer: This is a custom footer
15+
assets: sendgrid-java.jar
16+
env:
17+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18+
```
19+
20+
### Action Inputs
21+
22+
| Name | Description | Default |
23+
|----------------------|------------------------------------------------------------|--------------------------------|
24+
| `changelog-filename` | Filename of the changelog file | `CHANGES.md` or `CHANGELOG.md` |
25+
| `footer` | Custom release notes footer | |
26+
| `assets` | Space-separated list of assets to include with the release | |
27+
28+
The env var `GITHUB_TOKEN` must also be given which can be either `GITHUB_TOKEN` or a `repo`
29+
-scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
30+
31+
The `footer` input supports limited string parameter expansion. The table below lists the string literals and what they
32+
will expand into.
33+
34+
| Literal | Value |
35+
|--------------|--------------------------------------------------------|
36+
| `${version}` | The version of the release (i.e., the name of the tag) |
37+
38+
# Developing
39+
40+
## Requirements
41+
42+
* Node.js 12
43+
44+
## Contributing
45+
46+
Before submitting a pull request, run `npm run all` to build, format, lint, test the actions. This will also compile
47+
each action into a single file which is required in order to run them in GitHub workflows. Failure to do so will result
48+
in pull request check failures.

actions/release/ReleaseGitHub.test.ts

+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { describe, expect, jest, test } from "@jest/globals";
2+
import ReleaseGitHub, { ReleaseGitHubParams } from "./ReleaseGitHub";
3+
import { Context } from "@actions/github/lib/context";
4+
import path from "path";
5+
6+
const mockGetReleaseByTag = jest.fn();
7+
const mockUpdateRelease = jest.fn();
8+
const mockCreateRelease = jest.fn();
9+
const mockListReleaseAssets = jest.fn();
10+
const mockDeleteReleaseAsset = jest.fn();
11+
const mockUploadReleaseAsset = jest.fn();
12+
13+
jest.mock("@octokit/rest", () => ({
14+
Octokit: jest.fn(() => ({
15+
repos: {
16+
getReleaseByTag: mockGetReleaseByTag,
17+
updateRelease: mockUpdateRelease,
18+
createRelease: mockCreateRelease,
19+
listReleaseAssets: mockListReleaseAssets,
20+
deleteReleaseAsset: mockDeleteReleaseAsset,
21+
uploadReleaseAsset: mockUploadReleaseAsset,
22+
},
23+
})),
24+
}));
25+
26+
process.chdir(path.join(__dirname, "fixtures"));
27+
28+
describe("ReleaseGitHub", () => {
29+
describe("run", () => {
30+
const release = new ReleaseGitHub(
31+
{
32+
repo: { owner: "twilio", repo: "twilio-BASIC" },
33+
ref: "refs/tags/2021.11.12",
34+
} as Context,
35+
{
36+
assets: ["CHANGELOG.md"],
37+
} as ReleaseGitHubParams
38+
);
39+
40+
test("fully creates a release", async () => {
41+
mockGetReleaseByTag.mockImplementation(() => {
42+
throw new Error("NOT FOUND");
43+
});
44+
mockCreateRelease.mockReturnValue({ data: { id: 123 } });
45+
mockListReleaseAssets.mockReturnValue({ data: [] });
46+
47+
await release.run();
48+
expect(mockGetReleaseByTag).toHaveBeenCalledTimes(1);
49+
expect(mockCreateRelease).toHaveBeenCalledTimes(1);
50+
expect(mockListReleaseAssets).toHaveBeenCalledTimes(1);
51+
expect(mockUploadReleaseAsset).toHaveBeenCalledTimes(1);
52+
});
53+
});
54+
55+
describe("release", () => {
56+
const release = new ReleaseGitHub(
57+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
58+
{} as ReleaseGitHubParams
59+
);
60+
61+
test("creates a new release", async () => {
62+
mockGetReleaseByTag.mockImplementation(() => {
63+
throw new Error("NOT FOUND");
64+
});
65+
mockCreateRelease.mockReturnValue({ data: { id: 123 } });
66+
67+
const releaseId = await release.release("1.2.3", "NOTES");
68+
expect(releaseId).toEqual(123);
69+
expect(mockGetReleaseByTag).toHaveBeenCalledTimes(1);
70+
expect(mockCreateRelease).toHaveBeenCalledTimes(1);
71+
expect(mockUpdateRelease).not.toHaveBeenCalled();
72+
73+
const getReleaseParams: any = mockGetReleaseByTag.mock.calls[0][0];
74+
expect(getReleaseParams.tag).toEqual("1.2.3");
75+
76+
const createReleaseParams: any = mockCreateRelease.mock.calls[0][0];
77+
expect(createReleaseParams.tag_name).toEqual("1.2.3");
78+
expect(createReleaseParams.name).toEqual("1.2.3");
79+
expect(createReleaseParams.body).toEqual("NOTES");
80+
});
81+
82+
test("updates an existing release", async () => {
83+
mockGetReleaseByTag.mockReturnValue({ data: { id: 123 } });
84+
mockUpdateRelease.mockReturnValue({ data: { id: 123 } });
85+
86+
const releaseId = await release.release("1.2.3", "NOTES");
87+
expect(releaseId).toEqual(123);
88+
expect(mockGetReleaseByTag).toHaveBeenCalledTimes(1);
89+
expect(mockUpdateRelease).toHaveBeenCalledTimes(1);
90+
expect(mockCreateRelease).not.toHaveBeenCalled();
91+
92+
const updateReleaseParams: any = mockUpdateRelease.mock.calls[0][0];
93+
expect(updateReleaseParams.release_id).toEqual(123);
94+
expect(updateReleaseParams.tag_name).toEqual("1.2.3");
95+
expect(updateReleaseParams.name).toEqual("1.2.3");
96+
expect(updateReleaseParams.body).toEqual("NOTES");
97+
});
98+
});
99+
100+
describe("uploadAssets", () => {
101+
const release = new ReleaseGitHub(
102+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
103+
{ assets: ["CHANGELOG.md"] } as ReleaseGitHubParams
104+
);
105+
106+
test("uploads a new asset", async () => {
107+
mockListReleaseAssets.mockReturnValue({ data: [] });
108+
109+
await release.uploadAssets(123);
110+
expect(mockListReleaseAssets).toHaveBeenCalledTimes(1);
111+
expect(mockUploadReleaseAsset).toHaveBeenCalledTimes(1);
112+
expect(mockDeleteReleaseAsset).not.toHaveBeenCalled();
113+
114+
const params: any = mockUploadReleaseAsset.mock.calls[0][0];
115+
expect(params.release_id).toEqual(123);
116+
expect(params.name).toEqual("CHANGELOG.md");
117+
});
118+
119+
test("deletes an existing asset", async () => {
120+
mockListReleaseAssets.mockReturnValue({
121+
data: [{ id: 456, name: "CHANGES.md" }],
122+
});
123+
124+
await release.uploadAssets(123);
125+
expect(mockListReleaseAssets).toHaveBeenCalledTimes(1);
126+
expect(mockDeleteReleaseAsset).toHaveBeenCalledTimes(1);
127+
expect(mockUploadReleaseAsset).toHaveBeenCalledTimes(1);
128+
129+
const params: any = mockDeleteReleaseAsset.mock.calls[0][0];
130+
expect(params.release_id).toEqual(123);
131+
expect(params.asset_id).toEqual(456);
132+
});
133+
});
134+
135+
describe("getVersion", () => {
136+
test("parses a tag ref properly", () => {
137+
const release = new ReleaseGitHub(
138+
{ ref: "refs/tags/1.2.3" } as Context,
139+
{} as ReleaseGitHubParams
140+
);
141+
142+
const version = release.getVersion();
143+
expect(version).toEqual("1.2.3");
144+
});
145+
146+
test("throws on branch refs", () => {
147+
const release = new ReleaseGitHub(
148+
{ ref: "refs/heads/main" } as Context,
149+
{} as ReleaseGitHubParams
150+
);
151+
152+
expect(() => release.getVersion()).toThrow("Invalid ref");
153+
});
154+
155+
test("throws on bad refs", () => {
156+
const release = new ReleaseGitHub(
157+
{ ref: "bad-ref" } as Context,
158+
{} as ReleaseGitHubParams
159+
);
160+
161+
expect(() => release.getVersion()).toThrow("Invalid ref");
162+
});
163+
});
164+
165+
describe("getReleaseNotes", () => {
166+
const release = new ReleaseGitHub(
167+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
168+
{} as ReleaseGitHubParams
169+
);
170+
171+
test("handles the newest release", () => {
172+
const releaseNotes = release.getReleaseNotes("2021.11.12");
173+
expect(releaseNotes).toContain("Added widget");
174+
expect(releaseNotes).toContain("Updated docs");
175+
expect(releaseNotes).toContain("twilio-BASIC/2021.11.12/index.html");
176+
expect(releaseNotes).not.toContain("Added new thing");
177+
expect(releaseNotes).not.toContain("2021.1.1");
178+
});
179+
180+
test("handles the oldest release", () => {
181+
const releaseNotes = release.getReleaseNotes("2021.1.1");
182+
expect(releaseNotes).toContain("Added new thing");
183+
expect(releaseNotes).toContain("Removed old thing");
184+
expect(releaseNotes).not.toContain("Updated docs");
185+
});
186+
187+
test("handles long (golang) versions", () => {
188+
const releaseNotes = release.getReleaseNotes("v2021.11.12");
189+
expect(releaseNotes).toContain("Release Notes");
190+
});
191+
192+
test("throws when the version is not found", () => {
193+
expect(() => release.getReleaseNotes("2021.10.11")).toThrow(
194+
"not found in changelog"
195+
);
196+
});
197+
});
198+
199+
describe("getChangelogLines", () => {
200+
test("errors when the changelog does not exist", () => {
201+
const release = new ReleaseGitHub(
202+
{} as Context,
203+
{
204+
changelogFilename: "CHANGES.md",
205+
} as ReleaseGitHubParams
206+
);
207+
expect(() => release.getChangelogLines()).toThrow("Failed to find");
208+
});
209+
});
210+
211+
describe("getFooter", () => {
212+
test("contains the Twilio footer for Twilio repos", () => {
213+
const release = new ReleaseGitHub(
214+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
215+
{} as ReleaseGitHubParams
216+
);
217+
const footer = release.getFooter("1.2.3");
218+
expect(footer).toHaveLength(1);
219+
expect(footer[0]).toContain("twilio-BASIC/1.2.3/index.html");
220+
});
221+
222+
test("uses a different footer for twilio-go", () => {
223+
const release = new ReleaseGitHub(
224+
{ repo: { owner: "twilio", repo: "twilio-go" } } as Context,
225+
{} as ReleaseGitHubParams
226+
);
227+
const footer = release.getFooter("1.2.3");
228+
expect(footer).toHaveLength(1);
229+
expect(footer[0]).toContain("github.com/twilio/[email protected]");
230+
});
231+
232+
test("excludes the Twilio footer for non-Twilio repos", () => {
233+
const release = new ReleaseGitHub(
234+
{ repo: { owner: "sendgrid", repo: "sendgrid-BASIC" } } as Context,
235+
{} as ReleaseGitHubParams
236+
);
237+
const footer = release.getFooter("1.2.3");
238+
expect(footer).toHaveLength(0);
239+
});
240+
241+
test("includes a custom footer", () => {
242+
const release = new ReleaseGitHub(
243+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
244+
{ customFooter: "this is just a test" } as ReleaseGitHubParams
245+
);
246+
const footer = release.getFooter("2021.11.12");
247+
expect(footer).toHaveLength(2);
248+
expect(footer[1]).toEqual("this is just a test");
249+
});
250+
251+
test("expands expected variables in custom footer", () => {
252+
const release = new ReleaseGitHub(
253+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
254+
{
255+
customFooter: "the version is ${version}, okay",
256+
} as ReleaseGitHubParams
257+
);
258+
const footer = release.getFooter("1.2.3");
259+
expect(footer).toHaveLength(2);
260+
expect(footer[1]).toEqual("the version is 1.2.3, okay");
261+
});
262+
263+
test("errors on unexpected variables in custom footer", () => {
264+
const release = new ReleaseGitHub(
265+
{ repo: { owner: "twilio", repo: "twilio-BASIC" } } as Context,
266+
{
267+
customFooter: "the version is ${blur-sion}, okay",
268+
} as ReleaseGitHubParams
269+
);
270+
expect(() => release.getFooter("1.2.3")).toThrow("Unexpected variable");
271+
});
272+
});
273+
});

0 commit comments

Comments
 (0)