backend-manager 5.10.1 → 5.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -1
- package/docs/admin-post-route.md +8 -5
- package/docs/cdp-debugging.md +15 -30
- package/docs/ghostii.md +18 -11
- package/docs/marketing-campaigns.md +11 -7
- package/package.json +1 -1
- package/src/cli/index.js +7 -1
- package/src/defaults/CLAUDE.md +2 -0
- package/src/manager/events/cron/daily/blog-auto-publisher.js +61 -150
- package/src/manager/libraries/content/source-resolver.js +296 -109
- package/src/manager/libraries/email/generators/lib/image-host.js +66 -39
- package/src/manager/libraries/email/generators/newsletter.js +240 -109
- package/src/manager/routes/admin/post/post.js +69 -1
- package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
- package/src/test/fixtures/firebase-project/database-debug.log +8 -8
- package/src/test/fixtures/firebase-project/firestore-debug.log +55 -57
- package/src/test/fixtures/firebase-project/pubsub-debug.log +3 -3
- package/templates/backend-manager-config.json +1 -0
- package/test/email/newsletter-generate.js +4 -5
- package/test/helpers/content/blog-auto-publisher.js +116 -132
- package/test/helpers/content/ghostii-feed-integration.js +2 -2
- package/test/routes/admin/post-convert-image.js +158 -0
- package/test/routes/admin/post-download-error.js +89 -0
|
@@ -31,6 +31,12 @@ const { Octokit } = require('@octokit/rest');
|
|
|
31
31
|
const REPO_OWNER = 'itw-creative-works';
|
|
32
32
|
const REPO_NAME = 'newsletter-assets';
|
|
33
33
|
|
|
34
|
+
// When true, asset URLs use the parent CDN (cdn.{parentDomain}/newsletters/...)
|
|
35
|
+
// instead of raw.githubusercontent.com. Requires a Cloudflare redirect rule
|
|
36
|
+
// on the CDN domain that maps /newsletters/* to raw.githubusercontent.com.
|
|
37
|
+
// Set to false to revert to direct GitHub URLs.
|
|
38
|
+
const USE_CDN_URLS = true;
|
|
39
|
+
|
|
34
40
|
const PAGES_BASE = `https://${REPO_OWNER}.github.io/${REPO_NAME}`;
|
|
35
41
|
|
|
36
42
|
// Fallback RAW_BASE for callers that don't have a brand context
|
|
@@ -65,11 +71,14 @@ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
|
65
71
|
* @param {string} [args.subject] - Newsletter subject. Embedded in the commit message so
|
|
66
72
|
* `git log` reads as a human-browseable history.
|
|
67
73
|
* @param {string} [args.commitMessage] - Full override of the default commit message
|
|
74
|
+
* @param {string} [args.cdnBase] - CDN base URL (e.g. 'https://cdn.itwcreativeworks.com/newsletters').
|
|
75
|
+
* When provided and USE_CDN_URLS is true, asset URLs use this
|
|
76
|
+
* instead of raw.githubusercontent.com.
|
|
68
77
|
* @param {string} [args.token] - GitHub token (defaults to process.env.GH_TOKEN)
|
|
69
78
|
* @param {object} [args.assistant] - logger
|
|
70
79
|
* @returns {Promise<{ urls: string[], paths: string[], htmlUrl?: string, htmlPath?: string, previewUrl?: string, folderUrl: string, commitSha: string }>}
|
|
71
80
|
*/
|
|
72
|
-
async function uploadAssets({ images, html, markdown, summary, brandId, campaignId, subject, commitMessage, token, assistant }) {
|
|
81
|
+
async function uploadAssets({ images, html, markdown, summary, brandId, campaignId, subject, commitMessage, cdnBase, token, assistant }) {
|
|
73
82
|
const hasImages = Array.isArray(images) && images.length > 0;
|
|
74
83
|
const hasHtml = typeof html === 'string' && html.length > 0;
|
|
75
84
|
const hasMarkdown = typeof markdown === 'string' && markdown.length > 0;
|
|
@@ -92,7 +101,11 @@ async function uploadAssets({ images, html, markdown, summary, brandId, campaign
|
|
|
92
101
|
|
|
93
102
|
// Brand branch — each brand gets its own branch, zero cross-brand contention
|
|
94
103
|
const branch = brandId;
|
|
95
|
-
const
|
|
104
|
+
const gitRawBase = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${branch}`;
|
|
105
|
+
|
|
106
|
+
// CDN URLs: cdn.{parentDomain}/newsletters/{brandId}/content/{campaignId}/...
|
|
107
|
+
// The CDN redirect maps /newsletters/{brandId}/* → raw.githubusercontent.com/{brandId}/*
|
|
108
|
+
const assetBase = (USE_CDN_URLS && cdnBase) ? `${cdnBase}/${brandId}` : gitRawBase;
|
|
96
109
|
|
|
97
110
|
// 1. Build the list of files to commit. Validate each before we touch GitHub.
|
|
98
111
|
const files = [];
|
|
@@ -225,42 +238,55 @@ async function uploadAssets({ images, html, markdown, summary, brandId, campaign
|
|
|
225
238
|
});
|
|
226
239
|
}
|
|
227
240
|
|
|
228
|
-
// 4.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
commit_sha: branchSha,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const { data: newTree } = await octokit.rest.git.createTree({
|
|
236
|
-
owner: REPO_OWNER,
|
|
237
|
-
repo: REPO_NAME,
|
|
238
|
-
base_tree: baseCommit.tree.sha,
|
|
239
|
-
tree: treeItems,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// 5. Commit
|
|
241
|
+
// 4. Commit + push with retry. The newsletter pipeline calls uploadAssets
|
|
242
|
+
// twice sequentially (images then HTML). GitHub's API can return a stale
|
|
243
|
+
// ref on the second getRef if the first updateRef hasn't fully propagated.
|
|
244
|
+
// Retry with a fresh ref read + jitter handles this.
|
|
243
245
|
const defaultSubject = subject ? subject.trim() : `${files.length} newsletter asset${files.length === 1 ? '' : 's'}`;
|
|
244
246
|
const message = commitMessage || `[${brandId}] ${campaignId} — ${defaultSubject}`;
|
|
247
|
+
const MAX_RETRIES = 3;
|
|
248
|
+
let newCommit;
|
|
249
|
+
|
|
250
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
251
|
+
const { data: refData } = await octokit.rest.git.getRef({
|
|
252
|
+
owner: REPO_OWNER, repo: REPO_NAME, ref: `heads/${branch}`,
|
|
253
|
+
});
|
|
254
|
+
const tipSha = refData.object.sha;
|
|
255
|
+
|
|
256
|
+
const { data: baseCommit } = await octokit.rest.git.getCommit({
|
|
257
|
+
owner: REPO_OWNER, repo: REPO_NAME, commit_sha: tipSha,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const { data: newTree } = await octokit.rest.git.createTree({
|
|
261
|
+
owner: REPO_OWNER, repo: REPO_NAME,
|
|
262
|
+
base_tree: baseCommit.tree.sha,
|
|
263
|
+
tree: treeItems,
|
|
264
|
+
});
|
|
245
265
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
266
|
+
newCommit = (await octokit.rest.git.createCommit({
|
|
267
|
+
owner: REPO_OWNER, repo: REPO_NAME,
|
|
268
|
+
message,
|
|
269
|
+
tree: newTree.sha,
|
|
270
|
+
parents: [tipSha],
|
|
271
|
+
})).data;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await octokit.rest.git.updateRef({
|
|
275
|
+
owner: REPO_OWNER, repo: REPO_NAME,
|
|
276
|
+
ref: `heads/${branch}`,
|
|
277
|
+
sha: newCommit.sha,
|
|
278
|
+
});
|
|
279
|
+
break;
|
|
280
|
+
} catch (e) {
|
|
281
|
+
if (attempt < MAX_RETRIES && /fast.forward/i.test(e.message)) {
|
|
282
|
+
const jitter = 500 + Math.floor(Math.random() * 1500);
|
|
283
|
+
log(`updateRef race (attempt ${attempt}/${MAX_RETRIES}), retrying in ${jitter}ms...`);
|
|
284
|
+
await new Promise(r => setTimeout(r, jitter));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
264
290
|
|
|
265
291
|
// 7. Split the URL list by kind so callers can grab each independently.
|
|
266
292
|
const imageFiles = files.filter((f) => f.kind === 'image');
|
|
@@ -269,25 +295,25 @@ async function uploadAssets({ images, html, markdown, summary, brandId, campaign
|
|
|
269
295
|
const summaryFile = files.find((f) => f.kind === 'summary');
|
|
270
296
|
|
|
271
297
|
const result = {
|
|
272
|
-
urls: imageFiles.map((f) => `${
|
|
298
|
+
urls: imageFiles.map((f) => `${assetBase}/${f.path}`),
|
|
273
299
|
paths: imageFiles.map((f) => f.path),
|
|
274
300
|
folderUrl: `https://github.com/${REPO_OWNER}/${REPO_NAME}/tree/${branch}/${CONTENT_DIR}/${campaignId}`,
|
|
275
301
|
commitSha: newCommit.sha,
|
|
276
302
|
};
|
|
277
303
|
|
|
278
304
|
if (htmlFile) {
|
|
279
|
-
result.htmlUrl = `${
|
|
305
|
+
result.htmlUrl = `${assetBase}/${htmlFile.path}`;
|
|
280
306
|
result.htmlPath = htmlFile.path;
|
|
281
307
|
result.previewUrl = `${PAGES_BASE}/?path=${encodeURIComponent(htmlFile.path)}&branch=${encodeURIComponent(branch)}`;
|
|
282
308
|
}
|
|
283
309
|
|
|
284
310
|
if (markdownFile) {
|
|
285
|
-
result.markdownUrl = `${
|
|
311
|
+
result.markdownUrl = `${assetBase}/${markdownFile.path}`;
|
|
286
312
|
result.markdownPath = markdownFile.path;
|
|
287
313
|
}
|
|
288
314
|
|
|
289
315
|
if (summaryFile) {
|
|
290
|
-
result.summaryUrl = `${
|
|
316
|
+
result.summaryUrl = `${assetBase}/${summaryFile.path}`;
|
|
291
317
|
result.summaryPath = summaryFile.path;
|
|
292
318
|
}
|
|
293
319
|
|
|
@@ -310,6 +336,7 @@ function validateCampaignId(campaignId) {
|
|
|
310
336
|
|
|
311
337
|
module.exports = {
|
|
312
338
|
uploadAssets,
|
|
339
|
+
USE_CDN_URLS,
|
|
313
340
|
REPO_OWNER,
|
|
314
341
|
REPO_NAME,
|
|
315
342
|
RAW_BASE,
|