@mokoconsulting/mcp-mokosuite 1.1.0-dev.372 → 1.1.0

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.
@@ -6,7 +6,8 @@
6
6
  # (moko lint|build|test), then validates that compiled output exists and that
7
7
  # tools are properly registered. The build agent is platform-aware via the
8
8
  # repo's NATIVE manifest (package.json) — no metadata API call — so inline
9
- # npm install / npx tsc / npm run build / npm test are no longer needed here.
9
+ # Deps are installed via `npm ci` below so `moko lint|build|test` (which run
10
+ # `npx --no-install tsc`/eslint) can find typescript/eslint from devDependencies.
10
11
  #
11
12
  # GATE: `moko` is provided by mokocli on the runner PATH (/opt/mokocli/cli).
12
13
  # It becomes available only after the mokocli build agent lands on `main` and
@@ -21,12 +22,14 @@ on:
21
22
  paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json']
22
23
  pull_request:
23
24
  branches: [main]
24
- paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json']
25
+ # No paths filter: the folded-in universal pr-check jobs (Branch Policy,
26
+ # Require Docs, Wiki Reminder) must run on EVERY PR.
25
27
 
26
28
 
27
29
  permissions:
28
30
  contents: read
29
31
  pull-requests: write
32
+ issues: write # folded-in Wiki Reminder posts a sticky PR comment
30
33
 
31
34
  env:
32
35
  MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
@@ -56,6 +59,10 @@ jobs:
56
59
  with:
57
60
  node-version: ${{ matrix.node-version }}
58
61
 
62
+ - name: Install npm dependencies
63
+ run: |
64
+ if [ -f package-lock.json ]; then npm ci; else npm install --no-audit --no-fund; fi
65
+
59
66
  - name: Setup MokoCLI build agent (moko on PATH)
60
67
  env:
61
68
  MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
@@ -128,40 +135,315 @@ jobs:
128
135
  exit 1
129
136
  fi
130
137
 
