Compare commits

..

11 Commits

18 changed files with 130 additions and 683 deletions
+1 -1
View File
@@ -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>
+9 -9
View File
@@ -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
+90 -28
View File
@@ -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: >-
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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 }}"
+4 -4
View File
@@ -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 -4
View File
@@ -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 }}"
+2 -2
View File
@@ -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
+12 -12
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
-105
View File
@@ -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"
}
-50
View File
@@ -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)
}
-109
View File
@@ -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
}
-184
View File
@@ -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")
}
-58
View File
@@ -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)
}
-1
View File
@@ -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
} }
-108
View File
@@ -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
}