@mokoconsulting/mcp-mokosuite 1.0.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.
- package/.editorconfig +19 -0
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
- package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogit/actions/resolve-source-dir/action.yml +108 -0
- package/.mokogit/workflows/auto-assign.yml +76 -0
- package/.mokogit/workflows/auto-bump.yml +78 -0
- package/.mokogit/workflows/auto-dev-issue.yml +207 -0
- package/.mokogit/workflows/auto-release.yml +596 -0
- package/.mokogit/workflows/branch-cleanup.yml +60 -0
- package/.mokogit/workflows/cascade-dev.yml +198 -0
- package/.mokogit/workflows/changelog-validation.yml +101 -0
- package/.mokogit/workflows/ci-generic.yml +203 -0
- package/.mokogit/workflows/ci-issue-reporter.yml +75 -0
- package/.mokogit/workflows/cleanup.yml +87 -0
- package/.mokogit/workflows/gitleaks.yml +94 -0
- package/.mokogit/workflows/issue-branch.yml +81 -0
- package/.mokogit/workflows/notify.yml +73 -0
- package/.mokogit/workflows/npm-build-test.yml +83 -0
- package/.mokogit/workflows/npm-publish.yml +126 -0
- package/.mokogit/workflows/npm-sdk-check.yml +110 -0
- package/.mokogit/workflows/npm-tool-inventory.yml +80 -0
- package/.mokogit/workflows/pr-branch-check.yml +90 -0
- package/.mokogit/workflows/pr-check.yml +565 -0
- package/.mokogit/workflows/pre-release.yml +413 -0
- package/.mokogit/workflows/push-notify.yml +43 -0
- package/.mokogit/workflows/rc-revert.yml +72 -0
- package/.mokogit/workflows/repo-health.yml +700 -0
- package/.mokogit/workflows/repository-cleanup.yml +525 -0
- package/.mokogit/workflows/standards-compliance.yml +2507 -0
- package/.mokogit/workflows/sync-version-on-merge.yml +130 -0
- package/.mokogit/workflows/version-set.yml +131 -0
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +52 -0
- package/CODE_OF_CONDUCT.md +70 -0
- package/CONTRIBUTING.md +161 -0
- package/LICENSE +696 -0
- package/Makefile +70 -0
- package/README.md +83 -0
- package/SECURITY.md +34 -0
- package/config.example.json +21 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +124 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +53 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +181 -0
- package/dist/signing.d.ts +14 -0
- package/dist/signing.js +62 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +16 -0
- package/docs/API.md +63 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/INSTALLATION.md +102 -0
- package/docs/index.md +12 -0
- package/package.json +35 -0
- package/src/client.ts +142 -0
- package/src/config.ts +63 -0
- package/src/index.ts +336 -0
- package/src/signing.ts +67 -0
- package/src/types.ts +63 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# FILE INFORMATION
|
|
6
|
+
# DEFGROUP: MokoGIT.Workflow
|
|
7
|
+
# INGROUP: MokoCLI.Release
|
|
8
|
+
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
9
|
+
# PATH: /.mokogit/workflows/auto-release.yml
|
|
10
|
+
# VERSION: 05.02.00
|
|
11
|
+
# BRIEF: Universal build & release � detects platform from metadata API
|
|
12
|
+
#
|
|
13
|
+
# +=======================================================================+
|
|
14
|
+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
|
15
|
+
# +=======================================================================+
|
|
16
|
+
# | |
|
|
17
|
+
# | Reads metadata API (joomla|generic) to branch logic. |
|
|
18
|
+
# | |
|
|
19
|
+
# | Platform-specific: |
|
|
20
|
+
# | joomla: XML manifest, type-prefixed packages |
|
|
21
|
+
# | generic: README-only, no update stream |
|
|
22
|
+
# | |
|
|
23
|
+
# +=======================================================================+
|
|
24
|
+
|
|
25
|
+
name: "Universal: Build & Release"
|
|
26
|
+
|
|
27
|
+
on:
|
|
28
|
+
pull_request:
|
|
29
|
+
types: [opened, synchronize, closed]
|
|
30
|
+
branches:
|
|
31
|
+
- main
|
|
32
|
+
paths-ignore:
|
|
33
|
+
- '.mokogit/workflows/**'
|
|
34
|
+
- '*.md'
|
|
35
|
+
- 'wiki/**'
|
|
36
|
+
- '.editorconfig'
|
|
37
|
+
- '.gitignore'
|
|
38
|
+
- '.gitattributes'
|
|
39
|
+
- '.gitmessage'
|
|
40
|
+
- 'LICENSE'
|
|
41
|
+
# Daily safety-net: catch merges whose pull_request event never created a run
|
|
42
|
+
# (e.g. this workflow file being re-synced from the template concurrently with the
|
|
43
|
+
# merge). Off-round minute to avoid a fleet-wide spike. The safety-net job below
|
|
44
|
+
# only dispatches a release when main actually has unreleased changes.
|
|
45
|
+
schedule:
|
|
46
|
+
- cron: '37 8 * * *'
|
|
47
|
+
workflow_dispatch:
|
|
48
|
+
inputs:
|
|
49
|
+
action:
|
|
50
|
+
description: 'Action to perform'
|
|
51
|
+
required: false
|
|
52
|
+
type: choice
|
|
53
|
+
default: release
|
|
54
|
+
options:
|
|
55
|
+
- release
|
|
56
|
+
- promote-rc
|
|
57
|
+
|
|
58
|
+
env:
|
|
59
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
60
|
+
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
61
|
+
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
|
|
62
|
+
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
|
63
|
+
|
|
64
|
+
permissions:
|
|
65
|
+
contents: write
|
|
66
|
+
pull-requests: write
|
|
67
|
+
|
|
68
|
+
jobs:
|
|
69
|
+
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
|
|
70
|
+
promote-rc:
|
|
71
|
+
name: Promote to RC
|
|
72
|
+
runs-on: release
|
|
73
|
+
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
74
|
+
# Trigger-loop guard (#404 follow-up): NEVER run on bot/sync branches. A
|
|
75
|
+
# template-sync/provisioning push (e.g. chore/mokoonyx-template-sync-*) that opens or
|
|
76
|
+
# updates a PR to main must not spawn an RC-promote storm. We gate on the PR HEAD ref
|
|
77
|
+
# prefix and on [skip ci]/[skip sync] markers in the PR title. Legitimate release PRs
|
|
78
|
+
# from dev/feature/fix/patch/hotfix branches are unaffected. workflow_dispatch is exempt
|
|
79
|
+
# from the ref guard because a human explicitly chose to run it.
|
|
80
|
+
if: >-
|
|
81
|
+
!startsWith(github.event.repository.name, 'Template-') &&
|
|
82
|
+
(
|
|
83
|
+
(
|
|
84
|
+
(
|
|
85
|
+
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
|
86
|
+
(github.event.action == 'synchronize' && github.event.pull_request.merged != true)
|
|
87
|
+
) &&
|
|
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') &&
|
|
93
|
+
!contains(github.event.pull_request.title, '[skip ci]') &&
|
|
94
|
+
!contains(github.event.pull_request.title, '[skip sync]')
|
|
95
|
+
) ||
|
|
96
|
+
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
steps:
|
|
100
|
+
- name: Checkout repository
|
|
101
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
102
|
+
with:
|
|
103
|
+
token: ${{ secrets.MOKOGIT_TOKEN }}
|
|
104
|
+
fetch-depth: 1
|
|
105
|
+
submodules: recursive
|
|
106
|
+
|
|
107
|
+
- name: Setup MokoCLI tools
|
|
108
|
+
env:
|
|
109
|
+
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
110
|
+
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
111
|
+
run: |
|
|
112
|
+
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
|
113
|
+
echo Using pre-installed /opt/mokocli
|
|
114
|
+
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
|
115
|
+
else
|
|
116
|
+
echo Falling back to fresh clone
|
|
117
|
+
if ! command -v composer > /dev/null 2>&1; then
|
|
118
|
+
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
|
+
fi
|
|
120
|
+
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
|
|
123
|
+
cd /tmp/mokocli
|
|
124
|
+
# Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
|
|
125
|
+
# declared in mokocli composer.json (preferred-install=dist) before installing deps.
|
|
126
|
+
composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
|
|
127
|
+
composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
|
|
128
|
+
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
- name: Rename branch to rc
|
|
132
|
+
run: |
|
|
133
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
134
|
+
AUTH="Authorization: token ${{ github.token }}"
|
|
135
|
+
FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
|
|
136
|
+
PR="${{ github.event.pull_request.number }}"
|
|
137
|
+
|
|
138
|
+
# Resolve the source branch HEAD commit.
|
|
139
|
+
SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
|
|
140
|
+
|| { echo "::error::Source branch ${FROM} not found"; exit 1; }
|
|
141
|
+
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
|
|
142
|
+
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
|
|
143
|
+
|
|
144
|
+
# Point rc at the source commit via git push. Git's git/refs PATCH API
|
|
145
|
+
# returns HTTP 405 on ANY protected branch (force or not, even for a user in
|
|
146
|
+
# the force-push allowlist), so it cannot move a protected rc. git push honors
|
|
147
|
+
# 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"
|
|
149
|
+
git config --global user.name "mokogit-actions[bot]"
|
|
150
|
+
git config --global user.email "actions@mokoconsulting.tech"
|
|
151
|
+
git fetch --no-tags "$PUSH_URL" "${FROM}"
|
|
152
|
+
git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \
|
|
153
|
+
|| { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; }
|
|
154
|
+
echo "rc set to ${FROM} (${SRC_SHA})"
|
|
155
|
+
|
|
156
|
+
# Repoint the PR at rc, then delete the old source branch (non-fatal).
|
|
157
|
+
if [ -n "$PR" ]; then
|
|
158
|
+
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
159
|
+
"${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
|
|
160
|
+
fi
|
|
161
|
+
# Never delete permanent branches (dev/main/rc/...); only ephemeral feature branches.
|
|
162
|
+
case "$FROM" in
|
|
163
|
+
dev|main|master|rc|stable|production|release|develop|staging|beta|alpha)
|
|
164
|
+
echo "Keeping permanent branch ${FROM} (not deleting)" ;;
|
|
165
|
+
*)
|
|
166
|
+
curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true ;;
|
|
167
|
+
esac
|
|
168
|
+
echo "Renamed ${FROM} -> rc"
|
|
169
|
+
|
|
170
|
+
- name: Trigger RC deploy
|
|
171
|
+
run: |
|
|
172
|
+
# 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)"
|
|
174
|
+
|
|
175
|
+
- name: Checkout rc and configure git
|
|
176
|
+
run: |
|
|
177
|
+
git fetch origin rc
|
|
178
|
+
git checkout rc
|
|
179
|
+
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
180
|
+
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"
|
|
182
|
+
|
|
183
|
+
- name: Publish RC release
|
|
184
|
+
continue-on-error: true
|
|
185
|
+
run: |
|
|
186
|
+
php ${MOKO_CLI}/release_publish.php \
|
|
187
|
+
--path . --stability rc --bump minor --branch rc \
|
|
188
|
+
--token "${{ github.token }}"
|
|
189
|
+
|
|
190
|
+
- name: Update RC release notes from CHANGELOG.md
|
|
191
|
+
continue-on-error: true
|
|
192
|
+
run: |
|
|
193
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
194
|
+
TOKEN="${{ github.token }}"
|
|
195
|
+
|
|
196
|
+
# Extract [Unreleased] section via the shared mokocli command (#364 centralization).
|
|
197
|
+
NOTES=""
|
|
198
|
+
if [ -f "CHANGELOG.md" ]; then
|
|
199
|
+
NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true)
|
|
200
|
+
# release_notes.php echoes "Release Unreleased" when the section is empty; normalize
|
|
201
|
+
# to empty so the platform fallback below applies.
|
|
202
|
+
[ "$NOTES" = "Release Unreleased" ] && NOTES=""
|
|
203
|
+
fi
|
|
204
|
+
[ -z "$NOTES" ] && NOTES="Release candidate"
|
|
205
|
+
|
|
206
|
+
# Find the RC release and update its body
|
|
207
|
+
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
208
|
+
"${API_BASE}/releases/tags/release-candidate" \
|
|
209
|
+
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
210
|
+
|
|
211
|
+
if [ -n "$RELEASE_ID" ]; then
|
|
212
|
+
python3 -c "
|
|
213
|
+
import json, urllib.request
|
|
214
|
+
body = open('/dev/stdin').read()
|
|
215
|
+
payload = json.dumps({'body': body}).encode()
|
|
216
|
+
req = urllib.request.Request(
|
|
217
|
+
'${API_BASE}/releases/${RELEASE_ID}',
|
|
218
|
+
data=payload, method='PATCH',
|
|
219
|
+
headers={
|
|
220
|
+
'Authorization': 'token ${TOKEN}',
|
|
221
|
+
'Content-Type': 'application/json'
|
|
222
|
+
})
|
|
223
|
+
urllib.request.urlopen(req)
|
|
224
|
+
" <<< "$NOTES"
|
|
225
|
+
echo "RC release notes updated from CHANGELOG.md"
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
- name: Summary
|
|
229
|
+
if: always()
|
|
230
|
+
run: |
|
|
231
|
+
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
|
232
|
+
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
|
233
|
+
|
|
234
|
+
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
|
235
|
+
release:
|
|
236
|
+
name: Build & Release Pipeline
|
|
237
|
+
runs-on: release
|
|
238
|
+
# 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).
|
|
242
|
+
if: >-
|
|
243
|
+
!startsWith(github.event.repository.name, 'Template-') &&
|
|
244
|
+
(
|
|
245
|
+
(
|
|
246
|
+
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') &&
|
|
252
|
+
!contains(github.event.pull_request.title, '[skip ci]') &&
|
|
253
|
+
!contains(github.event.pull_request.title, '[skip sync]')
|
|
254
|
+
) ||
|
|
255
|
+
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
steps:
|
|
259
|
+
- name: Checkout repository
|
|
260
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
261
|
+
with:
|
|
262
|
+
token: ${{ secrets.MOKOGIT_TOKEN }}
|
|
263
|
+
fetch-depth: 0
|
|
264
|
+
submodules: recursive
|
|
265
|
+
|
|
266
|
+
- name: Configure git for bot pushes
|
|
267
|
+
run: |
|
|
268
|
+
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
269
|
+
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"
|
|
271
|
+
|
|
272
|
+
- name: Check for merge conflict markers
|
|
273
|
+
run: |
|
|
274
|
+
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --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)
|
|
275
|
+
if [ -n "$CONFLICTS" ]; then
|
|
276
|
+
echo "::error::Merge conflict markers found — aborting release"
|
|
277
|
+
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
|
278
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
279
|
+
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
|
280
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
281
|
+
exit 1
|
|
282
|
+
fi
|
|
283
|
+
echo "No conflict markers found"
|
|
284
|
+
|
|
285
|
+
- name: Setup MokoCLI tools
|
|
286
|
+
env:
|
|
287
|
+
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
288
|
+
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
289
|
+
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
|
290
|
+
run: |
|
|
291
|
+
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
|
292
|
+
echo Using pre-installed /opt/mokocli
|
|
293
|
+
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
|
294
|
+
else
|
|
295
|
+
echo Falling back to fresh clone
|
|
296
|
+
if ! command -v composer > /dev/null 2>&1; then
|
|
297
|
+
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
|
+
fi
|
|
299
|
+
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
|
|
302
|
+
cd /tmp/mokocli
|
|
303
|
+
# Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
|
|
304
|
+
# declared in mokocli composer.json (preferred-install=dist) before installing deps.
|
|
305
|
+
composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
|
|
306
|
+
composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
|
|
307
|
+
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
- name: "Detect platform"
|
|
311
|
+
id: platform
|
|
312
|
+
run: |
|
|
313
|
+
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
314
|
+
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
|
315
|
+
|
|
316
|
+
- name: "Determine version bump level"
|
|
317
|
+
id: bump
|
|
318
|
+
run: |
|
|
319
|
+
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
|
320
|
+
# Feature/dev branches: bump minor for the new stable release
|
|
321
|
+
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
|
322
|
+
case "$HEAD_REF" in
|
|
323
|
+
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
|
324
|
+
*) BUMP="minor" ;;
|
|
325
|
+
esac
|
|
326
|
+
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
327
|
+
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
|
328
|
+
|
|
329
|
+
- name: "Publish stable release"
|
|
330
|
+
run: |
|
|
331
|
+
BUMP_FLAG=""
|
|
332
|
+
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
|
333
|
+
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
|
334
|
+
fi
|
|
335
|
+
php ${MOKO_CLI}/release_publish.php \
|
|
336
|
+
--path . --stability stable ${BUMP_FLAG} --branch main \
|
|
337
|
+
--token "${{ github.token }}"
|
|
338
|
+
|
|
339
|
+
- name: "Read published version"
|
|
340
|
+
id: version
|
|
341
|
+
run: |
|
|
342
|
+
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
|
343
|
+
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
344
|
+
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
345
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
346
|
+
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
347
|
+
if [[ "$PLATFORM" == joomla* ]]; then
|
|
348
|
+
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
|
349
|
+
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
|
350
|
+
else
|
|
351
|
+
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
352
|
+
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
353
|
+
fi
|
|
354
|
+
echo "branch=main" >> "$GITHUB_OUTPUT"
|
|
355
|
+
echo "Published version: ${VERSION}"
|
|
356
|
+
|
|
357
|
+
- name: "Create semver tag for non-Joomla repos"
|
|
358
|
+
id: semver
|
|
359
|
+
if: |
|
|
360
|
+
steps.version.outputs.skip != 'true' &&
|
|
361
|
+
!startsWith(steps.platform.outputs.platform, 'joomla')
|
|
362
|
+
run: |
|
|
363
|
+
VERSION="${{ steps.version.outputs.version }}"
|
|
364
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
365
|
+
TOKEN="${{ github.token }}"
|
|
366
|
+
SEMVER_TAG="v${VERSION}"
|
|
367
|
+
|
|
368
|
+
echo "Creating semver tag: ${SEMVER_TAG}"
|
|
369
|
+
|
|
370
|
+
# Create the git tag via API
|
|
371
|
+
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
|
372
|
+
-X POST -H "Authorization: token ${TOKEN}" \
|
|
373
|
+
-H "Content-Type: application/json" \
|
|
374
|
+
"${API_BASE}/tags" \
|
|
375
|
+
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
|
376
|
+
|
|
377
|
+
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
378
|
+
echo "Created semver tag: ${SEMVER_TAG}"
|
|
379
|
+
elif [ "$HTTP_CODE" = "409" ]; then
|
|
380
|
+
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
|
381
|
+
else
|
|
382
|
+
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
|
386
|
+
|
|
387
|
+
- name: Update release notes and promote changelog
|
|
388
|
+
run: |
|
|
389
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
390
|
+
TOKEN="${{ github.token }}"
|
|
391
|
+
|
|
392
|
+
# Get the stable release info (version and ID)
|
|
393
|
+
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
394
|
+
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
|
|
395
|
+
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
396
|
+
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
|
|
397
|
+
VERSION=$(python3 -c "
|
|
398
|
+
import json, sys, re
|
|
399
|
+
r = json.load(sys.stdin)
|
|
400
|
+
name = r.get('name', '')
|
|
401
|
+
m = re.search(r'(\d+\.\d+\.\d+)', name)
|
|
402
|
+
print(m.group(1) if m else '')
|
|
403
|
+
" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
404
|
+
|
|
405
|
+
# Extract [Unreleased] section via the shared mokocli command (#364 centralization).
|
|
406
|
+
NOTES=""
|
|
407
|
+
if [ -f "CHANGELOG.md" ]; then
|
|
408
|
+
NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true)
|
|
409
|
+
# release_notes.php echoes "Release Unreleased" when the section is empty; normalize
|
|
410
|
+
# to empty so the platform fallback below applies.
|
|
411
|
+
[ "$NOTES" = "Release Unreleased" ] && NOTES=""
|
|
412
|
+
fi
|
|
413
|
+
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
414
|
+
|
|
415
|
+
# Update release body via API
|
|
416
|
+
if [ -n "$RELEASE_ID" ]; then
|
|
417
|
+
python3 -c "
|
|
418
|
+
import json, urllib.request
|
|
419
|
+
body = open('/dev/stdin').read()
|
|
420
|
+
payload = json.dumps({'body': body}).encode()
|
|
421
|
+
req = urllib.request.Request(
|
|
422
|
+
'${API_BASE}/releases/${RELEASE_ID}',
|
|
423
|
+
data=payload, method='PATCH',
|
|
424
|
+
headers={
|
|
425
|
+
'Authorization': 'token ${TOKEN}',
|
|
426
|
+
'Content-Type': 'application/json'
|
|
427
|
+
})
|
|
428
|
+
urllib.request.urlopen(req)
|
|
429
|
+
" <<< "$NOTES"
|
|
430
|
+
echo "Release notes updated from CHANGELOG.md"
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
# Promote [Unreleased] → [version] in CHANGELOG.md via the shared mokocli command (#364).
|
|
434
|
+
# changelog_promote.php is idempotent (won't re-promote an existing version), merges
|
|
435
|
+
# duplicate headings, drops blank release sections, and collapses whitespace — verified
|
|
436
|
+
# byte-identical to the former inline promoter across 8 fixtures.
|
|
437
|
+
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
|
438
|
+
php ${MOKO_CLI}/changelog_promote.php --path . --version "$VERSION"
|
|
439
|
+
git add CHANGELOG.md
|
|
440
|
+
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
|
441
|
+
git push origin main || true
|
|
442
|
+
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
|
|
443
|
+
fi
|
|
444
|
+
|
|
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
|
|
451
|
+
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"
|
|
479
|
+
|
|
480
|
+
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
|
|
481
|
+
if: steps.version.outputs.skip != 'true'
|
|
482
|
+
continue-on-error: true
|
|
483
|
+
run: |
|
|
484
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
485
|
+
TOKEN="${{ github.token }}"
|
|
486
|
+
|
|
487
|
+
# Delete rc branch (ephemeral — created by promote-rc)
|
|
488
|
+
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
489
|
+
"${API_BASE}/branches/rc" 2>/dev/null \
|
|
490
|
+
&& echo "Deleted rc branch" || echo "rc branch not found"
|
|
491
|
+
|
|
492
|
+
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
|
|
493
|
+
# (cascade-dev.yml), which runs after this release completes.
|
|
494
|
+
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
|
|
495
|
+
|
|
496
|
+
- name: "Step 12: Create version branch from main"
|
|
497
|
+
if: steps.version.outputs.skip != 'true'
|
|
498
|
+
continue-on-error: true
|
|
499
|
+
run: |
|
|
500
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
501
|
+
TOKEN="${{ github.token }}"
|
|
502
|
+
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
503
|
+
BRANCH_NAME="version/${VERSION}"
|
|
504
|
+
MAIN_SHA=$(git rev-parse HEAD)
|
|
505
|
+
|
|
506
|
+
# Delete old version branch if it exists (same version re-release)
|
|
507
|
+
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
|
508
|
+
|
|
509
|
+
# Create version/XX.YY.ZZ from main
|
|
510
|
+
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
|
511
|
+
|
|
512
|
+
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# -- Post-release: Reset dev version -----------------------------
|
|
517
|
+
- name: "Post-release: Reset dev version"
|
|
518
|
+
if: steps.version.outputs.skip != 'true'
|
|
519
|
+
continue-on-error: true
|
|
520
|
+
run: |
|
|
521
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
522
|
+
php ${MOKO_CLI}/version_reset_dev.php \
|
|
523
|
+
--token "${{ github.token }}" --api-base "${API_BASE}" \
|
|
524
|
+
--branch dev --path . 2>&1 || true
|
|
525
|
+
|
|
526
|
+
# -- Summary --------------------------------------------------------------
|
|
527
|
+
- name: Pipeline Summary
|
|
528
|
+
if: always()
|
|
529
|
+
run: |
|
|
530
|
+
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
531
|
+
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
532
|
+
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
|
533
|
+
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
|
534
|
+
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
|
535
|
+
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
|
536
|
+
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
537
|
+
else
|
|
538
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
539
|
+
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
|
540
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
541
|
+
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
|
542
|
+
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
543
|
+
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
|
544
|
+
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
545
|
+
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
546
|
+
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
547
|
+
echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
|
548
|
+
fi
|
|
549
|
+
|
|
550
|
+
# ── Scheduled safety-net ─────────────────────────────────────────────────────────
|
|
551
|
+
# A merge to main normally fires the `release` job via the pull_request `closed`
|
|
552
|
+
# event. If that event never creates a run (observed when this workflow file is
|
|
553
|
+
# being re-synced from the template in the same window as the merge), the release
|
|
554
|
+
# is silently skipped. This daily job self-heals that: if main's CHANGELOG
|
|
555
|
+
# [Unreleased] section still has content — which the release job empties on a
|
|
556
|
+
# successful promote — it dispatches a normal release. Dependency-free (awk only)
|
|
557
|
+
# so it runs on any runner; never touches the release job's own conditions.
|
|
558
|
+
scheduled-safety-net:
|
|
559
|
+
name: Scheduled release safety-net
|
|
560
|
+
runs-on: ubuntu-latest
|
|
561
|
+
if: github.event_name == 'schedule' && !startsWith(github.event.repository.name, 'Template-')
|
|
562
|
+
permissions:
|
|
563
|
+
contents: read
|
|
564
|
+
steps:
|
|
565
|
+
- name: Checkout main
|
|
566
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
567
|
+
with:
|
|
568
|
+
ref: main
|
|
569
|
+
fetch-depth: 1
|
|
570
|
+
|
|
571
|
+
- name: Dispatch release if main has unreleased changes
|
|
572
|
+
env:
|
|
573
|
+
MOKOGIT_TOKEN: ${{ github.token }}
|
|
574
|
+
run: |
|
|
575
|
+
# [Unreleased] is non-empty precisely when changes were merged but not yet
|
|
576
|
+
# released (the release job promotes it to a version and empties it).
|
|
577
|
+
NOTES=""
|
|
578
|
+
if [ -f CHANGELOG.md ]; then
|
|
579
|
+
NOTES=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{if(f)exit} f{print}' CHANGELOG.md)
|
|
580
|
+
fi
|
|
581
|
+
if [ -z "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]; then
|
|
582
|
+
echo "No unreleased changes on main — safety-net has nothing to do."
|
|
583
|
+
exit 0
|
|
584
|
+
fi
|
|
585
|
+
echo "Unreleased changes detected on main — a merge release was likely missed."
|
|
586
|
+
echo "Dispatching a release via the safety-net."
|
|
587
|
+
HTTP=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
588
|
+
-H "Authorization: token ${MOKOGIT_TOKEN}" \
|
|
589
|
+
-H "Content-Type: application/json" \
|
|
590
|
+
-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)"
|
|
596
|
+
fi
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# FILE INFORMATION
|
|
6
|
+
# DEFGROUP: MokoGIT.Workflow
|
|
7
|
+
# INGROUP: MokoCLI.Universal
|
|
8
|
+
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
9
|
+
# PATH: /.mokogit/workflows/branch-cleanup.yml
|
|
10
|
+
# VERSION: 01.00.00
|
|
11
|
+
# BRIEF: Delete feature branches after PR merge
|
|
12
|
+
|
|
13
|
+
name: "Branch Cleanup"
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
types: [closed]
|
|
18
|
+
|
|
19
|
+
env:
|
|
20
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
cleanup:
|
|
24
|
+
name: Delete merged branch
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
if: >-
|
|
27
|
+
github.event.pull_request.merged == true &&
|
|
28
|
+
github.event.pull_request.head.ref != 'dev' &&
|
|
29
|
+
github.event.pull_request.head.ref != 'main'
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- name: Delete source branch
|
|
33
|
+
# SECURITY: the PR head ref is attacker-controllable. Pass it (and the
|
|
34
|
+
# repo/token) through env so the Actions engine cannot splice it into the
|
|
35
|
+
# shell source, and validate it to a safe charset before use.
|
|
36
|
+
env:
|
|
37
|
+
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
38
|
+
REPO: ${{ github.repository }}
|
|
39
|
+
API_BASE: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
40
|
+
BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
41
|
+
run: |
|
|
42
|
+
set -euo pipefail
|
|
43
|
+
if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
|
44
|
+
echo "::error::unsafe branch name; refusing to proceed"; exit 1
|
|
45
|
+
fi
|
|
46
|
+
API="${API_BASE}/api/v1/repos/${REPO}/branches"
|
|
47
|
+
# URL-encode the branch name's slashes (no PHP dependency on the runner)
|
|
48
|
+
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
|
|
49
|
+
|
|
50
|
+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
|
51
|
+
-H "Authorization: token ${MOKOGIT_TOKEN}" \
|
|
52
|
+
"${API}/${ENCODED}" 2>/dev/null || true)
|
|
53
|
+
|
|
54
|
+
if [ "$STATUS" = "204" ]; then
|
|
55
|
+
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
56
|
+
elif [ "$STATUS" = "404" ]; then
|
|
57
|
+
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
58
|
+
else
|
|
59
|
+
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
|
|
60
|
+
fi
|