Skip to content

Commit c688040

Browse files
committed
feat: implement mergeCampaignBundleAdditively function and update campaign promos
1 parent 332a0aa commit c688040

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

‎scripts/pull-campaigns.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
extractPromoTable,
77
formatReport,
88
mapRowToPromo,
9+
mergeCampaignBundleAdditively,
910
parseCliArgs,
1011
parseConfluencePageReference,
1112
renderCampaignModule,
@@ -222,6 +223,45 @@ describe("buildCampaignBundle", () => {
222223
});
223224
});
224225

226+
describe("mergeCampaignBundleAdditively", () => {
227+
test("keeps existing promos and applies fresh updates", () => {
228+
const merged = mergeCampaignBundleAdditively(
229+
{
230+
bannerPromos: {
231+
legacy: {
232+
type: "banner",
233+
isActive: true,
234+
message: "Legacy promo",
235+
cta: { text: "Open", link: "https://example.com/legacy" },
236+
},
237+
},
238+
videoPromos: {},
239+
},
240+
{
241+
bannerPromos: {
242+
denoiser: {
243+
type: "banner",
244+
isActive: true,
245+
message: "Fresh promo",
246+
cta: { text: "Open", link: "https://example.com/fresh" },
247+
},
248+
},
249+
videoPromos: {},
250+
summary: "ignored",
251+
ignoredEntries: ["past-row"],
252+
},
253+
"2026-06-09",
254+
);
255+
256+
expect(Object.keys(merged.bannerPromos).sort()).toEqual([
257+
"denoiser",
258+
"legacy",
259+
]);
260+
expect(merged.summary).toContain("after additive merge");
261+
expect(merged.ignoredEntries).toEqual(["past-row"]);
262+
});
263+
});
264+
225265
describe("assertUrlsFromSource", () => {
226266
const sourceUrls = ["https://www.musehub.com/plugin/denoiser?x=1"];
227267

‎scripts/pull-campaigns.ts‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { execFile } from "node:child_process";
22
import { mkdir, writeFile } from "node:fs/promises";
33
import { dirname, resolve } from "node:path";
4+
import { pathToFileURL } from "node:url";
45
import { promisify } from "node:util";
56
import type { PromoData } from "../src/assets/data/promos/types";
67

@@ -105,6 +106,11 @@ export type CampaignBundle = {
105106
ignoredEntries: string[];
106107
};
107108

109+
type ExistingCampaignPromos = {
110+
bannerPromos: Record<string, PromoData>;
111+
videoPromos: Record<string, PromoData>;
112+
};
113+
108114
type ClaudeCliConfig = { command: string; model?: string };
109115
type ApiConfig = { apiKey: string; apiUrl: string; model: string };
110116
type LlmBackend =
@@ -572,6 +578,48 @@ export function buildCampaignBundle(
572578
return { bannerPromos, videoPromos, summary, ignoredEntries };
573579
}
574580

581+
export function mergeCampaignBundleAdditively(
582+
existing: ExistingCampaignPromos,
583+
fresh: CampaignBundle,
584+
today = new Date().toISOString().slice(0, 10),
585+
): CampaignBundle {
586+
const bannerPromos = {
587+
...existing.bannerPromos,
588+
...fresh.bannerPromos,
589+
};
590+
const videoPromos = {
591+
...existing.videoPromos,
592+
...fresh.videoPromos,
593+
};
594+
const summary = `${Object.keys(bannerPromos).length} banner + ${Object.keys(videoPromos).length} video promo(s) after additive merge as of ${today}`;
595+
596+
return {
597+
bannerPromos,
598+
videoPromos,
599+
summary,
600+
ignoredEntries: fresh.ignoredEntries,
601+
};
602+
}
603+
604+
async function loadExistingCampaignPromos(
605+
outputPath: string,
606+
): Promise<ExistingCampaignPromos> {
607+
try {
608+
const moduleHref = `${pathToFileURL(outputPath).href}?t=${Date.now()}`;
609+
const imported = (await import(moduleHref)) as {
610+
campaignBannerPromos?: Record<string, PromoData>;
611+
campaignVideoPromos?: Record<string, PromoData>;
612+
};
613+
614+
return {
615+
bannerPromos: imported.campaignBannerPromos ?? {},
616+
videoPromos: imported.campaignVideoPromos ?? {},
617+
};
618+
} catch {
619+
return { bannerPromos: {}, videoPromos: {} };
620+
}
621+
}
622+
575623
// ---------------------------------------------------------------------------
576624
// URL integrity
577625
// ---------------------------------------------------------------------------
@@ -845,7 +893,9 @@ export async function runPullCampaigns(
845893
}
846894

847895
const timeline = resolveTimeline(rows);
848-
const bundle = buildCampaignBundle(rows, timeline);
896+
const freshBundle = buildCampaignBundle(rows, timeline);
897+
const existingPromos = await loadExistingCampaignPromos(options.outputPath);
898+
const bundle = mergeCampaignBundleAdditively(existingPromos, freshBundle);
849899

850900
if (options.cleanCopy) {
851901
const backend = resolveLlmBackend(env);

‎src/assets/data/promos/campaigns.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
import type { PromoData } from "./types";
33

44
export const campaignBannerPromos: Record<string, PromoData> = {
5+
"80sDreamsBundle": {
6+
type: "banner",
7+
isActive: true,
8+
priority: 50,
9+
osTargets: ["Windows", "OS X"],
10+
startDate: "2026-06-11",
11+
endDate: "2026-06-25",
12+
message:
13+
"80s Dreams Bundle: The reverb and synth duo for retro-inspired sound. Create lush ambience and vintage tones.",
14+
tracking: {
15+
category: "Promo CTA",
16+
action: "Promo CTA button",
17+
name: "80s Dreams Bundle MuseHub",
18+
},
19+
cta: {
20+
text: "Get it on MuseHub",
21+
link: "https://www.musehub.com/bundle/80s-dreams-bundle?utm_source=au-web-banner&utm_medium=au-banner",
22+
},
23+
},
524
denoiser: {
625
type: "banner",
726
isActive: true,

0 commit comments

Comments
 (0)