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

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.
@@ -30,6 +30,13 @@ on:
30
30
  - '.mokogit/**'
31
31
  - '.gitea/**'
32
32
  - '.github/**'
33
+ - '**/*.md'
34
+ - 'wiki/**'
35
+ - '.editorconfig'
36
+ - '.gitignore'
37
+ - '.gitattributes'
38
+ - '.gitmessage'
39
+ - 'LICENSE'
33
40
 
34
41
  env:
35
42
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -61,6 +68,7 @@ jobs:
61
68
  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
62
69
  fi
63
70
  if [ -d "/opt/mokocli/cli" ]; then
71
+ git -C /opt/mokocli pull --ff-only --quiet 2>/dev/null || echo " (mokocli auto-refresh skipped)"
64
72
  echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
65
73
  else
66
74
  git clone --depth 1 --branch main --quiet \
@@ -7,7 +7,7 @@
7
7
  # INGROUP: MokoCLI.Release
8
8
  # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
9
  # PATH: /.mokogit/workflows/auto-release.yml
10
- # VERSION: 05.02.00
10
+ # VERSION: 06.02.00
11
11
  # BRIEF: Universal build & release � detects platform from metadata API
12
12
  #
13
13
  # +=======================================================================+
@@ -25,20 +25,56 @@
25
25
  name: "Universal: Build & Release"
26
26
 
27
27
  on:
28
+ # RC preview: build a release-candidate when a PR to main is opened / updated.
28
29
  pull_request:
29
30
  types: [opened, synchronize, closed]
30
31
  branches:
31
32
  - main
32
33
  paths-ignore:
33
- - '.mokogit/workflows/**'
34
- - '*.md'
34
+ - '.mokogit/**'
35
+ - '.gitea/**'
36
+ - '.github/**'
37
+ - '.vscode/**'
38
+ - '.idea/**'
39
+ - 'docs/**'
35
40
  - 'wiki/**'
41
+ - '**/*.md'
36
42
  - '.editorconfig'
37
43
  - '.gitignore'
38
44
  - '.gitattributes'
45
+ - '.gitmodules'
39
46
  - '.gitmessage'
40
- - 'LICENSE'
41
- # Daily safety-net: catch merges whose pull_request event never created a run
47
+ - 'phpstan.neon'
48
+ - 'LICENSE'
49
+ # Stable release on every PUSH to main (a merged promotion) — the same
50
+ # push-based principle as pre-release's dev-channel build. A push trigger is
51
+ # race-free vs the old pull_request:closed, which could miss its run when this
52
+ # workflow file re-synced from the template concurrently with the merge.
53
+ # Loop-safe: the workflow's own commits to main are either paths-ignored (the
54
+ # CHANGELOG promote step → **/*.md) or carry [skip ci] (mokocli's build commit
55
+ # + the changelog commit), and the release job's if: re-checks [skip ci], so a
56
+ # self-push never re-triggers a release. The non-code paths-ignore also means a
57
+ # docs/CI-only push to main does not release.
58
+ push:
59
+ branches:
60
+ - main
61
+ paths-ignore:
62
+ - '.mokogit/**'
63
+ - '.gitea/**'
64
+ - '.github/**'
65
+ - '.vscode/**'
66
+ - '.idea/**'
67
+ - 'docs/**'
68
+ - 'wiki/**'
69
+ - '**/*.md'
70
+ - '.editorconfig'
71
+ - '.gitignore'
72
+ - '.gitattributes'
73
+ - '.gitmodules'
74
+ - '.gitmessage'
75
+ - 'phpstan.neon'
76
+ - 'LICENSE'
77
+ # Daily safety-net: catch merges whose push event never created a run
42
78
  # (e.g. this workflow file being re-synced from the template concurrently with the
43
79
  # merge). Off-round minute to avoid a fleet-wide spike. The safety-net job below
44
80
  # only dispatches a release when main actually has unreleased changes.
@@ -65,11 +101,27 @@ permissions:
65
101
  contents: write
66
102
  pull-requests: write
67
103
 
