Skip to content

Commit be9b6c4

Browse files
committed
updates
Signed-off-by: Chris Lyons <52037738+mephmanx@users.noreply.github.com>
1 parent 13e25a7 commit be9b6c4

4 files changed

Lines changed: 223 additions & 1 deletion

File tree

.github/workflows/publish.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ jobs:
3737
- name: Build
3838
run: npm run build
3939

40+
- name: Resolve Bundle URL
41+
id: bundle
42+
shell: bash
43+
run: |
44+
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
45+
TAG="${GITHUB_REF_NAME}"
46+
URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/example-mfe.js"
47+
echo "bundle_url=${URL}" >> "$GITHUB_OUTPUT"
48+
else
49+
echo "bundle_url=" >> "$GITHUB_OUTPUT"
50+
fi
51+
4052
- name: Upload Build Artifact
4153
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
4254
with:
@@ -48,6 +60,18 @@ jobs:
4860
directus/cms-module.seed.json
4961
directus/cms-block-module.props.example.json
5062
63+
- name: Sync Module To Directus
64+
if: ${{ secrets.DIRECTUS_BASE_URL != '' && secrets.DIRECTUS_STATIC_TOKEN != '' }}
65+
env:
66+
DIRECTUS_BASE_URL: ${{ secrets.DIRECTUS_BASE_URL }}
67+
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN }}
68+
MODULE_SITE_KEY: ${{ secrets.DIRECTUS_MODULE_SITE_KEY }}
69+
MODULE_STATUS: published
70+
MODULE_REFRESH_TARGET: both
71+
MFE_MODULE_VERSION: ${{ github.ref_name }}
72+
MFE_BUNDLE_URL: ${{ steps.bundle.outputs.bundle_url }}
73+
run: npm run sync:directus
74+
5175
- name: Create GitHub Release (tag pushes)
5276
if: startsWith(github.ref, 'refs/tags/')
5377
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,28 @@ If your auth provider returns `access_token` in URL hash (implicit flow), the pr
9696

9797
If those fields are omitted in `props_json`, the MFE falls back to build-time defaults from env.
9898

99+
## Automated Directus Sync (No Manual Collection Edits)
100+
101+
`publish.yml` now supports automatic `cms_modules` upsert in Directus:
102+
103+
1. Add repository secrets:
104+
- `DIRECTUS_BASE_URL` (example: `https://cms.example.com`)
105+
- `DIRECTUS_STATIC_TOKEN` (service token with write access to `cms_modules`)
106+
- optional: `DIRECTUS_MODULE_SITE_KEY` (leave unset for global module)
107+
2. Publish tag `v*` (or run Publish workflow manually).
108+
3. Workflow will:
109+
- build artifact
110+
- compute release bundle URL for tag builds
111+
- run `npm run sync:directus` to upsert module record from `directus/cms-module.seed.json`
112+
- attach release artifacts
113+
114+
The sync script stores publish metadata in:
115+
116+
- `default_props.__mfe_release.bundleUrl`
117+
- `default_props.__mfe_release.moduleVersion`
118+
- `default_props.__mfe_release.releaseTag`
119+
- `default_props.__mfe_release.releaseSha`
120+
99121
## Important Runtime Note
100122

101123
This repo provides the MFE contract + bundle. Your shell runtime must include or load this module definition at runtime.
@@ -115,6 +137,7 @@ The bundle also self-registers at:
115137
- `npm run typecheck` - TS type check
116138
- `npm run build` - compile single JS + copy module definition
117139
- `npm run dev` - local preview server with live rebuild + preview harness
140+
- `npm run sync:directus` - upsert `cms_modules` record in Directus from seed file
118141

119142
## GitHub Actions
120143

