@mokoconsulting/mokoabs-mcp 0.0.0-stage → 4.0.2
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 +41 -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 +72 -0
- package/.mokogit/workflows/apidocs.yml +132 -0
- package/.mokogit/workflows/auto-assign.yml +76 -0
- package/.mokogit/workflows/auto-dev-issue.yml +207 -0
- package/.mokogit/workflows/auto-release.yml +1787 -0
- package/.mokogit/workflows/branch-cleanup.yml +63 -0
- package/.mokogit/workflows/cascade.yml +645 -0
- package/.mokogit/workflows/ci-issue-reporter.yml +100 -0
- package/.mokogit/workflows/cleanup.yml +112 -0
- package/.mokogit/workflows/makefile-lint.yml +240 -0
- package/.mokogit/workflows/npm-build-test.yml +507 -0
- package/.mokogit/workflows/npm-sdk-check.yml +110 -0
- package/.mokogit/workflows/npm-tool-inventory.yml +81 -0
- package/.mokogit/workflows/pr-branch-check.yml +92 -0
- package/.mokogit/workflows/pre-release.yml +1134 -0
- package/.mokogit/workflows/rc-revert.yml +75 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +37 -0
- package/CODE_OF_CONDUCT.md +70 -0
- package/CONTRIBUTING.md +164 -0
- package/LICENSE +696 -0
- package/Makefile +94 -0
- package/README.md +109 -2
- package/SECURITY.md +245 -0
- package/build/README.md +7 -0
- package/dist/README.md +7 -0
- package/dist/index.js +153 -0
- package/package.json +34 -5
- package/sample/config.example.json +15 -0
- package/source/client.ts +132 -0
- package/source/config.ts +139 -0
- package/source/credentialStore.ts +216 -0
- package/source/index.ts +235 -0
- package/source/tokenReader.ts +120 -0
- package/source/types.ts +63 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1787 @@
|
|
|
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: 07.24.00
|
|
11
|
+
# BRIEF: NEW (07.24.00): npmjs.org publish now FAILS LOUD (::error:: + exit $rc) on an AUTHORIZATION error (404 "not in this registry" / 401 / 403 / ENEEDAUTH / EOTP) instead of swallowing EVERY failure as a non-failing ::warning:: — a missing-scope / unauthorized / revoked / expired NPM_TOKEN previously hid behind a green release fleet-wide (the "secondary registry, not failing the run" catch masked a 404 PUT, which is exactly how npm reports an unauthorized scoped write; CI had in fact NEVER successfully published any MCP package to npmjs while every run reported success). Transient/5xx/network failures still stay a non-failing ::warning:: (npmjs is the SECONDARY mirror; the MokoGIT registry is primary and already hard-fails), and an unset NPM_TOKEN still skips cleanly — so repos that do not publish to npmjs are 100% unaffected. PREVIOUS (07.23.00): "Vendor PHP dependencies (build-time)" added to BOTH publish legs (promote-rc + stable) — this workflow had NO build-time composer vendoring at all, so any component declaring runtime PHP deps via a sub-package composer.json (vendor-dir admin/vendor, e.g. mPDF in MokonyxCRM/MokonyxCRMDCM) shipped its rc/stable zip WITHOUT admin/vendor while the component manifest lists <folder>vendor</folder> — JInstaller then aborts the ENTIRE package install on-site ("File does not exist [ROOT][TMP]/.../admin/vendor" → "Failed to copy administrator files"). The step mirrors pre-release.yml 07.15.00: no-op without a sub-package composer.json; provisions ext-gd when missing (the release runners' php-cli lacks it, which failed mpdf dependency RESOLUTION outright); verifies the CONFIGURED vendor-dir (composer config vendor-dir) materialized autoload.php; FAILS the build loud when vendoring fails for a package whose manifest ships <folder>vendor</folder> (a green build with an install-bricking artifact is strictly worse than a red build); fail-soft warning for packages whose manifests do not reference vendor/. Runs before `moko publish` (release_publish.php → release_package.php zips the plain working tree; vendor/ was never excluded — the tree simply never had it in CI). PREVIOUS (07.22.00): #498 — the #469 SRC_DIR gate no longer self-traps, and the fast-merge guard is MINOR-identity aware (fixes stable version never advancing; MokoCLI stuck re-publishing v10.20.00). (1) The gate read SRC_DIR with grep|sed, capturing the LITERAL unexpanded `$(shell …)` from the fleet-standard `SRC_DIR := $(shell [ -d src ] && echo src || echo source)` — an unmatchable directory that forced should_bump=false on EVERY push even when source changed. It now resolves SRC_DIR via `make --eval` expansion, falls back to the grep|sed read only if make is unavailable, nulls out a still-unexpanded `$(...)`/backtick value, and treats a declared-but-NONEXISTENT SRC_DIR (repos with code in cli/lib/bin, not src/source) as the non-source denylist fallback instead of an unmatchable INCLUSION prefix. (2) The "Guarantee monotonic release version (fast-merge bump guard)" compared the FULL version, so dev's per-merge PATCH bumps (10.20.00→10.20.04) fooled it into "normal path, no heal" and it re-published the already-released vMAJOR.MINOR.00. It now compares the MINOR identity (MAJOR.MINOR), so a fast dev→stable merge with no promote-rc cuts a new MINOR. Fleet-wide release-semantics change — validate on one repo before syncing. PREVIOUS (07.21.00): #469 — gate the version BUMP on whether the push touched the Makefile SRC_DIR. The former non-code paths-ignore denylist on the push trigger is REMOVED: a docs/workflow/sample push no longer skips the pipeline, it now RUNS in no-bump "update the existing stable release in place" mode (re-stamps the UNCHANGED current version via the existing `moko publish --bump none --no-commit`, refreshes notes/assets, no version advance, no new vNN tag). A new early step "Resolve SRC_DIR bump gate (#469)" (id: srcgate) decides should_bump: (1) ELECTRON (file-detected) or opt-out vars.RELEASE_SRCDIR_GATE=off -> BYPASS, always bump as today (electron has NO source dir, so a naive SRC_DIR test would never bump — a release-breaking trap); (2) else SRC_DIR is REQUIRED — a non-electron repo with no resolvable SRC_DIR FAILS LOUD (::error:: + exit 1), never silently never-bump; (3) else diff the push range (github.event.before..GITHUB_SHA, with a HEAD^1 / merge-base fallback) intersected with ${SRC_DIR}/ — source hit -> should_bump=true, none -> should_bump=false. workflow_dispatch always bumps. The self-heal monotonic bump + the new vNN semver tag are now gated on should_bump!='false' so no-bump mode keeps the version PUT; the publish + "Update release notes and reconcile changelog" (channel-pointer resolution) run on BOTH paths to refresh the current release. Loop-safety is unchanged: the job if: still re-checks [skip ci]/[skip sync], so the workflow's own commits never re-trigger. Fleet-wide release-semantics change — validate on one repo before syncing. PREVIOUS (07.20.00): the dev/promote bump + release steps now name the per-run token env var GITHUB_TOKEN (was the misleadingly-named MOKOGIT_TOKEN; the VALUE was always ${{ github.token }}). All token use in this workflow is SAME-REPO (release API, the release_bump_dev dev base bump, pushes to its own repo) — the org MOKOGIT_TOKEN secret is not referenced here. Naming-only; no behavior change. Mirrors the pre-release.yml fix (MokoGIT#1721). PREVIOUS: NEW (07.19.00): #1621 Phase 6 / .mokogit-private#76 — resolve the CURRENT stable release by its CHANNEL POINTER, not by a fixed tag_name=stable. Under version-primary releases (a release is born with the immutable tag_name=vNN and `stable`/`rc` attached as movable is_channel tags), GET /releases/tags/stable no longer resolves the current stable release, which would break two steps that read "the last released version". Both now call GET /repos/{owner}/{repo}/releases/channel/stable (#1702 — resolves whichever release the `stable` channel points at via ReleaseStreamMap) with a GRACEFUL FALLBACK to the legacy GET /releases/tags/stable for forges predating #1702 (the channel route 404s pre-rollout and the rolling `stable` tag still exists). (1) The monotonic-version guard ("Guarantee monotonic release version") resolves ROLLING_LAST by parsing the same `VERSION: NN.NN.NN` out of the channel-resolved release NAME. (2) "Update release notes and reconcile changelog" resolves the stable RELEASE_ID + VERSION the same channel-first way. The #386 integrity gate is UNCHANGED (it already keys off the immutable vNN version tag). SEQUENCING unchanged: do NOT enable in prod until #1702 server support is on stable/prod; works pre-#1702 via the fallback. PREVIOUS (07.18.00): #1621 Phase 6 — version/* BRANCHES retired in favor of dual-tagged version RELEASES. REMOVED three now-redundant steps: "Step 12: Create version branch from main", "Step 13: Prune superseded-major version branches", and "Advance rolling channel tag via git". A release's permanent version identity is now the git tag v<version>, attached by the publish as a MOVABLE channel tag on the dual-tagged release (MokoCLI release_publish -> release_create channel_tags[]/update_stream; the forge attaches/moves the tag at the release commit via AttachChannelTag server-side) — so a new cut is born dual-tagged (rolling `stable`/`release-candidate` primary tag + permanent v<version> channel tag) and the channel tag MOVES onto the new release instead of a new version/* branch being created. The rolling `stable` tag is still pinned by release_create forceTagRef() (#808), so the "Advance rolling channel tag" force-push was redundant. KEPT UNCHANGED: "Create semver tag" (v<version>), the #386 integrity gate (still resolves tags/stable to the built commit), and the monotonic guard (still reads the stable release name VERSION: NN.NN.NN). Pruning EXISTING version/* branches is a SEPARATE one-time manual reconcile — not done here. SEQUENCING: do NOT enable in prod until the #1621 SERVER channel_tags support is on stable/prod. PREVIOUS (07.17.00): Setup MokoCLI tools no longer installs a STALE mokocli during the registry publish-lag window (#1671). After the fresh registry install, the step now compares the installed /tmp/mokocli PROJECT_VERSION against the /opt/mokocli failsafe and PREFERS whichever is NEWER (sort -V), and the opportunistic /opt refresh is gated to only move FORWARD (never downgrade /opt when it is the newer, just-deployed copy). Backward-compatible: runners with no /opt/mokocli use the registry copy exactly as before. PREVIOUS (07.16.00): release-integrity + dev-channel fixes. (1) A new pre-publish "no stale (pre-merge) tree" guard (#428/MokoCLI#901) hard-FAILS the release if the triggering GITHUB_SHA is an ANCESTOR of the current branch tip (a concurrent merge landed mid-run) -- otherwise a green, version-bumped release silently ships old code missing that merge (reproduced: built ef8c1a5d while tip was 2740342). Fail-open on any unresolved/replica-lagged tip so a good release is never falsely blocked. Complements the mokocli engine assertion (packaged-tree == GITHUB_SHA), which cannot see that GITHUB_SHA itself trails the tip. (2) The "Post-release: Reset dev version" safety-net (#309/MokoCLI#900) no longer swallows failure: a failed dev-advance now emits ::error:: + a job-summary block (dev-channel would trail stable), and a SUCCESSFUL advance dispatches pre-release.yml (development) to actually publish the leading dev build -- the advance commit is [skip ci]+paths-ignored and never auto-fires. Stays green (continue-on-error) since stable already published. NEW (07.15.00): the rolling stable channel tag is now advanced via a git force-push BEFORE the #386 integrity gate. MokoGIT's tags API 405s on the release-attached `stable` tag, so release_publish.php could not move it ("Could not pin tag stable ... HTTP 405") and every Joomla/electron stable cut left the tag lagging the freshly-built commit -- red-failing a correct release on the integrity gate. A new step git-force-pushes refs/tags/stable to the built HEAD (force-push allowlist -- the same escape hatch the rc BRANCH pin already uses), so the gate passes and no manual tag repoint is needed. PREVIOUS (07.14.00): integrity gate #386 no longer false-fails. It compared the published tag against GITHUB_SHA (the triggering commit), but the publish step commits the CHANGELOG entry ("build NN.NN.NN [skip ci]") and mokocli pins the channel tag + semver tag to that POST-PUBLISH HEAD -- so every cut whose CHANGELOG wasn't pre-seeded on dev (e.g. a version set straight on the release branch) diverged by one commit and red-failed a correct release. The gate now compares against the actual built/published HEAD (git rev-parse HEAD, falling back to GITHUB_SHA), which equals what the tag is pinned to in both the commit and no-commit cases while still catching a tag pinned to a commit we did NOT build; the semver tag is pinned to the same built HEAD for consistency. PREVIOUS (07.13.00): RC is ON-DEMAND ONLY. The `pull_request` trigger (RC-preview on a PR to main/stable) is REMOVED and the promote-rc job now runs solely on workflow_dispatch action=promote-rc. Only dev + stable release automatically (dev via pre-release.yml push->dev; stable via the push->main release job here); alpha/beta/rc never auto-cut. PREVIOUS (07.12.00): electron repos create NO release on ANY channel (RC enabled=false + stable publish/tag/notes/integrity guarded on is_electron; opt back in with vars.ELECTRON_CI_BUILD=true) and build NO installer in CI — always built locally + uploaded manually; the dev/promote version BUMP still runs. Template-Generic #390. NEW (07.11.00): the electron installer build/attach steps (electron-builder via Wine/Xvfb) are gated to MANUAL workflow_dispatch only (`&& github.event_name == 'workflow_dispatch'`). An automatic push no longer auto-builds electron installers (heavy ~90m, runner-fragile — OOM/Wine-zombie; migrating to mokocli ElectronPackager, MokoCLI #756/#758). The version bump, CHANGELOG, tag and npm publish STILL run on push — only the electron installer is skipped and pushed MANUALLY via dispatch. Non-electron platforms (joomla/go/npm) unaffected. Template-Generic #390. Universal build & release - BUMP-ON-DEV: base version + CHANGELOG promoted on dev pre-promotion (promote-rc), main publishes --bump none --no-commit so it never diverges from dev. MokoCLI build agent (moko publish). NEW (07.06.00): npm stable publishing ABSORBED here from Template-NPM's retired npm-publish.yml as npm-gated dual-registry steps (MokoGIT registry via per-run token + npmjs.org best-effort), replacing the former npm-publish.yml dispatch. No-Makefile repos SKIP the release job gracefully (green) instead of failing. Conflict-marker scan reads git-tracked files only (git ls-files | xargs grep) so vendored/generated trees can't false-match. NEW (07.07.00): npm stamp normalizes the zero-padded Makefile PROJECT_VERSION (NN.NN.NN) to strict semver via awk before `npm version`, and an unstampable version now HARD-FAILS instead of silently keeping the old package.json version (#382). NEW (07.00.00): `moko assets:optimize` artifact-time step added before publish (graceful-skip on non-asset repos); Setup MokoCLI tools makes the fresh `moko` executable + first on PATH to fix exit-126 (mokocli #650).
|
|
12
|
+
#
|
|
13
|
+
# +=======================================================================+
|
|
14
|
+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
|
15
|
+
# +=======================================================================+
|
|
16
|
+
# | |
|
|
17
|
+
# | Native-manifest platform detect (joomla|generic) drives branching. |
|
|
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
|
+
# Stable release on every PUSH to main (a merged promotion) -- the same
|
|
29
|
+
# push-based principle as pre-release's dev-channel build. A push trigger is
|
|
30
|
+
# race-free vs the old pull_request:closed, which could miss its run when this
|
|
31
|
+
# workflow file re-synced from the template concurrently with the merge.
|
|
32
|
+
# Loop-safe: the workflow's own commits to main carry [skip ci]/[skip sync]
|
|
33
|
+
# (mokocli's build commit + the changelog-promote commit), and the release job's
|
|
34
|
+
# if: re-checks [skip ci]/[skip sync], so a self-push never re-triggers a release.
|
|
35
|
+
# #469: the former non-code paths-ignore denylist (which SKIPPED docs/CI-only
|
|
36
|
+
# pushes entirely) is REMOVED. A docs/workflow/sample push must now RUN the
|
|
37
|
+
# pipeline in no-bump "update existing release" mode (refresh notes/assets on the
|
|
38
|
+
# current stable release, no version bump, no new vNN tag) rather than be dropped.
|
|
39
|
+
# The bump-vs-no-bump decision is made in-job by the SRC_DIR-diff gate (the
|
|
40
|
+
# "Resolve SRC_DIR bump gate" step: steps.srcgate.outputs.should_bump), not by a
|
|
41
|
+
# trigger-level path filter — so no release signal is ever lost. Loop-safety is
|
|
42
|
+
# still fully covered by the [skip ci]/[skip sync] re-checks in the job if:.
|
|
43
|
+
push:
|
|
44
|
+
branches:
|
|
45
|
+
- main
|
|
46
|
+
- stable
|
|
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
|
+
packages: write
|
|
68
|
+
|
|
69
|
+
concurrency:
|
|
70
|
+
# Serialise the release path per CHANNEL BRANCH (ref), not per commit (#423):
|
|
71
|
+
# a rapid sequence of DISTINCT stable commits shares one group, so the newest
|
|
72
|
+
# push SUPERSEDES older *queued* releases (concurrency keeps one running + only
|
|
73
|
+
# the latest pending, cancelling older pending runs) -- avoiding a full
|
|
74
|
+
# build+publish+deploy of a commit that has already been superseded. Same-commit
|
|
75
|
+
# push / schedule / workflow_dispatch still collapse (same ref), preserving the
|
|
76
|
+
# fleet sync-burst de-dup. cancel-in-progress stays FALSE so a publish already in
|
|
77
|
+
# progress is NEVER cancelled (cancelling mid-publish could leave a half-created
|
|
78
|
+
# release -- npmjs published but Gitea-registry publish dropped); only *pending*
|
|
79
|
+
# superseded runs are dropped. Mirrors pre-release.yml's per-ref grouping.
|
|
80
|
+
group: auto-release-${{ github.workflow }}-${{ github.ref }}
|
|
81
|
+
cancel-in-progress: false
|
|
82
|
+
|
|
83
|
+
jobs:
|
|
84
|
+
# -- On-demand RC promote (workflow_dispatch action=promote-rc) --------------------
|
|
85
|
+
# RC is NO LONGER auto-built on a PR to main/stable. A release-candidate is cut ONLY
|
|
86
|
+
# when a human runs this workflow with action=promote-rc. Only dev + stable release
|
|
87
|
+
# automatically (dev via pre-release.yml push->dev; stable via the push->main job below).
|
|
88
|
+
promote-rc:
|
|
89
|
+
name: Promote to RC
|
|
90
|
+
# LITERAL runner label — see #366/#134. Gitea does not resolve the
|
|
91
|
+
# `${{ vars.RELEASE_RUNNER_LABEL || 'release' }}` fallback for an unset var in
|
|
92
|
+
# `runs-on`, so the job aborts with no runner. The registered label is `release`.
|
|
93
|
+
runs-on: release
|
|
94
|
+
# Own-repo writes: move rc + create release (contents:write), and dispatch
|
|
95
|
+
# deploy-rc (actions:write) -- all via the per-repo github.token.
|
|
96
|
+
permissions:
|
|
97
|
+
contents: write
|
|
98
|
+
pull-requests: write
|
|
99
|
+
actions: write
|
|
100
|
+
# Skip on template repos (Template-*) -- they scaffold other repos and do not release.
|
|
101
|
+
# RC is ON-DEMAND ONLY: this job runs solely on a manual workflow_dispatch with
|
|
102
|
+
# action=promote-rc. The former auto-trigger (PR dev->main/stable) was removed so a
|
|
103
|
+
# promotion PR no longer auto-cuts an RC (and template-sync PRs can't spawn an
|
|
104
|
+
# RC-promote storm). A human explicitly chooses to run it.
|
|
105
|
+
if: >-
|
|
106
|
+
github.event.repository.name != '.vault' &&
|
|
107
|
+
github.event_name == 'workflow_dispatch' &&
|
|
108
|
+
inputs.action == 'promote-rc'
|
|
109
|
+
|
|
110
|
+
steps:
|
|
111
|
+
- name: Checkout repository
|
|
112
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
113
|
+
with:
|
|
114
|
+
token: ${{ github.token }}
|
|
115
|
+
fetch-depth: 1
|
|
116
|
+
submodules: false
|
|
117
|
+
|
|
118
|
+
# #1672: the actions/checkout above intermittently aborts at step-0 under
|
|
119
|
+
# release-time load on the shared `release` runners (git fetch timeout /
|
|
120
|
+
# workspace contention). A plain re-run usually gets a clean checkout, so the
|
|
121
|
+
# fault is transient/environmental, not code. This fallback fires ONLY when
|
|
122
|
+
# the checkout above failed (step-level `if: failure()` — a LITERAL, never a
|
|
123
|
+
# continue-on-error expression, which Gitea would discard the whole workflow
|
|
124
|
+
# over) and re-materialises the workspace via a bounded 3-attempt git clone
|
|
125
|
+
# with backoff. It does NOT mask real failures: a genuinely bad ref makes all
|
|
126
|
+
# three attempts fail and the step exits 1 (loud). Independent step — no
|
|
127
|
+
# needs:+always() fan-in (which act_runner strands).
|
|
128
|
+
- name: "Checkout fallback (retry transient flake) (#1672)"
|
|
129
|
+
if: failure()
|
|
130
|
+
env:
|
|
131
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
132
|
+
run: |
|
|
133
|
+
set -u
|
|
134
|
+
TOK="$(printf '%s' "$GITHUB_TOKEN" | tr -d '[:space:]')"; echo "::add-mask::$TOK"
|
|
135
|
+
SERVER="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}"
|
|
136
|
+
CLONE_URL="https://x-access-token:${TOK}@${SERVER#https://}/${GITHUB_REPOSITORY}.git"
|
|
137
|
+
REF="${GITHUB_SHA:-${GITHUB_REF_NAME:-}}"
|
|
138
|
+
echo "Retrying checkout of ${GITHUB_REPOSITORY}@${REF:-<HEAD>} into ${GITHUB_WORKSPACE}"
|
|
139
|
+
ok=0
|
|
140
|
+
for attempt in 1 2 3; do
|
|
141
|
+
rm -rf "${GITHUB_WORKSPACE:?}/.git" 2>/dev/null || true
|
|
142
|
+
find "${GITHUB_WORKSPACE}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
143
|
+
if git clone --depth 1 "$CLONE_URL" "${GITHUB_WORKSPACE}" \
|
|
144
|
+
&& { [ -z "$REF" ] || git -C "${GITHUB_WORKSPACE}" fetch --depth 1 origin "$REF" && git -C "${GITHUB_WORKSPACE}" checkout -f FETCH_HEAD; }; then
|
|
145
|
+
ok=1; echo "Fallback checkout succeeded on attempt ${attempt}."; break
|
|
146
|
+
fi
|
|
147
|
+
echo "::warning::Fallback checkout attempt ${attempt} failed; backing off $((attempt * 5))s"
|
|
148
|
+
sleep $((attempt * 5))
|
|
149
|
+
done
|
|
150
|
+
if [ "$ok" != "1" ]; then
|
|
151
|
+
echo "::error::Checkout failed after 3 retry attempts (#1672) — this is not a transient flake, failing loudly."
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
- name: "Require Makefile (auto-release gate)"
|
|
156
|
+
# RC promotion is part of auto-release; it must also refuse to run without
|
|
157
|
+
# a Makefile (see the release job for rationale). Post-checkout step because
|
|
158
|
+
# a job-level if: cannot inspect the workspace.
|
|
159
|
+
run: |
|
|
160
|
+
if [ ! -f Makefile ] && [ ! -f makefile ] && [ ! -f GNUmakefile ]; then
|
|
161
|
+
echo "::error::auto-release (RC promote) requires a Makefile at the repository root -- none found. Aborting."
|
|
162
|
+
echo "## RC promote blocked: no Makefile at repo root" >> "$GITHUB_STEP_SUMMARY"
|
|
163
|
+
exit 1
|
|
164
|
+
fi
|
|
165
|
+
echo "Makefile present -- RC promote may proceed."
|
|
166
|
+
|
|
167
|
+
- name: Setup MokoCLI tools
|
|
168
|
+
run: |
|
|
169
|
+
# Install mokocli FRESH from the MokoGIT Composer registry on every run
|
|
170
|
+
# (PRIMARY) so CI always uses the latest published version. /opt/mokocli is
|
|
171
|
+
# a FAILSAFE only — its refresh unit (mokocli-sync.service) is disabled, so
|
|
172
|
+
# the on-disk snapshot rots and must NOT be preferred. Registry path
|
|
173
|
+
# /api/packages/** is anon-allowlisted (no GITHUB_TOKEN); it serves a dist
|
|
174
|
+
# zip (no git clone). --no-install: mokocli self-bootstraps via bundled lib/
|
|
175
|
+
# + a fallback PSR-4 autoloader (native ext-curl), so no vendor/ is needed.
|
|
176
|
+
if ! command -v composer > /dev/null 2>&1; then
|
|
177
|
+
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
|
|
178
|
+
fi
|
|
179
|
+
rm -rf /tmp/mokocli
|
|
180
|
+
REG='{"type":"composer","url":"https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer"}'
|
|
181
|
+
if composer create-project --no-install --no-interaction --no-progress --quiet \
|
|
182
|
+
--repository="${REG}" mokoconsulting/mokocli /tmp/mokocli >/dev/null 2>&1 \
|
|
183
|
+
&& [ -f /tmp/mokocli/cli/version_bump.php ]; then
|
|
184
|
+
echo "Installed mokocli fresh from the Composer registry (primary)"
|
|
185
|
+
# #1671: registry-publish LAG race. The registry install "succeeds" but can
|
|
186
|
+
# serve the PREVIOUS package for ~tens of seconds after a MokoCLI release
|
|
187
|
+
# (publish finishes AFTER the release run reports success). Meanwhile the
|
|
188
|
+
# /opt/mokocli deploy (pull-timer) may already carry the just-released,
|
|
189
|
+
# NEWER engine. So instead of blindly using the registry copy, compare the
|
|
190
|
+
# two on-disk versions and PREFER whichever is newer. The Makefile
|
|
191
|
+
# PROJECT_VERSION is the version source of truth and ships in both the
|
|
192
|
+
# composer dist and /opt/mokocli. Backward-compatible: with no /opt/mokocli
|
|
193
|
+
# (most consumer runners), the registry copy is used exactly as before.
|
|
194
|
+
mk_ver() { sed -n 's/^[[:space:]]*PROJECT_VERSION[[:space:]]*:*=[[:space:]]*//p' "$1" 2>/dev/null | head -n1 | tr -d '[:space:]'; }
|
|
195
|
+
ver_newer() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]; }
|
|
196
|
+
REG_VER="$(mk_ver /tmp/mokocli/Makefile)"
|
|
197
|
+
OPT_VER="$(mk_ver /opt/mokocli/Makefile)"
|
|
198
|
+
MOKO_ROOT=/tmp/mokocli
|
|
199
|
+
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -n "$OPT_VER" ] && [ -n "$REG_VER" ] \
|
|
200
|
+
&& ver_newer "$OPT_VER" "$REG_VER"; then
|
|
201
|
+
echo "::notice::/opt/mokocli (${OPT_VER}) is newer than the registry copy (${REG_VER}) — using /opt (#1671 publish-lag race)."
|
|
202
|
+
MOKO_ROOT=/opt/mokocli
|
|
203
|
+
else
|
|
204
|
+
echo "Using registry copy (${REG_VER:-unknown}); /opt/mokocli=${OPT_VER:-none}"
|
|
205
|
+
fi
|
|
206
|
+
echo "MOKO_CLI=${MOKO_ROOT}/cli" >> $GITHUB_ENV
|
|
207
|
+
# #650: make the chosen moko executable + FIRST on PATH so bare `moko`
|
|
208
|
+
# never resolves to a stale/non-executable copy (exit 126).
|
|
209
|
+
for d in bin cli; do
|
|
210
|
+
if [ -f "${MOKO_ROOT}/$d/moko" ]; then
|
|
211
|
+
# #1263 defense-in-depth: strip CR bytes so a CRLF-tainted registry
|
|
212
|
+
# dist or rotted /opt snapshot can't yield `/usr/bin/env: 'php\r'`
|
|
213
|
+
# (exit 127). tr interprets \r as a real CR (unlike portable sed).
|
|
214
|
+
# /opt may be root-owned — write via a temp file + sudo when needed.
|
|
215
|
+
if tr -d '\r' < "${MOKO_ROOT}/$d/moko" > "/tmp/moko.lf" 2>/dev/null; then
|
|
216
|
+
if cp "/tmp/moko.lf" "${MOKO_ROOT}/$d/moko" 2>/dev/null; then :; \
|
|
217
|
+
else sudo cp "/tmp/moko.lf" "${MOKO_ROOT}/$d/moko" 2>/dev/null || true; fi
|
|
218
|
+
rm -f "/tmp/moko.lf"
|
|
219
|
+
fi
|
|
220
|
+
chmod +x "${MOKO_ROOT}/$d/moko" 2>/dev/null || sudo chmod +x "${MOKO_ROOT}/$d/moko" 2>/dev/null || true
|
|
221
|
+
echo "${MOKO_ROOT}/$d" >> "$GITHUB_PATH"
|
|
222
|
+
fi
|
|
223
|
+
done
|
|
224
|
+
sudo chmod +x /opt/mokocli/*/moko 2>/dev/null || true
|
|
225
|
+
# Opportunistically refresh the /opt failsafe from this fresh copy when it
|
|
226
|
+
# differs — keeps the disabled-sync snapshot current so the fallback is
|
|
227
|
+
# never far behind. Best-effort: needs sudo, gated on a diff to avoid
|
|
228
|
+
# fleet-wide churn, never fails the job. #1671: only refresh FORWARD — skip
|
|
229
|
+
# when /opt is the newer copy we just chose, so we never downgrade it.
|
|
230
|
+
if [ "$MOKO_ROOT" = /tmp/mokocli ] && command -v sudo >/dev/null 2>&1 && [ -d /opt/mokocli ] \
|
|
231
|
+
&& ! cmp -s /tmp/mokocli/cli/version_reset_dev.php /opt/mokocli/cli/version_reset_dev.php 2>/dev/null; then
|
|
232
|
+
sudo cp -a /tmp/mokocli/. /opt/mokocli/ 2>/dev/null \
|
|
233
|
+
&& echo "Refreshed /opt/mokocli failsafe from the fresh registry copy" \
|
|
234
|
+
|| echo " (/opt failsafe refresh skipped — no sudo/perms)"
|
|
235
|
+
fi
|
|
236
|
+
elif [ -f /opt/mokocli/cli/version_bump.php ]; then
|
|
237
|
+
echo "::warning::Registry install failed — using /opt/mokocli FAILSAFE (may be stale)"
|
|
238
|
+
echo "MOKO_CLI=/opt/mokocli/cli" >> $GITHUB_ENV
|
|
239
|
+
else
|
|
240
|
+
echo "::error::mokocli unavailable: registry install failed and no /opt/mokocli failsafe present"
|
|
241
|
+
exit 1
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
- name: "Bump base version + promote changelog on dev (pre-promotion)"
|
|
245
|
+
env:
|
|
246
|
+
# Contents-API writes to protected dev need the admin-bypass token
|
|
247
|
+
# (github.token cannot bypass branch protection). Same token/primitive
|
|
248
|
+
# as version_reset_dev.php and the changelog persist below (#510).
|
|
249
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
250
|
+
# Pass the PR head ref via env — never interpolate ${{ }} into run:
|
|
251
|
+
# (a crafted branch name could inject shell). #1823
|
|
252
|
+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
253
|
+
run: |
|
|
254
|
+
# "Bump on dev" (release-engineering inversion): move the base-version
|
|
255
|
+
# bump AND the CHANGELOG [Unreleased] promotion to dev BEFORE the
|
|
256
|
+
# promotion, so the rc cut (force-pushed from dev below) and the eventual
|
|
257
|
+
# main INHERIT the final version + promoted changelog. Nothing new is
|
|
258
|
+
# minted on main, so dev stays a clean superset and dev->main stops
|
|
259
|
+
# conflicting on Makefile/manifest/package.json/CHANGELOG.
|
|
260
|
+
#
|
|
261
|
+
# Only dev-headed promotions bump minor. fix/patch/hotfix/bugfix work is
|
|
262
|
+
# patch-bumped per-merge on dev by pre-release.yml (there is NO separate
|
|
263
|
+
# auto-bump.yml) — do NOT double-bump them (mirrors the release job's bump-level gate). Only runs
|
|
264
|
+
# for a real dev promotion (PR head == dev), never for a workflow_dispatch
|
|
265
|
+
# promote-rc (no PR head) — which is a manual re-cut of the current dev.
|
|
266
|
+
if [ "$HEAD_REF" != "dev" ]; then
|
|
267
|
+
echo "PR head '${HEAD_REF:-<none>}' is not dev — skipping dev pre-bump (fix/hotfix or manual re-cut)."
|
|
268
|
+
exit 0
|
|
269
|
+
fi
|
|
270
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
271
|
+
# release_bump_dev.php: minor-bump the base on dev + promote CHANGELOG on
|
|
272
|
+
# dev, both via the Contents API. Idempotent + monotonic (never moves dev
|
|
273
|
+
# down), so a PR re-synchronize that re-runs this step is a safe no-op.
|
|
274
|
+
php ${MOKO_CLI}/release_bump_dev.php \
|
|
275
|
+
--path . --branch dev --bump minor \
|
|
276
|
+
--token "${GITHUB_TOKEN}" --api-base "${API_BASE}"
|
|
277
|
+
|
|
278
|
+
- name: "Resolve releases-enabled"
|
|
279
|
+
id: rel
|
|
280
|
+
run: |
|
|
281
|
+
# Releases on/off gate (.mokogit-private#73): the pre-promotion BUMP
|
|
282
|
+
# above runs on every repo; RC-PROMOTION (rename rc, deploy, publish)
|
|
283
|
+
# is gated on the repo's Releases unit so releases-off repos (Template-*)
|
|
284
|
+
# bump WITHOUT promoting/publishing. Replaces the old
|
|
285
|
+
# !startsWith(name,'Template-') name gate. Defaults ON unless the API
|
|
286
|
+
# explicitly returns has_releases=false (transient error never disables).
|
|
287
|
+
set +e
|
|
288
|
+
HR=$(curl -sf -H "Authorization: token ${{ github.token }}" "${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" | jq -r '.has_releases' 2>/dev/null)
|
|
289
|
+
if [ "$HR" = "false" ]; then EN=false; else EN=true; fi
|
|
290
|
+
# Electron: no CI release on ANY channel (local build + manual upload);
|
|
291
|
+
# opt back in with vars.ELECTRON_CI_BUILD=true. The dev bump above still runs.
|
|
292
|
+
if { [ -f electron-builder.yml ] || [ -f electron-builder.yaml ]; } && [ "${{ vars.ELECTRON_CI_BUILD }}" != "true" ]; then
|
|
293
|
+
EN=false
|
|
294
|
+
echo "::notice::electron repo — RC release disabled (local build + manual upload)"
|
|
295
|
+
fi
|
|
296
|
+
echo "enabled=${EN}" >> "$GITHUB_OUTPUT"
|
|
297
|
+
echo "RC-promotion gate: enabled=${EN} (has_releases=${HR:-unknown})"
|
|
298
|
+
|
|
299
|
+
- name: Rename branch to rc
|
|
300
|
+
if: steps.rel.outputs.enabled == 'true'
|
|
301
|
+
env:
|
|
302
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
303
|
+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
304
|
+
run: |
|
|
305
|
+
PUSH_TOK="$(printf '%s' "$GITHUB_TOKEN" | tr -d '[:space:]')"; [ -z "$PUSH_TOK" ] && PUSH_TOK="${{ github.token }}"; echo "::add-mask::$PUSH_TOK"
|
|
306
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
307
|
+
AUTH="Authorization: token ${{ github.token }}"
|
|
308
|
+
FROM="${HEAD_REF:-dev}"
|
|
309
|
+
PR="${{ github.event.pull_request.number }}"
|
|
310
|
+
|
|
311
|
+
# Resolve the source branch HEAD commit.
|
|
312
|
+
SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
|
|
313
|
+
|| { echo "::error::Source branch ${FROM} not found"; exit 1; }
|
|
314
|
+
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
|
|
315
|
+
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
|
|
316
|
+
|
|
317
|
+
# Point rc at the source commit via git push. Git's git/refs PATCH API
|
|
318
|
+
# returns HTTP 405 on ANY protected branch (force or not, even for a user in
|
|
319
|
+
# the force-push allowlist), so it cannot move a protected rc. git push honors
|
|
320
|
+
# the push + force-push allowlists and creates rc if it is absent.
|
|
321
|
+
PUSH_URL="https://x-access-token:${PUSH_TOK}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git"
|
|
322
|
+
git config --global user.name "mokogit-actions[bot]"
|
|
323
|
+
git config --global user.email "actions@mokoconsulting.tech"
|
|
324
|
+
git fetch --no-tags "$PUSH_URL" "${FROM}"
|
|
325
|
+
git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \
|
|
326
|
+
|| { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; }
|
|
327
|
+
echo "rc set to ${FROM} (${SRC_SHA})"
|
|
328
|
+
|
|
329
|
+
# Repoint the PR at rc, then delete the old source branch (non-fatal).
|
|
330
|
+
if [ -n "$PR" ]; then
|
|
331
|
+
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
332
|
+
"${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
|
|
333
|
+
fi
|
|
334
|
+
# Never delete permanent branches (dev/main/rc/...); only ephemeral feature branches.
|
|
335
|
+
case "$FROM" in
|
|
336
|
+
dev|main|master|rc|stable|production|release|develop|staging|beta|alpha)
|
|
337
|
+
echo "Keeping permanent branch ${FROM} (not deleting)" ;;
|
|
338
|
+
*)
|
|
339
|
+
curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true ;;
|
|
340
|
+
esac
|
|
341
|
+
echo "Renamed ${FROM} -> rc"
|
|
342
|
+
|
|
343
|
+
# RC deploy is now INLINE (see "Deploy to RC" below, after the rc checkout).
|
|
344
|
+
# The former "Trigger RC deploy" step dispatched a non-existent deploy-rc.yml
|
|
345
|
+
# and always warn-failed; the standalone deploy.yml is deleted in #1427.
|
|
346
|
+
|
|
347
|
+
- name: Checkout rc and configure git
|
|
348
|
+
if: steps.rel.outputs.enabled == 'true'
|
|
349
|
+
env:
|
|
350
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
351
|
+
run: |
|
|
352
|
+
PUSH_TOK="$(printf '%s' "$GITHUB_TOKEN" | tr -d '[:space:]')"; [ -z "$PUSH_TOK" ] && PUSH_TOK="${{ github.token }}"; echo "::add-mask::$PUSH_TOK"
|
|
353
|
+
# Pin the BRANCH ref (refs/heads/rc): the channel-named branch model (#1294)
|
|
354
|
+
# lets an `rc` release TAG and the `rc` BRANCH coexist, so a bare
|
|
355
|
+
# `git fetch origin rc` / `git checkout rc` is ambiguous and could grab the tag.
|
|
356
|
+
git fetch origin "+refs/heads/rc:refs/remotes/origin/rc"
|
|
357
|
+
git checkout -B rc origin/rc
|
|
358
|
+
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
359
|
+
git config --local user.name "mokogit-actions[bot]"
|
|
360
|
+
git remote set-url origin "https://x-access-token:${PUSH_TOK}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
361
|
+
|
|
362
|
+
# Build-time PHP dependency vendoring (universal, guarded — mirrors
|
|
363
|
+
# pre-release.yml 07.15.00). If a sub-package ships a composer.json, fetch its
|
|
364
|
+
# runtime deps into that package's CONFIGURED vendor dir (e.g. admin/vendor —
|
|
365
|
+
# mPDF) BEFORE `moko publish` zips the working tree. No-op without a
|
|
366
|
+
# sub-package composer.json. FAILS LOUD when vendoring fails for a package
|
|
367
|
+
# whose Joomla manifest ships <folder>vendor</folder>: shipping that zip
|
|
368
|
+
# without vendor/ makes JInstaller abort the whole package install on-site
|
|
369
|
+
# ("File does not exist .../admin/vendor" — MokonyxCRMDCM/MokonyxCRM), which
|
|
370
|
+
# is strictly worse than a red release run.
|
|
371
|
+
- name: Vendor PHP dependencies (build-time)
|
|
372
|
+
if: steps.rel.outputs.enabled == 'true'
|
|
373
|
+
run: |
|
|
374
|
+
set -uo pipefail
|
|
375
|
+
CJS="$(find source/packages src/packages -maxdepth 2 -name composer.json 2>/dev/null || true)"
|
|
376
|
+
if [ -z "$CJS" ]; then echo "No sub-package composer.json — nothing to vendor."; exit 0; fi
|
|
377
|
+
command -v composer >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq composer >/dev/null 2>&1 || true; }
|
|
378
|
+
# mPDF (and most PHP PDF/image libs) hard-require ext-gd; the runners'
|
|
379
|
+
# php-cli ships without it, which fails dependency RESOLUTION outright.
|
|
380
|
+
if ! php -m 2>/dev/null | grep -qi '^gd$'; then
|
|
381
|
+
PHPV="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null || true)"
|
|
382
|
+
sudo apt-get update -qq || true
|
|
383
|
+
sudo apt-get install -y -qq "php${PHPV}-gd" || sudo apt-get install -y -qq php-gd || echo "::warning::php-gd install failed (php ${PHPV})"
|
|
384
|
+
php -m 2>/dev/null | grep -qi '^gd$' || echo "::warning::ext-gd STILL missing after install attempt — vendoring will fail for gd-requiring packages"
|
|
385
|
+
fi
|
|
386
|
+
rc=0
|
|
387
|
+
for cj in $CJS; do
|
|
388
|
+
dir="$(dirname "$cj")"
|
|
389
|
+
echo "::notice::composer install (no-dev) in ${dir}"
|
|
390
|
+
ok=true
|
|
391
|
+
composer install --no-dev --no-interaction --no-progress --optimize-autoloader --working-dir="$dir" || ok=false
|
|
392
|
+
vd="$(composer config vendor-dir --working-dir="$dir" 2>/dev/null || true)"; vd="${vd:-vendor}"
|
|
393
|
+
[ -f "${dir}/${vd}/autoload.php" ] || ok=false
|
|
394
|
+
if [ "$ok" = "true" ]; then
|
|
395
|
+
echo "vendored: ${dir}/${vd}"
|
|
396
|
+
elif grep -qs '<folder>vendor</folder>' "${dir}"/*.xml; then
|
|
397
|
+
echo "::error::vendoring FAILED in ${dir} but its Joomla manifest ships <folder>vendor</folder> — the built zip would abort the whole package install on JInstaller ('File does not exist .../vendor'). Failing the build instead of shipping a broken package."
|
|
398
|
+
rc=1
|
|
399
|
+
else
|
|
400
|
+
echo "::warning::composer install failed in ${dir} — manifest does not ship vendor/; packaging the existing tree"
|
|
401
|
+
fi
|
|
402
|
+
done
|
|
403
|
+
exit $rc
|
|
404
|
+
|
|
405
|
+
- name: Publish RC release
|
|
406
|
+
if: steps.rel.outputs.enabled == 'true'
|
|
407
|
+
continue-on-error: true
|
|
408
|
+
run: |
|
|
409
|
+
# PILOT: call the MokoCLI build agent's publish verb (generic git-release).
|
|
410
|
+
# Standards preserved -- `moko publish` maps to release_publish.php UNCHANGED
|
|
411
|
+
# for the generic platform; the stability/bump/token contract is identical
|
|
412
|
+
# to the former direct invocation. No --branch: release_publish auto-detects
|
|
413
|
+
# the branch (here rc, from the checkout above).
|
|
414
|
+
#
|
|
415
|
+
# --bump none (bump-on-dev): the base was already minor-bumped on dev by
|
|
416
|
+
# the pre-promotion step above, and rc was force-pushed FROM that dev, so
|
|
417
|
+
# rc already carries the final base. Bumping again here would double-bump
|
|
418
|
+
# (dev=+1, rc=+2). --no-commit: don't commit a version change back to rc
|
|
419
|
+
# (the rc tip must stay identical to the promoted dev). The RC release
|
|
420
|
+
# still stamps the -rc suffix on the inherited base in-tree.
|
|
421
|
+
moko publish \
|
|
422
|
+
--path . --stability rc --bump none --no-commit \
|
|
423
|
+
--token "${{ github.token }}"
|
|
424
|
+
|
|
425
|
+
- name: "Deploy to RC (inline — replaces standalone deploy.yml, #1427)"
|
|
426
|
+
if: steps.rel.outputs.enabled == 'true' && vars.RC_SSH_USERNAME != ''
|
|
427
|
+
env:
|
|
428
|
+
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
429
|
+
run: |
|
|
430
|
+
# #1427: deploy is inlined into the release pipeline; the standalone
|
|
431
|
+
# deploy.yml is deleted. Gate = RC_SSH_USERNAME set (the same signal
|
|
432
|
+
# deploy.yml used) so only SSH-deploy-onboarded repos deploy; every
|
|
433
|
+
# other repo/platform skips green. Targets come from repo VARIABLES,
|
|
434
|
+
# never assumed. Deploy LOGIC stays server-side: mokocli deploy.php
|
|
435
|
+
# SSHes the restricted moko-deploy pattern, which builds+deploys+health-
|
|
436
|
+
# checks (with rollback) and, on RC, runs the clean-room RC_SEED + RC
|
|
437
|
+
# Build Tests. A failed RC deploy fails promote-rc — the stable gate.
|
|
438
|
+
set -euo pipefail
|
|
439
|
+
if [ -z "${DEPLOY_KEY:-}" ]; then
|
|
440
|
+
echo "::warning::RC_SSH_USERNAME set but DEPLOY_SSH_KEY secret missing — skipping RC deploy"; exit 0
|
|
441
|
+
fi
|
|
442
|
+
mkdir -p ~/.ssh
|
|
443
|
+
# DEPLOY_SSH_KEY is base64-encoded (single line) so act_runner masks it in logs
|
|
444
|
+
# and the multi-line key is never echoed. Decode when it base64-decodes to a
|
|
445
|
+
# private key; else treat as a legacy raw PEM (back-compat during rollout).
|
|
446
|
+
install -m600 /dev/null ~/.ssh/deploy_key # 0600 before any bytes (no umask window)
|
|
447
|
+
trap 'rm -f ~/.ssh/deploy_key' EXIT # never leave the key on a shared runner
|
|
448
|
+
if printf '%s' "$DEPLOY_KEY" | base64 -d 2>/dev/null | grep -q 'PRIVATE KEY'; then
|
|
449
|
+
printf '%s' "$DEPLOY_KEY" | base64 -d > ~/.ssh/deploy_key
|
|
450
|
+
else
|
|
451
|
+
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
|
452
|
+
fi
|
|
453
|
+
TAG="$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)-rc"
|
|
454
|
+
echo "Deploying rc tag ${TAG} to ${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }}"
|
|
455
|
+
php "${MOKO_CLI}/deploy.php" --tier rc --tag "$TAG" \
|
|
456
|
+
--ssh-host "${{ vars.RC_SSH_HOST }}" --ssh-port "${{ vars.RC_SSH_PORT }}" \
|
|
457
|
+
--ssh-user "${{ vars.RC_SSH_USERNAME }}" --ssh-key ~/.ssh/deploy_key
|
|
458
|
+
|
|
459
|
+
- name: Update RC release notes from CHANGELOG.md
|
|
460
|
+
if: steps.rel.outputs.enabled == 'true'
|
|
461
|
+
continue-on-error: true
|
|
462
|
+
run: |
|
|
463
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
464
|
+
TOKEN="${{ github.token }}"
|
|
465
|
+
|
|
466
|
+
# Extract [Unreleased] section via the shared mokocli command (#364 centralization).
|
|
467
|
+
NOTES=""
|
|
468
|
+
if [ -f "release_notes.md" ] && [ -s "release_notes.md" ]; then
|
|
469
|
+
NOTES="$(cat release_notes.md)"
|
|
470
|
+
fi
|
|
471
|
+
# #71: fall back to CHANGELOG [Unreleased] when no curated release_notes.md
|
|
472
|
+
if [ -z "$NOTES" ] && [ -f "CHANGELOG.md" ]; then
|
|
473
|
+
NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true)
|
|
474
|
+
# release_notes.php echoes "Release Unreleased" when the section is empty; normalize
|
|
475
|
+
# to empty so the platform fallback below applies.
|
|
476
|
+
[ "$NOTES" = "Release Unreleased" ] && NOTES=""
|
|
477
|
+
fi
|
|
478
|
+
[ -z "$NOTES" ] && NOTES="Release candidate"
|
|
479
|
+
|
|
480
|
+
# Find the RC release and update its body
|
|
481
|
+
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
482
|
+
"${API_BASE}/releases/tags/release-candidate" \
|
|
483
|
+
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
484
|
+
|
|
485
|
+
if [ -n "$RELEASE_ID" ]; then
|
|
486
|
+
python3 -c "
|
|
487
|
+
import json, urllib.request
|
|
488
|
+
body = open('/dev/stdin').read()
|
|
489
|
+
payload = json.dumps({'body': body}).encode()
|
|
490
|
+
req = urllib.request.Request(
|
|
491
|
+
'${API_BASE}/releases/${RELEASE_ID}',
|
|
492
|
+
data=payload, method='PATCH',
|
|
493
|
+
headers={
|
|
494
|
+
'Authorization': 'token ${TOKEN}',
|
|
495
|
+
'Content-Type': 'application/json'
|
|
496
|
+
})
|
|
497
|
+
urllib.request.urlopen(req)
|
|
498
|
+
" <<< "$NOTES"
|
|
499
|
+
echo "RC release notes updated from CHANGELOG.md"
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
- name: Summary
|
|
503
|
+
if: always()
|
|
504
|
+
run: |
|
|
505
|
+
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
|
506
|
+
echo "Base bumped + changelog promoted on dev (pre-promotion); rc cut from dev; RC release built" >> $GITHUB_STEP_SUMMARY
|
|
507
|
+
|
|
508
|
+
# -- Merged PR -> Build & Release (or promote RC to stable) -------------------------
|
|
509
|
+
release:
|
|
510
|
+
name: Build & Release Pipeline
|
|
511
|
+
# LITERAL runner label — see #366/#134. Gitea does not resolve the
|
|
512
|
+
# `${{ vars.RELEASE_RUNNER_LABEL || 'release' }}` fallback for an unset var in
|
|
513
|
+
# `runs-on`, so the job aborts with no runner. The registered label is `release`.
|
|
514
|
+
runs-on: release
|
|
515
|
+
# Release-create (vNN tag + release entry), rc-tag move, push, npm-registry publish,
|
|
516
|
+
# and the post-release dispatch are all contents/packages/actions WRITES. Gitea's
|
|
517
|
+
# auto-token derives perms from the JOB-level permissions block ONLY (workflow-level
|
|
518
|
+
# `permissions:` is NOT inherited into a job's token); without this block the token
|
|
519
|
+
# falls to the org default (empty) and POST /releases 404s fleet-wide (#1712).
|
|
520
|
+
permissions:
|
|
521
|
+
contents: write
|
|
522
|
+
packages: write
|
|
523
|
+
actions: write
|
|
524
|
+
# Skip on template repos (Template-*) -- they scaffold other repos and do not release.
|
|
525
|
+
# Stable release fires on a merged PR into main (pull_request:closed + merged) OR a
|
|
526
|
+
# direct PUSH to main. The pull_request path is the reliable one -- a PR-merge does not
|
|
527
|
+
# always emit a base-branch push event to Actions, so relying on push alone silently
|
|
528
|
+
# skipped releases (a nightly scheduled safety-net used to be the band-aid; it and its
|
|
529
|
+
# cron have been removed -- the pull_request path is the durable fix). The release
|
|
530
|
+
# steps already read github.event.pull_request.head.ref, so they handle both events.
|
|
531
|
+
# Loop guard: skip a self-push that carries
|
|
532
|
+
# [skip ci]/[skip sync] (mokocli's build commit + the changelog-promote commit).
|
|
533
|
+
# #469: the non-code paths-ignore was REMOVED, so a docs/CI-only push now reaches
|
|
534
|
+
# this job and runs in no-bump update mode (gated by steps.srcgate below) instead
|
|
535
|
+
# of being dropped; the [skip ci]/[skip sync] re-checks here remain the loop guard.
|
|
536
|
+
# workflow_dispatch stays exempt (explicit human run).
|
|
537
|
+
# STABLE-RELEASE GATE (default_branch == 'main'): a stable release only fires when
|
|
538
|
+
# the repo's DEFAULT branch is main. main's mere existence no longer arms stable
|
|
539
|
+
# releases — so every repo can pre-create all channel branches (dev/rc/beta/alpha/main)
|
|
540
|
+
# and have them sit idle. A repo promotes itself to "stable-ready" by setting its
|
|
541
|
+
# default to main; new/in-development repos keep default 'dev' and never auto-release.
|
|
542
|
+
if: >-
|
|
543
|
+
!startsWith(github.event.repository.name, 'Template-') && github.event.repository.name != '.vault' &&
|
|
544
|
+
(github.event.repository.default_branch == 'main' || github.event.repository.default_branch == 'stable') &&
|
|
545
|
+
(
|
|
546
|
+
(
|
|
547
|
+
github.event_name == 'push' &&
|
|
548
|
+
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
549
|
+
!contains(github.event.head_commit.message, '[skip sync]')
|
|
550
|
+
) ||
|
|
551
|
+
(
|
|
552
|
+
github.event_name == 'pull_request' &&
|
|
553
|
+
github.event.action == 'closed' &&
|
|
554
|
+
github.event.pull_request.merged == true &&
|
|
555
|
+
github.event.pull_request.base.ref == github.event.repository.default_branch &&
|
|
556
|
+
!contains(github.event.pull_request.title, '[skip ci]') &&
|
|
557
|
+
!contains(github.event.pull_request.title, '[skip sync]')
|
|
558
|
+
) ||
|
|
559
|
+
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
steps:
|
|
563
|
+
- name: Checkout repository
|
|
564
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
565
|
+
with:
|
|
566
|
+
token: ${{ github.token }}
|
|
567
|
+
fetch-depth: 0
|
|
568
|
+
submodules: false
|
|
569
|
+
|
|
570
|
+
# #1672: transient step-0 checkout aborts on the shared `release` runners make
|
|
571
|
+
# every stable release a dice-roll. This fallback fires ONLY when the checkout
|
|
572
|
+
# above failed (step-level `if: failure()` — a LITERAL, not a discarded
|
|
573
|
+
# continue-on-error expression) and re-materialises the workspace with a
|
|
574
|
+
# bounded 3-attempt git clone + backoff. fetch-depth: 0 is preserved with a
|
|
575
|
+
# FULL clone (no --depth) + tags, because the monotonic-guard and bump steps
|
|
576
|
+
# below need the parent commit and tag history. Fails loudly (exit 1) if all
|
|
577
|
+
# three attempts fail, so a real (non-transient) breakage is never masked.
|
|
578
|
+
# Independent step — no needs:+always() fan-in (act_runner would strand it).
|
|
579
|
+
- name: "Checkout fallback (retry transient flake) (#1672)"
|
|
580
|
+
if: failure()
|
|
581
|
+
env:
|
|
582
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
583
|
+
run: |
|
|
584
|
+
set -u
|
|
585
|
+
TOK="$(printf '%s' "$GITHUB_TOKEN" | tr -d '[:space:]')"; echo "::add-mask::$TOK"
|
|
586
|
+
SERVER="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}"
|
|
587
|
+
CLONE_URL="https://x-access-token:${TOK}@${SERVER#https://}/${GITHUB_REPOSITORY}.git"
|
|
588
|
+
REF="${GITHUB_SHA:-${GITHUB_REF_NAME:-}}"
|
|
589
|
+
echo "Retrying full-history checkout of ${GITHUB_REPOSITORY}@${REF:-<HEAD>} into ${GITHUB_WORKSPACE}"
|
|
590
|
+
ok=0
|
|
591
|
+
for attempt in 1 2 3; do
|
|
592
|
+
rm -rf "${GITHUB_WORKSPACE:?}/.git" 2>/dev/null || true
|
|
593
|
+
find "${GITHUB_WORKSPACE}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
594
|
+
if git clone "$CLONE_URL" "${GITHUB_WORKSPACE}" \
|
|
595
|
+
&& git -C "${GITHUB_WORKSPACE}" fetch --tags origin \
|
|
596
|
+
&& { [ -z "$REF" ] || git -C "${GITHUB_WORKSPACE}" checkout -f "$REF"; }; then
|
|
597
|
+
ok=1; echo "Fallback checkout succeeded on attempt ${attempt}."; break
|
|
598
|
+
fi
|
|
599
|
+
echo "::warning::Fallback checkout attempt ${attempt} failed; backing off $((attempt * 5))s"
|
|
600
|
+
sleep $((attempt * 5))
|
|
601
|
+
done
|
|
602
|
+
if [ "$ok" != "1" ]; then
|
|
603
|
+
echo "::error::Checkout failed after 3 retry attempts (#1672) — this is not a transient flake, failing loudly."
|
|
604
|
+
exit 1
|
|
605
|
+
fi
|
|
606
|
+
|
|
607
|
+
- name: "Detect Makefile (auto-release gate)"
|
|
608
|
+
id: gate
|
|
609
|
+
# auto-release drives its build/package targets through make, so a repo
|
|
610
|
+
# WITHOUT a root Makefile has nothing to release. This is not an error --
|
|
611
|
+
# many universal repos (deploy handlers, docs, config, e.g. .vault) sync
|
|
612
|
+
# this workflow but never release. Treat "no Makefile" as a clean SKIP:
|
|
613
|
+
# emit has_makefile=false and let every downstream build/release step
|
|
614
|
+
# no-op via its if:, so the job goes GREEN with "Release Skipped" instead
|
|
615
|
+
# of reding every main push on a non-release repo. Must be a post-checkout
|
|
616
|
+
# step (a job-level if: cannot inspect the workspace).
|
|
617
|
+
run: |
|
|
618
|
+
if [ -f Makefile ] || [ -f makefile ] || [ -f GNUmakefile ]; then
|
|
619
|
+
echo "has_makefile=true" >> "$GITHUB_OUTPUT"
|
|
620
|
+
echo "Makefile present -- auto-release may proceed."
|
|
621
|
+
else
|
|
622
|
+
echo "has_makefile=false" >> "$GITHUB_OUTPUT"
|
|
623
|
+
echo "## Release Skipped -- no Makefile" >> "$GITHUB_STEP_SUMMARY"
|
|
624
|
+
echo "No Makefile at the repository root -- this repo does not build a release. Skipping the release pipeline (not a failure)." >> "$GITHUB_STEP_SUMMARY"
|
|
625
|
+
echo "No Makefile at repo root -- skipping auto-release (not a release-bearing repo)."
|
|
626
|
+
fi
|
|
627
|
+
|
|
628
|
+
- name: "Detect electron project"
|
|
629
|
+
id: electron
|
|
630
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
631
|
+
# ADDITIVE + GATED: electron repos are detected by an electron-builder
|
|
632
|
+
# config at the repo root (mokocli platform detection does not know
|
|
633
|
+
# 'electron' yet -- see MokoGIT #1201). Every electron step below no-ops
|
|
634
|
+
# via `steps.electron.outputs.is_electron` so non-electron repos are
|
|
635
|
+
# 100% unaffected. Post-checkout (a job-level if: cannot see the tree).
|
|
636
|
+
run: |
|
|
637
|
+
if [ -f electron-builder.yml ] || [ -f electron-builder.yaml ]; then
|
|
638
|
+
echo "is_electron=true" >> "$GITHUB_OUTPUT"
|
|
639
|
+
echo "electron-builder config present -- electron installer build enabled."
|
|
640
|
+
else
|
|
641
|
+
echo "is_electron=false" >> "$GITHUB_OUTPUT"
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
- name: Configure git for bot pushes
|
|
645
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
646
|
+
env:
|
|
647
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
648
|
+
run: |
|
|
649
|
+
PUSH_TOK="$(printf '%s' "$GITHUB_TOKEN" | tr -d '[:space:]')"; [ -z "$PUSH_TOK" ] && PUSH_TOK="${{ github.token }}"; echo "::add-mask::$PUSH_TOK"
|
|
650
|
+
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
651
|
+
git config --local user.name "mokogit-actions[bot]"
|
|
652
|
+
git remote set-url origin "https://x-access-token:${PUSH_TOK}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
653
|
+
|
|
654
|
+
- name: Check for merge conflict markers
|
|
655
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
656
|
+
run: |
|
|
657
|
+
# Scan only git-tracked files so vendored/generated trees (e.g. phpcs'
|
|
658
|
+
# GitMergeConflictSniff fixtures under vendor/) and this workflow's own
|
|
659
|
+
# grep pattern can never be matched (back-port of Template-Joomla #177/#178).
|
|
660
|
+
FILES=$(git ls-files '*.php' '*.xml' '*.css' '*.js' '*.json' '*.md' '*.yml' '*.yaml' '*.ini' '*.txt')
|
|
661
|
+
CONFLICTS=$(printf '%s\n' "$FILES" | xargs -r grep -nE '^(<<<<<<< |>>>>>>> |=======$)' 2>/dev/null || true)
|
|
662
|
+
if [ -n "$CONFLICTS" ]; then
|
|
663
|
+
echo "::error::Merge conflict markers found -- aborting release"
|
|
664
|
+
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
|
665
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
666
|
+
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
|
667
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
668
|
+
exit 1
|
|
669
|
+
fi
|
|
670
|
+
echo "No conflict markers found"
|
|
671
|
+
|
|
672
|
+
- name: Setup MokoCLI tools
|
|
673
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
674
|
+
run: |
|
|
675
|
+
# Install mokocli FRESH from the MokoGIT Composer registry on every run
|
|
676
|
+
# (PRIMARY) so CI always uses the latest published version. /opt/mokocli is
|
|
677
|
+
# a FAILSAFE only — its refresh unit (mokocli-sync.service) is disabled, so
|
|
678
|
+
# the on-disk snapshot rots and must NOT be preferred. Registry path
|
|
679
|
+
# /api/packages/** is anon-allowlisted (no GITHUB_TOKEN); it serves a dist
|
|
680
|
+
# zip (no git clone). --no-install: mokocli self-bootstraps via bundled lib/
|
|
681
|
+
# + a fallback PSR-4 autoloader (native ext-curl), so no vendor/ is needed.
|
|
682
|
+
if ! command -v composer > /dev/null 2>&1; then
|
|
683
|
+
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
|
|
684
|
+
fi
|
|
685
|
+
rm -rf /tmp/mokocli
|
|
686
|
+
REG='{"type":"composer","url":"https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer"}'
|
|
687
|
+
if composer create-project --no-install --no-interaction --no-progress --quiet \
|
|
688
|
+
--repository="${REG}" mokoconsulting/mokocli /tmp/mokocli >/dev/null 2>&1 \
|
|
689
|
+
&& [ -f /tmp/mokocli/cli/version_bump.php ]; then
|
|
690
|
+
echo "Installed mokocli fresh from the Composer registry (primary)"
|
|
691
|
+
# #1671: registry-publish LAG race. The registry install "succeeds" but can
|
|
692
|
+
# serve the PREVIOUS package for ~tens of seconds after a MokoCLI release
|
|
693
|
+
# (publish finishes AFTER the release run reports success). Meanwhile the
|
|
694
|
+
# /opt/mokocli deploy (pull-timer) may already carry the just-released,
|
|
695
|
+
# NEWER engine. So instead of blindly using the registry copy, compare the
|
|
696
|
+
# two on-disk versions and PREFER whichever is newer. The Makefile
|
|
697
|
+
# PROJECT_VERSION is the version source of truth and ships in both the
|
|
698
|
+
# composer dist and /opt/mokocli. Backward-compatible: with no /opt/mokocli
|
|
699
|
+
# (most consumer runners), the registry copy is used exactly as before.
|
|
700
|
+
mk_ver() { sed -n 's/^[[:space:]]*PROJECT_VERSION[[:space:]]*:*=[[:space:]]*//p' "$1" 2>/dev/null | head -n1 | tr -d '[:space:]'; }
|
|
701
|
+
ver_newer() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]; }
|
|
702
|
+
REG_VER="$(mk_ver /tmp/mokocli/Makefile)"
|
|
703
|
+
OPT_VER="$(mk_ver /opt/mokocli/Makefile)"
|
|
704
|
+
MOKO_ROOT=/tmp/mokocli
|
|
705
|
+
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -n "$OPT_VER" ] && [ -n "$REG_VER" ] \
|
|
706
|
+
&& ver_newer "$OPT_VER" "$REG_VER"; then
|
|
707
|
+
echo "::notice::/opt/mokocli (${OPT_VER}) is newer than the registry copy (${REG_VER}) — using /opt (#1671 publish-lag race)."
|
|
708
|
+
MOKO_ROOT=/opt/mokocli
|
|
709
|
+
else
|
|
710
|
+
echo "Using registry copy (${REG_VER:-unknown}); /opt/mokocli=${OPT_VER:-none}"
|
|
711
|
+
fi
|
|
712
|
+
echo "MOKO_CLI=${MOKO_ROOT}/cli" >> $GITHUB_ENV
|
|
713
|
+
# #650: make the chosen moko executable + FIRST on PATH so bare `moko`
|
|
714
|
+
# never resolves to a stale/non-executable copy (exit 126).
|
|
715
|
+
for d in bin cli; do
|
|
716
|
+
if [ -f "${MOKO_ROOT}/$d/moko" ]; then
|
|
717
|
+
# #1263 defense-in-depth: strip CR bytes so a CRLF-tainted registry
|
|
718
|
+
# dist or rotted /opt snapshot can't yield `/usr/bin/env: 'php\r'`
|
|
719
|
+
# (exit 127). tr interprets \r as a real CR (unlike portable sed).
|
|
720
|
+
# /opt may be root-owned — write via a temp file + sudo when needed.
|
|
721
|
+
if tr -d '\r' < "${MOKO_ROOT}/$d/moko" > "/tmp/moko.lf" 2>/dev/null; then
|
|
722
|
+
if cp "/tmp/moko.lf" "${MOKO_ROOT}/$d/moko" 2>/dev/null; then :; \
|
|
723
|
+
else sudo cp "/tmp/moko.lf" "${MOKO_ROOT}/$d/moko" 2>/dev/null || true; fi
|
|
724
|
+
rm -f "/tmp/moko.lf"
|
|
725
|
+
fi
|
|
726
|
+
chmod +x "${MOKO_ROOT}/$d/moko" 2>/dev/null || sudo chmod +x "${MOKO_ROOT}/$d/moko" 2>/dev/null || true
|
|
727
|
+
echo "${MOKO_ROOT}/$d" >> "$GITHUB_PATH"
|
|
728
|
+
fi
|
|
729
|
+
done
|
|
730
|
+
sudo chmod +x /opt/mokocli/*/moko 2>/dev/null || true
|
|
731
|
+
# Opportunistically refresh the /opt failsafe from this fresh copy when it
|
|
732
|
+
# differs — keeps the disabled-sync snapshot current so the fallback is
|
|
733
|
+
# never far behind. Best-effort: needs sudo, gated on a diff to avoid
|
|
734
|
+
# fleet-wide churn, never fails the job. #1671: only refresh FORWARD — skip
|
|
735
|
+
# when /opt is the newer copy we just chose, so we never downgrade it.
|
|
736
|
+
if [ "$MOKO_ROOT" = /tmp/mokocli ] && command -v sudo >/dev/null 2>&1 && [ -d /opt/mokocli ] \
|
|
737
|
+
&& ! cmp -s /tmp/mokocli/cli/version_reset_dev.php /opt/mokocli/cli/version_reset_dev.php 2>/dev/null; then
|
|
738
|
+
sudo cp -a /tmp/mokocli/. /opt/mokocli/ 2>/dev/null \
|
|
739
|
+
&& echo "Refreshed /opt/mokocli failsafe from the fresh registry copy" \
|
|
740
|
+
|| echo " (/opt failsafe refresh skipped — no sudo/perms)"
|
|
741
|
+
fi
|
|
742
|
+
elif [ -f /opt/mokocli/cli/version_bump.php ]; then
|
|
743
|
+
echo "::warning::Registry install failed — using /opt/mokocli FAILSAFE (may be stale)"
|
|
744
|
+
echo "MOKO_CLI=/opt/mokocli/cli" >> $GITHUB_ENV
|
|
745
|
+
else
|
|
746
|
+
echo "::error::mokocli unavailable: registry install failed and no /opt/mokocli failsafe present"
|
|
747
|
+
exit 1
|
|
748
|
+
fi
|
|
749
|
+
|
|
750
|
+
- name: "Detect platform"
|
|
751
|
+
id: platform
|
|
752
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
753
|
+
run: |
|
|
754
|
+
# PILOT: native-manifest platform detection only -- the metadata-registry
|
|
755
|
+
# write-back (platform_detect.php --update) is REMOVED. The MokoCLI build
|
|
756
|
+
# agent no longer reads or mutates the metadata API; the platform is
|
|
757
|
+
# resolved from the in-tree manifest solely to drive the tag/branch logic
|
|
758
|
+
# below (joomla -> tag=stable; generic -> tag=v${VERSION}).
|
|
759
|
+
# Non-fatal (|| true) so a detection hiccup never blocks a release.
|
|
760
|
+
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
761
|
+
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
|
762
|
+
|
|
763
|
+
- name: "Resolve SRC_DIR bump gate (#469)"
|
|
764
|
+
id: srcgate
|
|
765
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
766
|
+
# #469: decide version-BUMP vs NO-BUMP (update-existing) from whether this
|
|
767
|
+
# push touched the Makefile source directory (SRC_DIR). This replaces the old
|
|
768
|
+
# non-code paths-ignore denylist on the push trigger: a docs/workflow/sample
|
|
769
|
+
# push no longer skips the pipeline — it runs in no-bump "update the current
|
|
770
|
+
# stable release in place" mode. SRC_DIR is read from the Makefile exactly as
|
|
771
|
+
# apidocs.yml does (the de-facto resolve-source-dir contract in this template).
|
|
772
|
+
#
|
|
773
|
+
# Gate logic (per the #469 "Resulting gate logic"):
|
|
774
|
+
# 1. ELECTRON / opt-out -> BYPASS: electron repos have NO source dir, so a
|
|
775
|
+
# naive SRC_DIR test would classify every push as non-source and NEVER
|
|
776
|
+
# bump (a release-breaking trap). Electron (file-detected) and an explicit
|
|
777
|
+
# opt-out (vars.RELEASE_SRCDIR_GATE=off) keep the existing always-bump
|
|
778
|
+
# behaviour. Do NOT infer the exclusion from an empty SRC_DIR.
|
|
779
|
+
# 2. Else SRC_DIR is REQUIRED: a non-excluded repo with no resolvable SRC_DIR
|
|
780
|
+
# is a misconfig -> FAIL LOUD (::error:: + exit 1), never silently never-bump.
|
|
781
|
+
# 3. Else diff the push range (before..sha, with a HEAD^ / merge-base
|
|
782
|
+
# fallback) intersected with ${SRC_DIR}/ : any source path -> should_bump
|
|
783
|
+
# =true (existing bump+new-release path); none -> should_bump=false
|
|
784
|
+
# (no-bump update-existing path). workflow_dispatch always bumps (an
|
|
785
|
+
# explicit human re-cut), as does a run with no resolvable diff range.
|
|
786
|
+
run: |
|
|
787
|
+
set -uo pipefail
|
|
788
|
+
# 1. Electron / opt-out bypass -> always bump as today.
|
|
789
|
+
if [ "${{ steps.electron.outputs.is_electron }}" = "true" ] || [ "${{ vars.RELEASE_SRCDIR_GATE }}" = "off" ]; then
|
|
790
|
+
echo "should_bump=true" >> "$GITHUB_OUTPUT"
|
|
791
|
+
echo "::notice::SRC_DIR gate BYPASSED (electron repo or vars.RELEASE_SRCDIR_GATE=off) — keeping always-bump behaviour (#469)."
|
|
792
|
+
exit 0
|
|
793
|
+
fi
|
|
794
|
+
# A manual re-cut is always an intentional bump+release.
|
|
795
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
796
|
+
echo "should_bump=true" >> "$GITHUB_OUTPUT"
|
|
797
|
+
echo "workflow_dispatch — always bump (explicit human re-cut) (#469)."
|
|
798
|
+
exit 0
|
|
799
|
+
fi
|
|
800
|
+
# Resolve the push diff range (shared by both gate modes below).
|
|
801
|
+
BEFORE="${{ github.event.before }}"
|
|
802
|
+
SHA="${GITHUB_SHA:-}"
|
|
803
|
+
RANGE=""
|
|
804
|
+
ZERO="0000000000000000000000000000000000000000"
|
|
805
|
+
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$ZERO" ] && git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
|
806
|
+
RANGE="${BEFORE}..${SHA}"
|
|
807
|
+
elif git rev-parse "${SHA}^1" >/dev/null 2>&1; then
|
|
808
|
+
# No usable before-sha (branch-create / forced / shallow): fall back to the
|
|
809
|
+
# first-parent diff of the pushed commit (the merged promotion's delta).
|
|
810
|
+
RANGE="${SHA}^1..${SHA}"
|
|
811
|
+
fi
|
|
812
|
+
if [ -z "$RANGE" ]; then
|
|
813
|
+
# No resolvable range (e.g. initial push): cannot prove a non-source push,
|
|
814
|
+
# so default to the safe existing behaviour — bump.
|
|
815
|
+
echo "should_bump=true" >> "$GITHUB_OUTPUT"
|
|
816
|
+
echo "::notice::#469 gate: no resolvable diff range (before=${BEFORE:-none}); defaulting to bump."
|
|
817
|
+
exit 0
|
|
818
|
+
fi
|
|
819
|
+
CHANGED="$(git diff --name-only "$RANGE" 2>/dev/null || true)"
|
|
820
|
+
if [ -z "$CHANGED" ]; then
|
|
821
|
+
echo "should_bump=true" >> "$GITHUB_OUTPUT"
|
|
822
|
+
echo "::notice::#469 gate: empty diff for ${RANGE}; defaulting to bump."
|
|
823
|
+
exit 0
|
|
824
|
+
fi
|
|
825
|
+
# 2. SRC_DIR INCLUSION mode — fleet standard is SRC_DIR=source. When the repo
|
|
826
|
+
# declares a Makefile SRC_DIR, bump iff the push touched ${SRC_DIR}/.
|
|
827
|
+
# Accept all three GNU make assignment forms: SRC_DIR=, SRC_DIR :=, SRC_DIR ?=.
|
|
828
|
+
# NOTE the trailing `|| true`: the step shell runs with `-e -o pipefail`,
|
|
829
|
+
# so on a repo with NO SRC_DIR the `grep` exits 1, pipefail propagates it,
|
|
830
|
+
# and the command-substitution assignment would abort the whole step under
|
|
831
|
+
# -e BEFORE the denylist fallback below. `|| true` keeps SRC_DIR empty and
|
|
832
|
+
# lets execution fall through to denylist mode. (#1831)
|
|
833
|
+
# #498 FIX: resolve SRC_DIR by asking `make` to EXPAND it. The fleet Makefile
|
|
834
|
+
# standard is `SRC_DIR := $(shell [ -d src ] && echo src || echo source)` — a
|
|
835
|
+
# GNU make function. A grep|sed read captures the LITERAL unexpanded
|
|
836
|
+
# "$(shell …)", an unmatchable directory that made INCLUSION mode classify
|
|
837
|
+
# EVERY push as non-source (should_bump=false forever: MokoCLI stuck
|
|
838
|
+
# re-publishing v10.20.00 while source changed). Ask make to evaluate it (runs
|
|
839
|
+
# the $(shell …) read-only); fall back to the grep|sed read only if make is
|
|
840
|
+
# unavailable, and NULL OUT a still-unexpanded $(...)/`...` value so it drops
|
|
841
|
+
# to the denylist mode below instead of self-trapping. (|| true keeps the step
|
|
842
|
+
# alive under -e -o pipefail — see #1831.)
|
|
843
|
+
SRC_DIR="$(make -s --no-print-directory --eval='__moko_srcdir__:; @printf "%s" "$(SRC_DIR)"' __moko_srcdir__ 2>/dev/null | tr -d '[:space:]' || true)"
|
|
844
|
+
if [ -z "$SRC_DIR" ]; then
|
|
845
|
+
SRC_DIR="$(grep -m1 -E '^SRC_DIR[[:space:]]*[:?]?=' Makefile 2>/dev/null | sed -E 's/.*[:?]?=[[:space:]]*//' | tr -d '[:space:]' || true)"
|
|
846
|
+
fi
|
|
847
|
+
case "$SRC_DIR" in *'$('*|*'`'*) SRC_DIR="" ;; esac
|
|
848
|
+
# A declared-but-NONEXISTENT SRC_DIR (e.g. expands to `source` on a repo whose
|
|
849
|
+
# code lives in cli/lib/bin) must NOT drive INCLUSION mode with an unmatchable
|
|
850
|
+
# prefix — fall through to the non-source denylist mode instead. (#498)
|
|
851
|
+
if [ -n "$SRC_DIR" ] && [ ! -d "$SRC_DIR" ]; then
|
|
852
|
+
echo "::notice::#469 gate: declared SRC_DIR='${SRC_DIR}' does not exist in the tree — using non-source denylist fallback (#498)."
|
|
853
|
+
SRC_DIR=""
|
|
854
|
+
fi
|
|
855
|
+
if [ -n "$SRC_DIR" ]; then
|
|
856
|
+
SRC_DIR="${SRC_DIR%/}"
|
|
857
|
+
SRC_HITS="$(printf '%s\n' "$CHANGED" | grep -E "^${SRC_DIR}/" || true)"
|
|
858
|
+
if [ -n "$SRC_HITS" ]; then
|
|
859
|
+
echo "should_bump=true" >> "$GITHUB_OUTPUT"
|
|
860
|
+
echo "#469 gate (SRC_DIR=${SRC_DIR}): push touched ${SRC_DIR}/ (range ${RANGE}) — BUMP + new release."
|
|
861
|
+
else
|
|
862
|
+
echo "should_bump=false" >> "$GITHUB_OUTPUT"
|
|
863
|
+
echo "::notice::#469 gate (SRC_DIR=${SRC_DIR}): push touched NO ${SRC_DIR}/ paths (range ${RANGE}) — NO bump; updating the current release in place."
|
|
864
|
+
fi
|
|
865
|
+
exit 0
|
|
866
|
+
fi
|
|
867
|
+
# 3. NON-SOURCE DENYLIST mode (#469/#1831) — repos with no single declarable
|
|
868
|
+
# SRC_DIR (e.g. MokoGIT, a Gitea fork with root-level Go source). NEVER
|
|
869
|
+
# hard-fail on a missing SRC_DIR: bump UNLESS the push touched ONLY
|
|
870
|
+
# non-source files (docs, CI/workflows, dotfiles). Any other change bumps.
|
|
871
|
+
NOBUMP='(^\.|(^|/)docs?/|\.(md|markdown|rst|adoc)$|^(README|CHANGELOG|LICENSE|COPYING|NOTICE|AUTHORS|CONTRIBUTORS|CODE_OF_CONDUCT|CONTRIBUTING|SECURITY|SUPPORT|GOVERNANCE)[^/]*$|^renovate\.json$)'
|
|
872
|
+
SRC_HITS="$(printf '%s\n' "$CHANGED" | grep -vE "$NOBUMP" || true)"
|
|
873
|
+
if [ -n "$SRC_HITS" ]; then
|
|
874
|
+
echo "should_bump=true" >> "$GITHUB_OUTPUT"
|
|
875
|
+
echo "#469 gate (denylist mode, no SRC_DIR): push touched source/non-doc files (range ${RANGE}) — BUMP + new release."
|
|
876
|
+
else
|
|
877
|
+
echo "should_bump=false" >> "$GITHUB_OUTPUT"
|
|
878
|
+
echo "::notice::#469 gate (denylist mode, no SRC_DIR): push touched ONLY non-source files (docs/CI/dotfiles) (range ${RANGE}) — NO bump; updating the current release in place."
|
|
879
|
+
fi
|
|
880
|
+
|
|
881
|
+
- name: "Determine version bump level"
|
|
882
|
+
id: bump
|
|
883
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
884
|
+
env:
|
|
885
|
+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
886
|
+
run: |
|
|
887
|
+
# Bump-on-dev: the base version is now bumped on DEV pre-promotion (the
|
|
888
|
+
# promote-rc job's release_bump_dev.php step), so main ALWAYS publishes
|
|
889
|
+
# the inherited version with NO bump. Historically a dev-headed promotion
|
|
890
|
+
# bumped minor here (on main) — that on-main bump is exactly the divergence
|
|
891
|
+
# this change removes. fix/patch/hotfix/bugfix were already 'none' (the
|
|
892
|
+
# per-merge patch bump happens on dev in pre-release.yml, not here). So
|
|
893
|
+
# every path is 'none' now.
|
|
894
|
+
HEAD_REF="${HEAD_REF:-dev}"
|
|
895
|
+
BUMP="none"
|
|
896
|
+
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
897
|
+
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF}) — base bumped on dev pre-promotion"
|
|
898
|
+
|
|
899
|
+
- name: "Detect mokohub packaging platforms"
|
|
900
|
+
id: mokohub
|
|
901
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
902
|
+
# ADDITIVE + GATED (Template-Generic#326): classify the mokohub-manifest /
|
|
903
|
+
# mokohub-recipes platforms from the detected platform key so their gated
|
|
904
|
+
# packaging steps below can fire. Purely additive — no existing platform
|
|
905
|
+
# (joomla/npm/mcp/client/generic/go/php/electron) reads these outputs, so
|
|
906
|
+
# their release path is 100% unchanged. Detection is read-only.
|
|
907
|
+
run: |
|
|
908
|
+
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
909
|
+
IS_MANIFEST=false
|
|
910
|
+
IS_RECIPES=false
|
|
911
|
+
case "$PLATFORM" in
|
|
912
|
+
mokohub-manifest*) IS_MANIFEST=true ;;
|
|
913
|
+
mokohub-recipes*) IS_RECIPES=true ;;
|
|
914
|
+
esac
|
|
915
|
+
echo "is_manifest=${IS_MANIFEST}" >> "$GITHUB_OUTPUT"
|
|
916
|
+
echo "is_recipes=${IS_RECIPES}" >> "$GITHUB_OUTPUT"
|
|
917
|
+
[ "$IS_MANIFEST" = "true" ] && echo "mokohub-manifest platform — manifest.json packaging enabled."
|
|
918
|
+
[ "$IS_RECIPES" = "true" ] && echo "mokohub-recipes platform — catalog.zip packaging enabled."
|
|
919
|
+
true
|
|
920
|
+
|
|
921
|
+
- name: "Gate npm publish (PLATFORM=npm + package.json + Makefile PROJECT_VERSION)"
|
|
922
|
+
id: npmpub
|
|
923
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
924
|
+
# A repo is npm-publishable ONLY when its Makefile PLATFORM is npm/nodejs --
|
|
925
|
+
# the canonical platform declaration, NOT file presence. Joomla/Go/other repos
|
|
926
|
+
# that merely ship a build-tooling package.json (e.g. a Joomla component with a
|
|
927
|
+
# frontend package.json) must never be treated as npm packages. On top of the
|
|
928
|
+
# platform gate we still require a package.json AND a Makefile PROJECT_VERSION
|
|
929
|
+
# (the mokocli source of truth the publish step reads) -- belt-and-suspenders,
|
|
930
|
+
# so a genuine npm repo missing PROJECT_VERSION does not fail the release.
|
|
931
|
+
run: |
|
|
932
|
+
PUB=false
|
|
933
|
+
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
934
|
+
# Electron repos build + publish their installers LOCALLY (manual upload),
|
|
935
|
+
# so they must NEVER npm-publish from CI even though they detect as nodejs
|
|
936
|
+
# (they ship a package.json). Mirror pre-release.yml's electron publish gate
|
|
937
|
+
# (Template-Generic #390): disable unless explicitly opted in with
|
|
938
|
+
# vars.ELECTRON_CI_BUILD=true. Non-electron repos are unaffected.
|
|
939
|
+
if [ "${{ steps.electron.outputs.is_electron }}" = "true" ] && [ "${{ vars.ELECTRON_CI_BUILD }}" != "true" ]; then
|
|
940
|
+
echo "publishable=false" >> "$GITHUB_OUTPUT"
|
|
941
|
+
echo "npm-publishable=false (electron repo -- CI npm publish disabled; local build + manual upload)"
|
|
942
|
+
else
|
|
943
|
+
case "$PLATFORM" in
|
|
944
|
+
npm*|nodejs*)
|
|
945
|
+
if [ -f package.json ] && grep -qE '^[[:space:]]*PROJECT_VERSION[[:space:]]*:?=' Makefile; then PUB=true; fi
|
|
946
|
+
;;
|
|
947
|
+
esac
|
|
948
|
+
echo "publishable=${PUB}" >> "$GITHUB_OUTPUT"
|
|
949
|
+
echo "npm-publishable=${PUB} (needs PLATFORM npm/nodejs + package.json + Makefile PROJECT_VERSION; PLATFORM=${PLATFORM})"
|
|
950
|
+
fi
|
|
951
|
+
|
|
952
|
+
- name: "Sterilize SVG assets (strip editor/Inkscape metadata) (#312)"
|
|
953
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
954
|
+
continue-on-error: true
|
|
955
|
+
run: |
|
|
956
|
+
set -uo pipefail
|
|
957
|
+
mapfile -t SVGS < <(find . -type f -name '*.svg' \
|
|
958
|
+
-not -path './.git/*' -not -path './node_modules/*' -not -path './vendor/*' 2>/dev/null || true)
|
|
959
|
+
if [ "${#SVGS[@]}" -eq 0 ]; then echo "no SVGs -- skipping (#312)"; exit 0; fi
|
|
960
|
+
echo "Sterilizing ${#SVGS[@]} SVG(s) -- stripping editor/Inkscape metadata..."
|
|
961
|
+
if pip install --quiet --disable-pip-version-check scour >/dev/null 2>&1 && command -v scour >/dev/null 2>&1; then
|
|
962
|
+
for fsvg in "${SVGS[@]}"; do
|
|
963
|
+
scour -i "$fsvg" -o "$fsvg.st" --remove-metadata --strip-xml-prolog --enable-comment-stripping >/dev/null 2>&1 \
|
|
964
|
+
&& mv "$fsvg.st" "$fsvg" || rm -f "$fsvg.st"
|
|
965
|
+
done
|
|
966
|
+
echo "SVG sterilization complete (scour, metadata-only)."
|
|
967
|
+
else
|
|
968
|
+
echo "::notice::scour unavailable -- SVG sterilization skipped (non-fatal, #312)."
|
|
969
|
+
fi
|
|
970
|
+
|
|
971
|
+
- name: Optimize assets (artifact-time)
|
|
972
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
973
|
+
continue-on-error: true
|
|
974
|
+
run: |
|
|
975
|
+
moko assets:optimize --path . 2>/dev/null || echo "::notice::moko assets:optimize unavailable or no assets — skipping"
|
|
976
|
+
|
|
977
|
+
# Build-time PHP dependency vendoring (universal, guarded — mirrors
|
|
978
|
+
# pre-release.yml 07.15.00). If a sub-package ships a composer.json, fetch its
|
|
979
|
+
# runtime deps into that package's CONFIGURED vendor dir (e.g. admin/vendor —
|
|
980
|
+
# mPDF) BEFORE `moko publish` (release_publish.php → release_package.php) zips
|
|
981
|
+
# the working tree. No-op without a sub-package composer.json. FAILS LOUD when
|
|
982
|
+
# vendoring fails for a package whose Joomla manifest ships
|
|
983
|
+
# <folder>vendor</folder>: shipping that zip without vendor/ makes JInstaller
|
|
984
|
+
# abort the whole package install on-site ("File does not exist
|
|
985
|
+
# .../admin/vendor" — MokonyxCRMDCM/MokonyxCRM), strictly worse than a red run.
|
|
986
|
+
- name: Vendor PHP dependencies (build-time)
|
|
987
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
988
|
+
run: |
|
|
989
|
+
set -uo pipefail
|
|
990
|
+
CJS="$(find source/packages src/packages -maxdepth 2 -name composer.json 2>/dev/null || true)"
|
|
991
|
+
if [ -z "$CJS" ]; then echo "No sub-package composer.json — nothing to vendor."; exit 0; fi
|
|
992
|
+
command -v composer >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq composer >/dev/null 2>&1 || true; }
|
|
993
|
+
# mPDF (and most PHP PDF/image libs) hard-require ext-gd; the runners'
|
|
994
|
+
# php-cli ships without it, which fails dependency RESOLUTION outright.
|
|
995
|
+
if ! php -m 2>/dev/null | grep -qi '^gd$'; then
|
|
996
|
+
PHPV="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null || true)"
|
|
997
|
+
sudo apt-get update -qq || true
|
|
998
|
+
sudo apt-get install -y -qq "php${PHPV}-gd" || sudo apt-get install -y -qq php-gd || echo "::warning::php-gd install failed (php ${PHPV})"
|
|
999
|
+
php -m 2>/dev/null | grep -qi '^gd$' || echo "::warning::ext-gd STILL missing after install attempt — vendoring will fail for gd-requiring packages"
|
|
1000
|
+
fi
|
|
1001
|
+
rc=0
|
|
1002
|
+
for cj in $CJS; do
|
|
1003
|
+
dir="$(dirname "$cj")"
|
|
1004
|
+
echo "::notice::composer install (no-dev) in ${dir}"
|
|
1005
|
+
ok=true
|
|
1006
|
+
composer install --no-dev --no-interaction --no-progress --optimize-autoloader --working-dir="$dir" || ok=false
|
|
1007
|
+
vd="$(composer config vendor-dir --working-dir="$dir" 2>/dev/null || true)"; vd="${vd:-vendor}"
|
|
1008
|
+
[ -f "${dir}/${vd}/autoload.php" ] || ok=false
|
|
1009
|
+
if [ "$ok" = "true" ]; then
|
|
1010
|
+
echo "vendored: ${dir}/${vd}"
|
|
1011
|
+
elif grep -qs '<folder>vendor</folder>' "${dir}"/*.xml; then
|
|
1012
|
+
echo "::error::vendoring FAILED in ${dir} but its Joomla manifest ships <folder>vendor</folder> — the built zip would abort the whole package install on JInstaller ('File does not exist .../vendor'). Failing the build instead of shipping a broken package."
|
|
1013
|
+
rc=1
|
|
1014
|
+
else
|
|
1015
|
+
echo "::warning::composer install failed in ${dir} — manifest does not ship vendor/; packaging the existing tree"
|
|
1016
|
+
fi
|
|
1017
|
+
done
|
|
1018
|
+
exit $rc
|
|
1019
|
+
|
|
1020
|
+
- name: "Guarantee monotonic release version (fast-merge bump guard)"
|
|
1021
|
+
id: monotonic
|
|
1022
|
+
# #469: in no-bump mode (a non-source push updating the existing release in
|
|
1023
|
+
# place) the version must stay PUT, so the self-healing minor bump must not
|
|
1024
|
+
# run — skip this guard when steps.srcgate decided should_bump=false. On the
|
|
1025
|
+
# bump path (should_bump=true, or electron/opt-out/dispatch where srcgate is
|
|
1026
|
+
# bypassed/true) it runs exactly as before.
|
|
1027
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.srcgate.outputs.should_bump != 'false'
|
|
1028
|
+
env:
|
|
1029
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
1030
|
+
run: |
|
|
1031
|
+
# P1-b (.mokogit-private#73): the base is normally minor-bumped on dev by
|
|
1032
|
+
# promote-rc pre-promotion. If the dev->stable PR is MERGED before
|
|
1033
|
+
# promote-rc runs (a fast merge), that bump is skipped and this job would
|
|
1034
|
+
# re-publish an ALREADY-RELEASED base (observed: stuck at 02.23.00). Enforce
|
|
1035
|
+
# "strictly above the last release": a no-op on the normal path, a self-
|
|
1036
|
+
# healing local bump when the inherited base did not advance.
|
|
1037
|
+
CUR=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
1038
|
+
[ -z "$CUR" ] && { echo "no base version; skip guard"; exit 0; }
|
|
1039
|
+
git fetch --tags --quiet origin 2>/dev/null || true
|
|
1040
|
+
# #813: the "last released version" is the MAX of two sources, not tags alone.
|
|
1041
|
+
# The rolling-release model publishes to a FIXED `stable` release whose NAME
|
|
1042
|
+
# ("... (VERSION: NN.NN.NN)") is the true last-published version, while the
|
|
1043
|
+
# immutable vNN.NN.NN tags can lag / fail to advance (#808/#386 stale-tag).
|
|
1044
|
+
# Trusting tags alone made this guard blind, so a direct fix/*->stable merge
|
|
1045
|
+
# RE-CUT an already-published base (observed 01.00.19 re-cut). Take the max of
|
|
1046
|
+
# (a) the highest vNN.NN.NN tag and (b) the rolling `stable` release version.
|
|
1047
|
+
TAG_LAST=$(git tag -l 'v[0-9][0-9].[0-9][0-9].[0-9][0-9]' | sed 's/^v//' | sort -V | tail -n1 || true)
|
|
1048
|
+
# #1702 / .mokogit-private#76: resolve the CURRENT stable release by its
|
|
1049
|
+
# CHANNEL POINTER, not by a fixed `tag_name=stable`. Under version-primary
|
|
1050
|
+
# releases the release tag_name is the immutable vNN, so GET
|
|
1051
|
+
# /releases/tags/stable no longer finds it; the channel endpoint
|
|
1052
|
+
# (releases/channel/stable) resolves whichever release the `stable`
|
|
1053
|
+
# channel currently points at via ReleaseStreamMap. Graceful fallback to
|
|
1054
|
+
# the legacy tags/stable call for forges predating #1702 (pre-rollout the
|
|
1055
|
+
# channel route 404s and the rolling `stable` tag still exists). The parsed
|
|
1056
|
+
# input is identical: the release NAME carries "VERSION: NN.NN.NN".
|
|
1057
|
+
RELEASE_JSON=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN:-${{ github.token }}}" \
|
|
1058
|
+
"${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/releases/channel/stable" 2>/dev/null || true)
|
|
1059
|
+
if [ -z "$RELEASE_JSON" ]; then
|
|
1060
|
+
RELEASE_JSON=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN:-${{ github.token }}}" \
|
|
1061
|
+
"${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/releases/tags/stable" 2>/dev/null || true)
|
|
1062
|
+
fi
|
|
1063
|
+
ROLLING_LAST=$(printf '%s' "${RELEASE_JSON:-}" \
|
|
1064
|
+
| grep -oE 'VERSION: [0-9]{2}\.[0-9]{2}\.[0-9]{2}' | grep -oE '[0-9]{2}\.[0-9]{2}\.[0-9]{2}' | head -n1 || true)
|
|
1065
|
+
LAST=$( { echo "$TAG_LAST"; echo "$ROLLING_LAST"; } | grep -E '^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$' | sort -V | tail -n1 || true)
|
|
1066
|
+
if [ -z "$LAST" ]; then echo "no prior release (no vNN.NN.NN tag, no rolling stable); skip guard"; exit 0; fi
|
|
1067
|
+
echo "monotonic guard: CUR=${CUR} TAG_LAST=${TAG_LAST:-none} ROLLING_LAST=${ROLLING_LAST:-none} -> LAST=${LAST}"
|
|
1068
|
+
# #498 FIX: the release IDENTITY is the MINOR (tag vMAJOR.MINOR.00). Dev's
|
|
1069
|
+
# per-merge PATCH bumps advance CUR within the SAME minor (e.g. 10.20.00 ->
|
|
1070
|
+
# 10.20.04), which satisfied the old full-version `CUR <= LAST` test and
|
|
1071
|
+
# WRONGLY skipped the heal — so a fast dev->stable merge re-published the
|
|
1072
|
+
# already-released vMAJOR.MINOR.00 forever (observed: MokoCLI stuck at
|
|
1073
|
+
# v10.20.00 while dev reached 10.20.04), violating this guard's own "never
|
|
1074
|
+
# re-publish an already-released base" intent. Compare the MINOR identity
|
|
1075
|
+
# (MAJOR.MINOR): heal (minor-bump) whenever CUR's minor has NOT advanced past
|
|
1076
|
+
# the last release's minor. The normal promote-rc path (dev already
|
|
1077
|
+
# minor-bumped) still no-ops.
|
|
1078
|
+
CUR_MI=$(printf '%s' "$CUR" | awk -F. '{printf "%s.%s", $1, $2}')
|
|
1079
|
+
LAST_MI=$(printf '%s' "$LAST" | awk -F. '{printf "%s.%s", $1, $2}')
|
|
1080
|
+
GREATEST_MI=$( { echo "$CUR_MI"; echo "$LAST_MI"; } | sort -V | tail -n1 || true)
|
|
1081
|
+
if [ "$CUR_MI" = "$LAST_MI" ] || [ "$GREATEST_MI" = "$LAST_MI" ]; then
|
|
1082
|
+
echo "::warning::inherited minor ${CUR_MI} <= last release minor ${LAST_MI} (CUR=${CUR} LAST=${LAST}); minor-bump skipped -> self-healing"
|
|
1083
|
+
if ! php ${MOKO_CLI}/version_bump.php --minor --min-version "$LAST" --path .; then
|
|
1084
|
+
echo "::error::monotonic bump guard failed to advance the base"; exit 1
|
|
1085
|
+
fi
|
|
1086
|
+
NEW=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
1087
|
+
echo "healed ${CUR} -> ${NEW} (publishing fresh; carrying forward to dev)"
|
|
1088
|
+
php ${MOKO_CLI}/release_bump_dev.php --path . --branch dev --bump minor \n --repo "${{ github.repository }}" --token "${GITHUB_TOKEN:-${{ github.token }}}" \n || echo "::warning::dev carry-forward failed (non-fatal; release still publishes the healed version)"
|
|
1089
|
+
else
|
|
1090
|
+
echo "base ${CUR} > last release ${LAST}: normal path, no heal"
|
|
1091
|
+
fi
|
|
1092
|
+
|
|
1093
|
+
# -- Release integrity: refuse to publish a STALE (pre-merge) tree ---------
|
|
1094
|
+
# #428 / MokoCLI #901: the release builds GITHUB_SHA (the triggering commit).
|
|
1095
|
+
# If a concurrent merge advanced the branch AFTER this run's trigger, we would
|
|
1096
|
+
# package a PRE-merge tree while stamping the new version -- a green release
|
|
1097
|
+
# that silently ships old code (reproduced: built ef8c1a5d while the tip was
|
|
1098
|
+
# the ladder-merge 2740342, missing only that merge's blobs). The mokocli
|
|
1099
|
+
# engine asserts packaged-tree == GITHUB_SHA, but it CANNOT see that GITHUB_SHA
|
|
1100
|
+
# itself trails the tip -- that is this guard's job. Fail-OPEN on any ambiguity
|
|
1101
|
+
# (unresolved tip / replica lag) so a good release is never falsely blocked; a
|
|
1102
|
+
# confident "built commit is an ancestor of the live tip" is a hard FAIL.
|
|
1103
|
+
- name: "Release integrity: no stale (pre-merge) tree"
|
|
1104
|
+
id: stale_guard
|
|
1105
|
+
if: steps.gate.outputs.has_makefile == 'true' && (steps.electron.outputs.is_electron != 'true' || vars.ELECTRON_CI_BUILD == 'true')
|
|
1106
|
+
run: |
|
|
1107
|
+
set -uo pipefail
|
|
1108
|
+
BUILT="${GITHUB_SHA:-}"
|
|
1109
|
+
BRANCH="${GITHUB_REF_NAME:-}"
|
|
1110
|
+
if [ -z "$BUILT" ] || [ -z "$BRANCH" ]; then
|
|
1111
|
+
echo "::warning::stale-tree guard: no GITHUB_SHA/branch to check (event=${GITHUB_EVENT_NAME:-?}); skipping (fail-open)."; exit 0
|
|
1112
|
+
fi
|
|
1113
|
+
# Fetch the CURRENT branch tip into a private tracking ref. Pin refs/heads/
|
|
1114
|
+
# explicitly: the channel-named-branch model lets a `stable` TAG and `stable`
|
|
1115
|
+
# BRANCH coexist, so a bare `git fetch origin stable` is ambiguous. Retry for
|
|
1116
|
+
# replica lag; a lagged read can only look EQUAL/behind -> never a false fail.
|
|
1117
|
+
TIP=""
|
|
1118
|
+
for i in 1 2 3 4; do
|
|
1119
|
+
if git fetch --quiet origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}-tipcheck" 2>/dev/null; then
|
|
1120
|
+
TIP="$(git rev-parse "refs/remotes/origin/${BRANCH}-tipcheck" 2>/dev/null || echo '')"
|
|
1121
|
+
[ -n "$TIP" ] && break
|
|
1122
|
+
fi
|
|
1123
|
+
sleep $((i * 2))
|
|
1124
|
+
done
|
|
1125
|
+
if [ -z "$TIP" ]; then
|
|
1126
|
+
echo "::warning::stale-tree guard: could not resolve tip of '${BRANCH}'; skipping (fail-open)."; exit 0
|
|
1127
|
+
fi
|
|
1128
|
+
if [ "$TIP" = "$BUILT" ]; then
|
|
1129
|
+
echo "Release integrity OK: building the current ${BRANCH} tip ${BUILT}."; exit 0
|
|
1130
|
+
fi
|
|
1131
|
+
if git merge-base --is-ancestor "$BUILT" "$TIP" 2>/dev/null; then
|
|
1132
|
+
# $BUILT is behind $TIP. Distinguish a genuine concurrent CODE merge
|
|
1133
|
+
# from this run's (or a prior release run's) OWN version-stamp commit
|
|
1134
|
+
# (#433). The "Publish stable release" step commits
|
|
1135
|
+
# "chore(release): build NN.NN.NN [skip ci]" onto the branch, which
|
|
1136
|
+
# advances the tip past $BUILT -- that is self-generated, NOT a
|
|
1137
|
+
# concurrent merge, and must not block (otherwise a re-run / the next
|
|
1138
|
+
# run false-fails forever and the release stalls: branch bumped, artifact
|
|
1139
|
+
# never published, [skip ci] blocking self-heal). If EVERY commit in
|
|
1140
|
+
# BUILT..TIP is such a release-stamp commit, proceed; if ANY other
|
|
1141
|
+
# commit is present, a real merge landed -> hard fail as before.
|
|
1142
|
+
FOREIGN=0
|
|
1143
|
+
for c in $(git rev-list "${BUILT}..${TIP}" 2>/dev/null); do
|
|
1144
|
+
SUBJ="$(git log -1 --format='%s' "$c" 2>/dev/null || echo '?')"
|
|
1145
|
+
case "$SUBJ" in
|
|
1146
|
+
"chore(release): build "*) : ;; # our own [skip ci] version stamp
|
|
1147
|
+
*) FOREIGN=1; break ;;
|
|
1148
|
+
esac
|
|
1149
|
+
done
|
|
1150
|
+
if [ "$FOREIGN" = "0" ]; then
|
|
1151
|
+
echo "::notice::stale-tree guard: '${BRANCH}' tip ${TIP} is ahead of ${BUILT} only by this run's own release-stamp commit(s) ('chore(release): build …'); not a concurrent merge -- proceeding (#433)."
|
|
1152
|
+
else
|
|
1153
|
+
echo "::error::Stale-tree guard: the triggering commit ${BUILT} is BEHIND the current '${BRANCH}' tip ${TIP} -- a concurrent merge landed during this run. Refusing to publish a pre-merge tree that would ship old code under the new version (#428 / MokoCLI #901). The newer commit gets its own release run; re-run this job against ${TIP} if a dedicated release for ${BUILT} is required."
|
|
1154
|
+
{
|
|
1155
|
+
echo "### ❌ Release blocked -- stale (pre-merge) tree"
|
|
1156
|
+
echo ""
|
|
1157
|
+
echo "Triggering commit \`${BUILT}\` is an **ancestor** of the current \`${BRANCH}\` tip \`${TIP}\`: a concurrent merge advanced the branch during this run. Publishing now would package code **missing that merge** while stamping the new version (the #901 bug). Blocked. See **#428 / MokoCLI #901**."
|
|
1158
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
1159
|
+
exit 1
|
|
1160
|
+
fi
|
|
1161
|
+
fi
|
|
1162
|
+
echo "::warning::stale-tree guard: built ${BUILT} != tip ${TIP} but is not its ancestor (diverged/unrelated); no pre-merge-tree risk detected -- proceeding."
|
|
1163
|
+
|
|
1164
|
+
- name: "Publish stable release"
|
|
1165
|
+
id: publish
|
|
1166
|
+
if: steps.gate.outputs.has_makefile == 'true' && (steps.electron.outputs.is_electron != 'true' || vars.ELECTRON_CI_BUILD == 'true')
|
|
1167
|
+
run: |
|
|
1168
|
+
# PILOT: call the MokoCLI build agent's publish verb (generic git-release).
|
|
1169
|
+
# `moko publish` maps to release_publish.php UNCHANGED -- same
|
|
1170
|
+
# stability/token contract, standards preserved. No --branch:
|
|
1171
|
+
# release_publish auto-detects the branch (here main).
|
|
1172
|
+
#
|
|
1173
|
+
# Bump-on-dev: --bump none + --no-commit. The base was bumped on dev
|
|
1174
|
+
# pre-promotion and inherited into main via the merge, so main neither
|
|
1175
|
+
# bumps NOR commits a version change. release_publish still stamps the
|
|
1176
|
+
# (unchanged) version in-tree so the built artifact is correct, but Step
|
|
1177
|
+
# 2c's commit/push to main is suppressed -- this is what removes the
|
|
1178
|
+
# main-only `chore(release): build` commit that diverged main from dev.
|
|
1179
|
+
# GITHUB_TOKEN is no longer needed here (nothing is persisted to main).
|
|
1180
|
+
# #469: this SAME --bump none --no-commit publish serves BOTH gate paths.
|
|
1181
|
+
# On the BUMP path it publishes the newly-advanced inherited version; on the
|
|
1182
|
+
# NO-BUMP path (steps.srcgate.should_bump=false — a non-source push) it
|
|
1183
|
+
# re-stamps the UNCHANGED current version onto the existing stable channel
|
|
1184
|
+
# release, re-issuing it in place with the refreshed (non-source) tree. The
|
|
1185
|
+
# version does not advance and the self-heal bump + new vNN tag are skipped
|
|
1186
|
+
# upstream, so no new version identity is minted — exactly the #469 contract.
|
|
1187
|
+
if [ "${{ steps.srcgate.outputs.should_bump }}" = "false" ]; then
|
|
1188
|
+
echo "::notice::#469 no-bump mode — re-issuing the existing stable release in place (same version, refreshed tree/assets/notes), no version advance, no new vNN tag."
|
|
1189
|
+
fi
|
|
1190
|
+
# #289: stamp <creationDate> (build date, UTC) + <author> (Makefile
|
|
1191
|
+
# PROJECT_AUTHOR) into Joomla manifests before publish, so the released
|
|
1192
|
+
# stable manifest carries them. release_publish (below) re-stamps <version>
|
|
1193
|
+
# idempotently. No-op on non-Joomla and on an older MokoCLI lacking the args.
|
|
1194
|
+
MG_BUILD_DATE="$(date -u +%Y-%m-%d)"
|
|
1195
|
+
MG_AUTHOR="$(grep -m1 -E '^PROJECT_AUTHOR[[:space:]]*:?=' Makefile 2>/dev/null | sed -E 's/.*:?=[[:space:]]*//' | sed 's/[[:space:]]*$//' || true)"
|
|
1196
|
+
MG_VER="$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '')"
|
|
1197
|
+
[ -n "$MG_VER" ] && php ${MOKO_CLI}/version_set_platform.php \
|
|
1198
|
+
--path . --version "$MG_VER" \
|
|
1199
|
+
--date "$MG_BUILD_DATE" ${MG_AUTHOR:+--author "$MG_AUTHOR"} 2>/dev/null || true
|
|
1200
|
+
|
|
1201
|
+
moko publish \
|
|
1202
|
+
--path . --stability stable --bump none --no-commit \
|
|
1203
|
+
--token "${{ github.token }}"
|
|
1204
|
+
|
|
1205
|
+
- name: "Read published version"
|
|
1206
|
+
id: version
|
|
1207
|
+
if: steps.gate.outputs.has_makefile == 'true'
|
|
1208
|
+
run: |
|
|
1209
|
+
# #741: consume the version the build agent already published (steps.publish
|
|
1210
|
+
# writes version=/base_version=/semver= to $GITHUB_OUTPUT via release_publish.php).
|
|
1211
|
+
# Falling back to version_read.php only if that output is absent (older
|
|
1212
|
+
# /opt/mokocli) keeps this backward-compatible while removing the second,
|
|
1213
|
+
# divergent resolution that silently false-skipped the npm publish gate.
|
|
1214
|
+
VERSION="${{ steps.publish.outputs.version }}"
|
|
1215
|
+
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
|
1216
|
+
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
1217
|
+
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
1218
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
1219
|
+
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
1220
|
+
# Feed-tag selection: joomla AND electron need a STABLE, ROLLING tag so
|
|
1221
|
+
# their update feed lives at a constant URL. electron (Template-Generic#333,
|
|
1222
|
+
# coordinates MokoGIT#1342): electron-updater's latest.yml / latest-linux.yml /
|
|
1223
|
+
# latest-mac.yml must sit at a fixed path so the MokoGIT electron-yml generator
|
|
1224
|
+
# can poll a constant URL; a moving v${VERSION} tag would relocate the feed every
|
|
1225
|
+
# release and break the updater. Everything else keeps the immutable v${VERSION} tag.
|
|
1226
|
+
if [[ "$PLATFORM" == joomla* || "$PLATFORM" == electron* ]]; then
|
|
1227
|
+
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
|
1228
|
+
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
|
1229
|
+
else
|
|
1230
|
+
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
1231
|
+
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
1232
|
+
fi
|
|
1233
|
+
echo "branch=main" >> "$GITHUB_OUTPUT"
|
|
1234
|
+
echo "Published version: ${VERSION}"
|
|
1235
|
+
|
|
1236
|
+
- name: "Deploy to prod (inline — replaces standalone deploy.yml, #1427)"
|
|
1237
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.version.outputs.skip != 'true' && vars.PROD_SSH_USERNAME != ''
|
|
1238
|
+
env:
|
|
1239
|
+
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
1240
|
+
run: |
|
|
1241
|
+
# #1427: prod deploy inlined here (release job runs on push to stable).
|
|
1242
|
+
# Gate = PROD_SSH_USERNAME set → only SSH-deploy repos deploy; others skip
|
|
1243
|
+
# green. Targets from repo VARIABLES. mokocli deploy.php → restricted
|
|
1244
|
+
# moko-deploy (build → push-guarded → recreate → health-check → rollback;
|
|
1245
|
+
# advances the floating :stable tag only after a healthy deploy). Must be
|
|
1246
|
+
# atomic with deleting deploy.yml — otherwise deploy.yml's push:[stable]
|
|
1247
|
+
# trigger would double-deploy prod.
|
|
1248
|
+
set -euo pipefail
|
|
1249
|
+
if [ -z "${DEPLOY_KEY:-}" ]; then
|
|
1250
|
+
echo "::warning::PROD_SSH_USERNAME set but DEPLOY_SSH_KEY secret missing — skipping prod deploy"; exit 0
|
|
1251
|
+
fi
|
|
1252
|
+
mkdir -p ~/.ssh
|
|
1253
|
+
# DEPLOY_SSH_KEY is base64-encoded (single line) so act_runner masks it in logs
|
|
1254
|
+
# and the multi-line key is never echoed. Decode when it base64-decodes to a
|
|
1255
|
+
# private key; else treat as a legacy raw PEM (back-compat during rollout).
|
|
1256
|
+
install -m600 /dev/null ~/.ssh/deploy_key # 0600 before any bytes (no umask window)
|
|
1257
|
+
trap 'rm -f ~/.ssh/deploy_key' EXIT # never leave the key on a shared runner
|
|
1258
|
+
if printf '%s' "$DEPLOY_KEY" | base64 -d 2>/dev/null | grep -q 'PRIVATE KEY'; then
|
|
1259
|
+
printf '%s' "$DEPLOY_KEY" | base64 -d > ~/.ssh/deploy_key
|
|
1260
|
+
else
|
|
1261
|
+
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
|
1262
|
+
fi
|
|
1263
|
+
# #1637: deploy the IMMUTABLE pushed commit, not a git-describe of the
|
|
1264
|
+
# runner's HEAD. release_publish's internal `checkout -B FETCH_HEAD` can
|
|
1265
|
+
# re-poison HEAD with a replica-lagged commit by this step, so `git describe`
|
|
1266
|
+
# / `git rev-parse HEAD` here can name an ANCESTOR — moko-deploy then built a
|
|
1267
|
+
# stale image while the release reported success (the favicon incident).
|
|
1268
|
+
# $GITHUB_SHA is the pushed stable commit (BUMP-ON-DEV: it already carries the
|
|
1269
|
+
# released VERSION + code) and cannot be re-poisoned. moko-deploy checks out
|
|
1270
|
+
# this exact 40-hex commit (falling back to the branch tip for non-SHA tags).
|
|
1271
|
+
TAG="${GITHUB_SHA}"
|
|
1272
|
+
echo "Deploying prod commit ${TAG} to ${{ vars.PROD_SSH_USERNAME }}@${{ vars.PROD_SSH_HOST }}"
|
|
1273
|
+
php "${MOKO_CLI}/deploy.php" --tier prod --tag "$TAG" \
|
|
1274
|
+
--ssh-host "${{ vars.PROD_SSH_HOST }}" --ssh-port "${{ vars.PROD_SSH_PORT }}" \
|
|
1275
|
+
--ssh-user "${{ vars.PROD_SSH_USERNAME }}" --ssh-key ~/.ssh/deploy_key
|
|
1276
|
+
|
|
1277
|
+
# -- Electron: build installers (Windows via Wine + Linux) and attach ------
|
|
1278
|
+
# No Windows/macOS runner: the `release` runner is Ubuntu, so we cross-build
|
|
1279
|
+
# the Windows target with Wine and build Linux natively. macOS is skipped
|
|
1280
|
+
# (cannot be cross-built off a Mac). All four steps no-op on non-electron repos.
|
|
1281
|
+
- name: "Setup Node (electron)"
|
|
1282
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.electron.outputs.is_electron == 'true' && steps.version.outputs.skip != 'true' && vars.ELECTRON_CI_BUILD == 'true'
|
|
1283
|
+
uses: actions/setup-node@v4
|
|
1284
|
+
with:
|
|
1285
|
+
node-version: 24.18.1
|
|
1286
|
+
|
|
1287
|
+
- name: "Setup pnpm (electron)"
|
|
1288
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.electron.outputs.is_electron == 'true' && steps.version.outputs.skip != 'true' && vars.ELECTRON_CI_BUILD == 'true'
|
|
1289
|
+
uses: pnpm/action-setup@v4
|
|
1290
|
+
with:
|
|
1291
|
+
version: 11.20.0
|
|
1292
|
+
|
|
1293
|
+
- name: "Build electron installers (Windows via Wine + Linux)"
|
|
1294
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.electron.outputs.is_electron == 'true' && steps.version.outputs.skip != 'true' && vars.ELECTRON_CI_BUILD == 'true'
|
|
1295
|
+
env:
|
|
1296
|
+
HUSKY: "0"
|
|
1297
|
+
run: |
|
|
1298
|
+
# electron-builder's NSIS target RUNS the built installer under Wine to
|
|
1299
|
+
# generate + sign the uninstaller, which needs 32-bit Wine libs (the NSIS
|
|
1300
|
+
# stub is a WoW64 exe) AND an X display. A bare `wine64` on an
|
|
1301
|
+
# under-provisioned `release` runner aborts with "wine process failed 1
|
|
1302
|
+
# ... ntdll.dll c0000135 / no driver could be loaded" (intermittent: only
|
|
1303
|
+
# when a job lands on a runner without wine32/xvfb pre-seeded). Provision
|
|
1304
|
+
# i386 Wine + Xvfb here so the Windows leg builds on ANY runner, and init
|
|
1305
|
+
# the prefix once, headless, so electron-builder's first Wine call does
|
|
1306
|
+
# not race prefix creation.
|
|
1307
|
+
sudo dpkg --add-architecture i386 || true
|
|
1308
|
+
sudo apt-get update -qq
|
|
1309
|
+
sudo apt-get install -y -qq xvfb >/dev/null 2>&1 || echo "::warning::xvfb install failed"
|
|
1310
|
+
sudo apt-get install -y -qq wine64 wine32 >/dev/null 2>&1 || sudo apt-get install -y -qq wine >/dev/null 2>&1 || sudo apt-get install -y -qq wine64 >/dev/null 2>&1 || echo "::warning::wine install failed -- Windows target may be skipped"
|
|
1311
|
+
export WINEDEBUG=-all
|
|
1312
|
+
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine}"
|
|
1313
|
+
xvfb-run -a wineboot --init >/dev/null 2>&1 || true
|
|
1314
|
+
pnpm install --frozen-lockfile
|
|
1315
|
+
# Compile the app into electron-builder's app dir (directories.app, e.g. ./build)
|
|
1316
|
+
# BEFORE packaging -- electron-builder validates the app dir up-front and aborts
|
|
1317
|
+
# with "Application directory ./build doesn't exist" otherwise. Prefer an explicit
|
|
1318
|
+
# repo script (compile:app); fall back to the esbuild entrypoint.
|
|
1319
|
+
if pnpm run 2>/dev/null | grep -qiE '(^|[[:space:]])compile:app([[:space:]]|$)'; then
|
|
1320
|
+
pnpm run compile:app
|
|
1321
|
+
elif [ -f esbuild.mjs ]; then
|
|
1322
|
+
npx --no-install preval-build-info-cli >/dev/null 2>&1 || true
|
|
1323
|
+
node esbuild.mjs
|
|
1324
|
+
else
|
|
1325
|
+
echo "::warning::no compile step found (add a 'compile:app' package.json script or esbuild.mjs) -- electron-builder may fail if the app dir is not pre-built"
|
|
1326
|
+
fi
|
|
1327
|
+
# xvfb-run gives Wine a virtual X display for the installer-run pass that
|
|
1328
|
+
# generates the NSIS uninstaller; without it Wine cannot create a window
|
|
1329
|
+
# and the build fails ("no driver could be loaded / $DISPLAY").
|
|
1330
|
+
xvfb-run -a npx --no-install electron-builder --win --linux --publish never || xvfb-run -a npx electron-builder --win --linux --publish never
|
|
1331
|
+
|
|
1332
|
+
- name: "Attach electron installers to the release"
|
|
1333
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.electron.outputs.is_electron == 'true' && steps.version.outputs.skip != 'true' && vars.ELECTRON_CI_BUILD == 'true'
|
|
1334
|
+
continue-on-error: true
|
|
1335
|
+
run: |
|
|
1336
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1337
|
+
TOKEN="${{ github.token }}"
|
|
1338
|
+
TAG="${{ steps.version.outputs.tag }}"
|
|
1339
|
+
RID=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
1340
|
+
if [ -z "$RID" ]; then echo "::warning::release ${TAG} not found -- skipping electron asset upload"; else
|
|
1341
|
+
n=0
|
|
1342
|
+
for f in $(find out dist_electron dist -maxdepth 2 -type f \( -name '*.exe' -o -name '*.AppImage' -o -name '*.deb' -o -name 'latest*.yml' \) 2>/dev/null); do
|
|
1343
|
+
name=$(basename "$f")
|
|
1344
|
+
curl -sf -X POST -H "Authorization: token ${TOKEN}" "${API_BASE}/releases/${RID}/assets?name=${name}" -F "attachment=@${f}" >/dev/null && { echo "attached ${name}"; n=$((n+1)); } || echo "::warning::failed to upload ${name}"
|
|
1345
|
+
done
|
|
1346
|
+
echo "Electron installers attached to ${TAG}: ${n} file(s)"
|
|
1347
|
+
fi
|
|
1348
|
+
|
|
1349
|
+
# -- mokohub-manifest: zip manifest.json -> attach to the stable release -----
|
|
1350
|
+
# ADDITIVE + GATED (Template-Generic#326). Gated on is_manifest so every other
|
|
1351
|
+
# platform is untouched. Best-effort attach (continue-on-error): the release is
|
|
1352
|
+
# already published, so a packaging hiccup must not red the pipeline.
|
|
1353
|
+
- name: "Package mokohub-manifest (zip manifest.json)"
|
|
1354
|
+
if: >-
|
|
1355
|
+
steps.gate.outputs.has_makefile == 'true' &&
|
|
1356
|
+
steps.mokohub.outputs.is_manifest == 'true' &&
|
|
1357
|
+
steps.version.outputs.skip != 'true'
|
|
1358
|
+
continue-on-error: true
|
|
1359
|
+
run: |
|
|
1360
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1361
|
+
TOKEN="${{ github.token }}"
|
|
1362
|
+
TAG="${{ steps.version.outputs.tag }}"
|
|
1363
|
+
VERSION="${{ steps.version.outputs.version }}"
|
|
1364
|
+
if [ ! -f manifest.json ]; then
|
|
1365
|
+
echo "::warning::mokohub-manifest: no manifest.json at repo root — nothing to package"
|
|
1366
|
+
exit 0
|
|
1367
|
+
fi
|
|
1368
|
+
ZIP="${RUNNER_TEMP:-/tmp}/manifest-${VERSION}.zip"
|
|
1369
|
+
( cd "$(dirname manifest.json)" && zip -j "$ZIP" manifest.json ) \
|
|
1370
|
+
|| { echo "::warning::mokohub-manifest: zip failed"; exit 0; }
|
|
1371
|
+
RID=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
1372
|
+
if [ -z "$RID" ]; then echo "::warning::release ${TAG} not found — skipping manifest asset upload"; exit 0; fi
|
|
1373
|
+
name="$(basename "$ZIP")"
|
|
1374
|
+
curl -sf -X POST -H "Authorization: token ${TOKEN}" "${API_BASE}/releases/${RID}/assets?name=${name}" -F "attachment=@${ZIP}" >/dev/null \
|
|
1375
|
+
&& echo "attached ${name} to ${TAG}" || echo "::warning::manifest asset upload failed (${name})"
|
|
1376
|
+
|
|
1377
|
+
# -- mokohub-recipes: build catalog.zip (via MokoCLI) -> release per stream --
|
|
1378
|
+
# ADDITIVE + GATED (Template-Generic#326). Delegate the per-stream signed
|
|
1379
|
+
# catalog.zip (all recipes + all.json) to the MokoCLI build agent (`moko
|
|
1380
|
+
# package`, MokoCLI #684 / epic mokohub-recipes#1). Gated on is_recipes;
|
|
1381
|
+
# best-effort so a not-yet-shipped MokoCLI verb can't red an already-published
|
|
1382
|
+
# release and no existing platform is affected.
|
|
1383
|
+
- name: "Package mokohub-recipes (catalog.zip via MokoCLI)"
|
|
1384
|
+
if: >-
|
|
1385
|
+
steps.gate.outputs.has_makefile == 'true' &&
|
|
1386
|
+
steps.mokohub.outputs.is_recipes == 'true' &&
|
|
1387
|
+
steps.version.outputs.skip != 'true'
|
|
1388
|
+
continue-on-error: true
|
|
1389
|
+
run: |
|
|
1390
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1391
|
+
VERSION="${{ steps.version.outputs.version }}"
|
|
1392
|
+
TAG="${{ steps.version.outputs.tag }}"
|
|
1393
|
+
# `moko package` for the mokohub-recipes platform builds the stable-stream
|
|
1394
|
+
# catalog.zip and attaches it to the release. --stability stable matches the
|
|
1395
|
+
# auto-release (stable) channel; the platform shim selects the catalog builder.
|
|
1396
|
+
moko package \
|
|
1397
|
+
--path . --version "$VERSION" --tag "$TAG" \
|
|
1398
|
+
--token "${{ github.token }}" --api-base "$API_BASE" \
|
|
1399
|
+
--repo "${GIT_REPO}" --out "${RUNNER_TEMP:-/tmp}" \
|
|
1400
|
+
--stability stable \
|
|
1401
|
+
|| echo "::warning::mokohub-recipes: moko package (catalog.zip) failed or unavailable — non-fatal (MokoCLI #684)"
|
|
1402
|
+
|
|
1403
|
+
- name: "Create semver tag for non-Joomla repos"
|
|
1404
|
+
id: semver
|
|
1405
|
+
# #469: a NEW immutable vNN tag is a version-bump artifact — skip it in
|
|
1406
|
+
# no-bump mode (should_bump=false). The version did not advance, so the
|
|
1407
|
+
# existing vNN tag already points at the current release; minting a new one
|
|
1408
|
+
# would imply a new version. On the bump path this is unchanged.
|
|
1409
|
+
if: |
|
|
1410
|
+
steps.gate.outputs.has_makefile == 'true' &&
|
|
1411
|
+
steps.srcgate.outputs.should_bump != 'false' &&
|
|
1412
|
+
steps.version.outputs.skip != 'true' &&
|
|
1413
|
+
!startsWith(steps.platform.outputs.platform, 'joomla') &&
|
|
1414
|
+
(steps.electron.outputs.is_electron != 'true' || vars.ELECTRON_CI_BUILD == 'true')
|
|
1415
|
+
run: |
|
|
1416
|
+
VERSION="${{ steps.version.outputs.version }}"
|
|
1417
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1418
|
+
TOKEN="${{ github.token }}"
|
|
1419
|
+
SEMVER_TAG="v${VERSION}"
|
|
1420
|
+
# #386: pin the semver tag to the actual built/published commit (the
|
|
1421
|
+
# post-publish HEAD the release/channel-tag is pinned to), NOT the
|
|
1422
|
+
# triggering commit -- the publish step commits the CHANGELOG entry, so
|
|
1423
|
+
# GITHUB_SHA is one commit stale.
|
|
1424
|
+
BUILT_SHA="$(git rev-parse HEAD 2>/dev/null || echo "${GITHUB_SHA}")"
|
|
1425
|
+
|
|
1426
|
+
echo "Creating semver tag: ${SEMVER_TAG}"
|
|
1427
|
+
|
|
1428
|
+
# Create the git tag via API
|
|
1429
|
+
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
|
1430
|
+
-X POST -H "Authorization: token ${TOKEN}" \
|
|
1431
|
+
-H "Content-Type: application/json" \
|
|
1432
|
+
"${API_BASE}/tags" \
|
|
1433
|
+
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"${BUILT_SHA}\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
|
1434
|
+
|
|
1435
|
+
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
1436
|
+
echo "Created semver tag: ${SEMVER_TAG}"
|
|
1437
|
+
elif [ "$HTTP_CODE" = "409" ]; then
|
|
1438
|
+
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
|
1439
|
+
else
|
|
1440
|
+
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
|
1441
|
+
fi
|
|
1442
|
+
|
|
1443
|
+
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
|
1444
|
+
|
|
1445
|
+
# -- "Advance rolling channel tag via git" REMOVED (#1621 Phase 6) ---------
|
|
1446
|
+
# This step force-pushed refs/tags/stable to the built HEAD so the #386 gate
|
|
1447
|
+
# below would pass. It is now REDUNDANT: the publish already pins the `stable`
|
|
1448
|
+
# channel ref via release_create's forceTagRef() (#808), and #1621 has the
|
|
1449
|
+
# forge attach/move the channel tag onto the dual-tagged release server-side
|
|
1450
|
+
# (channel_tags[] on create -> AttachChannelTag -> moveGitTag at the release
|
|
1451
|
+
# commit). The #386 gate still resolves `tags/stable` to the built commit, so
|
|
1452
|
+
# it stays valid without this duplicate git force-push.
|
|
1453
|
+
|
|
1454
|
+
- name: "Release integrity gate: artifact commit == tag commit (#386)"
|
|
1455
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.version.outputs.skip != 'true' && (steps.electron.outputs.is_electron != 'true' || vars.ELECTRON_CI_BUILD == 'true')
|
|
1456
|
+
# #386: assert the published tag's commit == the commit we actually built &
|
|
1457
|
+
# published, and FAIL on a definite mismatch (a tag pinned to a commit we did
|
|
1458
|
+
# NOT build -- e.g. a LATER branch HEAD advanced by a concurrent merge). The
|
|
1459
|
+
# publish step commits the CHANGELOG entry ("build NN.NN.NN [skip ci]") and
|
|
1460
|
+
# mokocli pins the tag to that POST-PUBLISH HEAD, which is one commit past the
|
|
1461
|
+
# triggering GITHUB_SHA -- so BUILT must be the current HEAD, not GITHUB_SHA
|
|
1462
|
+
# (using GITHUB_SHA false-failed every cut whose CHANGELOG wasn't pre-seeded on
|
|
1463
|
+
# dev). Fail-OPEN on any ambiguity (no tag / unresolvable) so a good release is
|
|
1464
|
+
# never falsely blocked.
|
|
1465
|
+
run: |
|
|
1466
|
+
set -uo pipefail
|
|
1467
|
+
BUILT="$(git rev-parse HEAD 2>/dev/null || echo "${GITHUB_SHA}")"
|
|
1468
|
+
# #386: assert against the MOVED channel pointer (the rolling "stable"
|
|
1469
|
+
# tag, force-pinned to the built commit by release_create forceTagRef),
|
|
1470
|
+
# NOT the immutable vNN.NN.NN minor tag. The version-primary /
|
|
1471
|
+
# minor-identity model (#1621/#76/#1725) deliberately never re-points
|
|
1472
|
+
# vNN.NN.NN on an in-place (#469/#772) cut, so checking
|
|
1473
|
+
# steps.version.outputs.tag (= v${VERSION} on generic/non-Joomla)
|
|
1474
|
+
# false-failed every no-bump release. joomla/electron already feed-tag
|
|
1475
|
+
# "stable", which is why they never hit this.
|
|
1476
|
+
TAG="stable"
|
|
1477
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1478
|
+
if [ -z "$TAG" ] || [ -z "$BUILT" ]; then
|
|
1479
|
+
echo "::notice::integrity gate: missing tag or built SHA - skipping (fail-open)."; exit 0
|
|
1480
|
+
fi
|
|
1481
|
+
TAG_SHA=$(curl -sf -H "Authorization: token ${{ github.token }}" "${API_BASE}/tags/${TAG}" \
|
|
1482
|
+
| python3 -c "import sys,json; print((json.load(sys.stdin).get('commit') or {}).get('sha',''))" 2>/dev/null || true)
|
|
1483
|
+
if [ -z "$TAG_SHA" ]; then
|
|
1484
|
+
echo "::notice::integrity gate: cannot resolve tag ${TAG} to a commit - skipping (fail-open)."; exit 0
|
|
1485
|
+
fi
|
|
1486
|
+
if [ "$TAG_SHA" != "$BUILT" ]; then
|
|
1487
|
+
echo "::error::Release integrity FAIL (#386): built commit ${BUILT} != tag ${TAG} commit ${TAG_SHA}. Published artifact may not match its tag (pre-merge/advanced-branch race). Failing so a stale release cannot ship silently."
|
|
1488
|
+
exit 1
|
|
1489
|
+
fi
|
|
1490
|
+
echo "Release integrity OK: tag ${TAG} -> ${BUILT}"
|
|
1491
|
+
|
|
1492
|
+
- name: Update release notes and reconcile changelog
|
|
1493
|
+
if: steps.gate.outputs.has_makefile == 'true' && (steps.electron.outputs.is_electron != 'true' || vars.ELECTRON_CI_BUILD == 'true')
|
|
1494
|
+
env:
|
|
1495
|
+
# Admin-bypass token for the idempotent Contents-API changelog reconcile
|
|
1496
|
+
# below (#510) — needed only on the fix/hotfix->main path (see comment).
|
|
1497
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
1498
|
+
run: |
|
|
1499
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1500
|
+
TOKEN="${{ github.token }}"
|
|
1501
|
+
|
|
1502
|
+
# Get the stable release info (version and ID).
|
|
1503
|
+
# #1702 / .mokogit-private#76: resolve the current stable release by its
|
|
1504
|
+
# CHANNEL POINTER (releases/channel/stable -> ReleaseStreamMap), not by a
|
|
1505
|
+
# fixed tag_name=stable. Version-primary releases carry tag_name=vNN, so
|
|
1506
|
+
# GET /releases/tags/stable no longer resolves the current stable release.
|
|
1507
|
+
# Graceful fallback to the legacy tags/stable call for forges predating
|
|
1508
|
+
# #1702 (the channel route 404s pre-rollout). Both return the same release
|
|
1509
|
+
# JSON shape (id + name "... (VERSION: NN.NN.NN)").
|
|
1510
|
+
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
1511
|
+
"${API_BASE}/releases/channel/stable" 2>/dev/null || true)
|
|
1512
|
+
[ -z "$RELEASE_JSON" ] && RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
1513
|
+
"${API_BASE}/releases/tags/stable" 2>/dev/null || true)
|
|
1514
|
+
[ -z "$RELEASE_JSON" ] && RELEASE_JSON='{}'
|
|
1515
|
+
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
1516
|
+
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
|
|
1517
|
+
VERSION=$(python3 -c "
|
|
1518
|
+
import json, sys, re
|
|
1519
|
+
r = json.load(sys.stdin)
|
|
1520
|
+
name = r.get('name', '')
|
|
1521
|
+
m = re.search(r'(\d+\.\d+\.\d+)', name)
|
|
1522
|
+
print(m.group(1) if m else '')
|
|
1523
|
+
" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
1524
|
+
|
|
1525
|
+
# Extract [Unreleased] section via the shared mokocli command (#364 centralization).
|
|
1526
|
+
NOTES=""
|
|
1527
|
+
if [ -f "release_notes.md" ] && [ -s "release_notes.md" ]; then
|
|
1528
|
+
NOTES="$(cat release_notes.md)"
|
|
1529
|
+
fi
|
|
1530
|
+
# #71 / #1725: fall back to the MINOR section of CHANGELOG when no curated
|
|
1531
|
+
# release_notes.md. A stable release is identified by its MINOR (the VERSION
|
|
1532
|
+
# resolved above from the release name is NN.NN.00 — the minor is the unit of
|
|
1533
|
+
# release identity), and within-minor patch notes are ACCUMULATED under that
|
|
1534
|
+
# one ## [NN.NN.00] heading. So read $VERSION — NOT [Unreleased], which the
|
|
1535
|
+
# accumulate step just emptied — to get the whole minor's stacked notes.
|
|
1536
|
+
# --channel stable makes an empty section fall back to the "Stable release"
|
|
1537
|
+
# label rather than a bare "Release NN.NN.00".
|
|
1538
|
+
if [ -z "$NOTES" ] && [ -f "CHANGELOG.md" ]; then
|
|
1539
|
+
NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version "${VERSION:-Unreleased}" --channel stable 2>/dev/null || true)
|
|
1540
|
+
fi
|
|
1541
|
+
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
1542
|
+
|
|
1543
|
+
# Update release body via API
|
|
1544
|
+
if [ -n "$RELEASE_ID" ]; then
|
|
1545
|
+
python3 -c "
|
|
1546
|
+
import json, urllib.request
|
|
1547
|
+
body = open('/dev/stdin').read()
|
|
1548
|
+
payload = json.dumps({'body': body}).encode()
|
|
1549
|
+
req = urllib.request.Request(
|
|
1550
|
+
'${API_BASE}/releases/${RELEASE_ID}',
|
|
1551
|
+
data=payload, method='PATCH',
|
|
1552
|
+
headers={
|
|
1553
|
+
'Authorization': 'token ${TOKEN}',
|
|
1554
|
+
'Content-Type': 'application/json'
|
|
1555
|
+
})
|
|
1556
|
+
urllib.request.urlopen(req)
|
|
1557
|
+
" <<< "$NOTES"
|
|
1558
|
+
echo "Release notes updated (release_notes.md if present, else CHANGELOG.md)"
|
|
1559
|
+
fi
|
|
1560
|
+
|
|
1561
|
+
# Bump-on-dev: the CHANGELOG [Unreleased] -> [version] promotion now
|
|
1562
|
+
# happens on DEV pre-promotion (promote-rc's release_bump_dev.php step),
|
|
1563
|
+
# so main INHERITS the already-promoted changelog via the merge and this
|
|
1564
|
+
# step normally has nothing to do. We keep an idempotent reconcile here as
|
|
1565
|
+
# a safety-net for the fix/hotfix->main path (a fix/* PR merges straight to
|
|
1566
|
+
# main WITHOUT going through dev/promote-rc, so its [Unreleased] section was
|
|
1567
|
+
# never promoted). changelog_promote.php --branch is idempotent (won't
|
|
1568
|
+
# re-promote an existing version) and merges/normalizes, so on the dev path
|
|
1569
|
+
# it is a no-op; on the fix path it promotes. Non-fatal (`|| true`): the
|
|
1570
|
+
# release is already published, and on the dev path there is nothing to do.
|
|
1571
|
+
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
|
1572
|
+
php ${MOKO_CLI}/changelog_promote.php --path . --version "$VERSION" --accumulate \
|
|
1573
|
+
--branch "${{ github.event.repository.default_branch }}" --token "${GITHUB_TOKEN}" --api-base "${API_BASE}" \
|
|
1574
|
+
|| echo "::notice::changelog reconcile on main was a no-op or non-fatal (already promoted on dev)"
|
|
1575
|
+
echo "Changelog reconciled on main for [${VERSION}] (idempotent, accumulate)"
|
|
1576
|
+
fi
|
|
1577
|
+
|
|
1578
|
+
# Publish the stable release notes to mokolive (#470 / .mokogit-private#85).
|
|
1579
|
+
# INERT BY DEFAULT: skips unless BOTH the MOKOLIVE_ENDPOINT and
|
|
1580
|
+
# MOKOLIVE_RELEASE_NOTES_CATEGORY_ID repo/org variables are set, so the
|
|
1581
|
+
# step is a no-op on every repo until an owner opts in. BEST-EFFORT:
|
|
1582
|
+
# continue-on-error + an ntfy alert on failure, so it never red-fails a
|
|
1583
|
+
# release that already published. Delegates to the MokoCLI companion
|
|
1584
|
+
# subcommand release_publish_notes.php (MokoCLI#951, built in parallel) —
|
|
1585
|
+
# the step is dependency-safe: if the token is unset it exits 0, and if the
|
|
1586
|
+
# subcommand is absent the php call simply fails into the best-effort ntfy.
|
|
1587
|
+
- name: Publish release notes to mokolive
|
|
1588
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.version.outputs.skip != 'true' &&
|
|
1589
|
+
vars.MOKOLIVE_ENDPOINT != '' && vars.MOKOLIVE_RELEASE_NOTES_CATEGORY_ID != ''
|
|
1590
|
+
continue-on-error: true
|
|
1591
|
+
env:
|
|
1592
|
+
MOKOLIVE_API_TOKEN: ${{ secrets.MOKOLIVE_API_TOKEN }}
|
|
1593
|
+
NTFY_TOPIC: ${{ vars.MOKOLIVE_NTFY_TOPIC || vars.NTFY_TOPIC || 'git-releases' }}
|
|
1594
|
+
run: |
|
|
1595
|
+
if [ -z "${MOKOLIVE_API_TOKEN:-}" ]; then echo "::notice::mokolive token unset; skipping release-notes publish"; exit 0; fi
|
|
1596
|
+
# Resolve the PREVIOUS stable version so the subcommand can compute the
|
|
1597
|
+
# bump against --min-bump. There is no steps.*.outputs for the prior
|
|
1598
|
+
# version (the monotonic guard computes it locally but does not export
|
|
1599
|
+
# it), so resolve it the same channel-first way the rest of this file
|
|
1600
|
+
# does: GET releases/channel/stable -> release NAME "VERSION: NN.NN.NN",
|
|
1601
|
+
# with the legacy tags/stable fallback for forges predating #1702. The
|
|
1602
|
+
# `stable` channel still points at the PRIOR release at this point —
|
|
1603
|
+
# the new release's channel-tag move happens in the publish step above,
|
|
1604
|
+
# but this lookup runs on a fresh curl so empty/unresolved is fine
|
|
1605
|
+
# (subcommand treats empty --prev-version as first-release -> publishes).
|
|
1606
|
+
PREV_VERSION=""
|
|
1607
|
+
PREV_JSON=$(curl -sf -H "Authorization: token ${{ github.token }}" \
|
|
1608
|
+
"${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/releases/channel/stable" 2>/dev/null || true)
|
|
1609
|
+
[ -z "$PREV_JSON" ] && PREV_JSON=$(curl -sf -H "Authorization: token ${{ github.token }}" \
|
|
1610
|
+
"${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/releases/tags/stable" 2>/dev/null || true)
|
|
1611
|
+
PREV_VERSION=$(printf '%s' "${PREV_JSON:-}" \
|
|
1612
|
+
| grep -oE 'VERSION: [0-9]{2}\.[0-9]{2}\.[0-9]{2}' | grep -oE '[0-9]{2}\.[0-9]{2}\.[0-9]{2}' | head -n1 || true)
|
|
1613
|
+
php ${MOKO_CLI}/release_publish_notes.php --path . \
|
|
1614
|
+
--version "${{ steps.version.outputs.version }}" \
|
|
1615
|
+
--prev-version "${PREV_VERSION}" \
|
|
1616
|
+
--min-bump "${{ vars.MOKOLIVE_PUBLISH_BUMP || 'patch' }}" \
|
|
1617
|
+
--endpoint "${{ vars.MOKOLIVE_ENDPOINT }}" \
|
|
1618
|
+
--category-id "${{ vars.MOKOLIVE_RELEASE_NOTES_CATEGORY_ID }}" \
|
|
1619
|
+
--token "${MOKOLIVE_API_TOKEN}" \
|
|
1620
|
+
--software-slug "${{ vars.MOKOLIVE_SOFTWARE_SLUG || github.event.repository.name }}" \
|
|
1621
|
+
--release-url "${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}" \
|
|
1622
|
+
|| curl -sS -H "Title: mokolive release-notes publish failed: ${GIT_REPO} ${{ steps.version.outputs.version }}" \
|
|
1623
|
+
-H "Tags: warning" -H "Priority: high" \
|
|
1624
|
+
-d "release:publish-notes failed for ${GIT_REPO} ${{ steps.version.outputs.version }}" \
|
|
1625
|
+
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
1626
|
+
|
|
1627
|
+
# -- npm stable publish: ABSORBED from Template-NPM's retired npm-publish.yml
|
|
1628
|
+
# Replaces the former "Dispatch npm publish" fan-out to the standalone
|
|
1629
|
+
# npm-publish.yml. Dual-registry, idempotent, gated on the npm platform so
|
|
1630
|
+
# every non-npm platform is 100% unaffected. MokoGIT npm registry uses the
|
|
1631
|
+
# per-run Actions token (packages:write, least-privilege, no shared secret);
|
|
1632
|
+
# npmjs.org uses NPM_TOKEN (best-effort, skipped if absent). A version already
|
|
1633
|
+
# present on a registry is a no-op. Version is the Makefile PROJECT_VERSION
|
|
1634
|
+
# (single source of truth), stamped into package.json in-tree only.
|
|
1635
|
+
- name: "Setup Node (npm publish)"
|
|
1636
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.npmpub.outputs.publishable == 'true' && (startsWith(steps.platform.outputs.platform, 'npm') || startsWith(steps.platform.outputs.platform, 'nodejs'))
|
|
1637
|
+
uses: actions/setup-node@v4
|
|
1638
|
+
with:
|
|
1639
|
+
node-version: '20'
|
|
1640
|
+
- name: "npm: resolve version, stamp package.json, build"
|
|
1641
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.npmpub.outputs.publishable == 'true' && (startsWith(steps.platform.outputs.platform, 'npm') || startsWith(steps.platform.outputs.platform, 'nodejs'))
|
|
1642
|
+
run: |
|
|
1643
|
+
set -euo pipefail
|
|
1644
|
+
VERSION=$(grep -E '^[[:space:]]*PROJECT_VERSION[[:space:]]*:?=' Makefile | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]')
|
|
1645
|
+
[ -z "$VERSION" ] && { echo "::error::No PROJECT_VERSION in Makefile - cannot determine npm publish version."; exit 1; }
|
|
1646
|
+
# Makefile PROJECT_VERSION is zero-padded NN.NN.NN (e.g. 01.00.01); npm needs
|
|
1647
|
+
# strict semver (no leading zeros), so normalize before stamping.
|
|
1648
|
+
SEMVER=$(printf '%s' "$VERSION" | awk -F. '{printf "%d.%d.%d",$1,$2,$3}')
|
|
1649
|
+
npm version "$SEMVER" --no-git-tag-version --allow-same-version
|
|
1650
|
+
npm ci --no-audit --no-fund || npm install --no-audit --no-fund
|
|
1651
|
+
npm run build --if-present
|
|
1652
|
+
echo "Prepared npm publish for ${SEMVER} (source: Makefile PROJECT_VERSION ${VERSION})"
|
|
1653
|
+
- name: "npm: publish to the MokoGIT registry (per-run Actions token)"
|
|
1654
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.npmpub.outputs.publishable == 'true' && (startsWith(steps.platform.outputs.platform, 'npm') || startsWith(steps.platform.outputs.platform, 'nodejs'))
|
|
1655
|
+
env:
|
|
1656
|
+
NODE_AUTH_TOKEN: ${{ github.token }}
|
|
1657
|
+
run: |
|
|
1658
|
+
set -euo pipefail
|
|
1659
|
+
HOST="${MOKOGIT_URL#https://}"; OWNER="${GIT_ORG}"
|
|
1660
|
+
REG="https://${HOST}/api/packages/${OWNER}/npm/"
|
|
1661
|
+
echo "//${HOST}/api/packages/${OWNER}/npm/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc
|
|
1662
|
+
set +e; out="$(npm publish --ignore-scripts --registry "${REG}" 2>&1)"; rc=$?; set -e
|
|
1663
|
+
rm -f .npmrc; echo "$out"
|
|
1664
|
+
if [ "$rc" -eq 0 ]; then echo "Published to the MokoGIT registry."
|
|
1665
|
+
elif printf '%s' "$out" | grep -qiE 'already exists|cannot publish over|EPUBLISHCONFLICT|409 Conflict'; then echo "::notice::MokoGIT registry already has this version - no-op (idempotent)."
|
|
1666
|
+
else echo "::error::MokoGIT-registry publish FAILED (npm exit ${rc}) - NOT 'already published'. See the npm error above."; exit "$rc"; fi
|
|
1667
|
+
- name: "npm: publish to npmjs.org (NPM_TOKEN, best-effort)"
|
|
1668
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.npmpub.outputs.publishable == 'true' && (startsWith(steps.platform.outputs.platform, 'npm') || startsWith(steps.platform.outputs.platform, 'nodejs'))
|
|
1669
|
+
env:
|
|
1670
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
1671
|
+
run: |
|
|
1672
|
+
set -euo pipefail
|
|
1673
|
+
if [ -z "${NPM_TOKEN:-}" ]; then echo "::notice::NPM_TOKEN not set - skipping the public npmjs.org publish (MokoGIT registry is primary)."; exit 0; fi
|
|
1674
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
|
1675
|
+
set +e; out="$(npm publish --ignore-scripts --registry https://registry.npmjs.org/ --access public 2>&1)"; rc=$?; set -e
|
|
1676
|
+
rm -f .npmrc; echo "$out"
|
|
1677
|
+
if [ "$rc" -eq 0 ]; then echo "Published to npmjs.org."
|
|
1678
|
+
elif printf '%s' "$out" | grep -qiE 'already exists|cannot publish over|EPUBLISHCONFLICT|409 Conflict'; then echo "::notice::npmjs.org already has this version - no-op."
|
|
1679
|
+
elif printf '%s' "$out" | grep -qiE 'not in this registry|ENEEDAUTH|EOTP|need auth|E401|E403|401 Unauthorized|403 Forbidden|404 Not Found|Forbidden'; then echo "::error::npmjs.org publish FAILED — authorization error (npm exit ${rc}). NPM_TOKEN lacks publish/create rights on the @mokoconsulting scope (new-scoped-name first publish, an unauthorized token, or a revoked/expired token). Failing loud so a broken publish token cannot hide behind a green release — a 404 PUT is how npm masks an unauthorized scoped write."; exit "$rc"
|
|
1680
|
+
else echo "::warning::npmjs.org publish failed (npm exit ${rc}) — transient/5xx/network issue; npmjs is the secondary mirror (MokoGIT registry is primary), not failing the run."; fi
|
|
1681
|
+
|
|
1682
|
+
# GitHub mirroring (release assets + main branch) moved to an out-of-band
|
|
1683
|
+
# OAuth push-mirror (Gitea repo setting) -- former Steps 9 & 10 removed.
|
|
1684
|
+
|
|
1685
|
+
- name: "Step 11: Delete rc branch (dev reset moved to cascade.yml)"
|
|
1686
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.version.outputs.skip != 'true'
|
|
1687
|
+
continue-on-error: true
|
|
1688
|
+
run: |
|
|
1689
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1690
|
+
TOKEN="${{ github.token }}"
|
|
1691
|
+
|
|
1692
|
+
# Delete rc branch (ephemeral -- created by promote-rc)
|
|
1693
|
+
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
1694
|
+
"${API_BASE}/branches/rc" 2>/dev/null \
|
|
1695
|
+
&& echo "Deleted rc branch" || echo "rc branch not found"
|
|
1696
|
+
|
|
1697
|
+
# dev is reset from main by the fleet cascade workflow (cascade.yml, which
|
|
1698
|
+
# rolled up the retired cascade-dev.yml): a push to main cascades down the
|
|
1699
|
+
# ladder, recreating/resetting dev from main after this release completes.
|
|
1700
|
+
echo "rc cleaned; dev reset handled by cascade.yml" >> $GITHUB_STEP_SUMMARY
|
|
1701
|
+
|
|
1702
|
+
# -- Version identity: #1621 Phase 6 retired the per-version branches ------
|
|
1703
|
+
# The old "Step 12: Create version branch from main" and "Step 13: Prune
|
|
1704
|
+
# superseded-major version branches" are REMOVED. A release's permanent
|
|
1705
|
+
# version identity is now a git TAG (v<version>) attached by the publish as a
|
|
1706
|
+
# movable channel tag on the dual-tagged release (MokoCLI release_publish ->
|
|
1707
|
+
# release_create channel_tags[]/update_stream; MokoGIT #1621), not a
|
|
1708
|
+
# version/* branch. No version/* consumer exists, so nothing creates them any
|
|
1709
|
+
# more. Pruning EXISTING version/* branches is a SEPARATE one-time manual
|
|
1710
|
+
# reconcile (do not prune here) — e.g. stop protecting version/* in
|
|
1711
|
+
# cleanup.yml and let the stale-branch sweep retire them, or a one-shot fleet
|
|
1712
|
+
# delete. See issue #1621 Phase 6.
|
|
1713
|
+
|
|
1714
|
+
# -- Post-release: Reset dev version (safety-net) ----------------
|
|
1715
|
+
- name: "Post-release: Reset dev version"
|
|
1716
|
+
if: steps.gate.outputs.has_makefile == 'true' && steps.version.outputs.skip != 'true'
|
|
1717
|
+
continue-on-error: true
|
|
1718
|
+
run: |
|
|
1719
|
+
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
1720
|
+
# Post-release SAFETY-NET: advance dev ABOVE the just-released version so the
|
|
1721
|
+
# dev update channel always leads stable (a dev build X.Y.Z-dev sorts BELOW
|
|
1722
|
+
# stable X.Y.Z, so dev MUST be bumped past the release or dev-channel updates
|
|
1723
|
+
# vanish). #309/#900: this was previously a SILENT swallow — a failed advance
|
|
1724
|
+
# under continue-on-error left the run green while dev drifted below stable.
|
|
1725
|
+
# Now the failure is LOUD (::error:: + job summary), and on success we also
|
|
1726
|
+
# explicitly (re)publish the leading dev build, because the advance commit is
|
|
1727
|
+
# `[skip ci]` and matches pre-release.yml's paths-ignore, so it never auto-fires.
|
|
1728
|
+
if php ${MOKO_CLI}/version_reset_dev.php \
|
|
1729
|
+
--token "${{ github.token }}" --api-base "${API_BASE}" \
|
|
1730
|
+
--branch dev --path . 2>&1; then
|
|
1731
|
+
# Advance landed → publish the leading dev build (best-effort; the release
|
|
1732
|
+
# already succeeded, so a publish hiccup must not red the pipeline).
|
|
1733
|
+
curl -sf -X POST \
|
|
1734
|
+
-H "Authorization: token ${{ github.token }}" \
|
|
1735
|
+
-H "Content-Type: application/json" \
|
|
1736
|
+
"${API_BASE}/actions/workflows/pre-release.yml/dispatches" \
|
|
1737
|
+
-d '{"ref":"dev","inputs":{"stability":"development","ref":"dev"}}' \
|
|
1738
|
+
&& echo "Triggered pre-release (development) to publish the leading dev build." \
|
|
1739
|
+
|| echo "::warning::Could not dispatch pre-release; the leading dev build will publish on the next real dev push."
|
|
1740
|
+
else
|
|
1741
|
+
# Do NOT silently swallow: a failed advance means the dev channel falls
|
|
1742
|
+
# BEHIND stable and dev-channel updates vanish fleet-wide. Surface loudly
|
|
1743
|
+
# (the stable release already published, so this stays green via
|
|
1744
|
+
# continue-on-error — but it is now impossible to miss).
|
|
1745
|
+
echo "::error::Post-release dev-advance FAILED — dev did not move above the released version; the dev update channel will trail stable until fixed. Most likely the CI Actions token lacks MERGE rights on protected 'dev' (org merge-whitelist). See MokoCLI #900 / Template-Joomla #309."
|
|
1746
|
+
{
|
|
1747
|
+
echo "### ⚠️ Dev-advance FAILED (dev-channel will trail stable)"
|
|
1748
|
+
echo ""
|
|
1749
|
+
echo "\`version_reset_dev.php\` could not advance \`dev\` above the released version, so the dev update channel falls behind stable and dev-channel sites stop seeing updates."
|
|
1750
|
+
echo ""
|
|
1751
|
+
echo "Likely cause: the CI Actions token lacks **MERGE** rights on protected \`dev\` (org merge-whitelist). See **MokoCLI #900** / **Template-Joomla #309**."
|
|
1752
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
1753
|
+
fi
|
|
1754
|
+
|
|
1755
|
+
# -- Summary --------------------------------------------------------------
|
|
1756
|
+
- name: Pipeline Summary
|
|
1757
|
+
if: always()
|
|
1758
|
+
run: |
|
|
1759
|
+
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
1760
|
+
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
1761
|
+
if [ "${{ steps.gate.outputs.has_makefile }}" != "true" ]; then
|
|
1762
|
+
echo "## Release Skipped -- no Makefile" >> $GITHUB_STEP_SUMMARY
|
|
1763
|
+
echo "No Makefile at the repository root -- this repo does not build a release." >> $GITHUB_STEP_SUMMARY
|
|
1764
|
+
elif [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
|
1765
|
+
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
|
1766
|
+
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
|
1767
|
+
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
|
1768
|
+
echo "## Already Released -- ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
1769
|
+
else
|
|
1770
|
+
# #469: label the run bump vs no-bump (update-in-place) for clarity.
|
|
1771
|
+
MODE="Build & Release Complete"
|
|
1772
|
+
[ "${{ steps.srcgate.outputs.should_bump }}" = "false" ] && MODE="Release Updated In Place (no bump, #469)"
|
|
1773
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1774
|
+
echo "## ${MODE} (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
|
1775
|
+
if [ "${{ steps.srcgate.outputs.should_bump }}" = "false" ]; then
|
|
1776
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1777
|
+
echo "Non-source push (no \`SRC_DIR/\` change): refreshed the existing stable release in place — version unchanged, no new \`vNN\` tag (#469)." >> $GITHUB_STEP_SUMMARY
|
|
1778
|
+
fi
|
|
1779
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
1780
|
+
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
|
1781
|
+
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
1782
|
+
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
|
1783
|
+
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
1784
|
+
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
1785
|
+
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
1786
|
+
echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
|
1787
|
+
fi
|