104
+ concurrency:
105
+ # Serialise the release path per COMMIT, not per ref: push / schedule /
106
+ # workflow_dispatch for the SAME main commit collapse into one group and
107
+ # de-dup as pending, so a fleet-wide burst does not stack N redundant
108
+ # publish pipelines for one release. Never cancel a publish already in
109
+ # progress (cancel-in-progress: false) -- cancelling mid-publish could leave
110
+ # a half-created release (npmjs published but Gitea-registry publish dropped).
111
+ group: auto-release-${{ github.event.head_commit.id || github.sha }}
112
+ cancel-in-progress: false
113
+
68
114
  jobs:
69
115
  # ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
70
116
  promote-rc:
71
117
  name: Promote to RC
72
118
  runs-on: release
119
+ # Own-repo writes: move rc + create release (contents:write), and dispatch
120
+ # deploy-rc (actions:write) — all via the per-repo github.token.
121
+ permissions:
122
+ contents: write
123
+ pull-requests: write
124
+ actions: write
73
125
  # Skip on template repos (Template-*) — they scaffold other repos and do not release.
74
126
  # Trigger-loop guard (#404 follow-up): NEVER run on bot/sync branches. A
75
127
  # template-sync/provisioning push (e.g. chore/mokoonyx-template-sync-*) that opens or
@@ -85,11 +137,7 @@ jobs:
85
137
  (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
86
138
  (github.event.action == 'synchronize' && github.event.pull_request.merged != true)
87
139
  ) &&
88
- !startsWith(github.event.pull_request.head.ref, 'chore/') &&
89
- !startsWith(github.event.pull_request.head.ref, 'sync/') &&
90
- !startsWith(github.event.pull_request.head.ref, 'bot/') &&
91
- !startsWith(github.event.pull_request.head.ref, 'mokoonyx-template-sync') &&
92
- !startsWith(github.event.pull_request.head.ref, 'template-sync') &&
140
+ github.event.pull_request.head.ref == 'dev' &&
93
141
  !contains(github.event.pull_request.title, '[skip ci]') &&
94
142
  !contains(github.event.pull_request.title, '[skip sync]')
95
143
  ) ||
@@ -100,7 +148,7 @@ jobs:
100
148
  - name: Checkout repository
101
149
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
102
150
  with:
103
- token: ${{ secrets.MOKOGIT_TOKEN }}
151
+ token: ${{ github.token }}
104
152
  fetch-depth: 1
105
153
  submodules: recursive
106
154
 
@@ -111,25 +159,47 @@ jobs:
111
159
  run: |
112
160
  if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
113
161
  echo Using pre-installed /opt/mokocli
162
+ # Keep the pre-installed copy current with mokocli main (auto-refresh; soft — never fails the run).
163
+ if git -C /opt/mokocli rev-parse --git-dir >/dev/null 2>&1; then
164
+ git -C /opt/mokocli pull --ff-only --quiet 2>/dev/null || echo " (mokocli git refresh skipped)"
165
+ REFRESH_TOK="$(printf '%s' "${MOKO_CLONE_TOKEN}" | tr -d '[:space:]')"
166
+ if [ -n "${REFRESH_TOK}" ] && command -v composer >/dev/null 2>&1; then
167
+ composer config -g http-basic.git.mokoconsulting.tech x-access-token "${REFRESH_TOK}" 2>/dev/null || true
168
+ ( cd /opt/mokocli && composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet ) 2>/dev/null || echo " (mokocli composer refresh skipped)"
169
+ fi
170
+ fi
114
171
  echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
115
172
  else
116
173
  echo Falling back to fresh clone
117
174
  if ! command -v composer > /dev/null 2>&1; then
118
175
  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
119
176
  fi
177
+ # Strip any stray whitespace/newline a pasted secret may carry, and mask
178
+ # it; the unquoted URL below otherwise word-splits on a trailing newline
179
+ # in MOKOGIT_TOKEN and git clone dies with "fatal: Too many arguments".
180
+ MOKO_CLONE_TOKEN="$(printf '%s' "${MOKO_CLONE_TOKEN}" | tr -d '[:space:]')"
181
+ echo "::add-mask::${MOKO_CLONE_TOKEN}"
182
+ if [ -z "${MOKO_CLONE_TOKEN}" ]; then
183
+ echo "::error::MOKOGIT_TOKEN secret is empty — cannot clone mokocli fallback." >&2
184
+ exit 1
185
+ fi
120
186
  rm -rf /tmp/mokocli