131
- ci-summary:
132
- name: CI Summary (sticky comment)
138
+ # NOTE: a standalone `ci-summary` job (needs: [build], if: always()) was removed.
139
+ # This Gitea act_runner never schedules a `needs:`+`if: always()` job — it parks in
140
+ # "waiting" (runner_id 0) forever and never dispatches. Under the org-governance
141
+ # `main` rule (status_check_contexts: ["*"], every check must be green) that stranded
142
+ # job was a permanent, silent block on every PR into main. The per-job status is
143
+ # already visible on the Actions run page and `wiki-reminder` posts a sticky PR
144
+ # comment on main PRs, so a separate summary job earned its keep only when it worked —
145
+ # which, on this runner, was never. If a build-result summary comment is wanted back,
146
+ # add it as a final STEP with `if: ${{ always() }}` inside the `build` job (step-level
147
+ # always() IS supported), not as a dependent job. (Matches ci-go.yml.)
148
+
149
+ # ══════════════════════════════════════════════════════════════════════════
150
+ # Universal PR-check jobs — folded in from "Universal: PR Check" so the NPM
151
+ # platform runs one CI workflow per PR (mirrors Template-Joomla #162).
152
+ # Branch-gated: Branch Policy on all PRs; docs gate + wiki reminder on main.
153
+ # ══════════════════════════════════════════════════════════════════════════
154
+
155
+ branch-policy:
156
+ name: Branch Policy
157
+ runs-on: ubuntu-latest
158
+ steps:
159
+ - name: Check branch merge target
160
+ run: |
161
+ HEAD="${{ github.head_ref }}"
162
+ BASE="${{ github.base_ref }}"
163
+
164
+ echo "PR: ${HEAD} → ${BASE}"
165
+
166
+ ALLOWED=true
167
+ REASON=""
168
+
169
+ case "$HEAD" in
170
+ feature/*|feat/*)
171
+ if [ "$BASE" != "dev" ]; then
172
+ ALLOWED=false
173
+ REASON="Feature branches must target 'dev', not '${BASE}'"
174
+ fi
175
+ ;;
176
+ fix/*|bugfix/*)
177
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
178
+ ALLOWED=false
179
+ REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
180
+ fi
181
+ ;;
182
+ patch/*)
183
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
184
+ ALLOWED=false
185
+ REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
186
+ fi
187
+ ;;
188
+ hotfix/*)
189
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
190
+ ALLOWED=false
191
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
192
+ fi
193
+ ;;
194
+ rc)
195
+ if [ "$BASE" != "main" ]; then
196
+ ALLOWED=false
197
+ REASON="RC branch can only merge into 'main', not '${BASE}'"
198
+ fi
199
+ ;;
200
+ dev)
201
+ if [ "$BASE" != "main" ]; then
202
+ ALLOWED=false
203
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
204
+ fi
205
+ ;;
206
+ esac
207
+
208
+ if [ "$ALLOWED" = false ]; then
209
+ echo "::error::${REASON}"
210
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
211
+ echo "" >> $GITHUB_STEP_SUMMARY
212
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
213
+ echo "" >> $GITHUB_STEP_SUMMARY
214
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
215
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
216
+ echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
217
+ echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
218
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
219
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
220
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
221
+ exit 1
222
+ fi
223
+
224
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
225
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
226
+
227
+ # ── Docs Update Gate (wiki-aware) ───────────────────────────────────────
228
+ #
229
+ # RULE (in order):
230
+ # 1. Short-circuit: if the PR diff touches CHANGELOG.md OR README.md, PASS
231
+ # immediately (deterministic; no wiki lookup).
232
+ # 2. Else consult the repo WIKI (<owner>/<repo>.wiki.git) HEAD commit date.
233
+ # Mode is chosen by the org/repo variable DOCS_WIKI_WINDOW (default 'pr'):
234
+ # - 'pr' → satisfied if wiki HEAD commit date >= this PR's
235
+ # created_at (docs touched since the PR opened).
236
+ # - '<N>h'/'<N>d' → satisfied if wiki HEAD commit is within that rolling
237
+ # window (now - duration). e.g. 24h, 72h, 7d.
238
+ # Fail-open: if the wiki is absent/unreachable or the lookup errors, do
239
+ # NOT hard-fail — treat as "unsatisfied → warn" per step 3.
240
+ # 3. Verdict: if neither is satisfied → emit ::warning:: (advisory) and PASS
241
+ # on non-main branches; hard-fail (exit 1) ONLY when the PR base is 'main'
242
+ # (release hygiene on the stable line).
243
+ #
244
+ # The job runs on EVERY PR so the required status context "Require Docs Update"
245
+ # always reports; the base-branch gate is enforced inside the step, not via a
246
+ # job-level `if:` that would leave the context missing on non-main PRs.
247
+ require-docs:
248
+ name: Require Docs Update
249
+ runs-on: ubuntu-latest
250
+ steps:
251
+ - name: Checkout
252
+ uses: actions/checkout@v4
253
+ with:
254
+ fetch-depth: 0
255
+
256
+ - name: Docs gate (docs diff, else wiki freshness)
257
+ env:
258
+ BASE_REF: ${{ github.base_ref }}
259
+ HEAD_REF: ${{ github.head_ref }}
260
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
261
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
262
+ PR_CREATED_AT: ${{ github.event.pull_request.created_at }}
263
+ SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
264
+ REPO: ${{ github.repository }}
265
+ TOKEN: ${{ github.token }}
266
+ DOCS_WIKI_WINDOW: ${{ vars.DOCS_WIKI_WINDOW || 'pr' }}
267
+ run: |
268
+ set -uo pipefail
269
+
270
+ # ── Step 1: docs diff short-circuit ─────────────────────────────
271
+ CHANGED="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || true)"
272
+ if [ -z "$CHANGED" ]; then
273
+ git fetch -q origin "$BASE_REF" 2>/dev/null || true
274
+ CHANGED="$(git diff --name-only "origin/${BASE_REF}...HEAD" 2>/dev/null || true)"
275
+ fi
276
+ echo "Changed files in PR:"
277
+ echo "$CHANGED"
278
+ if echo "$CHANGED" | grep -qxE 'CHANGELOG\.md|README\.md'; then
279
+ echo "Docs diff present (CHANGELOG.md or README.md) — gate satisfied."
280
+ echo "## Docs Update: Passed (docs diff)" >> "$GITHUB_STEP_SUMMARY"
281
+ exit 0
282
+ fi
283
+
284
+ # ── Step 2: wiki freshness (fail-open) ──────────────────────────
285
+ # Resolve wiki HEAD commit epoch via a shallow clone of the wiki repo.
286
+ # Any failure here is non-fatal: SATISFIED stays 0 → handled by step 3.
287
+ SATISFIED=0
288
+ WIKI_EPOCH=""
289
+ TOK="$(printf '%s' "${TOKEN:-}" | tr -d '[:space:]')"
290
+ HOSTPATH="${SERVER#https://}"; HOSTPATH="${HOSTPATH#http://}"
291
+ WIKI_URL="https://x-access-token:${TOK}@${HOSTPATH}/${REPO}.wiki.git"
292
+ echo "::add-mask::${TOK}"
293
+ rm -rf /tmp/docs-wiki
294
+ if git clone --depth 1 --quiet "${WIKI_URL}" /tmp/docs-wiki 2>/dev/null; then
295
+ WIKI_EPOCH="$(git -C /tmp/docs-wiki log -1 --format=%ct 2>/dev/null || true)"
296
+ fi
297
+
298
+ if [ -z "$WIKI_EPOCH" ]; then
299
+ echo "::warning::wiki absent/unreachable — treating docs gate as unsatisfied (fail-open)"
300
+ else
301
+ MODE="$(printf '%s' "${DOCS_WIKI_WINDOW}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
302
+ [ -n "$MODE" ] || MODE="pr"
303
+ if [ "$MODE" = "pr" ]; then
304
+ # PASS if wiki HEAD commit date >= PR created_at.
305
+ PR_EPOCH="$(date -u -d "${PR_CREATED_AT}" +%s 2>/dev/null || true)"
306
+ if [ -z "$PR_EPOCH" ]; then
307
+ echo "::warning::could not parse PR created_at ('${PR_CREATED_AT}') — docs gate unsatisfied (fail-open)"
308
+ elif [ "$WIKI_EPOCH" -ge "$PR_EPOCH" ]; then
309
+ SATISFIED=1
310
+ echo "Wiki updated since PR opened (wiki=${WIKI_EPOCH} >= pr=${PR_EPOCH})."
311
+ else
312
+ echo "Wiki not updated since PR opened (wiki=${WIKI_EPOCH} < pr=${PR_EPOCH})."
313
+ fi
314
+ else
315
+ # Duration window: <N>h / <N>d (also bare <N> = seconds).
316
+ NUM="$(printf '%s' "$MODE" | sed -E 's/[^0-9].*$//')"
317
+ UNIT="$(printf '%s' "$MODE" | sed -E 's/^[0-9]+//')"
318
+ SECS=0
319
+ case "$UNIT" in
320
+ h) SECS=$(( NUM * 3600 )) ;;
321
+ d) SECS=$(( NUM * 86400 )) ;;
322
+ m) SECS=$(( NUM * 60 )) ;;
323
+ s|"") SECS=$(( NUM )) ;;
324
+ *) echo "::warning::unrecognized DOCS_WIKI_WINDOW unit '${UNIT}'; falling back to 'pr'-style (>= PR created_at)"
325
+ PR_EPOCH="$(date -u -d "${PR_CREATED_AT}" +%s 2>/dev/null || echo 0)"
326
+ SECS=0 ;;
327
+ esac
328
+ if [ "$SECS" -gt 0 ]; then
329
+ NOW="$(date -u +%s)"
330
+ CUTOFF=$(( NOW - SECS ))
331
+ if [ "$WIKI_EPOCH" -ge "$CUTOFF" ]; then
332
+ SATISFIED=1
333
+ echo "Wiki updated within window ${MODE} (wiki=${WIKI_EPOCH} >= cutoff=${CUTOFF})."
334
+ else
335
+ echo "Wiki stale beyond window ${MODE} (wiki=${WIKI_EPOCH} < cutoff=${CUTOFF})."
336
+ fi
337
+ elif [ "${PR_EPOCH:-0}" -gt 0 ] && [ "$WIKI_EPOCH" -ge "${PR_EPOCH}" ]; then
338
+ SATISFIED=1
339
+ fi
340
+ fi
341
+ fi
342
+
343
+ # ── Step 3: verdict ─────────────────────────────────────────────
344
+ if [ "$SATISFIED" = "1" ]; then
345
+ echo "## Docs Update: Passed (wiki fresh)" >> "$GITHUB_STEP_SUMMARY"
346
+ exit 0
347
+ fi
348
+
349
+ if [ "$BASE_REF" = "main" ]; then
350
+ echo "::error::Docs gate unsatisfied: no CHANGELOG.md/README.md change and wiki not fresh (DOCS_WIKI_WINDOW='${DOCS_WIKI_WINDOW}'). Required on PRs into main."
351
+ {
352
+ echo "## Docs Update Required"
353
+ echo ""
354
+ echo "PRs merging into \`main\` must either update **CHANGELOG.md** or **README.md**, or the repo **wiki** must be fresh (mode: \`${DOCS_WIKI_WINDOW}\`)."
355
+ } >> "$GITHUB_STEP_SUMMARY"
356
+ exit 1
357
+ fi
358
+
359
+ echo "::warning::Docs gate unsatisfied (advisory on non-main branch '${BASE_REF}'): update CHANGELOG.md/README.md or the wiki."
360
+ {
361
+ echo "## Docs Update: Advisory"
362
+ echo ""
363
+ echo "No docs diff and wiki not fresh (mode: \`${DOCS_WIKI_WINDOW}\`). Non-blocking on \`${BASE_REF}\`; will hard-fail on \`main\`."
364
+ } >> "$GITHUB_STEP_SUMMARY"
365
+ exit 0
366
+
367
+ wiki-reminder:
368
+ name: Wiki Update Reminder
133
369
  runs-on: ubuntu-latest
134
- needs: [build]
135
- if: always()
370
+ if: ${{ github.base_ref == 'main' }}
136
371
  steps:
137
- - name: Upsert sticky CI summary comment
372
+ - name: Remind to update the wiki
138
373
  env:
139
- TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
374
+ TOKEN: ${{ github.token }}
140
375
  SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
141
376
  REPO: ${{ github.repository }}
142
377
  PR: ${{ github.event.pull_request.number }}
143
- BUILD: ${{ needs.build.result }}
144
- RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
145
- HEAD_REF: ${{ github.head_ref }}
146
- BASE_REF: ${{ github.base_ref }}
147
378
  run: |
148
379
  set -uo pipefail
149
- { [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; } || { echo "no token/PR; skip"; exit 0; }
150
- icon() { case "$1" in success) printf '✅';; failure) printf '❌';; skipped) printf '⏭️';; *) printf '⚪';; esac; }
151
- MARKER="<!-- ci-summary -->"
152
- BODY="$(printf '%s\n## MCP Build & Validate\n\n- %s Build & validate (`%s` → `%s`)\n\n_Updated %s · [run](%s)_' \
153
- "${MARKER}" \
154
- "$(icon "${BUILD}")" "${HEAD_REF}" "${BASE_REF}" \
155
- "$(date -u +%FT%TZ)" "${RUN_URL}")"
380
+ {
381
+ echo "## Wiki Update Reminder"
382
+ echo ""
383
+ echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
384
+ echo ""
385
+ echo "- ${SERVER}/${REPO}/wiki"
386
+ echo ""
387
+ echo "_Non-blocking reminder._"
388
+ } >> "$GITHUB_STEP_SUMMARY"
389
+ # Sticky PR comment: create-or-UPDATE a single comment keyed by a hidden
390
+ # marker (no duplicate reminders across pushes). Best-effort, never fails.
156
391
  LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
157
392
  EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
158
- CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
159
- | python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
160
- PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
161
- if [ -n "${CID}" ]; then
162
- curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
163
- echo "updated sticky CI summary #${CID} on PR #${PR}"
164
- else
165
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
166
- echo "created sticky CI summary on PR #${PR}"
393
+ MARKER="<!-- wiki-reminder -->"
394
+ BODY="$(printf '%s\n\n%s' "${MARKER}" "**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_")"
395
+ if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
396
+ CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
397
+ | python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
398
+ PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
399
+ if [ -n "${CID}" ]; then
400
+ curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
401
+ else
402
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
403
+ fi
404
+ fi
405
+ echo "Wiki reminder emitted (non-blocking, sticky)."
406
+
407
+ # ── Universal PR gate checks (folded from pr-check's Validate PR) ─────────
408
+ pr-gate-checks:
409
+ name: PR Gate Checks
410
+ runs-on: ubuntu-latest
411
+ steps:
412
+ - name: Checkout
413
+ uses: actions/checkout@v4
414
+
415
+ - name: Check for merge conflict markers
416
+ run: |
417
+ CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogit' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
418
+ if [ -n "$CONFLICTS" ]; then
419
+ echo "::error::Merge conflict markers found in source files"
420
+ echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
421
+ echo '```' >> $GITHUB_STEP_SUMMARY
422
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
423
+ echo '```' >> $GITHUB_STEP_SUMMARY
424
+ exit 1
425
+ fi
426
+ echo "No conflict markers found"
427
+
428
+ - name: Reject .gitea and .github workflow dirs
429
+ run: |
430
+ # .gitea/ and .github/ carry their own workflows that CONFLICT with the
431
+ # MokoGIT .mokogit/workflows/ system (duplicate / competing runs). They
432
+ # must never exist in-repo. Fail the PR if either directory is tracked so
433
+ # it gets deleted.
434
+ HITS=$(git ls-files | grep -E '^(\.gitea|\.github)/' || true)
435
+ if [ -n "$HITS" ]; then
436
+ echo "::error::.gitea/ and .github/ are not allowed - their workflows conflict with .mokogit/workflows/. Delete these directories."
437
+ printf '%s\n' "$HITS" | while IFS= read -r f; do
438
+ [ -n "$f" ] && echo "::error file=${f}::Remove ${f} (.gitea/.github workflows conflict with .mokogit/)"
439
+ done
440
+ {
441
+ echo "## Blocked: .gitea/.github present"
442
+ echo "These directories conflict with the MokoGIT \`.mokogit/workflows/\` system and must be deleted:"
443
+ echo '```'
444
+ printf '%s\n' "$HITS"
445
+ echo '```'
446
+ } >> "$GITHUB_STEP_SUMMARY"
447
+ exit 1
167
448
  fi
449
+ echo "No .gitea/.github dirs - OK"
@@ -7,19 +7,14 @@
7
7
  # INGROUP: MokoCLI.Release
8
8
  # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-NPM
9
9
  # PATH: /.mokogit/workflows/npm-publish.yml
10
- # VERSION: 03.00.00
11
- # BRIEF: Build + publish shim. The shared release engine (pre-release.yml /
12
- # auto-release.yml) owns version bump, tag, changelog and release
13
- # object; this workflow only builds and publishes the version the
14
- # engine has already written to package.json. It never bumps or
15
- # writes the version. Build + publish now delegate to the centralized
16
- # MokoCLI build agent (moko build / moko publish); `moko publish`
17
- # preserves the same dual-registry npm publish (npmjs.org + the MokoGIT
18
- # npm registry) that the former inline block performed.
19
- #
20
- # GATE: `moko` is provided by mokocli on the runner PATH (/opt/mokocli/cli),
21
- # available only after the mokocli build agent lands on `main` and the runner
22
- # image is rebuilt — authored/reviewed now, runs green later.
10
+ # VERSION: 04.00.01
11
+ # BRIEF: Build + publish shim. Version comes from the root Makefile
12
+ # (PROJECT_VERSION — the fleet-wide source of truth); this workflow never
13
+ # bumps it. It builds with plain npm and does an idempotent DUAL-REGISTRY
14
+ # publish: the MokoGIT npm registry using the per-run Actions token
15
+ # (GITHUB_TOKEN + packages:write — no shared MOKOGIT_TOKEN), and npmjs.org
16
+ # using NPM_TOKEN (best-effort; skipped if the secret is absent). A
17
+ # version already present on a registry is a no-op (idempotent).
23
18
 
24
19
  name: "Publish to npm"
25
20
 
@@ -29,125 +24,119 @@ on:
29
24
  - main
30
25
  workflow_dispatch:
31
26
 
27
+ # The MokoGIT-registry publish uses the run's own token instead of a shared
28
+ # org secret: least-privilege, auto-expiring, no per-repo provisioning.
29
+ permissions:
30
+ contents: read
31
+ packages: write
32
+
33
+ env:
34
+ MOKOGIT_HOST: git.mokoconsulting.tech
35
+
32
36
  jobs:
33
37
  publish:
38
+ name: "Build & dual-publish"
34
39
  runs-on: release
40
+ # Skip template repos, and self-pushes the release engine marks [skip ci]
41
+ # (its version-bump/changelog commits) or [skip publish].
35
42
  if: >-
43
+ !startsWith(github.event.repository.name, 'Template-') &&
36
44
  !contains(github.event.head_commit.message, '[skip ci]') &&
37
- !contains(github.event.head_commit.message, '[skip publish]') &&
38
- !startsWith(github.event.repository.name, 'Template-')
45
+ !contains(github.event.head_commit.message, '[skip publish]')
39
46
  steps:
40
47
  - name: Checkout
41
48
  uses: actions/checkout@v4
42
49
  with:
43
- fetch-depth: 2
50
+ fetch-depth: 1
44
51
 
45
- - name: Resolve source dir
46
- id: srcdir
47
- uses: ./.mokogit/actions/resolve-source-dir
52
+ - name: Setup Node.js
53
+ uses: actions/setup-node@v4
48
54
  with:
49
- mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
50
- strict: 'true'
55
+ node-version: '20'
51
56
 
52
- - name: Detect publishable changes
53
- id: detect
57
+ - name: Resolve version from the Makefile
58
+ id: v
54
59
  run: |
55
60
  set -euo pipefail
56
-
57
- # workflow_dispatch has no diff base — always treat as changed.
58
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
59
- echo "::notice::workflow_dispatch — forcing changed=true"
60
- echo "changed=true" >> "$GITHUB_OUTPUT"
61
- exit 0
62
- fi
63
-
64
- # Build the source-dir alternative from the resolved dir (escape ERE metachars).
65
- # Class lists ] first and [ last to avoid a [. collating-symbol parse; the
66
- # \\\\& replacement reaches sed as \\& = literal backslash + matched char.
67
- SRC_DIR='${{ steps.srcdir.outputs.source_dir }}'
68
- SRC_RE=$(printf '%s' "$SRC_DIR" | sed 's/[].^$*+?(){}|[]/\\\\&/g')
69
- PATTERN="^(${SRC_RE}|package\.json$|package-lock\.json$|tsconfig\.json$|README\.md$)"
70
- echo "Detect pattern: ${PATTERN}"
71
-
72
- BEFORE="${{ github.event.before }}"
73
- if [ -z "${BEFORE}" ] || ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
74
- BEFORE=$(git rev-parse HEAD^ 2>/dev/null || echo "")
75
- fi
76
- if [ -n "${BEFORE}" ]; then
77
- FILES=$(git diff --name-only "${BEFORE}" HEAD)
78
- else
79
- FILES=$(git show --name-only --pretty=format: HEAD)
80
- fi
81
-
82
- if printf '%s\n' "${FILES}" | grep -Eq "${PATTERN}"; then
83
- echo "changed=true" >> "$GITHUB_OUTPUT"
84
- else
85
- echo "::notice::No publishable changes under '${SRC_DIR}'. Files: ${FILES}"
86
- echo "changed=false" >> "$GITHUB_OUTPUT"
61
+ # PROJECT_VERSION in the root Makefile is the single source of truth
62
+ # (auto-filled per repo by workflow sync). Everything else derives from it.
63
+ VERSION=$(grep -E '^[[:space:]]*PROJECT_VERSION[[:space:]]*:?=' Makefile | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]')
64
+ if [ -z "$VERSION" ]; then
65
+ echo "::error::No PROJECT_VERSION found in Makefile — cannot determine publish version."; exit 1
87
66
  fi
67
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
68
+ # Stamp package.json to match, IN-TREE ONLY (never committed) so `npm publish`
69
+ # ships exactly the Makefile version regardless of any package.json drift.
70
+ npm version "${VERSION}" --no-git-tag-version --allow-same-version >/dev/null 2>&1 || \
71
+ echo "::warning::could not stamp package.json to ${VERSION} (continuing)"
72
+ echo "Publishing version ${VERSION} (source: Makefile PROJECT_VERSION)"
88
73
 
89
- - name: Setup Node.js
90
- if: steps.detect.outputs.changed == 'true'
91
- uses: actions/setup-node@v4
92
- with:
93
- node-version: '20'
74
+ - name: Install & build
75
+ run: |
76
+ set -euo pipefail
77
+ npm ci --no-audit --no-fund || npm install --no-audit --no-fund
78
+ npm run build --if-present
94
79
 
95
- - name: Setup MokoCLI build agent (moko on PATH)
96
- if: steps.detect.outputs.changed == 'true'
80
+ - name: Publish to the MokoGIT npm registry (per-run Actions token)
97
81
  env:
98
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
99
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
82
+ # The run's own Gitea Actions token; packages:write is granted above.
83
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
100
84
  run: |
101
- # Reach mokocli the SAME way the release workflows' "Setup MokoCLI tools"
102
- # step does: prefer the pre-installed /opt/mokocli, else a fresh clone. We
103
- # expose the `moko` binary on PATH so `moko build|publish` resolve — no new
104
- # install mechanism is introduced.
105
- if [ -x /opt/mokocli/cli/moko ] || [ -f /opt/mokocli/cli/moko ]; then
106
- echo Using pre-installed /opt/mokocli
107
- echo "/opt/mokocli/cli" >> "$GITHUB_PATH"
85
+ set -euo pipefail
86
+ OWNER='${{ github.repository_owner }}'
87
+ REG="https://${MOKOGIT_HOST}/api/packages/${OWNER}/npm/"
88
+ printf '//%s/api/packages/%s/npm/:_authToken=%s\n' "${MOKOGIT_HOST}" "${OWNER}" "${NODE_AUTH_TOKEN}" > .npmrc
89
+ # Capture output so we can tell a benign "already published" apart from a
90
+ # real failure (auth, network) — the latter MUST fail the run, not hide
91
+ # behind a warning. `set +e` so a non-zero exit doesn't kill the step
92
+ # before we classify it.
93
+ set +e
94
+ out="$(npm publish --registry "${REG}" 2>&1)"; rc=$?
95
+ set -e
96
+ rm -f .npmrc
97
+ echo "$out"
98
+ if [ "$rc" -eq 0 ]; then
99
+ echo "Published ${{ steps.v.outputs.version }} to the MokoGIT registry."
100
+ elif printf '%s' "$out" | grep -qiE 'cannot publish over|already exists|EPUBLISHCONFLICT|409 Conflict'; then
101
+ echo "::notice::MokoGIT registry already has ${{ steps.v.outputs.version }} — no-op (idempotent)."
108
102
  else
109
- echo Falling back to fresh clone
110
- MOKO_CLONE_TOKEN="$(printf '%s' "${MOKO_CLONE_TOKEN}" | tr -d '[:space:]')"
111
- echo "::add-mask::${MOKO_CLONE_TOKEN}"
112
- if [ -z "${MOKO_CLONE_TOKEN}" ]; then
113
- echo "::error::MOKOGIT_TOKEN secret is empty — cannot clone mokocli fallback." >&2
114
- exit 1
115
- fi
116
- if ! command -v composer > /dev/null 2>&1; then
117
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
118
- fi
119
- rm -rf /tmp/mokocli
120
- CLONE_URL="https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git"
121
- git clone --depth 1 --branch main --quiet "${CLONE_URL}" /tmp/mokocli
122
- ( cd /tmp/mokocli && composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet )
123
- echo "/tmp/mokocli/cli" >> "$GITHUB_PATH"
103
+ echo "::error::MokoGIT-registry publish FAILED (npm exit ${rc}) — this is NOT 'already published'. See the npm error above (e.g. E401 = the Actions token was rejected)."
104
+ exit "$rc"
124
105
  fi
125
106
 
126
- - name: Build
127
- if: steps.detect.outputs.changed == 'true'
128
- run: moko build
129
-
130
- - name: Read version to publish
131
- id: version
132
- if: steps.detect.outputs.changed == 'true'
107
+ - name: Publish to npmjs.org (NPM_TOKEN, best-effort)
108
+ env:
109
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
133
110
  run: |
134
111
  set -euo pipefail
135
- # READ-ONLY. The shared release engine (pre-release.yml / auto-release.yml)
136
- # has already bumped and written this version into package.json and created
137
- # the tag + release object. This shim only publishes it — it never bumps or
138
- # writes the version.
139
- VERSION=$(node -p "require('./package.json').version")
140
- echo "Publishing version ${VERSION} (set by the shared release engine)."
141
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
112
+ if [ -z "${NPM_TOKEN:-}" ]; then
113
+ echo "::notice::NPM_TOKEN not set — skipping the public npmjs.org publish (MokoGIT registry is the primary)."
114
+ exit 0
115
+ fi
116
+ printf '//registry.npmjs.org/:_authToken=%s\n' "${NPM_TOKEN}" > .npmrc
117
+ # npmjs.org is the best-effort secondary (MokoGIT registry is primary),
118
+ # so a real failure warns loudly but does not fail the run — yet we still
119
+ # distinguish "already published" (expected) from "token invalid" (needs
120
+ # attention) instead of collapsing both into one vague message.
121
+ set +e
122
+ out="$(npm publish --registry https://registry.npmjs.org/ --access public 2>&1)"; rc=$?
123
+ set -e
124
+ rm -f .npmrc
125
+ echo "$out"
126
+ if [ "$rc" -eq 0 ]; then
127
+ echo "Published ${{ steps.v.outputs.version }} to npmjs.org."
128
+ elif printf '%s' "$out" | grep -qiE 'cannot publish over|already exists|EPUBLISHCONFLICT|409 Conflict'; then
129
+ echo "::notice::npmjs.org already has ${{ steps.v.outputs.version }} — no-op."
130
+ else
131
+ echo "::warning::npmjs.org publish failed (npm exit ${rc}) — token likely invalid/expired. Secondary registry only; not failing the run. See npm error above."
132
+ fi
142
133
 
143
- - name: Publish to npmjs.org + git.mokoconsulting.tech
144
- if: steps.detect.outputs.changed == 'true'
134
+ - name: Summary
135
+ if: always()
145
136
  run: |
146
- # moko publish preserves the former inline behavior: idempotent dual-registry
147
- # npm publish to npmjs.org AND the MokoGIT npm registry, publishing the version
148
- # the release engine already wrote to package.json (never bumps). It reads the
149
- # registry auth tokens from the environment below.
150
- moko publish
151
- env:
152
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
153
- MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
137
+ {
138
+ echo "## npm publish — ${{ steps.v.outputs.version }}"
139
+ echo "- Version source: root \`Makefile\` PROJECT_VERSION"
140
+ echo "- MokoGIT registry: per-run Actions token (\`packages: write\`)"
141
+ echo "- npmjs.org: \`NPM_TOKEN\` (best-effort)"
142
+ } >> "$GITHUB_STEP_SUMMARY"