Compare commits
64 Commits
feat/vnext
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 45852ad4ed | |||
| 9ac91f6f28 | |||
| c067b67db5 | |||
| bf2aa07768 | |||
| a21eebeaad | |||
| 9239c931f2 | |||
| 9421bff48b | |||
| 85a904586e | |||
| f2864130b7 | |||
| 8681c3d5ee | |||
| 6ccb3f7027 | |||
| 3a375e41d6 | |||
| b48846eaf1 | |||
| 8504c96589 | |||
| 38d7c040ce | |||
| dc6b3dbf16 | |||
| 612de01a6f | |||
| 6f6a7b32c4 | |||
| 4b5cadfd79 | |||
| 506e4a49d6 | |||
| dd801ccce5 | |||
| b5d03ab769 | |||
| 78b7c9e4af | |||
| 56d33c2e3a | |||
| 385b96fda7 | |||
| fab2e2a2e2 | |||
| c537c63e98 | |||
| 0fc4adf1a7 | |||
| affba0a2cd | |||
| 66017e1668 | |||
| 0a66dacb86 | |||
| ae1aa3e21a | |||
| f2dd391bca | |||
| 485d6be31c | |||
| 2d42b4e475 | |||
| b5ce7fadcc | |||
| 4972cf71bc | |||
| 9e8d4c43bb | |||
| c0c121dfb5 | |||
| 5bc475e8f5 | |||
| 1f0fe7012d | |||
| b902244949 | |||
| c37ada7874 | |||
| 9496c7c42d | |||
| a049052e5a | |||
| fc66f8968f | |||
| b0885f6da3 | |||
| 1a2e33abb3 | |||
| 6aeb087613 | |||
| 94e6030e58 | |||
| 02c2033caa | |||
| 9edfeb6075 | |||
| ada91a9bc1 | |||
| 3ce6fd3e08 | |||
| dc14a3fcfc | |||
| f991f209d1 | |||
| 6093300fbb | |||
| 793e4885e8 | |||
| f4be31183f | |||
| 3d0cb7d98c | |||
| 7f436128fd | |||
| 010d3dcbcf | |||
| 8242e5713a | |||
| a7220ddb2b |
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
@@ -34,8 +34,7 @@ jobs:
|
|||||||
if: >-
|
if: >-
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request') &&
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
!startsWith(github.event.repository.name, 'Template-')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -53,7 +52,7 @@ jobs:
|
|||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
/tmp/mokocli
|
/tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /.mokogitea/workflows/auto-release.yml
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +=======================================================================+
|
# +=======================================================================+
|
||||||
@@ -64,14 +64,10 @@ jobs:
|
|||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
if: >-
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(
|
|
||||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
)
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -79,7 +75,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
@@ -95,7 +90,7 @@ jobs:
|
|||||||
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
|
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
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
@@ -115,20 +110,17 @@ jobs:
|
|||||||
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
|
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
|
||||||
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
|
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
|
||||||
|
|
||||||
# Point rc at the source commit. If rc already exists (a protected branch that
|
# Point rc at the source commit via git push. Gitea's git/refs PATCH API
|
||||||
# cannot be deleted), force-update its ref in place instead of delete+recreate:
|
# returns HTTP 405 on ANY protected branch (force or not, even for a user in
|
||||||
# deleting a protected branch fails, which then makes the recreate return HTTP 409.
|
# the force-push allowlist), so it cannot move a protected rc. git push honors
|
||||||
if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then
|
# the push + force-push allowlists and creates rc if it is absent.
|
||||||
echo "rc exists - force-updating to ${FROM} (${SRC_SHA})"
|
PUSH_URL="https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@${MOKOGITEA_URL#https://}/${GITEA_ORG}/${GITEA_REPO}.git"
|
||||||
curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
git config --global user.name "mokogitea-actions[bot]"
|
||||||
"${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \
|
git config --global user.email "actions@mokoconsulting.tech"
|
||||||
|| { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; }
|
git fetch --no-tags "$PUSH_URL" "${FROM}"
|
||||||
else
|
git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \
|
||||||
echo "Creating rc from ${FROM}"
|
|| { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; }
|
||||||
curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
echo "rc set to ${FROM} (${SRC_SHA})"
|
||||||
"${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \
|
|
||||||
|| { echo "::error::Failed to create rc from ${FROM}"; exit 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Repoint the PR at rc, then delete the old source branch (non-fatal).
|
# Repoint the PR at rc, then delete the old source branch (non-fatal).
|
||||||
if [ -n "$PR" ]; then
|
if [ -n "$PR" ]; then
|
||||||
@@ -196,13 +188,9 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Build & Release Pipeline
|
name: Build & Release Pipeline
|
||||||
runs-on: release
|
runs-on: release
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
if: >-
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(
|
|
||||||
github.event.pull_request.merged == true ||
|
github.event.pull_request.merged == true ||
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
)
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -210,7 +198,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Configure git for bot pushes
|
- name: Configure git for bot pushes
|
||||||
run: |
|
run: |
|
||||||
@@ -246,7 +233,7 @@ jobs:
|
|||||||
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
|
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
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
@@ -382,7 +369,7 @@ jobs:
|
|||||||
content = open('CHANGELOG.md').read()
|
content = open('CHANGELOG.md').read()
|
||||||
old = '## [Unreleased]'
|
old = '## [Unreleased]'
|
||||||
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||||
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
|
content = content.replace(old, new, 1)
|
||||||
open('CHANGELOG.md', 'w').write(content)
|
open('CHANGELOG.md', 'w').write(content)
|
||||||
" "$VERSION" "$DATE"
|
" "$VERSION" "$DATE"
|
||||||
git add CHANGELOG.md
|
git add CHANGELOG.md
|
||||||
@@ -426,7 +413,7 @@ jobs:
|
|||||||
&& echo "main branch pushed to GitHub mirror" \
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|| echo "WARNING: GitHub mirror push failed"
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
@@ -438,9 +425,17 @@ jobs:
|
|||||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
|
|
||||||
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
|
# Delete dev branch
|
||||||
# (cascade-dev.yml), which runs after this release completes.
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
|
|
||||||
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
|
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- name: "Step 12: Create version branch from main"
|
- name: "Step 12: Create version branch from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoStandards.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
@@ -33,8 +33,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
# URL-encode the branch name's slashes (no PHP dependency on the runner)
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
|
|
||||||
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
|||||||
@@ -1,106 +1,10 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||||
#
|
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||||
#
|
name: "Cascade Main → Dev (DISABLED)"
|
||||||
# FILE INFORMATION
|
on: workflow_dispatch
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Cascade
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.mokogitea/workflows/cascade-dev.yml
|
|
||||||
# VERSION: 02.00.00
|
|
||||||
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
|
|
||||||
|
|
||||||
name: "Cascade Main -> Dev"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
# ntfy destination is configured via repo or org variables (org vars are inherited).
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cascade:
|
noop:
|
||||||
name: Cascade main -> dev
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Open main -> dev PR (auto-merge if clean, else notify)
|
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||||
env:
|
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
run: |
|
|
||||||
set -uo pipefail
|
|
||||||
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
|
|
||||||
AUTH="Authorization: token ${TOKEN}"
|
|
||||||
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
|
|
||||||
|
|
||||||
# 0. dev must exist
|
|
||||||
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
|
|
||||||
echo "No dev branch - nothing to cascade."; exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 1. is main ahead of dev?
|
|
||||||
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
|
|
||||||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
|
|
||||||
if [ "${AHEAD:-0}" -eq 0 ]; then
|
|
||||||
echo "dev already up to date with main."; exit 0
|
|
||||||
fi
|
|
||||||
echo "main is ${AHEAD} commit(s) ahead of dev."
|
|
||||||
|
|
||||||
# 2. reuse an open main->dev PR, else create one
|
|
||||||
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
|
|
||||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
|
|
||||||
if [ -z "$PR" ]; then
|
|
||||||
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
|
||||||
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
|
|
||||||
PR=$(printf '%s' "$RESP" | jqnum number)
|
|
||||||
if [ -z "$PR" ]; then
|
|
||||||
echo "::warning::Could not open cascade PR: $RESP"; exit 0
|
|
||||||
fi
|
|
||||||
echo "Opened cascade PR #${PR}"
|
|
||||||
else
|
|
||||||
echo "Reusing open cascade PR #${PR}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. wait for MokoGitea to compute mergeability (conflict detection)
|
|
||||||
MERGEABLE=""
|
|
||||||
for _ in 1 2 3 4 5 6; do
|
|
||||||
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable)
|
|
||||||
case "$MERGEABLE" in True|False) break ;; esac
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
echo "mergeable=${MERGEABLE}"
|
|
||||||
|
|
||||||
notify() {
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO}: dev cascade needs manual merge" \
|
|
||||||
-H "Tags: warning,twisted_rightwards_arrows" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
|
|
||||||
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. auto-merge only if conflict-free; otherwise notify
|
|
||||||
if [ "$MERGEABLE" = "True" ]; then
|
|
||||||
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
|
||||||
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
|
|
||||||
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
|
||||||
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
|
|
||||||
else
|
|
||||||
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
|
|
||||||
notify "could not be auto-merged (HTTP ${CODE})."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
|
|
||||||
notify "has conflicts and cannot be merged automatically."
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoStandards.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/ci-generic.yml
|
# PATH: /.gitea/workflows/ci-generic.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||||
|
|
||||||
@@ -32,8 +32,6 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
name: Lint & Validate
|
name: Lint & Validate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -131,11 +129,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
|
needs: lint
|
||||||
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
|
|
||||||
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
|
|
||||||
# directly (same condition lint uses) instead of gating on lint's result.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: mokocli.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
|
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: MokoStandards.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
for BRANCH in $BRANCHES; do
|
for BRANCH in $BRANCHES; do
|
||||||
# Skip protected branches
|
# Skip protected branches
|
||||||
case "$BRANCH" in
|
case "$BRANCH" in
|
||||||
main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;;
|
main|master|develop|release/*|hotfix/*) continue ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# BRIEF: Build and deploy MokoGitea to dev environment on push to dev branch.
|
|
||||||
# Production deploy (deploy-mokogitea.yml) only succeeds if dev is healthy.
|
|
||||||
|
|
||||||
name: Deploy MokoGitea (Dev)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-mokogitea-dev
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: git.mokoconsulting.tech
|
|
||||||
IMAGE: mokoconsulting/mokogitea
|
|
||||||
DEPLOY_HOST: git.mokoconsulting.tech
|
|
||||||
DEPLOY_PORT: 2918
|
|
||||||
DEPLOY_USER: mokoconsulting
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-dev:
|
|
||||||
name: "Build & Deploy to Dev"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: config
|
|
||||||
run: |
|
|
||||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
|
|
||||||
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
|
||||||
echo "Version: ${VERSION}-dev"
|
|
||||||
|
|
||||||
- name: Write deploy key
|
|
||||||
env:
|
|
||||||
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
|
|
||||||
- name: Build and deploy to dev via SSH
|
|
||||||
env:
|
|
||||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
|
||||||
run: |
|
|
||||||
# Inject runner-side values (TAG, REGISTRY_TOKEN) into the remote shell's
|
|
||||||
# environment via a command prefix, then use a *quoted* heredoc so every
|
|
||||||
# $var below expands in exactly one place: the remote dev host. This avoids
|
|
||||||
# the local-vs-remote expansion trap that previously left TAG empty.
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
|
||||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
|
||||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
|
|
||||||
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
|
|
||||||
set -e
|
|
||||||
echo 'SSH connected to dev environment'
|
|
||||||
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
HEALTH_FMT='{{.State.Health.Status}}'
|
|
||||||
|
|
||||||
echo 'Cleaning Docker build cache...'
|
|
||||||
docker builder prune -af 2>/dev/null || true
|
|
||||||
docker image prune -af 2>/dev/null || true
|
|
||||||
|
|
||||||
echo 'Pulling source...'
|
|
||||||
SOURCE_DIR=/opt/gitea-dev/source
|
|
||||||
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
|
||||||
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git "$SOURCE_DIR"
|
|
||||||
fi
|
|
||||||
cd "$SOURCE_DIR"
|
|
||||||
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
|
|
||||||
git fetch origin dev
|
|
||||||
git reset --hard origin/dev
|
|
||||||
|
|
||||||
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
|
|
||||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
|
||||||
--tag "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" \
|
|
||||||
-f Dockerfile .
|
|
||||||
|
|
||||||
echo 'Pushing to registry...'
|
|
||||||
echo "$REGISTRY_TOKEN" | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
|
||||||
docker push "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
|
|
||||||
|
|
||||||
echo 'Restarting dev container...'
|
|
||||||
cd /opt/gitea-dev
|
|
||||||
# The dev service in the SHARED compose file reads its image tag from
|
|
||||||
# ${MOKOGITEA_DEV_TAG}. Drive it from the freshly built tag instead of
|
|
||||||
# rewriting the file with sed: the old sed pattern matched the *prod*
|
|
||||||
# service line (container_name: mokogitea) and left the dev service pinned
|
|
||||||
# to a stale image, so every dev deploy recreated old code while silently
|
|
||||||
# corrupting the prod image pin. The env-var only affects the dev service.
|
|
||||||
# Remove any lingering fixed-name container first so the recreate can't hit
|
|
||||||
# a name conflict, pin the project name for determinism, then force-recreate.
|
|
||||||
docker rm -f mokogitea-dev 2>/dev/null || true
|
|
||||||
MOKOGITEA_DEV_TAG="$TAG" docker compose -p gitea-dev up -d --force-recreate mokogitea-dev
|
|
||||||
|
|
||||||
echo 'Health check...'
|
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
|
||||||
sleep 15
|
|
||||||
if docker inspect --format="$HEALTH_FMT" mokogitea-dev 2>/dev/null | grep -q healthy; then
|
|
||||||
echo 'Dev container healthy!'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Waiting... (attempt $i/8)"
|
|
||||||
done
|
|
||||||
echo 'Health check failed'
|
|
||||||
docker logs mokogitea-dev --tail 20
|
|
||||||
exit 1
|
|
||||||
DEPLOY_EOF
|
|
||||||
|
|
||||||
- name: Verify dev instance
|
|
||||||
run: |
|
|
||||||
sleep 5
|
|
||||||
curl -sf https://git.dev.mokoconsulting.tech/api/healthz && echo " Dev API healthy"
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# BRIEF: Build MokoGitea Docker image, push to registry, and deploy
|
|
||||||
|
|
||||||
name: Deploy MokoGitea
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Version tag'
|
|
||||||
required: true
|
|
||||||
default: 'latest'
|
|
||||||
environment:
|
|
||||||
description: 'Target environment'
|
|
||||||
required: true
|
|
||||||
default: 'dev'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- dev
|
|
||||||
- production
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-mokogitea
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: git.mokoconsulting.tech
|
|
||||||
IMAGE: mokoconsulting/mokogitea
|
|
||||||
DEPLOY_HOST: git.mokoconsulting.tech
|
|
||||||
DEPLOY_PORT: 2918
|
|
||||||
DEPLOY_USER: mokoconsulting
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Verify dev environment is healthy
|
|
||||||
run: |
|
|
||||||
echo "Checking git.dev.mokoconsulting.tech health..."
|
|
||||||
if curl -sf --max-time 10 https://git.dev.mokoconsulting.tech/api/healthz; then
|
|
||||||
echo " Dev environment is healthy — proceeding with production deploy"
|
|
||||||
else
|
|
||||||
echo "::error::Dev environment is NOT healthy — blocking production deploy"
|
|
||||||
echo "Deploy to dev first (push to dev branch) and verify it passes before merging to main."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout source (for version detection)
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Determine settings
|
|
||||||
id: config
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
|
||||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
|
|
||||||
ENV="production"
|
|
||||||
else
|
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
|
||||||
ENV="${{ github.event.inputs.environment }}"
|
|
||||||
fi
|
|
||||||
if [ "$ENV" = "production" ]; then
|
|
||||||
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
|
|
||||||
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
|
||||||
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
|
||||||
echo "branch=main" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
|
||||||
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
|
||||||
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
|
||||||
echo "branch=dev" >> $GITHUB_OUTPUT
|
|
||||||
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Write deploy key
|
|
||||||
env:
|
|
||||||
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
|
|
||||||
- name: Build and deploy via SSH
|
|
||||||
env:
|
|
||||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
|
||||||
BRANCH: ${{ steps.config.outputs.branch }}
|
|
||||||
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
|
||||||
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
|
||||||
CONTAINER: ${{ steps.config.outputs.container }}
|
|
||||||
run: |
|
|
||||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
|
||||||
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
|
|
||||||
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
|
||||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
|
||||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
|
|
||||||
set -e
|
|
||||||
echo 'SSH connected'
|
|
||||||
|
|
||||||
echo 'Cleaning Docker build cache...'
|
|
||||||
docker builder prune -af 2>/dev/null || true
|
|
||||||
docker image prune -af 2>/dev/null || true
|
|
||||||
free -m | head -3
|
|
||||||
|
|
||||||
echo 'Pulling source...'
|
|
||||||
if [ ! -d $SOURCE_DIR/.git ]; then
|
|
||||||
git clone -b $BRANCH https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git $SOURCE_DIR
|
|
||||||
fi
|
|
||||||
cd $SOURCE_DIR
|
|
||||||
# Ensure remote points to MokoGitea-Fork (not the upstream fork)
|
|
||||||
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
|
|
||||||
git fetch origin $BRANCH
|
|
||||||
git reset --hard origin/$BRANCH
|
|
||||||
|
|
||||||
echo 'Building Docker image...'
|
|
||||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \
|
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
|
||||||
-f Dockerfile .
|
|
||||||
|
|
||||||
echo 'Pushing to registry...'
|
|
||||||
echo '$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
|
||||||
|
|
||||||
echo 'Restarting container...'
|
|
||||||
cd $COMPOSE_DIR
|
|
||||||
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
|
|
||||||
docker compose up -d $CONTAINER
|
|
||||||
|
|
||||||
echo 'Health check...'
|
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
|
||||||
sleep 15
|
|
||||||
if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then
|
|
||||||
echo 'Container healthy!'
|
|
||||||
docker inspect --format='$IMAGE_FMT' $CONTAINER
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Waiting... (attempt \$i/8)"
|
|
||||||
done
|
|
||||||
echo 'Health check failed'
|
|
||||||
docker logs $CONTAINER --tail 20
|
|
||||||
exit 1
|
|
||||||
DEPLOY_EOF
|
|
||||||
|
|
||||||
- name: Verify
|
|
||||||
run: |
|
|
||||||
sleep 5
|
|
||||||
curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy"
|
|
||||||
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
run: echo "::error::Deploy failed for ${{ steps.config.outputs.tag }}"
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# BRIEF: Build and deploy to the Dev environment on push to the dev branch.
|
||||||
|
# Portable across Go server repos: ALL deployment config comes from repo
|
||||||
|
# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded.
|
||||||
|
# The dev branch is the ongoing integration branch.
|
||||||
|
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
|
||||||
|
#
|
||||||
|
# Required repo VARIABLES (all tier-scoped so each environment is independent —
|
||||||
|
# a repo may host its rc/dev/prod tiers on different machines):
|
||||||
|
# DEV_SSH_HOST, DEV_SSH_PORT, DEV_SSH_USERNAME - SSH deploy target for the dev tier
|
||||||
|
# DEV_REGISTRY, DEV_REGISTRY_USER, DEV_IMAGE - container registry + login user + image
|
||||||
|
# DEV_CONTAINER - compose service/container to recreate
|
||||||
|
# DEV_COMPOSE_PROJECT - docker compose -p project name
|
||||||
|
# DEV_COMPOSE_DIR - dir containing docker-compose.yml on host
|
||||||
|
# DEV_SOURCE_DIR - build source checkout on host
|
||||||
|
# DEV_TAG_ENV - compose env-var name that pins the image tag
|
||||||
|
# DEV_HEALTH_URL - external URL to verify after deploy
|
||||||
|
# Required SECRETS (already configured org-wide; reused, not re-set):
|
||||||
|
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
|
||||||
|
# MOKOGITEA_TOKEN - registry/API token (org secret)
|
||||||
|
|
||||||
|
name: Deploy (Dev)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
# Manual trigger for isolated end-to-end tests.
|
||||||
|
# Runs on the ref it is dispatched from.
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-dev
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-dev:
|
||||||
|
name: "Build & Deploy to Dev"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: config
|
||||||
|
run: |
|
||||||
|
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
|
||||||
|
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: ${VERSION}-dev"
|
||||||
|
|
||||||
|
- name: Write deploy key
|
||||||
|
env:
|
||||||
|
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
- name: Build and deploy to RC via SSH
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
TAG: ${{ steps.config.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
|
||||||
|
# via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the
|
||||||
|
# remote. Repo variables (vars.*) are substituted inline by Actions before ssh.
|
||||||
|
ssh -i ~/.ssh/deploy_key -p ${{ vars.DEV_SSH_PORT }} \
|
||||||
|
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
||||||
|
${{ vars.DEV_SSH_USERNAME }}@${{ vars.DEV_SSH_HOST }} \
|
||||||
|
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
|
||||||
|
set -e
|
||||||
|
echo 'SSH connected to Dev environment'
|
||||||
|
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HEALTH_FMT='{{.State.Health.Status}}'
|
||||||
|
|
||||||
|
echo 'Cleaning Docker build cache...'
|
||||||
|
docker builder prune -af 2>/dev/null || true
|
||||||
|
docker image prune -af 2>/dev/null || true
|
||||||
|
|
||||||
|
echo 'Pulling source...'
|
||||||
|
SOURCE_DIR='${{ vars.DEV_SOURCE_DIR }}'
|
||||||
|
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
||||||
|
git clone -b dev ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
git remote set-url origin ${{ github.server_url }}/${{ github.repository }}.git 2>/dev/null || true
|
||||||
|
git fetch origin dev
|
||||||
|
git reset --hard origin/dev
|
||||||
|
|
||||||
|
echo "Building image: ${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG"
|
||||||
|
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||||
|
--tag "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" \
|
||||||
|
-f Dockerfile .
|
||||||
|
|
||||||
|
echo 'Pushing to registry...'
|
||||||
|
echo "$REGISTRY_TOKEN" | docker login ${{ vars.DEV_REGISTRY }} -u ${{ vars.DEV_REGISTRY_USER }} --password-stdin
|
||||||
|
docker push "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG"
|
||||||
|
|
||||||
|
echo 'Restarting Dev container...'
|
||||||
|
cd '${{ vars.DEV_COMPOSE_DIR }}'
|
||||||
|
# Drive the rc service image tag via its compose env-var (no sed on the shared
|
||||||
|
# file); remove any lingering fixed-name container first, then force-recreate.
|
||||||
|
docker rm -f '${{ vars.DEV_CONTAINER }}' 2>/dev/null || true
|
||||||
|
${{ vars.DEV_TAG_ENV }}="$TAG" docker compose -p '${{ vars.DEV_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.DEV_CONTAINER }}'
|
||||||
|
|
||||||
|
echo 'Health check...'
|
||||||
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
|
sleep 15
|
||||||
|
if docker inspect --format="$HEALTH_FMT" '${{ vars.DEV_CONTAINER }}' 2>/dev/null | grep -q healthy; then
|
||||||
|
echo 'Dev container healthy!'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... (attempt $i/8)"
|
||||||
|
done
|
||||||
|
echo 'Health check failed'
|
||||||
|
docker logs '${{ vars.DEV_CONTAINER }}' --tail 20
|
||||||
|
exit 1
|
||||||
|
DEPLOY_EOF
|
||||||
|
|
||||||
|
- name: Verify Dev instance
|
||||||
|
run: |
|
||||||
|
sleep 5
|
||||||
|
curl -sf "${{ vars.DEV_HEALTH_URL }}" && echo " Dev API healthy"
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# BRIEF: Build and deploy to the Prod environment on push to the main branch.
|
||||||
|
# Portable across Go server repos: ALL deployment config comes from repo
|
||||||
|
# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded.
|
||||||
|
# Prod deploys on merge to main (dev -> rc -> main pipeline).
|
||||||
|
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
|
||||||
|
#
|
||||||
|
# Required repo VARIABLES (all tier-scoped so each environment is independent —
|
||||||
|
# a repo may host its rc/dev/prod tiers on different machines):
|
||||||
|
# PROD_SSH_HOST, PROD_SSH_PORT, PROD_SSH_USERNAME - SSH deploy target for the prod tier
|
||||||
|
# PROD_REGISTRY, PROD_REGISTRY_USER, PROD_IMAGE - container registry + login user + image
|
||||||
|
# PROD_CONTAINER - compose service/container to recreate
|
||||||
|
# PROD_COMPOSE_PROJECT - docker compose -p project name
|
||||||
|
# PROD_COMPOSE_DIR - dir containing docker-compose.yml on host
|
||||||
|
# PROD_SOURCE_DIR - build source checkout on host
|
||||||
|
# PROD_TAG_ENV - compose env-var name that pins the image tag
|
||||||
|
# PROD_HEALTH_URL - external URL to verify after deploy
|
||||||
|
# Required SECRETS (already configured org-wide; reused, not re-set):
|
||||||
|
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
|
||||||
|
# MOKOGITEA_TOKEN - registry/API token (org secret)
|
||||||
|
|
||||||
|
name: Deploy (Prod)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
# Manual trigger for a prod re-deploy.
|
||||||
|
# Runs on the ref it is dispatched from (use main).
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-prod
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-prod:
|
||||||
|
name: "Build & Deploy to Prod"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: config
|
||||||
|
run: |
|
||||||
|
VERSION=$(git describe --tags --always 2>/dev/null || echo "$(git rev-parse --short HEAD)")
|
||||||
|
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: ${VERSION}"
|
||||||
|
|
||||||
|
- name: Write deploy key
|
||||||
|
env:
|
||||||
|
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
- name: Build and deploy to RC via SSH
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
TAG: ${{ steps.config.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
|
||||||
|
# via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the
|
||||||
|
# remote. Repo variables (vars.*) are substituted inline by Actions before ssh.
|
||||||
|
ssh -i ~/.ssh/deploy_key -p ${{ vars.PROD_SSH_PORT }} \
|
||||||
|
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
||||||
|
${{ vars.PROD_SSH_USERNAME }}@${{ vars.PROD_SSH_HOST }} \
|
||||||
|
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
|
||||||
|
set -e
|
||||||
|
echo 'SSH connected to Prod environment'
|
||||||
|
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HEALTH_FMT='{{.State.Health.Status}}'
|
||||||
|
|
||||||
|
echo 'Cleaning Docker build cache...'
|
||||||
|
docker builder prune -af 2>/dev/null || true
|
||||||
|
docker image prune -af 2>/dev/null || true
|
||||||
|
|
||||||
|
echo 'Pulling source...'
|
||||||
|
SOURCE_DIR='${{ vars.PROD_SOURCE_DIR }}'
|
||||||
|
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
||||||
|
git clone -b main ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
git remote set-url origin ${{ github.server_url }}/${{ github.repository }}.git 2>/dev/null || true
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
echo "Building image: ${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG"
|
||||||
|
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||||
|
--tag "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" \
|
||||||
|
-f Dockerfile .
|
||||||
|
|
||||||
|
echo 'Pushing to registry...'
|
||||||
|
echo "$REGISTRY_TOKEN" | docker login ${{ vars.PROD_REGISTRY }} -u ${{ vars.PROD_REGISTRY_USER }} --password-stdin
|
||||||
|
docker push "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG"
|
||||||
|
|
||||||
|
echo 'Restarting Prod container...'
|
||||||
|
cd '${{ vars.PROD_COMPOSE_DIR }}'
|
||||||
|
# Drive the rc service image tag via its compose env-var (no sed on the shared
|
||||||
|
# file); remove any lingering fixed-name container first, then force-recreate.
|
||||||
|
docker rm -f '${{ vars.PROD_CONTAINER }}' 2>/dev/null || true
|
||||||
|
${{ vars.PROD_TAG_ENV }}="$TAG" docker compose -p '${{ vars.PROD_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.PROD_CONTAINER }}'
|
||||||
|
|
||||||
|
echo 'Health check...'
|
||||||
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
|
sleep 15
|
||||||
|
if docker inspect --format="$HEALTH_FMT" '${{ vars.PROD_CONTAINER }}' 2>/dev/null | grep -q healthy; then
|
||||||
|
echo 'Prod container healthy!'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... (attempt $i/8)"
|
||||||
|
done
|
||||||
|
echo 'Health check failed'
|
||||||
|
docker logs '${{ vars.PROD_CONTAINER }}' --tail 20
|
||||||
|
exit 1
|
||||||
|
DEPLOY_EOF
|
||||||
|
|
||||||
|
- name: Verify Prod instance
|
||||||
|
run: |
|
||||||
|
sleep 5
|
||||||
|
curl -sf "${{ vars.PROD_HEALTH_URL }}" && echo " Prod API healthy"
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /.mokogitea/workflows/gitleaks.yml
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ name: "Universal: Notifications"
|
|||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows:
|
workflows:
|
||||||
- "Universal: Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla: Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Generic: Project CI"
|
- "Deploy"
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.CI
|
# INGROUP: mokocli.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /.mokogitea/workflows/pr-check.yml
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
@@ -47,15 +47,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
fix/*|bugfix/*)
|
fix/*|bugfix/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
patch/*)
|
patch/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
hotfix/*)
|
hotfix/*)
|
||||||
@@ -86,8 +86,7 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -97,80 +96,6 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
|
|
||||||
require-docs:
|
|
||||||
name: Require Docs Update
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
|
|
||||||
if: ${{ github.base_ref == 'main' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Require README.md and CHANGELOG.md in the PR diff
|
|
||||||
run: |
|
|
||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
|
||||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
||||||
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
|
|
||||||
if [ -z "$CHANGED" ]; then
|
|
||||||
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
|
|
||||||
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
echo "Changed files in PR:"
|
|
||||||
echo "$CHANGED"
|
|
||||||
MISSING=""
|
|
||||||
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
|
|
||||||
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
|
|
||||||
if [ -n "$MISSING" ]; then
|
|
||||||
echo "::error::PRs into main must update: ${MISSING}"
|
|
||||||
{
|
|
||||||
echo "## Docs Update Required"
|
|
||||||
echo ""
|
|
||||||
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
|
|
||||||
echo ""
|
|
||||||
echo "Not updated in this PR: **${MISSING}**"
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Docs update present (README.md + CHANGELOG.md)"
|
|
||||||
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
|
|
||||||
wiki-reminder:
|
|
||||||
name: Wiki Update Reminder
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.base_ref == 'main' }}
|
|
||||||
steps:
|
|
||||||
- name: Remind to update the wiki
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
PR: ${{ github.event.pull_request.number }}
|
|
||||||
run: |
|
|
||||||
set -uo pipefail
|
|
||||||
{
|
|
||||||
echo "## Wiki Update Reminder"
|
|
||||||
echo ""
|
|
||||||
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
|
|
||||||
echo ""
|
|
||||||
echo "- ${SERVER}/${REPO}/wiki"
|
|
||||||
echo ""
|
|
||||||
echo "_Non-blocking reminder._"
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
|
|
||||||
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
|
||||||
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
|
|
||||||
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
|
|
||||||
if [ "${existing:-0}" -eq 0 ]; then
|
|
||||||
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
|
|
||||||
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo "Wiki reminder emitted (non-blocking)."
|
|
||||||
|
|
||||||
# ── Secret Scanning ──────────────────────────────────────────────────
|
# ── Secret Scanning ──────────────────────────────────────────────────
|
||||||
gitleaks:
|
gitleaks:
|
||||||
name: Secret Scan
|
name: Secret Scan
|
||||||
@@ -201,8 +126,6 @@ jobs:
|
|||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -210,7 +133,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check for merge conflict markers
|
- name: Check for merge conflict markers
|
||||||
run: |
|
run: |
|
||||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
if [ -n "$CONFLICTS" ]; then
|
if [ -n "$CONFLICTS" ]; then
|
||||||
echo "::error::Merge conflict markers found in source files"
|
echo "::error::Merge conflict markers found in source files"
|
||||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -224,7 +147,7 @@ jobs:
|
|||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
# Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used.
|
# Platform comes from the MokoGitea metadata API (public GET).
|
||||||
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
|
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
|
||||||
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
|
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
@@ -350,7 +273,7 @@ jobs:
|
|||||||
joomla)
|
joomla)
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
if [ -z "$MANIFEST" ]; then
|
if [ -z "$MANIFEST" ]; then
|
||||||
echo "::warning::No Joomla manifest found (MokoSuite site)"
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "Manifest: ${MANIFEST}"
|
echo "Manifest: ${MANIFEST}"
|
||||||
@@ -570,9 +493,6 @@ jobs:
|
|||||||
name: Build RC Package
|
name: Build RC Package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [branch-policy, validate]
|
needs: [branch-policy, validate]
|
||||||
# Run only when both gates succeeded; always() forces evaluation so a skipped
|
|
||||||
# validate (e.g. template repos) skips this job cleanly instead of hanging.
|
|
||||||
if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger RC pre-release
|
- name: Trigger RC pre-release
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.02.00
|
# VERSION: 05.02.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
@@ -48,13 +48,9 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
if: >-
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
github.event_name == 'push'
|
github.event_name == 'push'
|
||||||
)
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -84,7 +80,7 @@ jobs:
|
|||||||
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
|
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
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: mokocli.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
@@ -25,8 +25,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.merged == false &&
|
github.event.pull_request.merged == false &&
|
||||||
startsWith(github.event.pull_request.head.ref, 'rc/') &&
|
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||||
!startsWith(github.event.repository.name, 'Template-')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Rename branch
|
- name: Rename branch
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /.mokogitea/workflows/repo-health.yml
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
name: Sync Workflows to Repos
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '.mokogitea/workflows/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout mokocli
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: MokoConsulting/MokoCLI
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1
|
|
||||||
with:
|
|
||||||
php-version: '8.1'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-dev --no-interaction
|
|
||||||
|
|
||||||
- name: Sync workflows to generic repos
|
|
||||||
run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
@@ -34,7 +34,6 @@ jobs:
|
|||||||
set-version:
|
set-version:
|
||||||
name: Set Version to ${{ inputs.version }}
|
name: Set Version to ${{ inputs.version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Validate version format
|
- name: Validate version format
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
|
||||||
# INGROUP: mokocli.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
|
||||||
# VERSION: 01.01.00
|
|
||||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
|
||||||
|
|
||||||
name: "Universal: Workflow Sync Trigger"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync workflows to live repos
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event.pull_request.merged == true &&
|
|
||||||
!contains(github.event.pull_request.title, '[skip sync]')))
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Determine platform from repo name
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
case "$REPO" in
|
|
||||||
Template-Joomla) PLATFORM="joomla" ;;
|
|
||||||
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
|
||||||
Template-Go) PLATFORM="go" ;;
|
|
||||||
Template-MCP) PLATFORM="mcp" ;;
|
|
||||||
Template-Generic) PLATFORM="" ;;
|
|
||||||
*) PLATFORM="" ;;
|
|
||||||
esac
|
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Platform: ${PLATFORM:-all}"
|
|
||||||
|
|
||||||
- name: Clone mokocli
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
|
||||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
|
||||||
|
|
||||||
- name: Install PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd /tmp/mokocli
|
|
||||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Run workflow sync
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
ARGS="--token ${MOKOGITEA_TOKEN}"
|
|
||||||
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
|
||||||
ARGS="${ARGS} --phase repos"
|
|
||||||
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [ -n "$PLATFORM" ]; then
|
|
||||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Metadata platform options are now **admin-configurable** instead of hardcoded: a new **Admin → Metadata** page edits the allowed `platform` values (persisted to `app.ini` `[metadata] PLATFORM_OPTIONS`, default `joomla,dolibarr,go,npm,generic`), and the repo Settings → Metadata platform dropdown reads from it. Changing the taxonomy no longer needs a code change/redeploy. A repo's existing platform value stays selectable even if later removed from the list (#777)
|
||||||
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
|
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
|
||||||
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
|
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
|
||||||
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
|
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
|
||||||
@@ -63,6 +64,8 @@
|
|||||||
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Repo metadata `org` is now **derived from the org profile** (the repository owner) instead of being stored/editable, so it can never drift when an organization is renamed. The `org` column is dropped from `repo_manifest` (migration #368) and the field is derived on read via `Repository.DerivedOrgName` (owner display name, falling back to the handle) across the metadata API, the Settings → Metadata page (now read-only), and the Joomla update-server feed. The API `PUT` and MCP `metadata_update` now ignore `org` (read-only, like the already-derived `display_name`) (#771)
|
||||||
|
- Admin Branding: uploading a custom **Nav Icon** now also sets the **app icon** (`logo.png`, the PWA / web-manifest icon and navbar fallback), so a branded instance shows its own installable app icon instead of the shipped default; resetting the Nav Icon reverts both (#773)
|
||||||
- Fork server binary now compiles: `routers/api/v1/api.go` called `organization.HasOrgOrUserVisible`, which had been renamed to `IsOwnerVisibleToDoer`; the one missed call site broke `go build` of the entire `routers/api/v1` package (CI's Lint & Validate does not run a full build, so it went unnoticed) (#735)
|
- Fork server binary now compiles: `routers/api/v1/api.go` called `organization.HasOrgOrUserVisible`, which had been renamed to `IsOwnerVisibleToDoer`; the one missed call site broke `go build` of the entire `routers/api/v1` package (CI's Lint & Validate does not run a full build, so it went unnoticed) (#735)
|
||||||
- Dev deploy workflow: the build/deploy step referenced runner-side values as `\$TAG` / `\$REGISTRY_TOKEN` inside an unquoted SSH heredoc, deferring expansion to the remote shell where those names are unset — the Docker tag collapsed to an empty `mokogitea:` and every dev deploy failed with `invalid reference format`. Runner values are now injected via an ssh env-prefix and the heredoc is quoted so each `$var` expands in exactly one place (#737)
|
- Dev deploy workflow: the build/deploy step referenced runner-side values as `\$TAG` / `\$REGISTRY_TOKEN` inside an unquoted SSH heredoc, deferring expansion to the remote shell where those names are unset — the Docker tag collapsed to an empty `mokogitea:` and every dev deploy failed with `invalid reference format`. Runner values are now injected via an ssh env-prefix and the heredoc is quoted so each `$var` expands in exactly one place (#737)
|
||||||
- Repaired unit-test compile and `go vet` failures: `CryptoRandomInt/String/Bytes` now return two values (updated `modules/util/util_test.go`), removed a redundant `&&` condition in `issue_comment.go`, and cleaned up isolated integration-test compile errors (#736)
|
- Repaired unit-test compile and `go vet` failures: `CryptoRandomInt/String/Bytes` now return two values (updated `modules/util/util_test.go`), removed a redundant `&&` condition in `issue_comment.go`, and cleaned up isolated integration-test compile errors (#736)
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
|
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
|
||||||
newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable),
|
newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable),
|
||||||
newMigration(367, "Add user allowlists to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
|
newMigration(367, "Add user allowlists to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
|
||||||
|
newMigration(368, "Drop org from repo manifest (derived from org profile)", v1_27.DropOrgFromRepoManifest),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/migrations/base"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DropOrgFromRepoManifest removes the org column from repo_manifest. The
|
||||||
|
// organization is now derived from the org profile (repository owner) on read
|
||||||
|
// rather than stored, so it never drifts on rename. See issue #771.
|
||||||
|
func DropOrgFromRepoManifest(x *xorm.Engine) error {
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
return base.DropTableColumns(sess, "repo_manifest", "org")
|
||||||
|
}
|
||||||
@@ -486,6 +486,23 @@ func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DerivedOrgName returns the organization name derived from the org profile
|
||||||
|
// (the repository owner). It prefers the owner's display name (FullName) and
|
||||||
|
// falls back to the owner handle (Name / OwnerName). The organization is always
|
||||||
|
// derived from the profile rather than stored, so it never drifts on rename.
|
||||||
|
func (repo *Repository) DerivedOrgName(ctx context.Context) string {
|
||||||
|
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
|
||||||
|
return repo.OwnerName
|
||||||
|
}
|
||||||
|
if repo.Owner.FullName != "" {
|
||||||
|
return repo.Owner.FullName
|
||||||
|
}
|
||||||
|
if repo.Owner.Name != "" {
|
||||||
|
return repo.Owner.Name
|
||||||
|
}
|
||||||
|
return repo.OwnerName
|
||||||
|
}
|
||||||
|
|
||||||
// MustOwner always returns a valid *user_model.User object to avoid
|
// MustOwner always returns a valid *user_model.User object to avoid
|
||||||
// conceptually impossible error handling.
|
// conceptually impossible error handling.
|
||||||
// It creates a fake object that contains error details
|
// It creates a fake object that contains error details
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ type RepoMetadata struct {
|
|||||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||||
|
|
||||||
// identity section
|
// identity section
|
||||||
|
// NOTE: organization is intentionally NOT stored here. It is derived from
|
||||||
|
// the org profile (repository owner) on read via Repository.DerivedOrgName,
|
||||||
|
// so it can never drift from the owner on rename. See issue #771.
|
||||||
Name string `xorm:"TEXT 'name'"` // project name
|
Name string `xorm:"TEXT 'name'"` // project name
|
||||||
Org string `xorm:"TEXT 'org'"` // organization name
|
|
||||||
Description string `xorm:"TEXT 'description'"` // project description
|
Description string `xorm:"TEXT 'description'"` // project description
|
||||||
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
||||||
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
// Metadata holds instance-level settings for the repository metadata feature.
|
||||||
|
var Metadata = struct {
|
||||||
|
// PlatformOptions is the admin-configurable list of allowed values for the
|
||||||
|
// repo metadata "platform" field. Editable via Admin -> Metadata (persisted
|
||||||
|
// to app.ini [metadata] PLATFORM_OPTIONS) so changing the taxonomy needs no
|
||||||
|
// code change or redeploy. See issue #777.
|
||||||
|
PlatformOptions []string
|
||||||
|
}{
|
||||||
|
PlatformOptions: []string{"joomla", "dolibarr", "go", "npm", "generic"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMetadataFrom(rootCfg ConfigProvider) {
|
||||||
|
sec := rootCfg.Section("metadata")
|
||||||
|
if opts := sec.Key("PLATFORM_OPTIONS").Strings(","); len(opts) > 0 {
|
||||||
|
Metadata.PlatformOptions = opts
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||||||
}
|
}
|
||||||
loadUIFrom(cfg)
|
loadUIFrom(cfg)
|
||||||
loadAdminFrom(cfg)
|
loadAdminFrom(cfg)
|
||||||
|
loadMetadataFrom(cfg)
|
||||||
loadAPIFrom(cfg)
|
loadAPIFrom(cfg)
|
||||||
loadMetricsFrom(cfg)
|
loadMetricsFrom(cfg)
|
||||||
loadCamoFrom(cfg)
|
loadCamoFrom(cfg)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// apiMetadata is the JSON representation of a repo manifest.
|
// apiMetadata is the JSON representation of a repo manifest.
|
||||||
type apiMetadata struct {
|
type apiMetadata struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Org string `json:"org"`
|
Org string `json:"org"` // read-only, derived from the org profile (repo owner)
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
LicenseSPDX string `json:"license_spdx"`
|
LicenseSPDX string `json:"license_spdx"`
|
||||||
LicenseName string `json:"license_name"`
|
LicenseName string `json:"license_name"`
|
||||||
@@ -83,14 +83,14 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
|||||||
// Return defaults from repo metadata.
|
// Return defaults from repo metadata.
|
||||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||||
Name: ctx.Repo.Repository.Name,
|
Name: ctx.Repo.Repository.Name,
|
||||||
Org: ctx.Repo.Repository.OwnerName,
|
Org: ctx.Repo.Repository.DerivedOrgName(ctx),
|
||||||
Description: ctx.Repo.Repository.Description,
|
Description: ctx.Repo.Repository.Description,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
Org: m.Org,
|
Org: ctx.Repo.Repository.DerivedOrgName(ctx),
|
||||||
Description: m.Description,
|
Description: m.Description,
|
||||||
|
|
||||||
LicenseSPDX: m.LicenseSPDX,
|
LicenseSPDX: m.LicenseSPDX,
|
||||||
@@ -157,7 +157,6 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
m = &repo_model.RepoMetadata{
|
m = &repo_model.RepoMetadata{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Name: ctx.Repo.Repository.Name,
|
Name: ctx.Repo.Repository.Name,
|
||||||
Org: ctx.Repo.Repository.OwnerName,
|
|
||||||
Description: ctx.Repo.Repository.Description,
|
Description: ctx.Repo.Repository.Description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +170,8 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setStr("name", &m.Name)
|
setStr("name", &m.Name)
|
||||||
setStr("org", &m.Org)
|
// org is intentionally ignored: it is derived from the org profile
|
||||||
|
// (repository owner) on read, not stored. See issue #771.
|
||||||
setStr("description", &m.Description)
|
setStr("description", &m.Description)
|
||||||
setStr("license_spdx", &m.LicenseSPDX)
|
setStr("license_spdx", &m.LicenseSPDX)
|
||||||
setStr("license_name", &m.LicenseName)
|
setStr("license_name", &m.LicenseName)
|
||||||
@@ -204,7 +204,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
|
|
||||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
Org: m.Org,
|
Org: ctx.Repo.Repository.DerivedOrgName(ctx),
|
||||||
Description: m.Description,
|
Description: m.Description,
|
||||||
|
|
||||||
LicenseSPDX: m.LicenseSPDX,
|
LicenseSPDX: m.LicenseSPDX,
|
||||||
|
|||||||
@@ -137,7 +137,28 @@ func BrandingUpload(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
destPath := filepath.Join(imgDir, filename)
|
// The nav icon doubles as the app icon (logo.png) — used as the PWA / web
|
||||||
|
// manifest icon and the navbar fallback — so branding the nav icon brands
|
||||||
|
// the app icon too. Write the uploaded image to both in a single pass. See #773.
|
||||||
|
targets := []string{filename}
|
||||||
|
if imageType == "nav-icon" {
|
||||||
|
targets = append(targets, "logo.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the uploaded image to each target independently (rewinding the
|
||||||
|
// stream between targets) so a failure on a later target never leaves an
|
||||||
|
// earlier one truncated/empty.
|
||||||
|
for i, name := range targets {
|
||||||
|
if i > 0 {
|
||||||
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
ctx.Flash.Error("Failed to write image")
|
||||||
|
log.Error("Seek branding image %s: %v", name, err)
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destPath := filepath.Join(imgDir, name)
|
||||||
dest, err := os.Create(destPath)
|
dest, err := os.Create(destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Flash.Error("Failed to save image")
|
ctx.Flash.Error("Failed to save image")
|
||||||
@@ -145,20 +166,21 @@ func BrandingUpload(ctx *context.Context) {
|
|||||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer dest.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(dest, file); err != nil {
|
if _, err := io.Copy(dest, file); err != nil {
|
||||||
|
dest.Close()
|
||||||
ctx.Flash.Error("Failed to write image")
|
ctx.Flash.Error("Failed to write image")
|
||||||
log.Error("Copy to %s: %v", destPath, err)
|
log.Error("Copy to %s: %v", destPath, err)
|
||||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
dest.Close()
|
||||||
|
|
||||||
// Remove SVG override if present
|
// Remove any SVG override so the uploaded PNG takes effect.
|
||||||
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
|
svgPath := filepath.Join(imgDir, name[:len(name)-4]+".svg")
|
||||||
if fileExists(svgPath) {
|
if fileExists(svgPath) {
|
||||||
os.Remove(svgPath)
|
os.Remove(svgPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Flash.Success("Branding image updated: " + imageType)
|
ctx.Flash.Success("Branding image updated: " + imageType)
|
||||||
log.Info("Branding image uploaded: %s (%d bytes)", filename, header.Size)
|
log.Info("Branding image uploaded: %s (%d bytes)", filename, header.Size)
|
||||||
@@ -183,16 +205,36 @@ func BrandingReset(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
path := filepath.Join(brandingImageDir(), filename)
|
// The nav icon also sets the app icon (logo.png), so resetting it reverts
|
||||||
if fileExists(path) {
|
// both back to the built-in default. See #773.
|
||||||
|
targets := []string{filename}
|
||||||
|
if imageType == "nav-icon" {
|
||||||
|
targets = append(targets, "logo.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
removedAny := false
|
||||||
|
failed := false
|
||||||
|
for _, name := range targets {
|
||||||
|
path := filepath.Join(brandingImageDir(), name)
|
||||||
|
if !fileExists(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := os.Remove(path); err != nil {
|
if err := os.Remove(path); err != nil {
|
||||||
ctx.Flash.Error("Failed to remove custom image")
|
ctx.Flash.Error("Failed to remove custom image")
|
||||||
log.Error("Remove %s: %v", path, err)
|
log.Error("Remove %s: %v", path, err)
|
||||||
|
failed = true
|
||||||
} else {
|
} else {
|
||||||
ctx.Flash.Success("Reset to default: " + imageType)
|
removedAny = true
|
||||||
log.Info("Branding reset to default: %s", filename)
|
log.Info("Branding reset to default: %s", name)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case failed:
|
||||||
|
// error flash already set
|
||||||
|
case removedAny:
|
||||||
|
ctx.Flash.Success("Reset to default: " + imageType)
|
||||||
|
default:
|
||||||
ctx.Flash.Info("Already using default: " + imageType)
|
ctx.Flash.Info("Already using default: " + imageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tplMetadata templates.TplName = "admin/metadata"
|
||||||
|
|
||||||
|
// Metadata shows the admin metadata-settings page.
|
||||||
|
func Metadata(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = "Metadata"
|
||||||
|
ctx.Data["PageIsAdminMetadata"] = true
|
||||||
|
ctx.Data["PlatformOptions"] = strings.Join(setting.Metadata.PlatformOptions, "\n")
|
||||||
|
ctx.HTML(http.StatusOK, tplMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePlatformOptions normalizes admin input (comma- or newline-separated) into
|
||||||
|
// a clean, lowercased, de-duplicated, order-preserving list of platform values.
|
||||||
|
func parsePlatformOptions(raw string) []string {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var out []string
|
||||||
|
for _, tok := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' }) {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(tok))
|
||||||
|
if v == "" || seen[v] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = true
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetadataSettings persists the admin metadata settings.
|
||||||
|
func MetadataSettings(ctx *context.Context) {
|
||||||
|
opts := parsePlatformOptions(ctx.FormString("platform_options"))
|
||||||
|
if len(opts) == 0 {
|
||||||
|
ctx.Flash.Error("Platform options cannot be empty")
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/metadata")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply in-memory so the change takes effect without a restart.
|
||||||
|
setting.Metadata.PlatformOptions = opts
|
||||||
|
|
||||||
|
// Persist to app.ini [metadata] PLATFORM_OPTIONS.
|
||||||
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error("Failed to load config: " + err.Error())
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/metadata")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.Section("metadata").Key("PLATFORM_OPTIONS").SetValue(strings.Join(opts, ","))
|
||||||
|
if err := cfg.SaveTo(setting.CustomConf); err != nil {
|
||||||
|
ctx.Flash.Error("Failed to save config: " + err.Error())
|
||||||
|
log.Error("SaveTo %s: %v", setting.CustomConf, err)
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Success("Metadata settings saved")
|
||||||
|
log.Info("Metadata platform options updated: %v", opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/metadata")
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
)
|
)
|
||||||
@@ -35,11 +36,32 @@ func Metadata(ctx *context.Context) {
|
|||||||
manifest = &repo_model.RepoMetadata{
|
manifest = &repo_model.RepoMetadata{
|
||||||
RepoID: repoID,
|
RepoID: repoID,
|
||||||
Name: ctx.Repo.Repository.Name,
|
Name: ctx.Repo.Repository.Name,
|
||||||
Org: ctx.Repo.Repository.OwnerName,
|
|
||||||
Description: ctx.Repo.Repository.Description,
|
Description: ctx.Repo.Repository.Description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Data["Manifest"] = manifest
|
ctx.Data["Manifest"] = manifest
|
||||||
|
// Organization is derived from the org profile (repo owner), not stored.
|
||||||
|
// It is displayed read-only. See issue #771.
|
||||||
|
ctx.Data["DerivedOrg"] = ctx.Repo.Repository.DerivedOrgName(ctx)
|
||||||
|
|
||||||
|
// Platform dropdown options come from the admin-configurable setting
|
||||||
|
// (Admin -> Metadata). Keep the repo's current value selectable even if it
|
||||||
|
// was later removed from the list, so saving does not silently drop it.
|
||||||
|
// See issue #777.
|
||||||
|
platformOpts := append([]string(nil), setting.Metadata.PlatformOptions...)
|
||||||
|
if manifest.Platform != "" {
|
||||||
|
found := false
|
||||||
|
for _, p := range platformOpts {
|
||||||
|
if p == manifest.Platform {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
platformOpts = append([]string{manifest.Platform}, platformOpts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Data["PlatformOptions"] = platformOpts
|
||||||
|
|
||||||
// Load repo-scoped custom fields.
|
// Load repo-scoped custom fields.
|
||||||
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
|
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
|
||||||
@@ -102,7 +124,7 @@ func saveMetadata(ctx *context.Context) {
|
|||||||
manifest := &repo_model.RepoMetadata{
|
manifest := &repo_model.RepoMetadata{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Name: ctx.FormString("name"),
|
Name: ctx.FormString("name"),
|
||||||
Org: ctx.FormString("org"),
|
// org is not stored: it is derived from the org profile on read. See issue #771.
|
||||||
Description: ctx.Repo.Repository.Description,
|
Description: ctx.Repo.Repository.Description,
|
||||||
LicenseSPDX: spdx,
|
LicenseSPDX: spdx,
|
||||||
LicenseName: licenseName,
|
LicenseName: licenseName,
|
||||||
@@ -158,4 +180,3 @@ func saveCustomFields(ctx *context.Context) {
|
|||||||
ctx.Flash.Success("Custom fields saved")
|
ctx.Flash.Success("Custom fields saved")
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -786,6 +786,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/reset", admin.BrandingReset)
|
m.Get("/reset", admin.BrandingReset)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Group("/metadata", func() {
|
||||||
|
m.Get("", admin.Metadata)
|
||||||
|
m.Post("/settings", admin.MetadataSettings)
|
||||||
|
})
|
||||||
|
|
||||||
m.Group("/monitor", func() {
|
m.Group("/monitor", func() {
|
||||||
m.Get("/stats", admin.MonitorStats)
|
m.Get("/stats", admin.MonitorStats)
|
||||||
m.Get("/cron", admin.CronTasks)
|
m.Get("/cron", admin.CronTasks)
|
||||||
|
|||||||
@@ -269,11 +269,8 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
|||||||
phpMinimum := meta.PHPMinimum
|
phpMinimum := meta.PHPMinimum
|
||||||
feedDescription := meta.Description
|
feedDescription := meta.Description
|
||||||
|
|
||||||
// Maintainer and URL always come from the org profile.
|
// Maintainer/organization and URL always come from the org profile.
|
||||||
maintainer := repo.Owner.FullName
|
maintainer := repo.DerivedOrgName(ctx)
|
||||||
if maintainer == "" {
|
|
||||||
maintainer = repo.Owner.Name
|
|
||||||
}
|
|
||||||
maintainerURL := repo.Owner.Website
|
maintainerURL := repo.Owner.Website
|
||||||
if maintainerURL == "" {
|
if maintainerURL == "" {
|
||||||
maintainerURL = fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
maintainerURL = fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>Nav Icon</strong> {{if .HasNavIcon}}<span class="ui mini green label">Custom</span>{{else}}<span class="ui mini grey label">Default</span>{{end}}
|
<strong>Nav Icon</strong> {{if .HasNavIcon}}<span class="ui mini green label">Custom</span>{{else}}<span class="ui mini grey label">Default</span>{{end}}
|
||||||
<div class="tw-text-text-light tw-text-sm tw-mt-1">Top-left corner across all pages. Square, 30x30px.</div>
|
<div class="tw-text-text-light tw-text-sm tw-mt-1">Top-left corner across all pages. Square, 30x30px. Also sets the app icon (PWA / web manifest); use at least 512x512px for a crisp installed icon.</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{{template "admin/layout_head" (dict "pageClass" "admin metadata")}}
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{svg "octicon-list-unordered" 16}} Metadata
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<h5>Platform Options</h5>
|
||||||
|
<p class="tw-text-text-light tw-text-sm">Allowed values for the repository metadata <strong>Platform</strong> field. One per line (or comma-separated). Changes take effect immediately and are saved to <code>app.ini</code> <code>[metadata] PLATFORM_OPTIONS</code>.</p>
|
||||||
|
<form method="post" action="{{AppSubUrl}}/-/admin/metadata/settings">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="platform_options" rows="6" class="tw-w-full tw-font-mono" placeholder="joomla dolibarr go npm generic">{{.PlatformOptions}}</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="ui primary small button tw-mt-2">{{svg "octicon-check" 14}} Save Settings</button>
|
||||||
|
</form>
|
||||||
|
<p class="tw-text-text-light tw-text-sm tw-mt-3">Existing repositories keep their stored platform value even if it is later removed from this list; it simply won't be offered to other repos.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -87,6 +87,9 @@
|
|||||||
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
||||||
{{svg "octicon-paintbrush" 16}} Branding
|
{{svg "octicon-paintbrush" 16}} Branding
|
||||||
</a>
|
</a>
|
||||||
|
<a class="{{if .PageIsAdminMetadata}}active {{end}}item" href="{{AppSubUrl}}/-/admin/metadata">
|
||||||
|
{{svg "octicon-list-unordered" 16}} Metadata
|
||||||
|
</a>
|
||||||
<a class="{{if .PageIsAdminLicenseTiers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/license-tiers">
|
<a class="{{if .PageIsAdminLicenseTiers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/license-tiers">
|
||||||
{{svg "octicon-key" 16}} License Tiers
|
{{svg "octicon-key" 16}} License Tiers
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Organization</label>
|
<label>Organization</label>
|
||||||
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
<input value="{{.DerivedOrg}}" placeholder="Organization" readonly>
|
||||||
|
<p class="help">Derived from the organization profile (repository owner).</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="three fields">
|
<div class="three fields">
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
<select name="platform" class="ui dropdown">
|
<select name="platform" class="ui dropdown">
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
{{$platform := .Manifest.Platform}}
|
{{$platform := .Manifest.Platform}}
|
||||||
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
|
{{range $val := .PlatformOptions}}
|
||||||
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
|
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
Reference in New Issue
Block a user