121
- CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
122
- git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
187
+ CLONE_URL="https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git"
188
+ git clone --depth 1 --branch main --quiet "${CLONE_URL}" /tmp/mokocli
123
189
  cd /tmp/mokocli
124
190
  # Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
125
191
  # declared in mokocli composer.json (preferred-install=dist) before installing deps.
126
192
  composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
193
+ export COMPOSER_AUTH="{\"http-basic\":{\"git.mokoconsulting.tech\":{\"username\":\"x-access-token\",\"password\":\"${MOKO_CLONE_TOKEN}\"}}}"
127
194
  composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
128
195
  echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
129
196
  fi
130
197
 
131
198
  - name: Rename branch to rc
199
+ env:
200
+ MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
132
201
  run: |
202
+ PUSH_TOK="$(printf '%s' "$MOKOGIT_TOKEN" | tr -d '[:space:]')"; [ -z "$PUSH_TOK" ] && PUSH_TOK="${{ github.token }}"; echo "::add-mask::$PUSH_TOK"
133
203
  API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
134
204
  AUTH="Authorization: token ${{ github.token }}"
135
205
  FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
@@ -145,7 +215,7 @@ jobs:
145
215
  # returns HTTP 405 on ANY protected branch (force or not, even for a user in
146
216
  # the force-push allowlist), so it cannot move a protected rc. git push honors
147
217
  # the push + force-push allowlists and creates rc if it is absent.
148
- PUSH_URL="https://x-access-token:${{ github.token }}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git"
218
+ PUSH_URL="https://x-access-token:${PUSH_TOK}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git"
149
219
  git config --global user.name "mokogit-actions[bot]"
150
220
  git config --global user.email "actions@mokoconsulting.tech"
151
221
  git fetch --no-tags "$PUSH_URL" "${FROM}"
@@ -170,15 +240,23 @@ jobs:
170
240
  - name: Trigger RC deploy
171
241
  run: |
172
242
  # Workflow-token pushes do NOT wake downstream workflows; dispatch deploy-rc explicitly.
173
- curl -sf -X POST -H "Authorization: token ${{ github.token }}" \n -H "Content-Type: application/json" \n "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/deploy-rc.yml/dispatches" \n -d '{"ref":"rc"}' \n && echo "Dispatched deploy-rc on rc" \n || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc? non-go platform)"
243
+ curl -sf -X POST -H "Authorization: token ${{ github.token }}" \
244
+ -H "Content-Type: application/json" \
245
+ "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/deploy-rc.yml/dispatches" \
246
+ -d '{"ref":"rc"}' \
247
+ && echo "Dispatched deploy-rc on rc" \
248
+ || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc? non-go platform)"
174
249
 
175
250
  - name: Checkout rc and configure git
251
+ env:
252
+ MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
176
253
  run: |
254
+ PUSH_TOK="$(printf '%s' "$MOKOGIT_TOKEN" | tr -d '[:space:]')"; [ -z "$PUSH_TOK" ] && PUSH_TOK="${{ github.token }}"; echo "::add-mask::$PUSH_TOK"
177
255
  git fetch origin rc
178
256
  git checkout rc
179
257
  git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
180
258
  git config --local user.name "mokogit-actions[bot]"
181
- git remote set-url origin "https://x-access-token:${{ github.token }}@git.mokoconsulting.tech/${{ github.repository }}.git"
259
+ git remote set-url origin "https://x-access-token:${PUSH_TOK}@git.mokoconsulting.tech/${{ github.repository }}.git"
182
260
 
183
261
  - name: Publish RC release
184
262
  continue-on-error: true
@@ -236,19 +314,28 @@ jobs:
236
314
  name: Build & Release Pipeline
237
315
  runs-on: release
238
316
  # Skip on template repos (Template-*) — they scaffold other repos and do not release.
239
- # Trigger-loop guard (#404 follow-up): a merged bot/sync PR (template-sync/provisioning)
240
- # must not fire a release. We gate the merged-PR path on the PR HEAD ref prefix and on
241
- # [skip ci]/[skip sync] title markers; workflow_dispatch stays exempt (explicit human run).
317
+ # Stable release fires on a merged PR into main (pull_request:closed + merged) OR a
318
+ # direct PUSH to main. The pull_request path is the reliable one a PR-merge does not
319
+ # always emit a base-branch push event to Actions, so relying on push alone silently
320
+ # skipped releases (the scheduled-safety-net below was the band-aid). The release
321
+ # steps already read github.event.pull_request.head.ref, so they handle both events.
322
+ # Loop guard: skip a self-push that carries
323
+ # [skip ci]/[skip sync] (mokocli's build commit + the changelog-promote commit); the
324
+ # non-code paths-ignore on the push trigger already drops docs/CI-only pushes.
325
+ # workflow_dispatch stays exempt (explicit human run).
242
326
  if: >-