@@ -134,3 +157,6 @@ Secrets expected by publish workflow:
134157
- `MFE_DEFAULT_GRAPHQL_HTTP_URL`
135158
- `MFE_DEFAULT_GRAPHQL_WS_URL`
136159
- `MFE_DEFAULT_GRAPHQL_AUTH_TOKEN`
160+
- `DIRECTUS_BASE_URL`
161+
- `DIRECTUS_STATIC_TOKEN`
162+
- `DIRECTUS_MODULE_SITE_KEY` (optional)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"build:bundle": "node scripts/build.mjs",
1313
"build:definition": "mkdir -p dist && cp module.definition.json dist/module.definition.json",
1414
"build": "npm run clean && npm run typecheck && npm run build:bundle && npm run build:definition",
15-
"dev": "npm run clean:dev && node scripts/dev.mjs"
15+
"dev": "npm run clean:dev && node scripts/dev.mjs",
16+
"sync:directus": "node scripts/sync-directus-module.mjs"
1617
},
1718
"devDependencies": {
1819
"dotenv": "^16.6.1",

scripts/sync-directus-module.mjs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
4+
function asString(value, fallback = "") {
5+
if (typeof value === "string") {
6+
return value.trim();
7+
}
8+
if (typeof value === "number") {
9+
return String(value);
10+
}
11+
return fallback;
12+
}
13+
14+
function isNullish(value) {
15+
return value === undefined || value === null;
16+
}
17+
18+
function stableJson(value) {
19+
return JSON.stringify(value);
20+
}
21+
22+
async function loadSeedFile() {
23+
const seedPath = path.resolve(process.cwd(), "directus/cms-module.seed.json");
24+
const content = await fs.readFile(seedPath, "utf8");
25+
return JSON.parse(content);
26+
}
27+
28+
function buildPayload(seed, env) {
29+
const payload = { ...seed };
30+
31+
if (env.MODULE_STATUS) {
32+
payload.status = asString(env.MODULE_STATUS, payload.status || "published");
33+
}
34+
if (env.MODULE_REFRESH_TARGET) {
35+
payload.refresh_target = asString(
36+
env.MODULE_REFRESH_TARGET,
37+
payload.refresh_target || "both",
38+
);
39+
}
40+
if (Object.prototype.hasOwnProperty.call(env, "MODULE_SITE_KEY")) {
41+
const siteKey = asString(env.MODULE_SITE_KEY);
42+
payload.site_key = siteKey || null;
43+
}
44+
45+
const bundleUrl = asString(env.MFE_BUNDLE_URL);
46+
const moduleVersion = asString(env.MFE_MODULE_VERSION || env.GITHUB_REF_NAME || "");
47+
const releaseTag = asString(env.GITHUB_REF_NAME || "");
48+
const releaseSha = asString(env.GITHUB_SHA || "");
49+
50+
const defaultProps =
51+
payload.default_props && typeof payload.default_props === "object"
52+
? { ...payload.default_props }
53+
: {};
54+
55+
const releaseMeta = {
56+
bundleUrl,
57+
moduleVersion,
58+
releaseTag,
59+
releaseSha,
60+
publishedAt: new Date().toISOString(),
61+
};
62+
63+
// Keep runtime metadata under a reserved key.
64+
defaultProps.__mfe_release = releaseMeta;
65+
payload.default_props = defaultProps;
66+
67+
return payload;
68+
}
69+
70+
async function directusRequest(baseUrl, token, method, resourcePath, body) {
71+
const response = await fetch(`${baseUrl.replace(/\/+$/g, "")}${resourcePath}`, {
72+
method,
73+
headers: {
74+
Authorization: `Bearer ${token}`,
75+
"Content-Type": "application/json",
76+
},
77+
body: body ? stableJson(body) : undefined,
78+
});
79+
80+
if (!response.ok) {
81+
const text = await response.text();
82+
throw new Error(
83+
`Directus ${method} ${resourcePath} failed (${response.status}): ${text || "empty response"}`,
84+
);
85+
}
86+
87+
const text = await response.text();
88+
if (!text) {
89+
return {};
90+
}
91+
try {
92+
return JSON.parse(text);
93+
} catch {
94+
return { raw: text };
95+
}
96+
}
97+
98+
async function findExistingModule(baseUrl, token, moduleKey, siteKey) {
99+
const query = new URLSearchParams();
100+
query.set("fields", "id,module_key,site_key,status,refresh_target");
101+
query.set("limit", "1");
102+
query.set("filter[module_key][_eq]", moduleKey);
103+
if (isNullish(siteKey) || siteKey === "") {
104+
query.set("filter[site_key][_null]", "true");
105+
} else {
106+
query.set("filter[site_key][_eq]", String(siteKey));
107+
}
108+
109+
const response = await directusRequest(
110+
baseUrl,
111+
token,
112+
"GET",
113+
`/items/cms_modules?${query.toString()}`,
114+
);
115+
const rows = Array.isArray(response?.data) ? response.data : [];
116+
return rows[0] ?? null;
117+
}
118+
119+
async function upsertModule(baseUrl, token, payload) {
120+
const moduleKey = asString(payload.module_key);
121+
if (!moduleKey) {
122+
throw new Error("Seed payload is missing module_key.");
123+
}
124+
125+
const existing = await findExistingModule(baseUrl, token, moduleKey, payload.site_key);
126+
if (existing?.id) {
127+
const updated = await directusRequest(
128+
baseUrl,
129+
token,
130+
"PATCH",
131+
`/items/cms_modules/${encodeURIComponent(existing.id)}`,
132+
payload,
133+
);
134+
return { action: "updated", id: existing.id, data: updated?.data ?? null };
135+
}
136+
137+
const created = await directusRequest(baseUrl, token, "POST", "/items/cms_modules", payload);
138+
return { action: "created", id: created?.data?.id ?? null, data: created?.data ?? null };
139+
}
140+
141+
async function main() {
142+
const baseUrl = asString(process.env.DIRECTUS_BASE_URL);
143+
const token = asString(process.env.DIRECTUS_STATIC_TOKEN);
144+
if (!baseUrl || !token) {
145+
throw new Error(
146+
"DIRECTUS_BASE_URL and DIRECTUS_STATIC_TOKEN are required for Directus sync.",
147+
);
148+
}
149+
150+
const seed = await loadSeedFile();
151+
const payload = buildPayload(seed, process.env);
152+
const result = await upsertModule(baseUrl, token, payload);
153+
154+
const summary = {
155+
action: result.action,
156+
id: result.id,
157+
module_key: payload.module_key,
158+
site_key: payload.site_key ?? null,
159+
status: payload.status,
160+
refresh_target: payload.refresh_target,
161+
bundle_url: payload?.default_props?.__mfe_release?.bundleUrl ?? "",
162+
module_version: payload?.default_props?.__mfe_release?.moduleVersion ?? "",
163+
};
164+
165+
console.log("[sync-directus-module] success", stableJson(summary));
166+
}
167+
168+
main().catch((error) => {
169+
console.error("[sync-directus-module] failed:", error instanceof Error ? error.message : error);
170+
process.exit(1);
171+
});

0 commit comments

Comments
 (0)