Skip to content

Commit 6630081

Browse files
committed
Workflow and Action to create newsletter
Workflow fix Newsletter test Newsletter test, mark 2 Workflow tweak
1 parent c5bb307 commit 6630081

File tree

14 files changed

+1533
-1
lines changed

14 files changed

+1533
-1
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: render-newsletter
2+
description: Render the newsletter
3+
inputs:
4+
text_path:
5+
description: 'Path to Markdown file containing the post body'
6+
required: true
7+
out_path:
8+
description: 'Path where the output should be written'
9+
required: false
10+
11+
sendgrid_api_key:
12+
description: 'Sendgrid API Key'
13+
required: false
14+
sendgrid_list_id:
15+
description: 'SendGrid contacts list to send the newsletter to'
16+
required: false
17+
suppression_group_id:
18+
description: ''
19+
required: false
20+
template_path:
21+
description: ''
22+
required: false
23+
site_yaml:
24+
description: 'Path to YAML file with the properties for "site" context variable'
25+
required: false
26+
27+
context:
28+
description: 'JSON-encoded context to include when rendering newsletter'
29+
required: false
30+
31+
runs:
32+
using: 'node12'
33+
main: 'dist/index.js'

.github/actions/render-newsletter/dist/index.js

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "render-newsletter",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"build": "ncc build src/index.ts -o dist -m"
9+
},
10+
"author": "",
11+
"license": "ISC",
12+
"devDependencies": {
13+
"@types/js-yaml": "3",
14+
"@types/marked": "^3.0.2",
15+
"@types/node-fetch": "^2.5.12",
16+
"@vercel/ncc": "^0.31.1",
17+
"typescript": "^4.4.4"
18+
},
19+
"dependencies": {
20+
"@types/node": "12",
21+
"gray-matter": "^4.0.3",
22+
"handlebars": "^4.7.7",
23+
"js-yaml": "^3.13.1",
24+
"marked": "3.0.2",
25+
"node-fetch": "^2.6.6"
26+
}
27+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { promisify } from 'util';
4+
import * as gm from 'gray-matter';
5+
import * as hb from 'handlebars';
6+
import * as marked from 'marked';
7+
import * as yaml from 'js-yaml';
8+
import fetch from 'node-fetch';
9+
10+
const readFile = promisify(fs.readFile);
11+
const writeFile = promisify(fs.writeFile);
12+
13+
14+
const API_BASE = 'https://api.sendgrid.com/v3';
15+
type SingleSendParams = {
16+
html: string,
17+
listId: string,
18+
suppressionGroup: number,
19+
token: string,
20+
sendAt?: Date,
21+
subject: string,
22+
};
23+
async function singleSend(params: SingleSendParams) {
24+
return await fetch(`${API_BASE}/marketing/singlesends`, {
25+
method: 'POST',
26+
headers: {
27+
'Authorization': `Bearer ${params.token}`,
28+
'Content-type': 'application/json',
29+
},
30+
body: JSON.stringify({
31+
name: 'Test Send',
32+
send_at: params.sendAt?.toISOString(),
33+
send_to: {
34+
list_ids: [params.listId]
35+
},
36+
email_config: {
37+
subject: params.subject,
38+
html_content: params.html,
39+
suppression_group_id: params.suppressionGroup
40+
}
41+
})
42+
});
43+
}
44+
45+
46+
type Options = {
47+
apiKey?: string,
48+
path: string,
49+
output?: string,
50+
template?: string,
51+
context?: any,
52+
listId?: string,
53+
suppressionGroupId?: number,
54+
siteYaml?: string,
55+
};
56+
57+
async function loadTemplate(path?: string, options?: CompileOptions) {
58+
const data = path ? await readFile(path) : '{{{ content }}}';
59+
return hb.compile(data.toString(), options);
60+
}
61+
62+
function splitTitleFromName(basename: string) {
63+
const m = basename.match(/^([^.]*)/);
64+
65+
return [m[0], basename.slice(m[0].length)];
66+
}
67+
68+
function contextFromPath(filepath: string) {
69+
const basename = path.basename(filepath);
70+
const [title, ext] = splitTitleFromName(basename);
71+
const m = title.match(/(\d{4})-(\d\d?)-(\d\d?)-/);
72+
73+
const slug = m ? title.slice(m.index + m[0].length) : title;
74+
const date = m ?
75+
new Date(parseInt(m[1]), parseInt(m[2])-1, parseInt(m[3]))
76+
: new Date();
77+
78+
return {
79+
title,
80+
slug,
81+
ext,
82+
basename,
83+
date,
84+
year: date.getFullYear(),
85+
month: date.getMonth()+1,
86+
day: date.getDate(),
87+
};
88+
}
89+
90+
function postUrl(post: ReturnType<typeof contextFromPath>, site: any) {
91+
const siteUrl = site.url;
92+
const basePath = site.baseurl ?? '';
93+
94+
return `${siteUrl}${basePath}/${post.year}/${post.month}/${post.day}/${post.slug}.html`;
95+
}
96+
97+
function postContext(data: any, path: string, site?: any) {
98+
const post = contextFromPath(path);
99+
100+
return Object.assign({ url: site ? postUrl(post, site) : '' },
101+
post, data);
102+
}
103+
104+
async function siteContext(path?: string) {
105+
if (!path)
106+
return {};
107+
108+
const contents = await readFile(path);
109+
return await yaml.load(contents.toString());
110+
}
111+
112+
async function render(opts: Options) {
113+
const pFile = readFile(opts.path);
114+
const pTemplate = loadTemplate(opts.template);
115+
const site = await siteContext(opts.siteYaml);
116+
117+
// if ()
118+
119+
const raw = await pFile;
120+
const { content, data } = gm(raw.toString('utf8'));
121+
122+
const rendered = marked(content, {
123+
headerPrefix: 'heading-',
124+
gfm: true,
125+
});
126+
127+
const template = await pTemplate;
128+
const context = Object.assign({
129+
content: rendered,
130+
post: postContext(data, opts.path, site),
131+
site,
132+
}, opts.context);
133+
const text = template(context);
134+
135+
return {
136+
text,
137+
context
138+
};
139+
}
140+
141+
async function run(options: Options) {
142+
const { text, context } = await render(options);
143+
// console.log(context);
144+
145+
if (options.output) {
146+
await writeFile(options.output, text);
147+
} else if (options.apiKey) {
148+
const response = await singleSend({
149+
html: text,
150+
listId: options.listId,
151+
suppressionGroup: options.suppressionGroupId,
152+
token: options.apiKey,
153+
sendAt: new Date(Date.now() + 60000),
154+
subject: `Test Newsletter: ${context.post.title}`,
155+
});
156+
157+
console.log(response.status, response.statusText, response.headers, await response.text());
158+
} else {
159+
console.log(text);
160+
}
161+
}
162+
163+
async function runAction() {
164+
const {
165+
INPUT_SENDGRID_LIST_ID: listId,
166+
INPUT_SENDGRID_API_KEY: apiKey,
167+
INPUT_TEMPLATE_PATH: template,
168+
INPUT_TEXT_PATH: path,
169+
INPUT_CONTEXT: context,
170+
INPUT_OUT_PATH: outPath,
171+
INPUT_SUPPRESSION_GROUP_ID: suppressionGroupId,
172+
INPUT_SITE_YAML: siteYaml,
173+
} = process.env;
174+
175+
if (!path) {
176+
console.error(
177+
'Missing required environment variable INPUT_TEXT_PATH'
178+
);
179+
process.exit(1);
180+
}
181+
182+
await run({
183+
apiKey,
184+
path,
185+
template,
186+
output: outPath,
187+
context: context ? JSON.parse(context) : {},
188+
listId: listId,
189+
suppressionGroupId: suppressionGroupId ? parseInt(suppressionGroupId) : undefined,
190+
siteYaml,
191+
});
192+
}
193+
194+
async function testRun() {
195+
process.env['INPUT_SENDGRID_LIST_ID'] = "559adb5e-7164-4ac8-bbb5-1398d4ff0df9";
196+
// process.env['INPUT_SENDGRID_API_KEY'] = apiKey;
197+
process.env['INPUT_TEXT_PATH'] = __dirname + '/../../../../_posts/2021-11-16-communications-lead.md';
198+
process.env['INPUT_TEMPLATE_PATH'] = __dirname + '/../../../workflows/newsletter_template.html';
199+
process.env['INPUT_CONTEXT'] = `{}`;
200+
process.env['INPUT_SUPPRESSION_GROUP_ID'] = '17889';
201+
process.env['INPUT_SITE_YAML'] = __dirname + '/../../../../_config.yml';
202+
203+
await runAction();
204+
}
205+
206+
runAction();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["es2015"],
4+
"target": "es2015",
5+
"moduleResolution": "node"
6+
}
7+
}

0 commit comments

Comments
 (0)