243
327
  !startsWith(github.event.repository.name, 'Template-') &&
244
328
  (
245
329
  (
330
+ github.event_name == 'push' &&
331
+ !contains(github.event.head_commit.message, '[skip ci]') &&
332
+ !contains(github.event.head_commit.message, '[skip sync]')
333
+ ) ||
334
+ (
335
+ github.event_name == 'pull_request' &&
336
+ github.event.action == 'closed' &&
246
337
  github.event.pull_request.merged == true &&
247
- !startsWith(github.event.pull_request.head.ref, 'chore/') &&
248
- !startsWith(github.event.pull_request.head.ref, 'sync/') &&
249
- !startsWith(github.event.pull_request.head.ref, 'bot/') &&
250
- !startsWith(github.event.pull_request.head.ref, 'mokoonyx-template-sync') &&
251
- !startsWith(github.event.pull_request.head.ref, 'template-sync') &&
338
+ github.event.pull_request.base.ref == 'main' &&
252
339
  !contains(github.event.pull_request.title, '[skip ci]') &&
253
340
  !contains(github.event.pull_request.title, '[skip sync]')
254
341
  ) ||
@@ -259,15 +346,18 @@ jobs:
259
346
  - name: Checkout repository
260
347
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
261
348
  with:
262
- token: ${{ secrets.MOKOGIT_TOKEN }}
349
+ token: ${{ github.token }}
263
350
  fetch-depth: 0
264
351
  submodules: recursive
265
352
 
266
353
  - name: Configure git for bot pushes
354
+ env:
355
+ MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
267
356
  run: |
357
+ PUSH_TOK="$(printf '%s' "$MOKOGIT_TOKEN" | tr -d '[:space:]')"; [ -z "$PUSH_TOK" ] && PUSH_TOK="${{ github.token }}"; echo "::add-mask::$PUSH_TOK"
268
358
  git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
269
359
  git config --local user.name "mokogit-actions[bot]"
270
- git remote set-url origin "https://x-access-token:${{ github.token }}@git.mokoconsulting.tech/${{ github.repository }}.git"
360
+ git remote set-url origin "https://x-access-token:${PUSH_TOK}@git.mokoconsulting.tech/${{ github.repository }}.git"
271
361
 
272
362
  - name: Check for merge conflict markers
273
363
  run: |
@@ -286,7 +376,6 @@ jobs:
286
376
  env:
287
377
  MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
288
378
  MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
289
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
290
379
  run: |
291
380
  if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
292
381
  echo Using pre-installed /opt/mokocli
@@ -296,13 +385,23 @@ jobs:
296
385
  if ! command -v composer > /dev/null 2>&1; then
297
386
  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
298
387
  fi
388
+ # Strip any stray whitespace/newline a pasted secret may carry, and mask
389
+ # it; the unquoted URL below otherwise word-splits on a trailing newline
390
+ # in MOKOGIT_TOKEN and git clone dies with "fatal: Too many arguments".
391
+ MOKO_CLONE_TOKEN="$(printf '%s' "${MOKO_CLONE_TOKEN}" | tr -d '[:space:]')"
392
+ echo "::add-mask::${MOKO_CLONE_TOKEN}"
393
+ if [ -z "${MOKO_CLONE_TOKEN}" ]; then
394
+ echo "::error::MOKOGIT_TOKEN secret is empty — cannot clone mokocli fallback." >&2
395
+ exit 1
396
+ fi
299
397
  rm -rf /tmp/mokocli
300
- CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
301
- git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
398
+ CLONE_URL="https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git"
399
+ git clone --depth 1 --branch main --quiet "${CLONE_URL}" /tmp/mokocli
302
400
  cd /tmp/mokocli
303
401
  # Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
304
402
  # declared in mokocli composer.json (preferred-install=dist) before installing deps.
305
403
  composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
404
+ export COMPOSER_AUTH="{\"http-basic\":{\"git.mokoconsulting.tech\":{\"username\":\"x-access-token\",\"password\":\"${MOKO_CLONE_TOKEN}\"}}}"
306
405
  composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
307
406
  echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
308
407
  fi
@@ -310,7 +409,14 @@ jobs:
310
409
  - name: "Detect platform"
311
410
  id: platform
312
411
  run: |
313
- php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
412
+ # Detect the platform from the repo's NATIVE manifest and emit it to
413
+ # $GITHUB_OUTPUT for downstream steps. READ-ONLY: the former metadata
414
+ # write-back (platform_detect.php --update --token --owner --repo) has
415
+ # been REMOVED — platform is now derived from the native manifest by the
416
+ # MokoCLI build agent, so this workflow no longer writes to the metadata
417
+ # registry. Non-fatal (|| true) so a detection hiccup never blocks a release.
418
+ php ${MOKO_CLI}/platform_detect.php --path . --github-output \
419
+ 2>/dev/null || true
314
420
  php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
315
421
 
316
422
  - name: "Determine version bump level"
@@ -437,45 +543,22 @@ jobs:
437
543
  if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
438
544
  php ${MOKO_CLI}/changelog_promote.php --path . --version "$VERSION"
439
545
  git add CHANGELOG.md
440
- git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
546
+ git commit -m "chore: promote changelog [Unreleased] → [${VERSION}] [skip ci]" || true
441
547
  git push origin main || true
442
548
  echo "Changelog promoted: [Unreleased] → [${VERSION}]"
443
549
  fi
444
550
 
445
- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
446
- - name: "Step 9: Mirror release to GitHub"
447
- if: >-
448
- steps.version.outputs.skip != 'true' &&
449
- secrets.GH_MIRROR_TOKEN != ''
450
- continue-on-error: true
551
+ - name: Dispatch npm publish (bump has persisted to main)
451
552
  run: |
452
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
453
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
454
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
455
- API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
456
- php ${MOKO_CLI}/release_mirror.php \
457
- --version "$VERSION" --tag "$RELEASE_TAG" \
458
- --token "${{ github.token }}" --api-base "$API_BASE" \
459
- --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
460
- --branch main 2>&1 || true
461
- echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
462
-
463
- # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
464
- - name: "Step 10: Push main to GitHub mirror"
465
- if: >-
466
- steps.version.outputs.skip != 'true' &&
467
- secrets.GH_MIRROR_TOKEN != ''
468
- continue-on-error: true
469
- run: |
470
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
471
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
472
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
473
- git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
474
- git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
475
- git fetch origin main --depth=1
476
- git push github origin/main:refs/heads/main --force 2>/dev/null \
477
- && echo "main branch pushed to GitHub mirror" \
478
- || echo "WARNING: GitHub mirror push failed"
553
+ curl -sf -X POST -H "Authorization: token ${{ github.token }}" \
554
+ -H "Content-Type: application/json" \
555
+ "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/npm-publish.yml/dispatches" \
556
+ -d '{"ref":"main"}' \
557
+ && echo "Dispatched npm-publish on main" \
558
+ || echo "::warning::npm-publish dispatch failed (no npm-publish.yml on this repo? non-npm platform)"
559
+
560
+ # GitHub mirroring (release assets + main branch) moved to an out-of-band
561
+ # OAuth push-mirror (Gitea repo setting) — former Steps 9 & 10 removed.
479
562
 
480
563
  - name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
481
564
  if: steps.version.outputs.skip != 'true'
@@ -519,9 +602,13 @@ jobs:
519
602
  continue-on-error: true
520
603
  run: |
521
604
  API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
605
+ # No `|| true`: let a failed dev-version advance surface as a red step.
606
+ # `continue-on-error: true` above still keeps the release itself green,
607
+ # but a swallowed exit code is what let this go unnoticed for months
608
+ # (dev silently drifting below stable → dev-channel updates vanish).
522
609
  php ${MOKO_CLI}/version_reset_dev.php \
523
610
  --token "${{ github.token }}" --api-base "${API_BASE}" \
524
- --branch dev --path . 2>&1 || true
611
+ --branch dev --path . 2>&1
525
612
 
526
613
  # -- Summary --------------------------------------------------------------
527
614
  - name: Pipeline Summary
@@ -559,8 +646,10 @@ jobs:
559
646
  name: Scheduled release safety-net
560
647
  runs-on: ubuntu-latest
561
648
  if: github.event_name == 'schedule' && !startsWith(github.event.repository.name, 'Template-')
649
+ # Own-repo write: dispatch a release when main has unreleased changes.
562
650
  permissions:
563
651
  contents: read
652
+ actions: write
564
653
  steps:
565
654
  - name: Checkout main
566
655
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -571,6 +660,8 @@ jobs:
571
660
  - name: Dispatch release if main has unreleased changes
572
661
  env:
573
662
  MOKOGIT_TOKEN: ${{ github.token }}
663
+ NTFY_URL: ${{ vars.NTFY_URL }}
664
+ NTFY_TOPIC: ${{ vars.NTFY_TOPIC }}
574
665
  run: |
575
666
  # [Unreleased] is non-empty precisely when changes were merged but not yet
576
667
  # released (the release job promotes it to a version and empties it).
@@ -583,14 +674,36 @@ jobs:
583
674
  exit 0
584
675
  fi
585
676
  echo "Unreleased changes detected on main — a merge release was likely missed."
586
- echo "Dispatching a release via the safety-net."
677
+ API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
678
+ # Snapshot the newest run id BEFORE dispatching. Gitea can return 2xx to
679
+ # workflow_dispatch WITHOUT creating a run (see mokocli#499), so an HTTP
680
+ # 204 is NOT proof of recovery — confirm a new run actually appears.
681
+ before=$(curl -s -H "Authorization: token ${MOKOGIT_TOKEN}" "${API}/actions/tasks?limit=1" \
682
+ | python3 -c "import sys,json;r=json.load(sys.stdin).get('workflow_runs',[]);print(r[0]['id'] if r else 0)" 2>/dev/null || echo 0)
683
+ echo "Dispatching a release via the safety-net (newest run before = ${before})."
587
684
  HTTP=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
588
685
  -H "Authorization: token ${MOKOGIT_TOKEN}" \
589
686
  -H "Content-Type: application/json" \
590
687
  -d '{"ref":"main","inputs":{"action":"release"}}' \
591
- "${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}/actions/workflows/auto-release.yml/dispatches")
592
- if [ "$HTTP" = "204" ] || [ "$HTTP" = "201" ] || [ "$HTTP" = "200" ]; then
593
- echo "Release dispatched (HTTP $HTTP)."
594
- else
595
- echo "::warning::Safety-net release dispatch failed (HTTP $HTTP)"
688
+ "${API}/actions/workflows/auto-release.yml/dispatches")
689
+ echo "Dispatch HTTP ${HTTP}."
690
+ # Verify a NEW run actually materialised (a 2xx alone is not proof).
691
+ created=0
692
+ for i in $(seq 1 12); do
693
+ sleep 10
694
+ newest=$(curl -s -H "Authorization: token ${MOKOGIT_TOKEN}" "${API}/actions/tasks?limit=8" \
695
+ | python3 -c "import sys,json;rs=json.load(sys.stdin).get('workflow_runs',[]);print(max([r['id'] for r in rs] or [0]))" 2>/dev/null || echo 0)
696
+ if [ "${newest:-0}" -gt "${before:-0}" ]; then created=1; break; fi
697
+ done
698
+ if [ "$created" = "1" ]; then
699
+ echo "Release run created — safety-net recovery succeeded."
700
+ exit 0
701
+ fi
702
+ # No run materialised: turn the silent no-op into a LOUD, actionable failure.
703
+ echo "::error::Safety-net dispatched a release (HTTP ${HTTP}) but NO run was created — Gitea workflow_dispatch no-run (mokocli#499). main has UNRELEASED changes that did not publish. Recover by merging a change to main or running auto-release from the web UI."
704
+ if [ -n "${NTFY_URL:-}" ] && [ -n "${NTFY_TOPIC:-}" ]; then
705
+ curl -s -X POST -H "Title: Release recovery FAILED" -H "Priority: high" \
706
+ -d "Safety-net could not recover a missed release on ${{ github.repository }}: workflow_dispatch created no run (mokocli#499). Trigger a release manually." \
707
+ "${NTFY_URL%/}/${NTFY_TOPIC}" >/dev/null 2>&1 || true
596
708
  fi
709
+ exit 1