Compare commits
11 Commits
development
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab45b87b66 | |||
| 615a1d42b4 | |||
| 422313018b | |||
| 1a98c60b7d | |||
| c32852285e | |||
| 071899e09e | |||
| 23cf618bf7 | |||
| 0177288ab1 | |||
| deb04a13d1 | |||
| 3ddfc7a504 | |||
| b77bfe47ef |
@@ -4,7 +4,7 @@
|
|||||||
<name>MokoGitea</name>
|
<name>MokoGitea</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||||
<version>06.18.06</version>
|
<version>06.19.00</version>
|
||||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# 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)
|
||||||
@@ -43,19 +43,19 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform 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
|
||||||
fi
|
fi
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
if [ -d "/opt/mokocli/cli" ]; then
|
||||||
echo "MOKO_CLI=/opt/moko-platform/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/moko-platform.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
/tmp/moko-platform-api
|
/tmp/mokocli
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
@@ -66,25 +66,25 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform 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
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/moko-platform
|
echo Using pre-installed /opt/mokocli
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; 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
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.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/moko-platform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/moko-platform-api
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
@@ -109,6 +109,40 @@ jobs:
|
|||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Update RC release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
|
NOTES=""
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
fi
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release candidate"
|
||||||
|
|
||||||
|
# Find the RC release and update its body
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/release-candidate" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "RC release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@@ -149,26 +183,26 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup moko-platform 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
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/moko-platform
|
echo Using pre-installed /opt/mokocli
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; 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
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.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/moko-platform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/moko-platform-api
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: "Determine version bump level"
|
- name: "Determine version bump level"
|
||||||
@@ -194,22 +228,32 @@ jobs:
|
|||||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: Update release notes and promote changelog
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Get the stable release info (version and ID)
|
||||||
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
|
||||||
|
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
||||||
|
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
|
||||||
|
VERSION=$(python3 -c "
|
||||||
|
import json, sys, re
|
||||||
|
r = json.load(sys.stdin)
|
||||||
|
name = r.get('name', '')
|
||||||
|
m = re.search(r'(\d+\.\d+\.\d+)', name)
|
||||||
|
print(m.group(1) if m else '')
|
||||||
|
" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
# Extract [Unreleased] section from changelog
|
||||||
|
NOTES=""
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
||||||
else
|
|
||||||
NOTES="Stable release"
|
|
||||||
fi
|
fi
|
||||||
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
|
|
||||||
# Update release body via API
|
# Update release body via API
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json, urllib.request
|
import json, urllib.request
|
||||||
@@ -219,7 +263,7 @@ jobs:
|
|||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
data=payload, method='PATCH',
|
data=payload, method='PATCH',
|
||||||
headers={
|
headers={
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
'Authorization': 'token ${TOKEN}',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
urllib.request.urlopen(req)
|
urllib.request.urlopen(req)
|
||||||
@@ -227,6 +271,24 @@ jobs:
|
|||||||
echo "Release notes updated from CHANGELOG.md"
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
|
||||||
|
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
version, date = sys.argv[1], sys.argv[2]
|
||||||
|
content = open('CHANGELOG.md').read()
|
||||||
|
old = '## [Unreleased]'
|
||||||
|
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||||
|
content = content.replace(old, new, 1)
|
||||||
|
open('CHANGELOG.md', 'w').write(content)
|
||||||
|
" "$VERSION" "$DATE"
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
||||||
|
git push origin main || true
|
||||||
|
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoStandards.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# 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,11 +33,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup MokoStandards tools
|
- name: Setup MokoStandards tools
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 06.18.06
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
@@ -19,7 +19,7 @@ permissions:
|
|||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://code.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-branch:
|
create-branch:
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: mokocli.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# 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
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.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
|
||||||
@@ -60,25 +60,25 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Setup moko-platform 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
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/moko-platform
|
echo Using pre-installed /opt/mokocli
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; 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
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.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/moko-platform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# 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.
|
||||||
|
|||||||
+2
-2
@@ -7,6 +7,6 @@
|
|||||||
|
|
||||||
## [06.19.00] --- 2026-06-20
|
## [06.19.00] --- 2026-06-20
|
||||||
|
|
||||||
## [06.19.00] --- 2026-06-19
|
## [06.19.00] --- 2026-06-20
|
||||||
|
|
||||||
## [06.18.00] --- 2026-06-19
|
## [06.19.00] --- 2026-06-19
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package licensing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(LicenseActivation))
|
|
||||||
}
|
|
||||||
|
|
||||||
// LicenseActivation tracks a domain that has activated a license.
|
|
||||||
type LicenseActivation struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
LicenseID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
Domain string `xorm:"VARCHAR(255) NOT NULL"`
|
|
||||||
IPAddress string `xorm:"VARCHAR(64)"`
|
|
||||||
JoomlaVer string `xorm:"VARCHAR(20)"`
|
|
||||||
ActivatedAt timeutil.TimeStamp `xorm:"CREATED"`
|
|
||||||
LastSeenAt timeutil.TimeStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LicenseActivation) TableName() string {
|
|
||||||
return "license_activation"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetActivationsByLicense returns all domain activations for a license.
|
|
||||||
func GetActivationsByLicense(ctx context.Context, licenseID int64) ([]*LicenseActivation, error) {
|
|
||||||
var acts []*LicenseActivation
|
|
||||||
return acts, db.GetEngine(ctx).Where("license_id = ?", licenseID).Find(&acts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountActivations returns the number of activated domains for a license.
|
|
||||||
func CountActivations(ctx context.Context, licenseID int64) (int64, error) {
|
|
||||||
return db.GetEngine(ctx).Where("license_id = ?", licenseID).Count(new(LicenseActivation))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActivateDomain registers a domain for a license. Returns the activation
|
|
||||||
// (existing or new) and whether it was newly created.
|
|
||||||
func ActivateDomain(ctx context.Context, licenseID int64, domain, ip, joomlaVer string, maxDomains int) (*LicenseActivation, bool, error) {
|
|
||||||
// Check if already activated
|
|
||||||
existing := new(LicenseActivation)
|
|
||||||
has, err := db.GetEngine(ctx).
|
|
||||||
Where("license_id = ? AND domain = ?", licenseID, domain).
|
|
||||||
Get(existing)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if has {
|
|
||||||
// Update last seen
|
|
||||||
existing.LastSeenAt = timeutil.TimeStampNow()
|
|
||||||
existing.IPAddress = ip
|
|
||||||
if joomlaVer != "" {
|
|
||||||
existing.JoomlaVer = joomlaVer
|
|
||||||
}
|
|
||||||
_, _ = db.GetEngine(ctx).ID(existing.ID).Cols("last_seen_at", "ip_address", "joomla_ver").Update(existing)
|
|
||||||
return existing, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check domain limit (0 = unlimited)
|
|
||||||
if maxDomains > 0 {
|
|
||||||
count, err := CountActivations(ctx, licenseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if count >= int64(maxDomains) {
|
|
||||||
return nil, false, ErrDomainLimitReached{LicenseID: licenseID, Max: maxDomains}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
act := &LicenseActivation{
|
|
||||||
LicenseID: licenseID,
|
|
||||||
Domain: domain,
|
|
||||||
IPAddress: ip,
|
|
||||||
JoomlaVer: joomlaVer,
|
|
||||||
}
|
|
||||||
_, err = db.GetEngine(ctx).Insert(act)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return act, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeactivateDomain removes a domain activation.
|
|
||||||
func DeactivateDomain(ctx context.Context, licenseID int64, domain string) error {
|
|
||||||
_, err := db.GetEngine(ctx).
|
|
||||||
Where("license_id = ? AND domain = ?", licenseID, domain).
|
|
||||||
Delete(new(LicenseActivation))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrDomainLimitReached is returned when a license has reached its max activated domains.
|
|
||||||
type ErrDomainLimitReached struct {
|
|
||||||
LicenseID int64
|
|
||||||
Max int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrDomainLimitReached) Error() string {
|
|
||||||
return "license domain limit reached"
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package licensing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(LicenseAuditLog))
|
|
||||||
}
|
|
||||||
|
|
||||||
// LicenseAuditLog records status transitions and other license events.
|
|
||||||
type LicenseAuditLog struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
LicenseID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
Action string `xorm:"VARCHAR(50) NOT NULL"` // status_change, tier_change, domain_activate, domain_deactivate
|
|
||||||
OldValue string `xorm:"VARCHAR(100)"`
|
|
||||||
NewValue string `xorm:"VARCHAR(100)"`
|
|
||||||
CreatedAt timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LicenseAuditLog) TableName() string {
|
|
||||||
return "license_audit_log"
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogLicenseAudit records a license event.
|
|
||||||
func LogLicenseAudit(ctx context.Context, licenseID int64, action, oldVal, newVal string) error {
|
|
||||||
entry := &LicenseAuditLog{
|
|
||||||
LicenseID: licenseID,
|
|
||||||
Action: action,
|
|
||||||
OldValue: oldVal,
|
|
||||||
NewValue: newVal,
|
|
||||||
}
|
|
||||||
_, err := db.GetEngine(ctx).Insert(entry)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuditLog returns audit entries for a license, newest first.
|
|
||||||
func GetAuditLog(ctx context.Context, licenseID int64) ([]*LicenseAuditLog, error) {
|
|
||||||
var entries []*LicenseAuditLog
|
|
||||||
return entries, db.GetEngine(ctx).
|
|
||||||
Where("license_id = ?", licenseID).
|
|
||||||
OrderBy("created_at DESC").
|
|
||||||
Find(&entries)
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package licensing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(LicenseEntitlement))
|
|
||||||
}
|
|
||||||
|
|
||||||
// LicenseEntitlement maps a license to an individual product (repo) it can access.
|
|
||||||
type LicenseEntitlement struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
LicenseID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
ProductCode string `xorm:"VARCHAR(30) NOT NULL"`
|
|
||||||
RepoOwner string `xorm:"VARCHAR(100) NOT NULL DEFAULT 'MokoConsulting'"`
|
|
||||||
RepoName string `xorm:"VARCHAR(100) NOT NULL"`
|
|
||||||
IsCustom bool `xorm:"NOT NULL DEFAULT false"` // true = manually added, survives tier changes
|
|
||||||
CreatedAt timeutil.TimeStamp `xorm:"CREATED"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (LicenseEntitlement) TableName() string {
|
|
||||||
return "license_entitlement"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEntitlementsByLicense returns all entitlements for a license.
|
|
||||||
func GetEntitlementsByLicense(ctx context.Context, licenseID int64) ([]*LicenseEntitlement, error) {
|
|
||||||
var ents []*LicenseEntitlement
|
|
||||||
return ents, db.GetEngine(ctx).Where("license_id = ?", licenseID).Find(&ents)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasEntitlement checks if a license has access to a specific product code.
|
|
||||||
func HasEntitlement(ctx context.Context, licenseID int64, productCode string) (bool, error) {
|
|
||||||
return db.GetEngine(ctx).
|
|
||||||
Where("license_id = ? AND product_code = ?", licenseID, productCode).
|
|
||||||
Exist(new(LicenseEntitlement))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCustomEntitlement adds a manual entitlement that survives tier changes.
|
|
||||||
func AddCustomEntitlement(ctx context.Context, licenseID int64, productCode, repoName string) error {
|
|
||||||
ent := &LicenseEntitlement{
|
|
||||||
LicenseID: licenseID,
|
|
||||||
ProductCode: productCode,
|
|
||||||
RepoOwner: "MokoConsulting",
|
|
||||||
RepoName: repoName,
|
|
||||||
IsCustom: true,
|
|
||||||
}
|
|
||||||
_, err := db.GetEngine(ctx).Insert(ent)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RebuildEntitlements deletes non-custom entitlements and rebuilds from the product tier.
|
|
||||||
// Custom entitlements (manually added) are preserved.
|
|
||||||
func RebuildEntitlements(ctx context.Context, licenseID int64, tierKey string) error {
|
|
||||||
// Delete non-custom entitlements
|
|
||||||
_, err := db.GetEngine(ctx).
|
|
||||||
Where("license_id = ? AND is_custom = ?", licenseID, false).
|
|
||||||
Delete(new(LicenseEntitlement))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up tier
|
|
||||||
tier, err := GetProductTierByKey(ctx, tierKey)
|
|
||||||
if err != nil || tier == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse repos JSON
|
|
||||||
var repos []string
|
|
||||||
if err := json.Unmarshal([]byte(tier.Repos), &repos); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build product code from repo name (lowercase, stripped)
|
|
||||||
for _, repoName := range repos {
|
|
||||||
productCode := repoName
|
|
||||||
// Check if this entitlement already exists (custom)
|
|
||||||
exists, err := db.GetEngine(ctx).
|
|
||||||
Where("license_id = ? AND product_code = ?", licenseID, productCode).
|
|
||||||
Exist(new(LicenseEntitlement))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ent := &LicenseEntitlement{
|
|
||||||
LicenseID: licenseID,
|
|
||||||
ProductCode: productCode,
|
|
||||||
RepoOwner: "MokoConsulting",
|
|
||||||
RepoName: repoName,
|
|
||||||
IsCustom: false,
|
|
||||||
}
|
|
||||||
if _, err := db.GetEngine(ctx).Insert(ent); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package licensing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"hash/crc32"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(License))
|
|
||||||
}
|
|
||||||
|
|
||||||
// License represents a consumer-facing license with a DLID (Download ID).
|
|
||||||
type License struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
UserID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
DLID string `xorm:"VARCHAR(36) UNIQUE NOT NULL"`
|
|
||||||
Tier string `xorm:"VARCHAR(30) NOT NULL DEFAULT 'base'"`
|
|
||||||
MaxDomains int `xorm:"NOT NULL DEFAULT 1"`
|
|
||||||
Status string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'active'"` // active, expired, revoked, suspended
|
|
||||||
ExpiresAt timeutil.TimeStamp `xorm:"INDEX"`
|
|
||||||
Notes string `xorm:"TEXT"`
|
|
||||||
CreatedAt timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
|
||||||
UpdatedAt timeutil.TimeStamp `xorm:"UPDATED"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (License) TableName() string {
|
|
||||||
return "license"
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExpired returns true if the license has a set expiry that has passed.
|
|
||||||
func (l *License) IsExpired() bool {
|
|
||||||
if l.ExpiresAt == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return time.Unix(int64(l.ExpiresAt), 0).Before(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsActive returns true if the license status is "active" and not expired.
|
|
||||||
func (l *License) IsActive() bool {
|
|
||||||
return l.Status == "active" && !l.IsExpired()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateDLID creates a new DLID: 28 random hex chars + 4 CRC32 checksum chars,
|
|
||||||
// formatted as 8-8-8-8 groups.
|
|
||||||
func GenerateDLID() (string, error) {
|
|
||||||
b := make([]byte, 14) // 14 bytes = 28 hex chars
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
prefix := hex.EncodeToString(b)
|
|
||||||
checksum := crc32.ChecksumIEEE([]byte(prefix))
|
|
||||||
full := fmt.Sprintf("%s%04x", prefix, checksum&0xFFFF)
|
|
||||||
// Format as 8-8-8-8
|
|
||||||
return fmt.Sprintf("%s-%s-%s-%s", full[0:8], full[8:16], full[16:24], full[24:32]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateDLIDFormat checks if a DLID has valid format and CRC32 checksum.
|
|
||||||
// This is a client-side check that catches typos without a database hit.
|
|
||||||
func ValidateDLIDFormat(dlid string) bool {
|
|
||||||
clean := strings.ReplaceAll(dlid, "-", "")
|
|
||||||
if len(clean) != 32 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Validate hex
|
|
||||||
if _, err := hex.DecodeString(clean); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// CRC32 check: last 4 chars should match CRC32 of first 28
|
|
||||||
prefix := clean[:28]
|
|
||||||
expected := fmt.Sprintf("%04x", crc32.ChecksumIEEE([]byte(prefix))&0xFFFF)
|
|
||||||
return clean[28:32] == expected
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLicense creates a new license with an auto-generated DLID.
|
|
||||||
func CreateLicense(ctx context.Context, userID int64, tier string, maxDomains int, expiresAt timeutil.TimeStamp) (*License, error) {
|
|
||||||
dlid, err := GenerateDLID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
license := &License{
|
|
||||||
UserID: userID,
|
|
||||||
DLID: dlid,
|
|
||||||
Tier: tier,
|
|
||||||
MaxDomains: maxDomains,
|
|
||||||
Status: "active",
|
|
||||||
ExpiresAt: expiresAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.GetEngine(ctx).Insert(license)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return license, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLicenseByDLID looks up a license by its DLID string.
|
|
||||||
func GetLicenseByDLID(ctx context.Context, dlid string) (*License, error) {
|
|
||||||
license := new(License)
|
|
||||||
has, err := db.GetEngine(ctx).Where("dlid = ?", dlid).Get(license)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !has {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return license, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLicenseByID returns a license by primary key.
|
|
||||||
func GetLicenseByID(ctx context.Context, id int64) (*License, error) {
|
|
||||||
license := new(License)
|
|
||||||
has, err := db.GetEngine(ctx).ID(id).Get(license)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !has {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return license, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLicensesByUser returns all licenses for a user.
|
|
||||||
func GetLicensesByUser(ctx context.Context, userID int64) ([]*License, error) {
|
|
||||||
var licenses []*License
|
|
||||||
return licenses, db.GetEngine(ctx).Where("user_id = ?", userID).Find(&licenses)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLicenseTier changes a license's tier, rebuilds entitlements, and logs the change.
|
|
||||||
func UpdateLicenseTier(ctx context.Context, licenseID int64, newTier string) error {
|
|
||||||
license, err := GetLicenseByID(ctx, licenseID)
|
|
||||||
if err != nil || license == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldTier := license.Tier
|
|
||||||
_, err = db.GetEngine(ctx).ID(licenseID).Cols("tier", "updated_at").Update(&License{Tier: newTier})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := LogLicenseAudit(ctx, licenseID, "tier_change", oldTier, newTier); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return RebuildEntitlements(ctx, licenseID, newTier)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLicenseStatus updates the status field and logs the transition.
|
|
||||||
func SetLicenseStatus(ctx context.Context, licenseID int64, status string) error {
|
|
||||||
license, err := GetLicenseByID(ctx, licenseID)
|
|
||||||
if err != nil || license == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldStatus := license.Status
|
|
||||||
_, err = db.GetEngine(ctx).ID(licenseID).Cols("status", "updated_at").Update(&License{Status: status})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return LogLicenseAudit(ctx, licenseID, "status_change", oldStatus, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeLicense permanently revokes a license.
|
|
||||||
func RevokeLicense(ctx context.Context, licenseID int64) error {
|
|
||||||
return SetLicenseStatus(ctx, licenseID, "revoked")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SuspendLicense temporarily suspends a license.
|
|
||||||
func SuspendLicense(ctx context.Context, licenseID int64) error {
|
|
||||||
return SetLicenseStatus(ctx, licenseID, "suspended")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReactivateLicense restores a suspended or expired license to active.
|
|
||||||
func ReactivateLicense(ctx context.Context, licenseID int64) error {
|
|
||||||
return SetLicenseStatus(ctx, licenseID, "active")
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package licensing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(ProductTier))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProductTier defines a licensing tier and its entitled repositories.
|
|
||||||
type ProductTier struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
TierKey string `xorm:"VARCHAR(30) UNIQUE NOT NULL"`
|
|
||||||
TierName string `xorm:"VARCHAR(100) NOT NULL"`
|
|
||||||
Repos string `xorm:"TEXT"` // JSON array of repo names
|
|
||||||
MaxDomains int `xorm:"NOT NULL DEFAULT 1"`
|
|
||||||
SortOrder int `xorm:"NOT NULL DEFAULT 0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ProductTier) TableName() string {
|
|
||||||
return "product_tier"
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoList parses the Repos JSON field into a string slice.
|
|
||||||
func (t *ProductTier) RepoList() []string {
|
|
||||||
var repos []string
|
|
||||||
if t.Repos == "" {
|
|
||||||
return repos
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal([]byte(t.Repos), &repos)
|
|
||||||
return repos
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProductTierByKey looks up a tier by its key (e.g. "pos", "suite").
|
|
||||||
func GetProductTierByKey(ctx context.Context, key string) (*ProductTier, error) {
|
|
||||||
tier := new(ProductTier)
|
|
||||||
has, err := db.GetEngine(ctx).Where("tier_key = ?", key).Get(tier)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !has {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return tier, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllProductTiers returns all tiers ordered by sort_order.
|
|
||||||
func GetAllProductTiers(ctx context.Context) ([]*ProductTier, error) {
|
|
||||||
var tiers []*ProductTier
|
|
||||||
return tiers, db.GetEngine(ctx).OrderBy("sort_order ASC").Find(&tiers)
|
|
||||||
}
|
|
||||||
@@ -435,7 +435,6 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(355, "Migrate update server metadata to repo manifest", v1_27.MigrateUpdateServerFieldsToManifest),
|
newMigration(355, "Migrate update server metadata to repo manifest", v1_27.MigrateUpdateServerFieldsToManifest),
|
||||||
newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType),
|
newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType),
|
||||||
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
|
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
|
||||||
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
|
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package v1_27
|
|
||||||
|
|
||||||
import (
|
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddLicensingTables creates the license, license_entitlement, license_activation,
|
|
||||||
// and product_tier tables for the consumer-facing DLID licensing system (#617).
|
|
||||||
func AddLicensingTables(x *xorm.Engine) error {
|
|
||||||
type License struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
UserID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
DLID string `xorm:"VARCHAR(36) UNIQUE NOT NULL"`
|
|
||||||
Tier string `xorm:"VARCHAR(30) NOT NULL DEFAULT 'base'"`
|
|
||||||
MaxDomains int `xorm:"NOT NULL DEFAULT 1"`
|
|
||||||
Status string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'active'"`
|
|
||||||
ExpiresAt int64 `xorm:"INDEX"`
|
|
||||||
Notes string `xorm:"TEXT"`
|
|
||||||
CreatedAt int64 `xorm:"INDEX CREATED"`
|
|
||||||
UpdatedAt int64 `xorm:"UPDATED"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LicenseEntitlement struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
LicenseID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
ProductCode string `xorm:"VARCHAR(30) NOT NULL"`
|
|
||||||
RepoOwner string `xorm:"VARCHAR(100) NOT NULL DEFAULT 'MokoConsulting'"`
|
|
||||||
RepoName string `xorm:"VARCHAR(100) NOT NULL"`
|
|
||||||
IsCustom bool `xorm:"NOT NULL DEFAULT false"`
|
|
||||||
CreatedAt int64 `xorm:"CREATED"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LicenseActivation struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
LicenseID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
Domain string `xorm:"VARCHAR(255) NOT NULL"`
|
|
||||||
IPAddress string `xorm:"VARCHAR(64)"`
|
|
||||||
JoomlaVer string `xorm:"VARCHAR(20)"`
|
|
||||||
ActivatedAt int64 `xorm:"CREATED"`
|
|
||||||
LastSeenAt int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductTier struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
TierKey string `xorm:"VARCHAR(30) UNIQUE NOT NULL"`
|
|
||||||
TierName string `xorm:"VARCHAR(100) NOT NULL"`
|
|
||||||
Repos string `xorm:"TEXT"`
|
|
||||||
MaxDomains int `xorm:"NOT NULL DEFAULT 1"`
|
|
||||||
SortOrder int `xorm:"NOT NULL DEFAULT 0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LicenseAuditLog struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
LicenseID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
Action string `xorm:"VARCHAR(50) NOT NULL"`
|
|
||||||
OldValue string `xorm:"VARCHAR(100)"`
|
|
||||||
NewValue string `xorm:"VARCHAR(100)"`
|
|
||||||
CreatedAt int64 `xorm:"INDEX CREATED"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := x.Sync(new(License), new(LicenseEntitlement), new(LicenseActivation), new(ProductTier), new(LicenseAuditLog)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add composite unique indexes
|
|
||||||
if _, err := x.Exec("CREATE UNIQUE INDEX IF NOT EXISTS UQE_license_entitlement_lic_prod ON license_entitlement (license_id, product_code)"); err != nil {
|
|
||||||
// MySQL doesn't support IF NOT EXISTS for indexes — try without
|
|
||||||
x.Exec("CREATE UNIQUE INDEX UQE_license_entitlement_lic_prod ON license_entitlement (license_id, product_code)")
|
|
||||||
}
|
|
||||||
if _, err := x.Exec("CREATE UNIQUE INDEX IF NOT EXISTS UQE_license_activation_lic_domain ON license_activation (license_id, domain)"); err != nil {
|
|
||||||
x.Exec("CREATE UNIQUE INDEX UQE_license_activation_lic_domain ON license_activation (license_id, domain)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed product tiers
|
|
||||||
tiers := []ProductTier{
|
|
||||||
{TierKey: "base", TierName: "MokoSuite Base", Repos: `["MokoSuite"]`, MaxDomains: 1, SortOrder: 0},
|
|
||||||
{TierKey: "crm", TierName: "MokoSuite CRM", Repos: `["MokoSuite","MokoSuiteCRM"]`, MaxDomains: 3, SortOrder: 10},
|
|
||||||
{TierKey: "erp", TierName: "MokoSuite ERP", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP"]`, MaxDomains: 3, SortOrder: 20},
|
|
||||||
{TierKey: "child", TierName: "MokoSuite Child", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteChild"]`, MaxDomains: 3, SortOrder: 25},
|
|
||||||
{TierKey: "create", TierName: "MokoSuite Create", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteCreate"]`, MaxDomains: 3, SortOrder: 26},
|
|
||||||
{TierKey: "npo", TierName: "MokoSuite NPO", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteNPO"]`, MaxDomains: 3, SortOrder: 27},
|
|
||||||
{TierKey: "hrm", TierName: "MokoSuite HRM", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteHRM"]`, MaxDomains: 3, SortOrder: 30},
|
|
||||||
{TierKey: "mrp", TierName: "MokoSuite MRP", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP","MokoSuiteMRP"]`, MaxDomains: 3, SortOrder: 35},
|
|
||||||
{TierKey: "pos", TierName: "MokoSuite POS", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP","MokoSuitePOS"]`, MaxDomains: 5, SortOrder: 40},
|
|
||||||
{TierKey: "shop", TierName: "MokoSuite Shop", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP","MokoSuiteShop"]`, MaxDomains: 5, SortOrder: 45},
|
|
||||||
{TierKey: "restaurant", TierName: "MokoSuite Restaurant", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP","MokoSuitePOS","MokoSuiteRestaurant"]`, MaxDomains: 5, SortOrder: 50},
|
|
||||||
{TierKey: "suite", TierName: "MokoSuite Suite", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP","MokoSuitePOS","MokoSuiteShop","MokoSuiteHRM","MokoSuiteMRP","MokoSuiteChild","MokoSuiteCreate","MokoSuiteNPO","MokoSuiteRestaurant","MokoSuiteForms"]`, MaxDomains: 10, SortOrder: 90},
|
|
||||||
{TierKey: "enterprise", TierName: "MokoSuite Enterprise", Repos: `["MokoSuite","MokoSuiteCRM","MokoSuiteERP","MokoSuitePOS","MokoSuiteShop","MokoSuiteHRM","MokoSuiteMRP","MokoSuiteChild","MokoSuiteCreate","MokoSuiteNPO","MokoSuiteRestaurant","MokoSuiteForms","MokoSuiteCommunity","MokoSuiteBackup","MokoSuiteStoreLocator","MokoSuiteOpenGraph","MokoSuiteCross"]`, MaxDomains: 0, SortOrder: 100},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range tiers {
|
|
||||||
// Only insert if the tier doesn't already exist
|
|
||||||
count, err := x.Where("tier_key = ?", t.TierKey).Count(new(ProductTier))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
if _, err := x.Insert(&t); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user