Compare commits
44 Commits
feat/vnext
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -4,7 +4,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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
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
|
||||||
@@ -53,7 +53,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"
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
#
|
#
|
||||||
# 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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/auto-release.yml
|
# PATH: /.mokogitea/workflows/auto-release.yml
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from metadata API
|
||||||
#
|
#
|
||||||
# +=======================================================================+
|
# +=======================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +=======================================================================+
|
# +=======================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads metadata API (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
# | joomla: XML manifest, type-prefixed packages |
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -95,7 +95,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 +115,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
|
||||||
@@ -231,7 +228,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -246,7 +243,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
|
||||||
@@ -377,13 +374,47 @@ jobs:
|
|||||||
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
||||||
DATE=$(date +%Y-%m-%d)
|
DATE=$(date +%Y-%m-%d)
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import sys
|
import sys, re
|
||||||
version, date = sys.argv[1], sys.argv[2]
|
version, date = sys.argv[1], sys.argv[2]
|
||||||
content = open('CHANGELOG.md').read()
|
lines = open('CHANGELOG.md').read().split('\n')
|
||||||
old = '## [Unreleased]'
|
h2 = re.compile(r'^##\s+\[([^\]]+)\]')
|
||||||
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
header, sections, cur = [], [], None
|
||||||
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
|
for ln in lines:
|
||||||
open('CHANGELOG.md', 'w').write(content)
|
m = h2.match(ln)
|
||||||
|
if m:
|
||||||
|
if cur: sections.append(cur)
|
||||||
|
cur = {'label': m.group(1).strip(), 'head': ln, 'body': []}
|
||||||
|
elif cur is None:
|
||||||
|
header.append(ln)
|
||||||
|
else:
|
||||||
|
cur['body'].append(ln)
|
||||||
|
if cur: sections.append(cur)
|
||||||
|
def nonempty(b): return any(x.strip() for x in b)
|
||||||
|
def trim(b):
|
||||||
|
b = b[:]
|
||||||
|
while b and not b[0].strip(): b.pop(0)
|
||||||
|
while b and not b[-1].strip(): b.pop()
|
||||||
|
return b
|
||||||
|
unreleased, order, bykey = [], [], {}
|
||||||
|
for s in sections:
|
||||||
|
key = s['label'].lower()
|
||||||
|
if key == 'unreleased':
|
||||||
|
if nonempty(s['body']): unreleased += s['body']
|
||||||
|
continue
|
||||||
|
if not nonempty(s['body']): continue # drop blank release sections
|
||||||
|
if key in bykey: bykey[key]['body'] += [''] + s['body'] # merge duplicate heading (never drop content)
|
||||||
|
else: bykey[key] = s; order.append(key)
|
||||||
|
rest, seen = [bykey[k] for k in order], set(order)
|
||||||
|
out = []
|
||||||
|
htxt = '\n'.join(header).rstrip()
|
||||||
|
if htxt: out += [htxt, '']
|
||||||
|
out += ['## [Unreleased]', '']
|
||||||
|
promote = bool(unreleased) and version.lower() not in seen
|
||||||
|
if unreleased and not promote: out += trim(unreleased) + ['']
|
||||||
|
if promote: out += ['## [%s] --- %s' % (version, date), ''] + trim(unreleased) + ['']
|
||||||
|
for s in rest: out += [s['head'], ''] + trim(s['body']) + ['']
|
||||||
|
res = re.sub(r'\n{3,}', '\n\n', '\n'.join(out)).rstrip('\n') + '\n'
|
||||||
|
open('CHANGELOG.md', 'w').write(res)
|
||||||
" "$VERSION" "$DATE"
|
" "$VERSION" "$DATE"
|
||||||
git add CHANGELOG.md
|
git add CHANGELOG.md
|
||||||
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoCLI.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Cascade
|
# INGROUP: MokoCLI.Cascade
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/cascade-dev.yml
|
# PATH: /.mokogitea/workflows/cascade-dev.yml
|
||||||
# VERSION: 02.00.00
|
# VERSION: 02.01.00
|
||||||
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
|
# BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify
|
||||||
|
|
||||||
name: "Cascade Main -> Dev"
|
name: "Cascade Main -> Dev"
|
||||||
|
|
||||||
@@ -16,6 +16,10 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
# Daily safety net: catches drift even when main only received [skip ci] pushes
|
||||||
|
# (which never fire the push trigger above). Off-round minute to avoid a fleet-wide spike.
|
||||||
|
schedule:
|
||||||
|
- cron: '23 7 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -33,7 +37,13 @@ jobs:
|
|||||||
name: Cascade main -> dev
|
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)
|
- name: Checkout (full history for merge/resolve)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Cascade main -> dev (auto-resolve version stamps, else notify)
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
@@ -41,7 +51,7 @@ jobs:
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
|
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
|
||||||
AUTH="Authorization: token ${TOKEN}"
|
AUTH="Authorization: token ${TOKEN}"
|
||||||
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
|
jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
|
||||||
|
|
||||||
# 0. dev must exist
|
# 0. dev must exist
|
||||||
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
|
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
|
||||||
@@ -61,8 +71,8 @@ jobs:
|
|||||||
| 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 "")
|
| 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
|
if [ -z "$PR" ]; then
|
||||||
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
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."}')
|
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution."}')
|
||||||
PR=$(printf '%s' "$RESP" | jqnum number)
|
PR=$(printf '%s' "$RESP" | jqget number)
|
||||||
if [ -z "$PR" ]; then
|
if [ -z "$PR" ]; then
|
||||||
echo "::warning::Could not open cascade PR: $RESP"; exit 0
|
echo "::warning::Could not open cascade PR: $RESP"; exit 0
|
||||||
fi
|
fi
|
||||||
@@ -71,15 +81,6 @@ jobs:
|
|||||||
echo "Reusing open cascade PR #${PR}"
|
echo "Reusing open cascade PR #${PR}"
|
||||||
fi
|
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() {
|
notify() {
|
||||||
curl -sS \
|
curl -sS \
|
||||||
-H "Title: ${REPO}: dev cascade needs manual merge" \
|
-H "Title: ${REPO}: dev cascade needs manual merge" \
|
||||||
@@ -90,17 +91,100 @@ jobs:
|
|||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# 4. auto-merge only if conflict-free; otherwise notify
|
# 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}" | jqget mergeable)
|
||||||
|
case "$MERGEABLE" in True|False) break ;; esac
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "mergeable=${MERGEABLE}"
|
||||||
|
|
||||||
|
# 4a. conflict-free -> merge via API (existing behaviour)
|
||||||
if [ "$MERGEABLE" = "True" ]; then
|
if [ "$MERGEABLE" = "True" ]; then
|
||||||
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
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}')
|
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
|
||||||
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
||||||
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
|
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
|
||||||
else
|
exit 0
|
||||||
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
|
|
||||||
notify "could not be auto-merged (HTTP ${CODE})."
|
|
||||||
fi
|
fi
|
||||||
else
|
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
|
||||||
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
|
notify "could not be auto-merged (HTTP ${CODE})."
|
||||||
notify "has conflicts and cannot be merged automatically."
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 4b. conflicts -> try to auto-resolve if they are ONLY VERSION-stamp lines.
|
||||||
|
echo "PR not cleanly mergeable; checking whether conflicts are VERSION-stamp-only..."
|
||||||
|
git config user.name "MokoGitea Cascade"
|
||||||
|
git config user.email "actions@mokoconsulting.tech"
|
||||||
|
git fetch --quiet origin main dev
|
||||||
|
git checkout -B dev origin/dev
|
||||||
|
|
||||||
|
if git merge --no-ff --no-commit origin/main >/dev/null 2>&1; then
|
||||||
|
# Became clean at git level (e.g. mergeability was still computing) -> commit + push.
|
||||||
|
git commit -m "chore(sync): cascade main -> dev [skip ci]" >/dev/null
|
||||||
|
git push origin dev
|
||||||
|
echo "Cascade merged cleanly at git level and pushed to dev."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONFLICTS=$(git diff --name-only --diff-filter=U)
|
||||||
|
echo "Conflicted files:"; echo "${CONFLICTS}"
|
||||||
|
|
||||||
|
# A conflict is "stamp-only" when every line inside every conflict block matches
|
||||||
|
# a version-stamp pattern (VERSION: header, <version> element, or CHANGELOG title).
|
||||||
|
is_stamp_only() {
|
||||||
|
awk '
|
||||||
|
/^<<<<<<< / { inc=1; next }
|
||||||
|
inc && /^=======$/ { next }
|
||||||
|
/^>>>>>>> / { inc=0; next }
|
||||||
|
inc { if ($0 !~ /(VERSION:|<version>|# Changelog)/) { bad=1 } }
|
||||||
|
END { exit(bad ? 1 : 0) }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
# Resolve a stamp-only file by keeping dev (ours) for the conflicting lines,
|
||||||
|
# preserving all auto-merged content around them.
|
||||||
|
keep_ours() {
|
||||||
|
awk '
|
||||||
|
/^<<<<<<< / { inc=1; side="ours"; next }
|
||||||
|
inc && /^=======$/ { side="theirs"; next }
|
||||||
|
/^>>>>>>> / { inc=0; next }
|
||||||
|
{ if (!inc) { print; next } if (side=="ours") print }
|
||||||
|
' "$1" > "$1.resolved" && mv "$1.resolved" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
ALL_STAMP=1
|
||||||
|
for f in ${CONFLICTS}; do
|
||||||
|
if ! is_stamp_only "$f"; then
|
||||||
|
echo "::notice::$f has non-stamp conflicts -> manual resolution required."
|
||||||
|
ALL_STAMP=0; break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ALL_STAMP" != "1" ]; then
|
||||||
|
git merge --abort || true
|
||||||
|
notify "has non-version-stamp conflicts and cannot be auto-resolved."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All conflicts are VERSION-stamp-only; resolving in favour of dev."
|
||||||
|
for f in ${CONFLICTS}; do
|
||||||
|
keep_ours "$f"
|
||||||
|
git add "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Best-effort: normalise stamps to dev's version if mokocli is available.
|
||||||
|
if [ -f /opt/mokocli/cli/version_check.php ]; then
|
||||||
|
php /opt/mokocli/cli/version_check.php --fix || true
|
||||||
|
git add -A
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "chore(sync): cascade main -> dev (auto-resolved version stamps) [skip ci]" >/dev/null
|
||||||
|
git push origin dev
|
||||||
|
echo "Cascade auto-resolved and pushed to dev."
|
||||||
|
|
||||||
|
# Close the now-redundant PR (its changes are in dev) with an explanatory comment.
|
||||||
|
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \
|
||||||
|
-d '{"body":"Auto-resolved VERSION-stamp-only conflicts and pushed the merge to dev. Closing."}' >/dev/null || true
|
||||||
|
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \
|
||||||
|
-d '{"state":"closed"}' >/dev/null || true
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoCLI.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/ci-generic.yml
|
# PATH: /.mokogitea/workflows/ci-generic.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
@@ -131,7 +131,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
|
# Independent job (no `needs: lint`): the MokoGitea Actions scheduler does not
|
||||||
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
|
# 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
|
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
|
||||||
# directly (same condition lint uses) instead of gating on lint's result.
|
# directly (same condition lint uses) instead of gating on lint's result.
|
||||||
|
|||||||
@@ -4,7 +4,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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
||||||
|
|
||||||
- name: Report CI failure
|
- name: Report CI failure
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: MokoCLI.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
# PATH: /.mokogitea/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
|
||||||
|
|||||||
+14
-14
@@ -39,17 +39,10 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Verify dev environment is healthy
|
# NOTE: removed a "Verify dev environment is healthy" gate that curled
|
||||||
run: |
|
# git.dev and `exit 1` on failure — gating a PROD deploy on a live DEV
|
||||||
echo "Checking git.dev.mokoconsulting.tech health..."
|
# health check is a cross-tier false-negative (a transient dev blip blocked
|
||||||
if curl -sf --max-time 10 https://git.dev.mokoconsulting.tech/api/healthz; then
|
# prod). Pre-prod validation is the dev -> rc -> main pipeline + RC env.
|
||||||
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)
|
- name: Checkout source (for version detection)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -66,17 +59,19 @@ jobs:
|
|||||||
ENV="${{ github.event.inputs.environment }}"
|
ENV="${{ github.event.inputs.environment }}"
|
||||||
fi
|
fi
|
||||||
if [ "$ENV" = "production" ]; then
|
if [ "$ENV" = "production" ]; then
|
||||||
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
|
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||||
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
||||||
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
||||||
echo "branch=main" >> $GITHUB_OUTPUT
|
echo "branch=main" >> $GITHUB_OUTPUT
|
||||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag_env=MOKOGITEA_TAG" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||||
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
||||||
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
||||||
echo "branch=dev" >> $GITHUB_OUTPUT
|
echo "branch=dev" >> $GITHUB_OUTPUT
|
||||||
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
|
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag_env=MOKOGITEA_DEV_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Write deploy key
|
- name: Write deploy key
|
||||||
@@ -95,6 +90,7 @@ jobs:
|
|||||||
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
||||||
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
||||||
CONTAINER: ${{ steps.config.outputs.container }}
|
CONTAINER: ${{ steps.config.outputs.container }}
|
||||||
|
TAG_ENV: ${{ steps.config.outputs.tag_env }}
|
||||||
run: |
|
run: |
|
||||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
||||||
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
|
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
|
||||||
@@ -134,8 +130,12 @@ jobs:
|
|||||||
|
|
||||||
echo 'Restarting container...'
|
echo 'Restarting container...'
|
||||||
cd $COMPOSE_DIR
|
cd $COMPOSE_DIR
|
||||||
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
|
# Drive ONLY the target service's tag via its compose env-var (no sed —
|
||||||
docker compose up -d $CONTAINER
|
# a blanket 'sed s|mokogitea:...|' clobbered the dev + rc service tags too).
|
||||||
|
# Remove any lingering fixed-name container first (avoids the "name already
|
||||||
|
# in use" conflict), then force-recreate under the shared compose project.
|
||||||
|
docker rm -f $CONTAINER 2>/dev/null || true
|
||||||
|
env $TAG_ENV="$TAG" docker compose -p gitea-dev up -d --force-recreate $CONTAINER
|
||||||
|
|
||||||
echo 'Health check...'
|
echo 'Health check...'
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: MokoCLI.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/gitleaks.yml
|
# PATH: /.mokogitea/workflows/gitleaks.yml
|
||||||
# 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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: MokoCLI.Automation
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: MokoCLI.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/notify.yml
|
# PATH: /.mokogitea/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
|
||||||
@@ -46,13 +46,13 @@ jobs:
|
|||||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
URL="${{ github.event.workflow_run.html_url }}"
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
curl -sS \
|
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
|
||||||
-H "Title: ${REPO} released" \
|
-H "Title: ${REPO} released" \
|
||||||
-H "Tags: white_check_mark,package" \
|
-H "Tags: white_check_mark,package" \
|
||||||
-H "Priority: default" \
|
-H "Priority: default" \
|
||||||
-H "Click: ${URL}" \
|
-H "Click: ${URL}" \
|
||||||
-d "${WORKFLOW} completed successfully." \
|
-d "${WORKFLOW} completed successfully." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
|
||||||
|
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: github.event.workflow_run.conclusion == 'failure'
|
if: github.event.workflow_run.conclusion == 'failure'
|
||||||
@@ -61,10 +61,10 @@ jobs:
|
|||||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
URL="${{ github.event.workflow_run.html_url }}"
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
curl -sS \
|
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
|
||||||
-H "Title: ${REPO} workflow failed" \
|
-H "Title: ${REPO} workflow failed" \
|
||||||
-H "Tags: x,warning" \
|
-H "Tags: x,warning" \
|
||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-H "Click: ${URL}" \
|
-H "Click: ${URL}" \
|
||||||
-d "${WORKFLOW} failed. Check the run for details." \
|
-d "${WORKFLOW} failed. Check the run for details." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# 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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/pr-check.yml
|
# PATH: /.mokogitea/workflows/pr-check.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
@@ -224,7 +224,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"
|
||||||
@@ -258,8 +258,9 @@ jobs:
|
|||||||
while IFS= read -r -d '' file; do
|
while IFS= read -r -d '' file; do
|
||||||
# Skip vendor, node_modules, and index.html stub files
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
# Check first 10 lines for JEXEC or JPATH guard
|
# Scan the whole file for the JEXEC/JPATH guard: it is placed after
|
||||||
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
# the SPDX/file-header docblock, which commonly runs past 20 lines.
|
||||||
|
if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
|
||||||
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# 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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
# PATH: /.mokogitea/workflows/pre-release.yml
|
||||||
# VERSION: 05.02.00
|
# VERSION: 05.02.01
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup MokoCLI tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
@@ -84,7 +84,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
|
||||||
@@ -162,7 +162,13 @@ jobs:
|
|||||||
git add -A
|
git add -A
|
||||||
git diff --cached --quiet || {
|
git diff --cached --quiet || {
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
git push origin HEAD 2>&1
|
# Push the bump commit, but do NOT fail the release if the target branch
|
||||||
|
# is protected and the release identity is not on the push allowlist.
|
||||||
|
# The build proceeds from the in-tree bumped version regardless; if the
|
||||||
|
# push is rejected, the next run simply re-bumps from the same base.
|
||||||
|
if ! git push origin HEAD 2>&1; then
|
||||||
|
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-detect element via manifest_element.php
|
# Auto-detect element via manifest_element.php
|
||||||
|
|||||||
@@ -4,7 +4,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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#
|
#
|
||||||
# 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/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/repo-health.yml
|
# PATH: /.mokogitea/workflows/repo-health.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
|
|||||||
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 }}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGitea.Workflow.Template
|
# DEFGROUP: MokoGitea.Workflow.Template
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoCLI.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
# PATH: /.mokogitea/workflows/version-set.yml
|
# PATH: /.mokogitea/workflows/version-set.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
|
|||||||
@@ -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}
|
|
||||||
@@ -63,6 +63,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
|
||||||
|
|||||||
@@ -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,27 +137,49 @@ 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
|
||||||
dest, err := os.Create(destPath)
|
// manifest icon and the navbar fallback — so branding the nav icon brands
|
||||||
if err != nil {
|
// the app icon too. Write the uploaded image to both in a single pass. See #773.
|
||||||
ctx.Flash.Error("Failed to save image")
|
targets := []string{filename}
|
||||||
log.Error("Create %s: %v", destPath, err)
|
if imageType == "nav-icon" {
|
||||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
targets = append(targets, "logo.png")
|
||||||
return
|
|
||||||
}
|
|
||||||
defer dest.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(dest, file); err != nil {
|
|
||||||
ctx.Flash.Error("Failed to write image")
|
|
||||||
log.Error("Copy to %s: %v", destPath, err)
|
|
||||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove SVG override if present
|
// Write the uploaded image to each target independently (rewinding the
|
||||||
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
|
// stream between targets) so a failure on a later target never leaves an
|
||||||
if fileExists(svgPath) {
|
// earlier one truncated/empty.
|
||||||
os.Remove(svgPath)
|
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)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error("Failed to save image")
|
||||||
|
log.Error("Create %s: %v", destPath, err)
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(dest, file); err != nil {
|
||||||
|
dest.Close()
|
||||||
|
ctx.Flash.Error("Failed to write image")
|
||||||
|
log.Error("Copy to %s: %v", destPath, err)
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dest.Close()
|
||||||
|
|
||||||
|
// Remove any SVG override so the uploaded PNG takes effect.
|
||||||
|
svgPath := filepath.Join(imgDir, name[:len(name)-4]+".svg")
|
||||||
|
if fileExists(svgPath) {
|
||||||
|
os.Remove(svgPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Flash.Success("Branding image updated: " + imageType)
|
ctx.Flash.Success("Branding image updated: " + imageType)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ 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)
|
||||||
|
|
||||||
// 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 +104,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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user