Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b1278e7e2 | |||
| 7756ba2c2c | |||
| b115fa324b |
@@ -1,251 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/branch-protection.yml
|
||||
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||
#
|
||||
# +========================================================================+
|
||||
# | BRANCH PROTECTION SETUP |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Applies protection rules for: main, dev, rc, beta, alpha |
|
||||
# | |
|
||||
# | main — Require PR, block rejected reviews, no force push |
|
||||
# | dev — Allow push, no force push, no delete |
|
||||
# | rc — Allow push, no force push, no delete |
|
||||
# | beta — Allow push, no force push, no delete |
|
||||
# | alpha — Allow push, no force push, no delete |
|
||||
# | |
|
||||
# | jmiller has override authority on all branches. |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: Branch Protection Setup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Preview mode (no changes)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
repos:
|
||||
description: 'Comma-separated repo names (empty = all governed repos)'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
env:
|
||||
GITEA_URL: https://git.mokoconsulting.tech
|
||||
GITEA_ORG: MokoConsulting
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
protect:
|
||||
name: Apply Branch Protection Rules
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Determine target repos
|
||||
id: repos
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
API="${GITEA_URL}/api/v1"
|
||||
|
||||
# Platform/standards/infra repos to exclude
|
||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
|
||||
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||
|
||||
if [ -n "${{ inputs.repos }}" ]; then
|
||||
# User-specified repos
|
||||
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
||||
else
|
||||
# Fetch all org repos
|
||||
PAGE=1
|
||||
REPOS=""
|
||||
while true; do
|
||||
BATCH=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
|
||||
| jq -r '.[].name // empty')
|
||||
[ -z "$BATCH" ] && break
|
||||
REPOS="$REPOS $BATCH"
|
||||
PAGE=$((PAGE + 1))
|
||||
done
|
||||
|
||||
# Filter out excluded repos
|
||||
FILTERED=""
|
||||
for REPO in $REPOS; do
|
||||
SKIP=false
|
||||
for EX in $EXCLUDE; do
|
||||
if [ "$REPO" = "$EX" ]; then
|
||||
SKIP=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$SKIP" = "false" ]; then
|
||||
FILTERED="$FILTERED $REPO"
|
||||
fi
|
||||
done
|
||||
REPOS="$FILTERED"
|
||||
fi
|
||||
|
||||
echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
|
||||
COUNT=$(echo "$REPOS" | wc -w)
|
||||
echo "📋 Target repos (${COUNT}): $REPOS"
|
||||
|
||||
- name: Apply protection rules
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||
run: |
|
||||
API="${GITEA_URL}/api/v1"
|
||||
REPOS="${{ steps.repos.outputs.repos }}"
|
||||
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
SKIPPED=0
|
||||
|
||||
# ── Rule definitions ──────────────────────────────────────
|
||||
# Only the CI bot (jmiller token) can push directly.
|
||||
# All human contributors must use PRs.
|
||||
# Force push disabled on all branches.
|
||||
|
||||
RULE_MAIN='{
|
||||
"rule_name": "main",
|
||||
"enable_push": true,
|
||||
"enable_push_whitelist": true,
|
||||
"push_whitelist_usernames": ["jmiller"],
|
||||
"enable_force_push": false,
|
||||
"enable_force_push_allowlist": false,
|
||||
"force_push_allowlist_usernames": [],
|
||||
"enable_merge_whitelist": false,
|
||||
"required_approvals": 0,
|
||||
"dismiss_stale_approvals": true,
|
||||
"block_on_rejected_reviews": true,
|
||||
"block_on_outdated_branch": false,
|
||||
"priority": 1
|
||||
}'
|
||||
|
||||
RULE_DEV='{
|
||||
"rule_name": "dev",
|
||||
"enable_push": true,
|
||||
"enable_push_whitelist": true,
|
||||
"push_whitelist_usernames": ["jmiller"],
|
||||
"enable_force_push": false,
|
||||
"enable_force_push_allowlist": false,
|
||||
"force_push_allowlist_usernames": [],
|
||||
"enable_merge_whitelist": false,
|
||||
"required_approvals": 0,
|
||||
"block_on_rejected_reviews": false,
|
||||
"priority": 2
|
||||
}'
|
||||
|
||||
RULE_RC='{
|
||||
"rule_name": "rc",
|
||||
"enable_push": true,
|
||||
"enable_push_whitelist": true,
|
||||
"push_whitelist_usernames": ["jmiller"],
|
||||
"enable_force_push": false,
|
||||
"enable_force_push_allowlist": false,
|
||||
"force_push_allowlist_usernames": [],
|
||||
"enable_merge_whitelist": false,
|
||||
"required_approvals": 0,
|
||||
"block_on_rejected_reviews": false,
|
||||
"priority": 3
|
||||
}'
|
||||
|
||||
RULE_BETA='{
|
||||
"rule_name": "beta",
|
||||
"enable_push": true,
|
||||
"enable_push_whitelist": true,
|
||||
"push_whitelist_usernames": ["jmiller"],
|
||||
"enable_force_push": false,
|
||||
"enable_force_push_allowlist": false,
|
||||
"force_push_allowlist_usernames": [],
|
||||
"enable_merge_whitelist": false,
|
||||
"required_approvals": 0,
|
||||
"block_on_rejected_reviews": false,
|
||||
"priority": 4
|
||||
}'
|
||||
|
||||
RULE_ALPHA='{
|
||||
"rule_name": "alpha",
|
||||
"enable_push": true,
|
||||
"enable_push_whitelist": true,
|
||||
"push_whitelist_usernames": ["jmiller"],
|
||||
"enable_force_push": false,
|
||||
"enable_force_push_allowlist": false,
|
||||
"force_push_allowlist_usernames": [],
|
||||
"enable_merge_whitelist": false,
|
||||
"required_approvals": 0,
|
||||
"block_on_rejected_reviews": false,
|
||||
"priority": 5
|
||||
}'
|
||||
|
||||
RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
|
||||
RULE_NAMES=("main" "dev" "rc" "beta" "alpha")
|
||||
|
||||
# ── Apply rules to each repo ──────────────────────────────
|
||||
for REPO in $REPOS; do
|
||||
echo ""
|
||||
echo "═══ ${REPO} ═══"
|
||||
|
||||
for i in "${!RULES[@]}"; do
|
||||
RULE="${RULES[$i]}"
|
||||
NAME="${RULE_NAMES[$i]}"
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo " [DRY RUN] Would apply rule: ${NAME}"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Delete existing rule if present (idempotent recreate)
|
||||
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
|
||||
curl -sS -o /dev/null -w "" \
|
||||
-X DELETE \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
|
||||
|
||||
# Create rule
|
||||
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$RULE" \
|
||||
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
|
||||
|
||||
HTTP=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP" = "201" ]; then
|
||||
echo " ✅ ${NAME}"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " ✅ Success: ${SUCCESS}"
|
||||
echo " ❌ Failed: ${FAILED}"
|
||||
echo " ⏭️ Skipped: ${SKIPPED}"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
if [ "$FAILED" -gt 0 ]; then
|
||||
echo "::warning::${FAILED} rule(s) failed to apply"
|
||||
fi
|
||||
@@ -4,7 +4,7 @@
|
||||
<name>MokoGitea</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>05.14.00</version>
|
||||
<version>01.00.00</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
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
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
@@ -0,0 +1,270 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: false
|
||||
type: choice
|
||||
default: release
|
||||
options:
|
||||
- release
|
||||
- promote-rc
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
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
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
# Ensure PHP + Composer are available
|
||||
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
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||
- name: "Step 10: Push main to GitHub mirror"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||
git fetch origin main --depth=1
|
||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Delete old version branch if it exists (same version re-release)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||
|
||||
# Create version/XX.YY.ZZ from main
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||
|
||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
- name: "Post-release: Reset dev version"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -1,48 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
|
||||
name: "Branch Cleanup"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Delete merged branch
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.head.ref != 'dev' &&
|
||||
github.event.pull_request.head.ref != 'main'
|
||||
|
||||
steps:
|
||||
- name: Delete source branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "$STATUS" = "404" ]; then
|
||||
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
@@ -103,17 +103,6 @@ jobs:
|
||||
|
||||
$SSH_CMD "echo 'SSH connected'"
|
||||
|
||||
# Pre-deploy cleanup: free disk and memory for the build
|
||||
$SSH_CMD "
|
||||
echo 'Cleaning Docker build cache and unused images...'
|
||||
docker builder prune -af 2>/dev/null || true
|
||||
docker image prune -af 2>/dev/null || true
|
||||
echo 'Clearing swap...'
|
||||
sudo swapoff -a && sudo swapon -a 2>/dev/null || true
|
||||
echo 'Cleanup complete'
|
||||
free -m | head -3
|
||||
"
|
||||
|
||||
# Pull latest source
|
||||
$SSH_CMD "
|
||||
set -e
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 05.14.00
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.CI
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: PR gate — branch policy + code validation before merge
|
||||
|
||||
name: "Universal: PR Check"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||
branch-policy:
|
||||
name: Branch Policy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check branch merge target
|
||||
run: |
|
||||
HEAD="${{ github.head_ref }}"
|
||||
BASE="${{ github.base_ref }}"
|
||||
|
||||
echo "PR: ${HEAD} → ${BASE}"
|
||||
|
||||
ALLOWED=true
|
||||
REASON=""
|
||||
|
||||
case "$HEAD" in
|
||||
feature/*|feat/*)
|
||||
if [ "$BASE" != "dev" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
fix/*|bugfix/*)
|
||||
if [ "$BASE" != "dev" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
patch/*)
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
hotfix/*)
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
rc)
|
||||
if [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
dev)
|
||||
if [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ALLOWED" = false ]; then
|
||||
echo "::error::${REASON}"
|
||||
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Code Validation ────────────────────────────────────────────────────
|
||||
validate:
|
||||
name: Validate PR
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: PHP syntax check
|
||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||
run: |
|
||||
ERRORS=0
|
||||
while IFS= read -r -d '' file; do
|
||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||
echo "PHP lint: ${ERRORS} error(s)"
|
||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||
|
||||
- name: Validate platform manifest
|
||||
run: |
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
case "$PLATFORM" in
|
||||
joomla)
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Manifest: ${MANIFEST}"
|
||||
if command -v php &> /dev/null; then
|
||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||
fi
|
||||
for ELEMENT in name version description; do
|
||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||
done
|
||||
echo "Joomla manifest valid"
|
||||
;;
|
||||
dolibarr)
|
||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||
if [ -z "$MOD_FILE" ]; then
|
||||
echo "::error::No mod*.class.php found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Dolibarr module: ${MOD_FILE}"
|
||||
;;
|
||||
*)
|
||||
echo "Generic platform — no manifest validation"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Check update stream format
|
||||
run: |
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
case "$PLATFORM" in
|
||||
joomla)
|
||||
if [ -f "updates.xml" ]; then
|
||||
if command -v php &> /dev/null; then
|
||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||
fi
|
||||
echo "updates.xml valid"
|
||||
fi
|
||||
;;
|
||||
dolibarr)
|
||||
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Check changelog has unreleased entry
|
||||
run: |
|
||||
if [ ! -f "CHANGELOG.md" ]; then
|
||||
echo "::warning::No CHANGELOG.md found"
|
||||
exit 0
|
||||
fi
|
||||
# Check for content under [Unreleased] section
|
||||
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||
exit 1
|
||||
fi
|
||||
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||
|
||||
- name: Verify package source
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "::warning::No src/ or htdocs/ directory"
|
||||
exit 0
|
||||
fi
|
||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||
echo "Source: ${FILE_COUNT} files"
|
||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||
|
||||
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||
pre-release:
|
||||
name: Build RC Package
|
||||
runs-on: ubuntu-latest
|
||||
needs: [branch-policy, validate]
|
||||
|
||||
steps:
|
||||
- name: Trigger RC pre-release
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ github.head_ref }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,233 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Pre-release channel'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- release-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
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
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Read current version (bump already handled by push workflow)
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||
|
||||
# Strip any existing suffix from version before applying stability
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||
|
||||
# Verify version consistency across all files
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Update VERSION variable with suffix
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
|
||||
# Read back element outputs
|
||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
- name: Update updates.xml
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
|
||||
if [ ! -f "updates.xml" ]; then
|
||||
echo "No updates.xml -- skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHA_FLAG=""
|
||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||
|
||||
php ${MOKO_CLI}/updates_xml_build.php \
|
||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
${SHA_FLAG}
|
||||
|
||||
# Commit and push
|
||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git add updates.xml
|
||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||
fi
|
||||
|
||||
- name: "Sync updates.xml to all branches"
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
|
||||
for BRANCH in main dev; do
|
||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||
echo "Syncing updates.xml -> ${BRANCH}"
|
||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||
git add updates.xml
|
||||
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
||||
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||
fi
|
||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||
done
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
--token "${TOKEN}" \
|
||||
--api-base "${API_BASE}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,312 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/update-server.yml
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||
#
|
||||
# Thin wrapper around moko-platform CLI tools.
|
||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||
#
|
||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||
|
||||
name: "Update Server"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Stability tag'
|
||||
required: true
|
||||
default: 'development'
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- stable
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-xml:
|
||||
name: Update Server
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||
run: |
|
||||
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
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform 2>/dev/null || true
|
||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve stability and bump version
|
||||
id: meta
|
||||
run: |
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
|
||||
# Configure git for bot pushes
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
# Auto-bump patch version
|
||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||
|
||||
# Strip any existing suffix before applying stability
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
# Determine stability from branch or manual input
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
STABILITY="${{ inputs.stability }}"
|
||||
elif [[ "$BRANCH" == rc/* ]]; then
|
||||
STABILITY="rc"
|
||||
elif [[ "$BRANCH" == beta/* ]]; then
|
||||
STABILITY="beta"
|
||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||
STABILITY="alpha"
|
||||
else
|
||||
STABILITY="development"
|
||||
fi
|
||||
|
||||
# Version suffix per stability stream
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
*) SUFFIX=""; TAG="stable" ;;
|
||||
esac
|
||||
|
||||
# Propagate version with stability suffix to all manifest files
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Re-read version (now includes suffix from version_set_platform)
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Commit version bump if changed
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||
git push
|
||||
}
|
||||
|
||||
- name: Create release and upload package
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Create or update Gitea release
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
# Build package and upload
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
- name: Update updates.xml
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
|
||||
if [ ! -f "updates.xml" ]; then
|
||||
echo "No updates.xml — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHA_FLAG=""
|
||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||
|
||||
php ${MOKO_CLI}/updates_xml_build.php \
|
||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
${SHA_FLAG}
|
||||
|
||||
# Commit and push updates.xml
|
||||
git add updates.xml
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||
git push
|
||||
}
|
||||
|
||||
- name: Sync updates.xml to main
|
||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||
python3 -c "
|
||||
import base64, json, urllib.request, sys
|
||||
with open('updates.xml', 'rb') as f:
|
||||
content = base64.b64encode(f.read()).decode()
|
||||
payload = json.dumps({
|
||||
'content': content,
|
||||
'sha': '${FILE_SHA}',
|
||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||
'branch': 'main'
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/contents/updates.xml',
|
||||
data=payload, method='PUT',
|
||||
headers={
|
||||
'Authorization': 'token ${GITEA_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
try:
|
||||
urllib.request.urlopen(req)
|
||||
print('updates.xml synced to main')
|
||||
except Exception as e:
|
||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||
"
|
||||
fi
|
||||
|
||||
- name: SFTP deploy to dev server
|
||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
run: |
|
||||
# Permission check: admin or maintain role required
|
||||
ACTOR="${{ github.actor }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||
case "$PERMISSION" in
|
||||
admin|maintain|write) ;;
|
||||
*)
|
||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||
|
||||
PORT="${DEV_PORT:-22}"
|
||||
REMOTE="${DEV_PATH%/}"
|
||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||
if [ -n "$DEV_KEY" ]; then
|
||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
fi
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
+5376
File diff suppressed because it is too large
Load Diff
+293
-161
@@ -1,161 +1,293 @@
|
||||
# Contributing to Moko Consulting Projects
|
||||
|
||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||
|
||||
## Branching Workflow
|
||||
|
||||
```
|
||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||
```
|
||||
|
||||
### Step by step
|
||||
|
||||
1. **Create a feature branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull
|
||||
git checkout -b feature/my-change
|
||||
```
|
||||
|
||||
2. **Work and commit** on your feature branch. Push to origin.
|
||||
|
||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||
|
||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||
- This automatically renames the source branch to `rc` (release candidate)
|
||||
- An RC pre-release is built and uploaded
|
||||
|
||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||
- When the draft PR is created, the branch is renamed to `rc`
|
||||
|
||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||
|
||||
7. **Merging to main** triggers the stable release pipeline:
|
||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||
- Stability suffix stripped (clean version)
|
||||
- Gitea release created with ZIP/tar.gz packages
|
||||
- `updates.xml` updated (Joomla extensions)
|
||||
- `dev` branch recreated from `main`
|
||||
|
||||
### Branch summary
|
||||
|
||||
| Branch | Purpose | Created by |
|
||||
|--------|---------|-----------|
|
||||
| `feature/*` | New features and fixes | Developer |
|
||||
| `dev` | Integration branch | Auto-recreated after release |
|
||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||
| `main` | Stable releases | Protected, merge only |
|
||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||
|
||||
### Protected branches
|
||||
|
||||
| Branch | Direct push | Merge via |
|
||||
|--------|------------|-----------|
|
||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `feature/*` | Open | N/A (source branch) |
|
||||
|
||||
## Version Policy
|
||||
|
||||
### Format
|
||||
|
||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||
|
||||
- **XX** — Major version (breaking changes)
|
||||
- **YY** — Minor version (new features, bumped on release to main)
|
||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||
|
||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||
|
||||
### Stability suffixes
|
||||
|
||||
Each branch appends a suffix to indicate stability:
|
||||
|
||||
| Branch | Suffix | Example |
|
||||
|--------|--------|---------|
|
||||
| `main` | (none) | `02.09.00` |
|
||||
| `dev` | `-dev` | `02.09.01-dev` |
|
||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||
| `beta` | `-beta` | `02.09.01-beta` |
|
||||
| `rc` | `-rc` | `02.09.01-rc` |
|
||||
|
||||
### Auto version bump
|
||||
|
||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||
|
||||
1. Patch version incremented
|
||||
2. Stability suffix `-dev` applied
|
||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||
4. Commit created with `[skip ci]` to avoid loops
|
||||
|
||||
### Release version flow
|
||||
|
||||
Version bumps happen at specific release events:
|
||||
|
||||
| Event | Bump | Example |
|
||||
|-------|------|---------|
|
||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||
|
||||
### Release stream copies
|
||||
|
||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||
|
||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||
|
||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||
|
||||
### Version files
|
||||
|
||||
The version tools update all files containing version stamps:
|
||||
|
||||
- `.mokogitea/manifest.xml` (canonical source)
|
||||
- Joomla XML manifests (`<version>` tag)
|
||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||
- `package.json`, `pyproject.toml`
|
||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||
|
||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||
|
||||
## Code Standards
|
||||
|
||||
- **PHP**: PSR-12, tabs for indentation
|
||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Use conventional commit format:
|
||||
|
||||
```
|
||||
type(scope): short description
|
||||
|
||||
Optional body with context.
|
||||
|
||||
Authored-by: Moko Consulting
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||
|
||||
Special flags in commit messages:
|
||||
- `[skip ci]` — skip all CI workflows
|
||||
- `[skip bump]` — skip auto version bump only
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Use the repository's issue tracker with the appropriate template.
|
||||
|
||||
---
|
||||
|
||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||
# Contribution Guidelines
|
||||
|
||||
This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find.
|
||||
|
||||
| Topic | Document |
|
||||
| :---- | :------- |
|
||||
| Backend (Go modules, API v1) | [docs/guideline-backend.md](docs/guideline-backend.md) |
|
||||
| Frontend (npm, UI guidelines) | [docs/guideline-frontend.md](docs/guideline-frontend.md) |
|
||||
| Maintainers, TOC, labels, merge queue, commit format for mergers | [docs/community-governance.md](docs/community-governance.md) |
|
||||
| Release cycle, backports, tagging releases | [docs/release-management.md](docs/release-management.md) |
|
||||
|
||||
<details><summary>Table of Contents</summary>
|
||||
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Introduction](#introduction)
|
||||
- [AI Contribution Policy](#ai-contribution-policy)
|
||||
- [Issues](#issues)
|
||||
- [How to report issues](#how-to-report-issues)
|
||||
- [Types of issues](#types-of-issues)
|
||||
- [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
|
||||
- [Issue locking](#issue-locking)
|
||||
- [Building Gitea](#building-gitea)
|
||||
- [Styleguide](#styleguide)
|
||||
- [Copyright](#copyright)
|
||||
- [Testing](#testing)
|
||||
- [Translation](#translation)
|
||||
- [Code review](#code-review)
|
||||
- [Pull request format](#pull-request-format)
|
||||
- [PR title and summary](#pr-title-and-summary)
|
||||
- [Breaking PRs](#breaking-prs)
|
||||
- [What is a breaking PR?](#what-is-a-breaking-pr)
|
||||
- [How to handle breaking PRs?](#how-to-handle-breaking-prs)
|
||||
- [Maintaining open PRs](#maintaining-open-prs)
|
||||
- [Reviewing PRs](#reviewing-prs)
|
||||
- [For PR authors](#for-pr-authors)
|
||||
- [Documentation](#documentation)
|
||||
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
|
||||
|
||||
</details>
|
||||
|
||||
## Introduction
|
||||
|
||||
It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \
|
||||
Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
|
||||
|
||||
For configuring IDEs for Gitea development, see the [contributed IDE configurations](contrib/ide/).
|
||||
|
||||
## AI Contribution Policy
|
||||
|
||||
Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly and disclose that use clearly.
|
||||
|
||||
1. Review AI-generated code closely before marking a pull request ready for review.
|
||||
2. Manually test the changes and add appropriate automated tests where feasible.
|
||||
3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review.
|
||||
4. Disclose AI-assisted content clearly.
|
||||
5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model.
|
||||
6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit.
|
||||
|
||||
Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves.
|
||||
|
||||
We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process.
|
||||
|
||||
## Issues
|
||||
|
||||
### How to report issues
|
||||
|
||||
Please search the issues on the issue tracker with a variety of related keywords to ensure that your issue has not already been reported.
|
||||
|
||||
If your issue has not been reported yet, [open an issue](https://github.com/go-gitea/gitea/issues/new)
|
||||
and answer the questions so we can understand and reproduce the problematic behavior. \
|
||||
Please write clear and concise instructions so that we can reproduce the behavior — even if it seems obvious. \
|
||||
The more detailed and specific you are, the faster we can fix the issue. \
|
||||
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
|
||||
Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
|
||||
|
||||
Please be kind—remember that Gitea comes at no cost to you, and you're getting free help.
|
||||
|
||||
### Types of issues
|
||||
|
||||
Typically, issues fall in one of the following categories:
|
||||
|
||||
- `bug`: Something in the frontend or backend behaves unexpectedly
|
||||
- `security issue`: bug that has serious implications such as leaking another users data. Please do not file such issues on the public tracker and send a mail to security@gitea.io instead
|
||||
- `feature`: Completely new functionality. You should describe this feature in enough detail that anyone who reads the issue can understand how it is supposed to be implemented
|
||||
- `enhancement`: An existing feature should get an upgrade
|
||||
- `refactoring`: Parts of the code base don't conform with other parts and should be changed to improve Gitea's maintainability
|
||||
|
||||
### Discuss your design before the implementation
|
||||
|
||||
We welcome submissions. \
|
||||
If you want to change or add something, please let everyone know what you're working on — [file an issue](https://github.com/go-gitea/gitea/issues/new) or comment on an existing one before starting your work!
|
||||
|
||||
Significant changes such as new features must go through the change proposal process before they can be accepted. \
|
||||
This is mainly to save yourself the trouble of implementing it, only to find out that your proposed implementation has some potential problems. \
|
||||
Furthermore, this process gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits inside
|
||||
the goals for the project and tools.
|
||||
|
||||
Pull requests should not be the place for architecture discussions.
|
||||
|
||||
### Issue locking
|
||||
|
||||
Commenting on closed or merged issues/PRs is strongly discouraged.
|
||||
Such comments will likely be overlooked as some maintainers may not view notifications on closed issues, thinking that the item is resolved.
|
||||
As such, commenting on closed/merged issues/PRs may be disabled prior to the scheduled auto-locking if a discussion starts or if unrelated comments are posted.
|
||||
If further discussion is needed, we encourage you to open a new issue instead and we recommend linking to the issue/PR in question for context.
|
||||
|
||||
## Building Gitea
|
||||
|
||||
See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
|
||||
|
||||
## Styleguide
|
||||
|
||||
You should always run `make fmt` before committing to conform to Gitea's styleguide.
|
||||
|
||||
## Copyright
|
||||
|
||||
New code files that you contribute should use the standard copyright header:
|
||||
|
||||
```
|
||||
// Copyright <current year> The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
```
|
||||
|
||||
Afterwards, copyright should only be modified when the copyright author changes.
|
||||
|
||||
## Testing
|
||||
|
||||
Before submitting a pull request, run all tests to make sure your changes don't cause a regression elsewhere.
|
||||
|
||||
Here's how to run the test suite:
|
||||
|
||||
- code lint
|
||||
|
||||
| | |
|
||||
| :-------------------- | :--------------------------------------------------------------------------- |
|
||||
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|
||||
|``make lint-frontend`` | lint frontend files |
|
||||
|``make lint-backend`` | lint backend files |
|
||||
|
||||
- run tests (we suggest running them on Linux)
|
||||
|
||||
| Command | Action | |
|
||||
|:----------------------------------------------|:-----------------------------------------------------| ------------------------------------------- |
|
||||
| ``make test-backend[\#SpecificTestName]`` | run unit test(s) | |
|
||||
| ``make test-integration[\#SpecificTestName]`` | run [integration](tests/integration) test(s) | [More details](tests/integration/README.md) |
|
||||
| ``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | |
|
||||
|
||||
- E2E test environment variables
|
||||
|
||||
| Variable | Description |
|
||||
| :-------------------------------- | :---------------------------------------------------------- |
|
||||
| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output |
|
||||
| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` |
|
||||
| ``GITEA_TEST_E2E_TIMEOUT_FACTOR`` | Timeout multiplier (default: 4 on CI, 1 locally) |
|
||||
|
||||
## Translation
|
||||
|
||||
All translation work happens on [Crowdin](https://translate.gitea.com).
|
||||
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.json).
|
||||
It is synced regularly with Crowdin. \
|
||||
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
|
||||
Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version.
|
||||
|
||||
The tool `go run build/backport-locale.go` can be used to backport locales from the main branch to release branches that were missed.
|
||||
|
||||
## Code review
|
||||
|
||||
How labels, milestones, and the merge queue work is documented in [docs/community-governance.md](docs/community-governance.md).
|
||||
|
||||
### Pull request format
|
||||
|
||||
Please try to make your pull request easy to review for us. \
|
||||
For that, please read the [*Best Practices for Faster Reviews*](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) guide. \
|
||||
It has lots of useful tips for any project you may want to contribute to. \
|
||||
Some of the key points:
|
||||
|
||||
- Make small pull requests. \
|
||||
The smaller, the faster to review and the more likely it will be merged soon.
|
||||
- Don't make changes unrelated to your PR. \
|
||||
Maybe there are typos on some comments, maybe refactoring would be welcome on a function... \
|
||||
but if that is not related to your PR, please make *another* PR for that.
|
||||
- Split big pull requests into multiple small ones. \
|
||||
An incremental change will be faster to review than a huge PR.
|
||||
- Allow edits by maintainers. This way, the maintainers will take care of merging the PR later on instead of you.
|
||||
|
||||
### PR title and summary
|
||||
|
||||
In the PR title, describe the problem you are fixing, not how you are fixing it. \
|
||||
Use the first comment as a summary of your PR. \
|
||||
In the PR summary, you can describe exactly how you are fixing this problem.
|
||||
|
||||
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message:
|
||||
|
||||
```text
|
||||
type(scope)!: subject
|
||||
```
|
||||
|
||||
The allowed types are `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, and `test`. The generic `chore` type is intentionally not accepted; pick a more descriptive type instead.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
fix(web): prevent avatar upload crash on empty file
|
||||
feat(api): add pagination to repo hooks list
|
||||
ci(workflows): lint PR titles with commitlint
|
||||
```
|
||||
|
||||
Keep this summary up-to-date as the PR evolves. \
|
||||
If your PR changes the UI, you must add **after** screenshots in the PR summary. \
|
||||
If you are not implementing a new feature, you should also post **before** screenshots for comparison.
|
||||
|
||||
If you are implementing a new feature, your PR will only be merged if your screenshots are up to date.\
|
||||
Furthermore, feature PRs will only be merged if their summary contains a clear usage description (understandable for users) and testing description (understandable for reviewers).
|
||||
You should strive to combine both into a single description.
|
||||
|
||||
Another requirement for merging PRs is that the PR is labeled correctly.\
|
||||
However, this is not your job as a contributor, but the job of the person merging your PR.\
|
||||
If you think that your PR was labeled incorrectly, or notice that it was merged without labels, please let us know.
|
||||
|
||||
If your PR closes some issues, you must note that in a way that both GitHub and Gitea understand, i.e. by appending a paragraph like
|
||||
|
||||
```text
|
||||
Fixes/Closes/Resolves #<ISSUE_NR_X>.
|
||||
Fixes/Closes/Resolves #<ISSUE_NR_Y>.
|
||||
```
|
||||
|
||||
to your summary. \
|
||||
Each issue that will be closed must stand on a separate line.
|
||||
|
||||
### Breaking PRs
|
||||
|
||||
#### What is a breaking PR?
|
||||
|
||||
A PR is breaking if it meets one of the following criteria:
|
||||
|
||||
- It changes API output in an incompatible way for existing users
|
||||
- It removes a setting that an admin could previously set (i.e. via `app.ini`)
|
||||
- An admin must do something manually to restore the old behavior
|
||||
|
||||
In particular, this means that adding new settings is not breaking.\
|
||||
Changing the default value of a setting or replacing the setting with another one is breaking, however.
|
||||
|
||||
#### How to handle breaking PRs?
|
||||
|
||||
If your PR has a breaking change, you must add two things to the summary of your PR:
|
||||
|
||||
1. A reasoning why this breaking change is necessary
|
||||
2. A `BREAKING` section explaining in simple terms (understandable for a typical user) how this PR affects users and how to mitigate these changes. This section can look for example like
|
||||
|
||||
```md
|
||||
## :warning: BREAKING :warning:
|
||||
```
|
||||
|
||||
Breaking PRs will not be merged as long as not both of these requirements are met.
|
||||
|
||||
### Maintaining open PRs
|
||||
|
||||
Code review starts when you open a non-draft PR or move a draft out of draft state. After that, do not rebase or squash your branch; it makes new changes harder to review.
|
||||
|
||||
Merge the base branch into yours only when you need to, for example because of conflicting changes elsewhere. That limits unnecessary CI runs.
|
||||
|
||||
Every PR is squash-merged, so merge commits on your branch do not matter for final history. The squash produces a single commit; mergers follow the [commit message format](docs/community-governance.md#commit-messages) in the governance guide.
|
||||
|
||||
### Reviewing PRs
|
||||
|
||||
Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest.
|
||||
|
||||
#### For PR authors
|
||||
|
||||
- **Response**: When answering reviewer questions, use real-world cases or examples and avoid speculation.
|
||||
- **Discussion**: A discussion is always welcome and should be used to clarify the changes and the intent of the PR.
|
||||
- **Help**: If you need help with the PR or comments are unclear, ask for clarification.
|
||||
|
||||
Guidance for reviewers, the merge queue, and the squash commit message format is in [docs/community-governance.md](docs/community-governance.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs).
|
||||
**The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.**
|
||||
|
||||
## Developer Certificate of Origin (DCO)
|
||||
|
||||
We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \
|
||||
No further action is required. \
|
||||
You can also decide to sign off your commits by adding the following line at the end of your commit messages:
|
||||
|
||||
```
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
```
|
||||
|
||||
If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`.
|
||||
|
||||
We assume in good faith that the information you provide is legally binding.
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(LicenseKey))
|
||||
}
|
||||
|
||||
// LicenseKey represents an individual key issued from a LicensePackage.
|
||||
type LicenseKey struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
PackageID int64 `xorm:"INDEX NOT NULL"` // FK to license_package
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user that issued it
|
||||
KeyHash string `xorm:"UNIQUE NOT NULL"` // SHA-256 of the raw key
|
||||
KeyPrefix string `xorm:"NOT NULL"` // first 8 chars for display
|
||||
LicenseeName string `xorm:""` // customer name
|
||||
LicenseeEmail string `xorm:""` // customer email
|
||||
DomainRestriction string `xorm:"TEXT"` // comma-separated allowed domains
|
||||
MaxSites int `xorm:"NOT NULL DEFAULT 0"` // 0 = use package default
|
||||
IsInternal bool `xorm:"NOT NULL DEFAULT false"` // true = base org/repo key
|
||||
IsActive bool `xorm:"NOT NULL DEFAULT true"`
|
||||
StartsUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"` // custom start, 0 = creation
|
||||
ExpiresUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"` // 0 = never
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (LicenseKey) TableName() string {
|
||||
return "license_key"
|
||||
}
|
||||
|
||||
// GenerateKeyString creates a random license key in MOKO-XXXX-XXXX-XXXX-XXXX format.
|
||||
func GenerateKeyString() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hex := strings.ToUpper(hex.EncodeToString(b))
|
||||
return fmt.Sprintf("MOKO-%s-%s-%s-%s", hex[0:4], hex[4:8], hex[8:12], hex[12:16]), nil
|
||||
}
|
||||
|
||||
// HashKey returns the SHA-256 hash of a raw key string.
|
||||
func HashKey(rawKey string) string {
|
||||
h := sha256.Sum256([]byte(rawKey))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// CreateLicenseKey generates a new key, hashes it, stores it, and returns the raw key.
|
||||
// The raw key is only available at creation time.
|
||||
func CreateLicenseKey(ctx context.Context, key *LicenseKey) (rawKey string, err error) {
|
||||
rawKey, err = GenerateKeyString()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GenerateKeyString: %w", err)
|
||||
}
|
||||
|
||||
key.KeyHash = HashKey(rawKey)
|
||||
key.KeyPrefix = rawKey[:12] + "..."
|
||||
|
||||
if _, err := db.GetEngine(ctx).Insert(key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return rawKey, nil
|
||||
}
|
||||
|
||||
// GetLicenseKeyByHash looks up a key by its SHA-256 hash.
|
||||
func GetLicenseKeyByHash(ctx context.Context, hash string) (*LicenseKey, error) {
|
||||
key := new(LicenseKey)
|
||||
has, err := db.GetEngine(ctx).Where("key_hash = ?", hash).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, db.ErrNotExist{Resource: "LicenseKey"}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// GetLicenseKeyByID returns a key by its ID.
|
||||
func GetLicenseKeyByID(ctx context.Context, id int64) (*LicenseKey, error) {
|
||||
key := new(LicenseKey)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, db.ErrNotExist{Resource: "LicenseKey", ID: id}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// ListLicenseKeys returns all keys for the given owner.
|
||||
func ListLicenseKeys(ctx context.Context, ownerID int64) ([]*LicenseKey, error) {
|
||||
keys := make([]*LicenseKey, 0, 20)
|
||||
return keys, db.GetEngine(ctx).Where("owner_id = ?", ownerID).Find(&keys)
|
||||
}
|
||||
|
||||
// ListLicenseKeysByPackage returns all keys for a specific package.
|
||||
func ListLicenseKeysByPackage(ctx context.Context, packageID int64) ([]*LicenseKey, error) {
|
||||
keys := make([]*LicenseKey, 0, 20)
|
||||
return keys, db.GetEngine(ctx).Where("package_id = ?", packageID).Find(&keys)
|
||||
}
|
||||
|
||||
// CountKeysByPackage returns the number of keys for a package.
|
||||
func CountKeysByPackage(ctx context.Context, packageID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("package_id = ?", packageID).Count(new(LicenseKey))
|
||||
}
|
||||
|
||||
// UpdateLicenseKey updates a license key.
|
||||
func UpdateLicenseKey(ctx context.Context, key *LicenseKey) error {
|
||||
_, err := db.GetEngine(ctx).ID(key.ID).AllCols().Update(key)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteLicenseKey deletes a license key by ID.
|
||||
func DeleteLicenseKey(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Delete(new(LicenseKey))
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateLicenseKey validates a raw key string against the database.
|
||||
// Returns the key record and its associated package, or an error.
|
||||
func ValidateLicenseKey(ctx context.Context, rawKey string) (*LicenseKey, *LicensePackage, error) {
|
||||
hash := HashKey(rawKey)
|
||||
key, err := GetLicenseKeyByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid license key")
|
||||
}
|
||||
|
||||
if !key.IsActive {
|
||||
return nil, nil, fmt.Errorf("license key is deactivated")
|
||||
}
|
||||
|
||||
now := timeutil.TimeStampNow()
|
||||
if key.StartsUnix > 0 && now < key.StartsUnix {
|
||||
return nil, nil, fmt.Errorf("license key not yet active")
|
||||
}
|
||||
if key.ExpiresUnix > 0 && now > key.ExpiresUnix {
|
||||
return nil, nil, fmt.Errorf("license key has expired")
|
||||
}
|
||||
|
||||
pkg, err := GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("license package not found")
|
||||
}
|
||||
|
||||
if !pkg.IsActive {
|
||||
return nil, nil, fmt.Errorf("license package is deactivated")
|
||||
}
|
||||
|
||||
return key, pkg, nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(LicenseKeyUsage))
|
||||
}
|
||||
|
||||
// LicenseKeyUsage tracks update check activity for a license key.
|
||||
type LicenseKeyUsage struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
KeyID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Domain string `xorm:""` // requesting domain from extra_query
|
||||
IPAddress string `xorm:""`
|
||||
UserAgent string `xorm:"TEXT"`
|
||||
VersionFrom string `xorm:""` // version the client is updating from
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
func (LicenseKeyUsage) TableName() string {
|
||||
return "license_key_usage"
|
||||
}
|
||||
|
||||
// RecordUsage inserts a usage tracking entry.
|
||||
func RecordUsage(ctx context.Context, usage *LicenseKeyUsage) error {
|
||||
_, err := db.GetEngine(ctx).Insert(usage)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRecentUsage returns the most recent usage entries for a key.
|
||||
func GetRecentUsage(ctx context.Context, keyID int64, limit int) ([]*LicenseKeyUsage, error) {
|
||||
usages := make([]*LicenseKeyUsage, 0, limit)
|
||||
return usages, db.GetEngine(ctx).Where("key_id = ?", keyID).
|
||||
OrderBy("created_unix DESC").Limit(limit).Find(&usages)
|
||||
}
|
||||
|
||||
// CountUsageByKey returns the total number of update checks for a key.
|
||||
func CountUsageByKey(ctx context.Context, keyID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("key_id = ?", keyID).Count(new(LicenseKeyUsage))
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(LicensePackage))
|
||||
}
|
||||
|
||||
// LicensePackage defines a purchasable subscription tier that determines
|
||||
// what update streams a group of license keys can access.
|
||||
type LicensePackage struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user that owns this package
|
||||
Name string `xorm:"NOT NULL"` // e.g. "Pro Annual", "Lifetime"
|
||||
Description string `xorm:"TEXT"`
|
||||
DurationDays int `xorm:"NOT NULL DEFAULT 0"` // 0 = unlimited/lifetime
|
||||
MaxSites int `xorm:"NOT NULL DEFAULT 0"` // 0 = unlimited
|
||||
RepoScope string `xorm:"TEXT NOT NULL DEFAULT 'all'"` // "all" = org-wide, or JSON array of repo IDs
|
||||
// AllowedChannels defines which update streams keys from this package
|
||||
// can access. JSON array, e.g. ["stable","rc"]. Empty = all channels.
|
||||
AllowedChannels string `xorm:"TEXT"`
|
||||
IsActive bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (LicensePackage) TableName() string {
|
||||
return "license_package"
|
||||
}
|
||||
|
||||
// CreateLicensePackage creates a new license package.
|
||||
func CreateLicensePackage(ctx context.Context, pkg *LicensePackage) error {
|
||||
_, err := db.GetEngine(ctx).Insert(pkg)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLicensePackageByID returns a license package by ID.
|
||||
func GetLicensePackageByID(ctx context.Context, id int64) (*LicensePackage, error) {
|
||||
pkg := new(LicensePackage)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, db.ErrNotExist{Resource: "LicensePackage", ID: id}
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// FindLicensePackageOptions for db.Find/db.Count.
|
||||
type FindLicensePackageOptions struct {
|
||||
db.ListOptions
|
||||
OwnerID int64
|
||||
}
|
||||
|
||||
func (opts FindLicensePackageOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// ListLicensePackages returns all packages for the given owner.
|
||||
func ListLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
|
||||
pkgs := make([]*LicensePackage, 0, 10)
|
||||
return pkgs, db.GetEngine(ctx).Where("owner_id = ?", ownerID).Find(&pkgs)
|
||||
}
|
||||
|
||||
// UpdateLicensePackage updates a license package.
|
||||
func UpdateLicensePackage(ctx context.Context, pkg *LicensePackage) error {
|
||||
_, err := db.GetEngine(ctx).ID(pkg.ID).AllCols().Update(pkg)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteLicensePackage deletes a license package by ID.
|
||||
func DeleteLicensePackage(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Delete(new(LicensePackage))
|
||||
return err
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
)
|
||||
|
||||
const (
|
||||
MasterPackageName = "Master (Internal)"
|
||||
MasterPackageDesc = "Auto-created master package with unlimited access to all channels."
|
||||
)
|
||||
|
||||
// EnsureMasterKey ensures that a master license package and key exist for the given owner.
|
||||
// Returns the master key's raw key string only if it was just created (empty string otherwise).
|
||||
func EnsureMasterKey(ctx context.Context, ownerID int64) (rawKey string, err error) {
|
||||
// Check if a master package already exists.
|
||||
pkgs, err := ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var masterPkg *LicensePackage
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Name == MasterPackageName {
|
||||
masterPkg = pkg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create master package if it doesn't exist.
|
||||
if masterPkg == nil {
|
||||
masterPkg = &LicensePackage{
|
||||
OwnerID: ownerID,
|
||||
Name: MasterPackageName,
|
||||
Description: MasterPackageDesc,
|
||||
DurationDays: 0, // lifetime
|
||||
MaxSites: 0, // unlimited
|
||||
RepoScope: "all",
|
||||
IsActive: true,
|
||||
}
|
||||
if err := CreateLicensePackage(ctx, masterPkg); err != nil {
|
||||
return "", fmt.Errorf("CreateLicensePackage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a master key already exists for this package.
|
||||
keys, err := ListLicenseKeysByPackage(ctx, masterPkg.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if key.IsInternal {
|
||||
return "", nil // already exists, don't return raw key
|
||||
}
|
||||
}
|
||||
|
||||
// Create the master key.
|
||||
masterKey := &LicenseKey{
|
||||
PackageID: masterPkg.ID,
|
||||
OwnerID: ownerID,
|
||||
IsInternal: true,
|
||||
IsActive: true,
|
||||
}
|
||||
rawKey, err = CreateLicenseKey(ctx, masterKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("CreateLicenseKey: %w", err)
|
||||
}
|
||||
|
||||
return rawKey, nil
|
||||
}
|
||||
|
||||
// GetMasterKey returns the master key for an owner, if it exists.
|
||||
func GetMasterKey(ctx context.Context, ownerID int64) (*LicenseKey, error) {
|
||||
key := new(LicenseKey)
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ? AND is_internal = ?", ownerID, true).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(UpdateStreamConfig))
|
||||
}
|
||||
|
||||
// UpdateStreamConfig stores update stream settings at org or repo level.
|
||||
// When OwnerID is set and RepoID is 0, it's an org-level default.
|
||||
// When RepoID is set, it's a per-repo override.
|
||||
type UpdateStreamConfig struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user
|
||||
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` // 0 = org-level default
|
||||
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, custom
|
||||
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both
|
||||
RequireKey bool `xorm:"NOT NULL DEFAULT false"` // require license key for update feed
|
||||
// CustomStreams is a JSON array of stream definitions.
|
||||
// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
|
||||
CustomStreams string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (UpdateStreamConfig) TableName() string {
|
||||
return "update_stream_config"
|
||||
}
|
||||
|
||||
// StreamDef defines a single update stream/channel.
|
||||
type StreamDef struct {
|
||||
Name string `json:"name"` // e.g. "stable", "lts", "nightly"
|
||||
Suffix string `json:"suffix"` // tag suffix to match, e.g. "-lts", "-rc"
|
||||
Description string `json:"description"` // human-readable label
|
||||
}
|
||||
|
||||
// DefaultJoomlaStreams returns the standard Joomla update streams.
|
||||
func DefaultJoomlaStreams() []StreamDef {
|
||||
return []StreamDef{
|
||||
{Name: "stable", Suffix: "", Description: "Stable releases"},
|
||||
{Name: "release-candidate", Suffix: "-rc", Description: "Release candidates"},
|
||||
{Name: "beta", Suffix: "-beta", Description: "Beta testing"},
|
||||
{Name: "alpha", Suffix: "-alpha", Description: "Alpha / early access"},
|
||||
{Name: "development", Suffix: "-dev", Description: "Development builds"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomStreams parses the CustomStreams JSON field.
|
||||
func (c *UpdateStreamConfig) GetCustomStreams() []StreamDef {
|
||||
if c.CustomStreams == "" {
|
||||
return nil
|
||||
}
|
||||
var streams []StreamDef
|
||||
if err := json.Unmarshal([]byte(c.CustomStreams), &streams); err != nil {
|
||||
return nil
|
||||
}
|
||||
return streams
|
||||
}
|
||||
|
||||
// GetActiveStreams returns the effective streams for this config.
|
||||
func (c *UpdateStreamConfig) GetActiveStreams() []StreamDef {
|
||||
if c.StreamMode == "custom" {
|
||||
if custom := c.GetCustomStreams(); len(custom) > 0 {
|
||||
return custom
|
||||
}
|
||||
}
|
||||
return DefaultJoomlaStreams()
|
||||
}
|
||||
|
||||
// GetOrgConfig returns the org-level update stream config.
|
||||
func GetOrgConfig(ctx context.Context, ownerID int64) (*UpdateStreamConfig, error) {
|
||||
cfg := new(UpdateStreamConfig)
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ? AND repo_id = 0", ownerID).Get(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return &UpdateStreamConfig{OwnerID: ownerID, StreamMode: "joomla"}, nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetRepoConfig returns the repo-level override, or nil if none exists.
|
||||
func GetRepoConfig(ctx context.Context, repoID int64) (*UpdateStreamConfig, error) {
|
||||
cfg := new(UpdateStreamConfig)
|
||||
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetEffectiveStreams resolves the streams for a repo: repo override → org default → Joomla default.
|
||||
func GetEffectiveStreams(ctx context.Context, ownerID, repoID int64) []StreamDef {
|
||||
// Check repo-level override first.
|
||||
repoCfg, err := GetRepoConfig(ctx, repoID)
|
||||
if err == nil && repoCfg != nil {
|
||||
return repoCfg.GetActiveStreams()
|
||||
}
|
||||
|
||||
// Fall back to org-level config.
|
||||
orgCfg, err := GetOrgConfig(ctx, ownerID)
|
||||
if err == nil && orgCfg != nil {
|
||||
return orgCfg.GetActiveStreams()
|
||||
}
|
||||
|
||||
return DefaultJoomlaStreams()
|
||||
}
|
||||
|
||||
// SaveConfig creates or updates an update stream config.
|
||||
func SaveConfig(ctx context.Context, cfg *UpdateStreamConfig) error {
|
||||
existing := new(UpdateStreamConfig)
|
||||
var has bool
|
||||
var err error
|
||||
if cfg.RepoID > 0 {
|
||||
has, err = db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
|
||||
} else {
|
||||
has, err = db.GetEngine(ctx).Where("owner_id = ? AND repo_id = 0", cfg.OwnerID).Get(existing)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
cfg.ID = existing.ID
|
||||
_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
|
||||
} else {
|
||||
_, err = db.GetEngine(ctx).Insert(cfg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MatchStreamFromTag determines which stream a tag belongs to based on the given stream definitions.
|
||||
func MatchStreamFromTag(tagName string, isPrerelease bool, streams []StreamDef) string {
|
||||
lower := strings.ToLower(tagName)
|
||||
|
||||
// Check custom suffixes (longest match first to avoid "-rc" matching before "-rc-special").
|
||||
var bestMatch string
|
||||
bestLen := 0
|
||||
for _, s := range streams {
|
||||
if s.Suffix == "" {
|
||||
continue // stable/default stream handled below
|
||||
}
|
||||
if strings.Contains(lower, s.Suffix) && len(s.Suffix) > bestLen {
|
||||
bestMatch = s.Name
|
||||
bestLen = len(s.Suffix)
|
||||
}
|
||||
}
|
||||
if bestMatch != "" {
|
||||
return bestMatch
|
||||
}
|
||||
|
||||
// If prerelease and no suffix matched, use the first prerelease stream.
|
||||
if isPrerelease {
|
||||
for _, s := range streams {
|
||||
if s.Suffix != "" {
|
||||
return s.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: first stream with empty suffix (stable).
|
||||
for _, s := range streams {
|
||||
if s.Suffix == "" {
|
||||
return s.Name
|
||||
}
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
@@ -412,10 +412,6 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(332, "Add org-level branch protection rulesets", v1_27.AddOrgProtectedBranchTable),
|
||||
newMigration(333, "Add require_2fa to user table for org enforcement", v1_27.AddRequire2FAToUser),
|
||||
newMigration(334, "Add actions user whitelist to protected branches", v1_27.AddActionsUserWhitelistToProtectedBranch),
|
||||
newMigration(335, "Add license key tables for update server", v1_27.AddLicenseKeyTables),
|
||||
newMigration(336, "Add update stream config table", v1_27.AddUpdateStreamConfigTable),
|
||||
newMigration(337, "Add key_plain column to license_key", v1_27.AddKeyPlainToLicenseKey),
|
||||
newMigration(338, "Add platform and require_key to update_stream_config", v1_27.AddPlatformAndRequireKeyToStreamConfig),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type licensePackage335 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Name string `xorm:"NOT NULL"`
|
||||
Description string `xorm:"TEXT"`
|
||||
DurationDays int `xorm:"NOT NULL DEFAULT 0"`
|
||||
MaxSites int `xorm:"NOT NULL DEFAULT 0"`
|
||||
RepoScope string `xorm:"TEXT NOT NULL DEFAULT 'all'"`
|
||||
AllowedChannels string `xorm:"TEXT"`
|
||||
IsActive bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (licensePackage335) TableName() string {
|
||||
return "license_package"
|
||||
}
|
||||
|
||||
type licenseKey335 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
PackageID int64 `xorm:"INDEX NOT NULL"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
KeyHash string `xorm:"UNIQUE NOT NULL"`
|
||||
KeyPrefix string `xorm:"NOT NULL"`
|
||||
LicenseeName string `xorm:""`
|
||||
LicenseeEmail string `xorm:""`
|
||||
DomainRestriction string `xorm:"TEXT"`
|
||||
MaxSites int `xorm:"NOT NULL DEFAULT 0"`
|
||||
IsInternal bool `xorm:"NOT NULL DEFAULT false"`
|
||||
IsActive bool `xorm:"NOT NULL DEFAULT true"`
|
||||
StartsUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"`
|
||||
ExpiresUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (licenseKey335) TableName() string {
|
||||
return "license_key"
|
||||
}
|
||||
|
||||
type licenseKeyUsage335 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
KeyID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Domain string `xorm:""`
|
||||
IPAddress string `xorm:""`
|
||||
UserAgent string `xorm:"TEXT"`
|
||||
VersionFrom string `xorm:""`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
func (licenseKeyUsage335) TableName() string {
|
||||
return "license_key_usage"
|
||||
}
|
||||
|
||||
// AddLicenseKeyTables creates the license_package, license_key, and
|
||||
// license_key_usage tables for the update server license system.
|
||||
func AddLicenseKeyTables(x *xorm.Engine) error {
|
||||
return x.Sync(
|
||||
new(licensePackage335),
|
||||
new(licenseKey335),
|
||||
new(licenseKeyUsage335),
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type updateStreamConfig336 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
|
||||
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"`
|
||||
CustomStreams string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (updateStreamConfig336) TableName() string {
|
||||
return "update_stream_config"
|
||||
}
|
||||
|
||||
// AddUpdateStreamConfigTable creates the update_stream_config table.
|
||||
func AddUpdateStreamConfigTable(x *xorm.Engine) error {
|
||||
return x.Sync(new(updateStreamConfig336))
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
type licenseKey337 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
KeyPlain string `xorm:""`
|
||||
}
|
||||
|
||||
func (licenseKey337) TableName() string {
|
||||
return "license_key"
|
||||
}
|
||||
|
||||
// AddKeyPlainToLicenseKey adds the key_plain column to license_key table.
|
||||
func AddKeyPlainToLicenseKey(x *xorm.Engine) error {
|
||||
return x.Sync(new(licenseKey337))
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
type updateStreamConfig338 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"`
|
||||
RequireKey bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
func (updateStreamConfig338) TableName() string {
|
||||
return "update_stream_config"
|
||||
}
|
||||
|
||||
// AddPlatformAndRequireKeyToStreamConfig adds platform and require_key
|
||||
// columns to update_stream_config.
|
||||
func AddPlatformAndRequireKeyToStreamConfig(x *xorm.Engine) error {
|
||||
return x.Sync(new(updateStreamConfig338))
|
||||
}
|
||||
@@ -405,11 +405,8 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
|
||||
perm.units = repo.Units
|
||||
|
||||
// anonymous user visit private repo.
|
||||
// Still process unit-level anonymous access so that units with
|
||||
// AnonymousAccessMode (e.g. public wiki on a private repo) are visible.
|
||||
if user == nil && repo.IsPrivate {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -673,14 +673,6 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
||||
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
||||
}
|
||||
|
||||
// Include private repos that have at least one unit with public anonymous access.
|
||||
// This enables discovery of repos where e.g. wiki or releases are public.
|
||||
cond = cond.Or(builder.In("`repository`.id",
|
||||
builder.Select("repo_id").From("repo_unit").Where(
|
||||
builder.Gt{"anonymous_access_mode": 0},
|
||||
),
|
||||
))
|
||||
|
||||
if user != nil {
|
||||
// 2. Be able to see all repositories that we have unit independent access to
|
||||
// 3. Be able to see all repositories through team membership(s)
|
||||
|
||||
@@ -81,7 +81,6 @@ func initDefaultConfig() {
|
||||
Instance: &InstanceStruct{
|
||||
WebBanner: config.NewOption[WebBannerType]("instance.web_banner"),
|
||||
MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"),
|
||||
LandingPage: config.NewOption[LandingPageType]("instance.landing_page"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,35 +52,7 @@ func (m MaintenanceModeType) IsActive() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LandingPageType configures the default page for unauthenticated visitors.
|
||||
// Mode values: "home", "explore", "organizations", "login", or "custom".
|
||||
// When Mode is "custom", CustomPath holds the redirect target (e.g. "/MokoConsulting").
|
||||
type LandingPageType struct {
|
||||
Mode string // home, explore, organizations, login, custom
|
||||
CustomPath string // only used when Mode == "custom"
|
||||
}
|
||||
|
||||
// URL returns the redirect path for the configured landing page.
|
||||
func (lp LandingPageType) URL() string {
|
||||
switch lp.Mode {
|
||||
case "explore":
|
||||
return "/explore"
|
||||
case "organizations":
|
||||
return "/explore/organizations"
|
||||
case "login":
|
||||
return "/user/login"
|
||||
case "custom":
|
||||
if lp.CustomPath != "" {
|
||||
return lp.CustomPath
|
||||
}
|
||||
return "/"
|
||||
default:
|
||||
return "/"
|
||||
}
|
||||
}
|
||||
|
||||
type InstanceStruct struct {
|
||||
WebBanner *config.Option[WebBannerType]
|
||||
MaintenanceMode *config.Option[MaintenanceModeType]
|
||||
LandingPage *config.Option[LandingPageType]
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// LicensePackage represents a license package (subscription tier).
|
||||
type LicensePackage struct {
|
||||
ID int64 `json:"id"`
|
||||
OwnerID int64 `json:"owner_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
MaxSites int `json:"max_sites"`
|
||||
RepoScope string `json:"repo_scope"`
|
||||
AllowedChannels string `json:"allowed_channels"`
|
||||
IsActive bool `json:"is_active"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateLicensePackageOption options for creating a license package.
|
||||
type CreateLicensePackageOption struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Description string `json:"description"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
MaxSites int `json:"max_sites"`
|
||||
RepoScope string `json:"repo_scope"`
|
||||
AllowedChannels string `json:"allowed_channels"`
|
||||
}
|
||||
|
||||
// EditLicensePackageOption options for editing a license package.
|
||||
type EditLicensePackageOption struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
DurationDays *int `json:"duration_days"`
|
||||
MaxSites *int `json:"max_sites"`
|
||||
RepoScope *string `json:"repo_scope"`
|
||||
AllowedChannels *string `json:"allowed_channels"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// LicenseKey represents a license key (response — never includes raw key except on creation).
|
||||
type LicenseKey struct {
|
||||
ID int64 `json:"id"`
|
||||
PackageID int64 `json:"package_id"`
|
||||
OwnerID int64 `json:"owner_id"`
|
||||
KeyPrefix string `json:"key_prefix"`
|
||||
LicenseeName string `json:"licensee_name"`
|
||||
LicenseeEmail string `json:"licensee_email"`
|
||||
DomainRestriction string `json:"domain_restriction"`
|
||||
MaxSites int `json:"max_sites"`
|
||||
IsInternal bool `json:"is_internal"`
|
||||
IsActive bool `json:"is_active"`
|
||||
// swagger:strfmt date-time
|
||||
StartsAt *time.Time `json:"starts_at"`
|
||||
// swagger:strfmt date-time
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// LicenseKeyCreated is the response when a key is first created (includes raw key).
|
||||
type LicenseKeyCreated struct {
|
||||
LicenseKey
|
||||
// RawKey is the full license key string. Only returned on creation.
|
||||
RawKey string `json:"raw_key"`
|
||||
}
|
||||
|
||||
// CreateLicenseKeyOption options for creating a license key.
|
||||
type CreateLicenseKeyOption struct {
|
||||
PackageID int64 `json:"package_id" binding:"Required"`
|
||||
LicenseeName string `json:"licensee_name"`
|
||||
LicenseeEmail string `json:"licensee_email"`
|
||||
DomainRestriction string `json:"domain_restriction"`
|
||||
MaxSites int `json:"max_sites"`
|
||||
// StartsAt is optional; defaults to now.
|
||||
StartsAt *time.Time `json:"starts_at"`
|
||||
// ExpiresAt is optional; auto-calculated from package duration if not set.
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// EditLicenseKeyOption options for editing a license key.
|
||||
type EditLicenseKeyOption struct {
|
||||
LicenseeName *string `json:"licensee_name"`
|
||||
LicenseeEmail *string `json:"licensee_email"`
|
||||
DomainRestriction *string `json:"domain_restriction"`
|
||||
MaxSites *int `json:"max_sites"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// LicenseKeyUsage represents a usage tracking entry.
|
||||
type LicenseKeyUsage struct {
|
||||
ID int64 `json:"id"`
|
||||
KeyID int64 `json:"key_id"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Domain string `json:"domain"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
VersionFrom string `json:"version_from"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -2144,13 +2144,6 @@
|
||||
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
||||
"repo.settings.pulls.default_allow_edits_from_maintainers": "Allow edits from maintainers by default",
|
||||
"repo.settings.releases_desc": "Enable Repository Releases",
|
||||
"repo.settings.unit_visibility": "Visibility",
|
||||
"repo.settings.unit_visibility_private": "Private (follow repo visibility)",
|
||||
"repo.settings.unit_visibility_public": "Public (anyone can read)",
|
||||
"repo.settings.unit_visibility_releases_help": "Controls whether the releases page is visible to anonymous visitors.",
|
||||
"repo.settings.update_platform": "Update Server Platform",
|
||||
"repo.settings.update_platform_both": "Both (Joomla + Dolibarr)",
|
||||
"repo.settings.require_update_key": "Require license key for update feed access",
|
||||
"repo.settings.packages_desc": "Enable Repository Packages Registry",
|
||||
"repo.settings.projects_desc": "Enable Projects",
|
||||
"repo.settings.projects_mode_desc": "Projects Mode (which kinds of projects to show)",
|
||||
@@ -2615,45 +2608,6 @@
|
||||
"repo.release.detail": "Release details",
|
||||
"repo.release.tags": "Tags",
|
||||
"repo.release.new_release": "New Release",
|
||||
"repo.release.update_feed": "Update Feed",
|
||||
"repo.licenses": "Licenses",
|
||||
"repo.licenses.packages": "License Packages",
|
||||
"repo.licenses.package_name": "Package",
|
||||
"repo.licenses.duration": "Duration",
|
||||
"repo.licenses.channels": "Channels",
|
||||
"repo.licenses.keys_issued": "Keys",
|
||||
"repo.licenses.status": "Status",
|
||||
"repo.licenses.lifetime": "Lifetime",
|
||||
"repo.licenses.days": "days",
|
||||
"repo.licenses.all_channels": "All channels",
|
||||
"repo.licenses.active": "Active",
|
||||
"repo.licenses.inactive": "Inactive",
|
||||
"repo.licenses.none": "No License Packages",
|
||||
"repo.licenses.none_desc": "License packages can be created via the API to gate access to update streams.",
|
||||
"repo.licenses.issued_keys": "Issued Keys",
|
||||
"repo.licenses.key_prefix": "Key",
|
||||
"repo.licenses.licensee": "Licensee",
|
||||
"repo.licenses.expires": "Expires",
|
||||
"repo.licenses.never": "Never",
|
||||
"repo.licenses.new_package": "New Package",
|
||||
"repo.licenses.description": "Description",
|
||||
"repo.licenses.max_sites": "Max Sites",
|
||||
"repo.licenses.channels_help": "Comma-separated channel names (e.g. stable,release-candidate). Leave empty for all channels.",
|
||||
"repo.licenses.create_package": "Create License Package",
|
||||
"repo.licenses.create_new_package": "Create New License Package",
|
||||
"repo.licenses.package_created": "License package created successfully.",
|
||||
"repo.licenses.generate_key": "Generate Key",
|
||||
"repo.licenses.key_created": "License Key Created",
|
||||
"repo.licenses.key_created_copy": "Copy this key now. It will not be shown again.",
|
||||
"repo.licenses.revoke": "Revoke",
|
||||
"repo.licenses.edit_package": "Edit License Package",
|
||||
"repo.licenses.delete_package": "Delete Package",
|
||||
"repo.licenses.package_updated": "License package updated.",
|
||||
"repo.licenses.package_deleted": "License package deleted.",
|
||||
"repo.licenses.key_revoked": "License key revoked.",
|
||||
"repo.licenses.master_key_created": "Master License Key Created",
|
||||
"repo.licenses.master_key_created_copy": "This is your organization master key with unlimited access to all update channels. Copy it now — it will not be shown again.",
|
||||
"repo.licenses.update_feeds": "Update Feed URLs",
|
||||
"repo.release.draft": "Draft",
|
||||
"repo.release.prerelease": "Pre-Release",
|
||||
"repo.release.stable": "Stable",
|
||||
@@ -2795,18 +2749,6 @@
|
||||
"org.form.create_org_not_allowed": "You are not allowed to create an organization.",
|
||||
"org.settings": "Settings",
|
||||
"org.settings.options": "Organization",
|
||||
"org.settings.update_streams": "Licenses & Update Streams",
|
||||
"org.settings.update_streams_desc": "Configure the default update streams for all repositories in this organization. Repos can override with their own settings.",
|
||||
"org.settings.stream_mode": "Stream Mode",
|
||||
"org.settings.stream_mode_joomla": "Standard Joomla streams (stable, release-candidate, beta, alpha, development)",
|
||||
"org.settings.stream_mode_custom": "Custom streams (define your own channels and tag patterns)",
|
||||
"org.settings.default_streams": "Active Streams",
|
||||
"org.settings.default_streams_joomla": "These are the currently active update streams. Release tags are matched to streams by their suffix.",
|
||||
"org.settings.stream_name": "Stream Name",
|
||||
"org.settings.stream_suffix": "Tag Suffix",
|
||||
"org.settings.custom_streams": "Custom Stream Definitions (JSON)",
|
||||
"org.settings.custom_streams_help": "JSON array of stream objects. Each needs: name, suffix, description. Example: [{\"name\":\"lts\",\"suffix\":\"-lts\",\"description\":\"Long-term support\"}]",
|
||||
"org.settings.update_streams_saved": "Update stream settings saved.",
|
||||
"org.settings.full_name": "Full Name",
|
||||
"org.settings.email": "Contact Email Address",
|
||||
"org.settings.website": "Website",
|
||||
@@ -3386,14 +3328,6 @@
|
||||
"admin.config.common.start_time": "Start time",
|
||||
"admin.config.common.end_time": "End time",
|
||||
"admin.config.common.skip_time_check": "Leave time empty (clear the field) to skip time check",
|
||||
"admin.config.instance_landing_page": "Default Landing Page",
|
||||
"admin.config.landing_page.home": "Home — default home page",
|
||||
"admin.config.landing_page.explore": "Explore — repository explore page",
|
||||
"admin.config.landing_page.organizations": "Organizations — organization explore page",
|
||||
"admin.config.landing_page.login": "Login — redirect to login page",
|
||||
"admin.config.landing_page.custom": "Custom path — redirect to a specific URL path",
|
||||
"admin.config.landing_page.custom_path": "Custom path",
|
||||
"admin.config.landing_page.custom_path_help": "Internal path to redirect unauthenticated visitors to (e.g. /MokoConsulting or /MokoConsulting/MokoGitea/wiki).",
|
||||
"admin.config.instance_maintenance": "Instance Maintenance",
|
||||
"admin.config.instance_maintenance_mode.admin_web_access_only": "Only allow admin to access the web UI",
|
||||
"admin.config.instance_web_banner.enabled": "Show banner",
|
||||
|
||||
@@ -1347,18 +1347,6 @@ func Routes() *web.Router {
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
|
||||
})
|
||||
}, reqRepoReader(unit.TypeReleases))
|
||||
m.Group("/license-packages", func() {
|
||||
m.Combo("").Get(repo.ListLicensePackages).
|
||||
Post(bind(api.CreateLicensePackageOption{}), repo.CreateLicensePackage)
|
||||
}, reqToken(), reqAdmin())
|
||||
m.Group("/license-keys", func() {
|
||||
m.Combo("").Get(repo.ListLicenseKeys).
|
||||
Post(bind(api.CreateLicenseKeyOption{}), repo.CreateLicenseKey)
|
||||
m.Group("/{id}", func() {
|
||||
m.Delete("", repo.DeleteLicenseKey)
|
||||
m.Get("/usage", repo.GetLicenseKeyUsage)
|
||||
})
|
||||
}, reqToken(), reqAdmin())
|
||||
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
|
||||
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
|
||||
m.Group("/push_mirrors", func() {
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
|
||||
return &structs.LicensePackage{
|
||||
ID: pkg.ID,
|
||||
OwnerID: pkg.OwnerID,
|
||||
Name: pkg.Name,
|
||||
Description: pkg.Description,
|
||||
DurationDays: pkg.DurationDays,
|
||||
MaxSites: pkg.MaxSites,
|
||||
RepoScope: pkg.RepoScope,
|
||||
AllowedChannels: pkg.AllowedChannels,
|
||||
IsActive: pkg.IsActive,
|
||||
Created: time.Unix(int64(pkg.CreatedUnix), 0),
|
||||
Updated: time.Unix(int64(pkg.UpdatedUnix), 0),
|
||||
}
|
||||
}
|
||||
|
||||
func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey {
|
||||
lk := &structs.LicenseKey{
|
||||
ID: key.ID,
|
||||
PackageID: key.PackageID,
|
||||
OwnerID: key.OwnerID,
|
||||
KeyPrefix: key.KeyPrefix,
|
||||
LicenseeName: key.LicenseeName,
|
||||
LicenseeEmail: key.LicenseeEmail,
|
||||
DomainRestriction: key.DomainRestriction,
|
||||
MaxSites: key.MaxSites,
|
||||
IsInternal: key.IsInternal,
|
||||
IsActive: key.IsActive,
|
||||
Created: time.Unix(int64(key.CreatedUnix), 0),
|
||||
}
|
||||
if key.StartsUnix > 0 {
|
||||
t := time.Unix(int64(key.StartsUnix), 0)
|
||||
lk.StartsAt = &t
|
||||
}
|
||||
if key.ExpiresUnix > 0 {
|
||||
t := time.Unix(int64(key.ExpiresUnix), 0)
|
||||
lk.ExpiresAt = &t
|
||||
}
|
||||
return lk
|
||||
}
|
||||
|
||||
// ListLicensePackages lists license packages for the repo owner.
|
||||
func ListLicensePackages(ctx *context.APIContext) {
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]*structs.LicensePackage, len(pkgs))
|
||||
for i, pkg := range pkgs {
|
||||
result[i] = toLicensePackageAPI(pkg)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// CreateLicensePackage creates a new license package.
|
||||
func CreateLicensePackage(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.CreateLicensePackageOption)
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
Name: form.Name,
|
||||
Description: form.Description,
|
||||
DurationDays: form.DurationDays,
|
||||
MaxSites: form.MaxSites,
|
||||
RepoScope: form.RepoScope,
|
||||
AllowedChannels: form.AllowedChannels,
|
||||
IsActive: true,
|
||||
}
|
||||
if pkg.RepoScope == "" {
|
||||
pkg.RepoScope = "all"
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, toLicensePackageAPI(pkg))
|
||||
}
|
||||
|
||||
// ListLicenseKeys lists license keys for the repo owner.
|
||||
func ListLicenseKeys(ctx *context.APIContext) {
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]*structs.LicenseKey, len(keys))
|
||||
for i, key := range keys {
|
||||
result[i] = toLicenseKeyAPI(key)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// CreateLicenseKey creates a new license key.
|
||||
func CreateLicenseKey(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.CreateLicenseKeyOption)
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
PackageID: form.PackageID,
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
LicenseeName: form.LicenseeName,
|
||||
LicenseeEmail: form.LicenseeEmail,
|
||||
DomainRestriction: form.DomainRestriction,
|
||||
MaxSites: form.MaxSites,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if form.StartsAt != nil {
|
||||
key.StartsUnix = timeutil.TimeStamp(form.StartsAt.Unix())
|
||||
}
|
||||
|
||||
if form.ExpiresAt != nil {
|
||||
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
|
||||
} else {
|
||||
// Auto-calculate from package duration.
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if pkg.DurationDays > 0 {
|
||||
start := time.Now()
|
||||
if form.StartsAt != nil {
|
||||
start = *form.StartsAt
|
||||
}
|
||||
expires := start.AddDate(0, 0, pkg.DurationDays)
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &structs.LicenseKeyCreated{
|
||||
LicenseKey: *toLicenseKeyAPI(key),
|
||||
RawKey: rawKey,
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
// DeleteLicenseKey deletes a license key.
|
||||
func DeleteLicenseKey(ctx *context.APIContext) {
|
||||
if err := licenses.DeleteLicenseKey(ctx, ctx.PathParamInt64("id")); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetLicenseKeyUsage returns usage logs for a license key.
|
||||
func GetLicenseKeyUsage(ctx *context.APIContext) {
|
||||
usages, err := licenses.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]*structs.LicenseKeyUsage, len(usages))
|
||||
for i, u := range usages {
|
||||
result[i] = &structs.LicenseKeyUsage{
|
||||
ID: u.ID,
|
||||
KeyID: u.KeyID,
|
||||
RepoID: u.RepoID,
|
||||
Domain: u.Domain,
|
||||
IPAddress: u.IPAddress,
|
||||
UserAgent: u.UserAgent,
|
||||
VersionFrom: u.VersionFrom,
|
||||
Created: time.Unix(int64(u.CreatedUnix), 0),
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
+3
-12
@@ -48,18 +48,9 @@ func Home(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
// Check non-logged users landing page.
|
||||
} else {
|
||||
// Dynamic landing page from admin config takes priority.
|
||||
landingPage := setting.Config().Instance.LandingPage.Value(ctx)
|
||||
if landingPage.Mode != "" && landingPage.Mode != "home" {
|
||||
ctx.Redirect(setting.AppSubURL + landingPage.URL())
|
||||
return
|
||||
}
|
||||
// Fall back to static app.ini setting.
|
||||
if setting.LandingPageURL != setting.LandingPageHome {
|
||||
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
|
||||
return
|
||||
}
|
||||
} else if setting.LandingPageURL != setting.LandingPageHome {
|
||||
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
|
||||
return
|
||||
}
|
||||
|
||||
// Check auto-login.
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplOrgLicenses templates.TplName = "org/licenses"
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// Licenses shows the org-level license packages and keys.
|
||||
func Licenses(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
|
||||
org := ctx.Org.Organization
|
||||
ownerID := org.ID
|
||||
|
||||
// Auto-create master key if org owner.
|
||||
if ctx.Org.IsOwner {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
return
|
||||
}
|
||||
if newMasterKey != "" {
|
||||
ctx.Data["NewMasterKey"] = newMasterKey
|
||||
}
|
||||
}
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
}
|
||||
|
||||
var display []LicensePackageDisplay
|
||||
for _, pkg := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(pkg.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
|
||||
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgLicenses)
|
||||
}
|
||||
|
||||
// LicensesCreatePackage handles POST to create a new org-level license package.
|
||||
func LicensesCreatePackage(ctx *context.Context) {
|
||||
name := ctx.FormString("name")
|
||||
if name == "" {
|
||||
ctx.Flash.Error("Package name is required")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
|
||||
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
Name: name,
|
||||
Description: ctx.FormString("description"),
|
||||
DurationDays: durationDays,
|
||||
MaxSites: maxSites,
|
||||
AllowedChannels: ctx.FormString("allowed_channels"),
|
||||
RepoScope: "all",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("CreateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_created"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
}
|
||||
|
||||
// LicensesGenerateKey handles POST to generate a key from an org package.
|
||||
func LicensesGenerateKey(ctx *context.Context) {
|
||||
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
|
||||
if packageID == 0 {
|
||||
ctx.Flash.Error("Invalid package")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
PackageID: packageID,
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if pkg.DurationDays > 0 {
|
||||
expires := time.Now().AddDate(0, 0, pkg.DurationDays)
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-render with the new key shown.
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
|
||||
ctx.Data["NewKeyCreated"] = rawKey
|
||||
|
||||
ownerID := ctx.Org.Organization.ID
|
||||
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
|
||||
var display []LicensePackageDisplay
|
||||
for _, p := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: p,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(p.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgLicenses)
|
||||
}
|
||||
|
||||
// LicensesRevokeKey handles POST to revoke an org license key.
|
||||
func LicensesRevokeKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsUpdateStreams templates.TplName = "org/settings/update_streams"
|
||||
|
||||
// SettingsUpdateStreams shows the org-level update stream settings.
|
||||
func SettingsUpdateStreams(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("org.settings.update_streams")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsUpdateStreams"] = true
|
||||
|
||||
orgID := ctx.Org.Organization.ID
|
||||
|
||||
cfg, err := licenses.GetOrgConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["StreamConfig"] = cfg
|
||||
ctx.Data["EffectiveStreams"] = cfg.GetActiveStreams()
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsUpdateStreams)
|
||||
}
|
||||
|
||||
// SettingsUpdateStreamsPost saves the org-level update stream settings.
|
||||
func SettingsUpdateStreamsPost(ctx *context.Context) {
|
||||
orgID := ctx.Org.Organization.ID
|
||||
|
||||
cfg := &licenses.UpdateStreamConfig{
|
||||
OwnerID: orgID,
|
||||
RepoID: 0,
|
||||
StreamMode: ctx.FormString("stream_mode"),
|
||||
CustomStreams: ctx.FormString("custom_streams"),
|
||||
}
|
||||
|
||||
if cfg.StreamMode == "" {
|
||||
cfg.StreamMode = "joomla"
|
||||
}
|
||||
|
||||
if err := licenses.SaveConfig(ctx, cfg); err != nil {
|
||||
ctx.ServerError("SaveConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.update_streams_saved"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/update-streams")
|
||||
}
|
||||
@@ -128,15 +128,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
}
|
||||
|
||||
// Only public pull don't need auth.
|
||||
// For private repos, also allow anonymous pull if the specific unit
|
||||
// (code or wiki) has AnonymousAccessMode >= Read.
|
||||
isPublicPull := repoExist && isPull && !repo.IsPrivate
|
||||
if repoExist && isPull && repo.IsPrivate {
|
||||
repoUnit := repo.MustGetUnit(ctx, unitType)
|
||||
if repoUnit.AnonymousAccessMode >= perm.AccessModeRead {
|
||||
isPublicPull = true
|
||||
}
|
||||
}
|
||||
isPublicPull := repoExist && !repo.IsPrivate && isPull
|
||||
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict
|
||||
|
||||
// don't allow anonymous pulls if organization is not public
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplLicenses templates.TplName = "repo/licenses"
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// Licenses shows the license packages and keys for a repo.
|
||||
func Licenses(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
// Auto-create master package + key if admin and none exist.
|
||||
if ctx.Repo.Permission.IsAdmin() {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
return
|
||||
}
|
||||
if newMasterKey != "" {
|
||||
ctx.Data["NewMasterKey"] = newMasterKey
|
||||
}
|
||||
}
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
}
|
||||
|
||||
var display []LicensePackageDisplay
|
||||
for _, pkg := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(pkg.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplLicenses)
|
||||
}
|
||||
|
||||
// LicensesCreatePackage handles POST to create a new license package.
|
||||
func LicensesCreatePackage(ctx *context.Context) {
|
||||
name := ctx.FormString("name")
|
||||
if name == "" {
|
||||
ctx.Flash.Error("Package name is required")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
|
||||
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
Name: name,
|
||||
Description: ctx.FormString("description"),
|
||||
DurationDays: durationDays,
|
||||
MaxSites: maxSites,
|
||||
AllowedChannels: ctx.FormString("allowed_channels"),
|
||||
RepoScope: "all",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("CreateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_created"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
|
||||
// LicensesGenerateKey handles POST to generate a new key from a package.
|
||||
func LicensesGenerateKey(ctx *context.Context) {
|
||||
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
|
||||
if packageID == 0 {
|
||||
ctx.Flash.Error("Invalid package")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
PackageID: packageID,
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Auto-calculate expiry from package duration.
|
||||
if pkg.DurationDays > 0 {
|
||||
expires := time.Now().AddDate(0, 0, pkg.DurationDays)
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
ctx.Data["NewKeyCreated"] = rawKey
|
||||
|
||||
// Re-render the page with the new key displayed.
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
|
||||
var display []LicensePackageDisplay
|
||||
for _, p := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: p,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(p.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplLicenses)
|
||||
}
|
||||
|
||||
// LicensesRevokeKey handles POST to revoke a license key.
|
||||
func LicensesRevokeKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
|
||||
const tplLicensesEditPackage templates.TplName = "repo/licenses_edit_package"
|
||||
|
||||
// LicensesEditPackage shows the edit form for a license package.
|
||||
func LicensesEditPackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses.edit_package")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["Package"] = pkg
|
||||
ctx.HTML(http.StatusOK, tplLicensesEditPackage)
|
||||
}
|
||||
|
||||
// LicensesEditPackagePost saves edits to a license package.
|
||||
func LicensesEditPackagePost(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
pkg.Name = ctx.FormString("name")
|
||||
pkg.Description = ctx.FormString("description")
|
||||
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
|
||||
pkg.DurationDays = durationDays
|
||||
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
|
||||
pkg.MaxSites = maxSites
|
||||
pkg.AllowedChannels = ctx.FormString("allowed_channels")
|
||||
pkg.IsActive = ctx.FormString("is_active") == "on"
|
||||
|
||||
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("UpdateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_updated"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
|
||||
// LicensesDeletePackage deletes a license package. Site admin only.
|
||||
func LicensesDeletePackage(ctx *context.Context) {
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("DeleteLicensePackage", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_deleted"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
licenses_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
@@ -101,8 +99,6 @@ func SettingsCtxData(ctx *context.Context) {
|
||||
|
||||
// Settings show a repository's settings page
|
||||
func Settings(ctx *context.Context) {
|
||||
repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
ctx.Data["RepoUpdateConfig"] = repoCfg
|
||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||
}
|
||||
|
||||
@@ -514,17 +510,6 @@ func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config c
|
||||
return repoUnit
|
||||
}
|
||||
|
||||
// applyUnitVisibility sets AnonymousAccessMode on a unit based on the form value.
|
||||
// Values: "" or "not-set" = none, "anonymous-read" = anonymous read.
|
||||
func applyUnitVisibility(unit *repo_model.RepoUnit, visibility string) {
|
||||
switch visibility {
|
||||
case "anonymous-read":
|
||||
unit.AnonymousAccessMode = perm.AccessModeRead
|
||||
default:
|
||||
unit.AnonymousAccessMode = perm.AccessModeNone
|
||||
}
|
||||
}
|
||||
|
||||
func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.RepoSettingForm)
|
||||
repo := ctx.Repo.Repository
|
||||
@@ -542,9 +527,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
|
||||
u := newRepoUnit(repo, unit_model.TypeCode, nil)
|
||||
applyUnitVisibility(&u, form.CodeVisibility)
|
||||
units = append(units, u)
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil))
|
||||
} else if !unit_model.TypeCode.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
|
||||
}
|
||||
@@ -561,9 +544,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}))
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
|
||||
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
|
||||
u := newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig))
|
||||
applyUnitVisibility(&u, form.WikiVisibility)
|
||||
units = append(units, u)
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig)))
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
|
||||
} else {
|
||||
if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
|
||||
@@ -600,13 +581,11 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}))
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
|
||||
} else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
|
||||
u := newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
|
||||
EnableTimetracker: form.EnableTimetracker,
|
||||
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
|
||||
EnableDependencies: form.EnableIssueDependencies,
|
||||
})
|
||||
applyUnitVisibility(&u, form.IssuesVisibility)
|
||||
units = append(units, u)
|
||||
}))
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
|
||||
} else {
|
||||
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
|
||||
@@ -626,9 +605,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
|
||||
u := newRepoUnit(repo, unit_model.TypeReleases, nil)
|
||||
applyUnitVisibility(&u, form.ReleasesVisibility)
|
||||
units = append(units, u)
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil))
|
||||
} else if !unit_model.TypeReleases.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
|
||||
}
|
||||
@@ -675,22 +652,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Save update server platform and require-key settings.
|
||||
updatePlatform := form.UpdatePlatform
|
||||
if updatePlatform == "" {
|
||||
updatePlatform = "joomla"
|
||||
}
|
||||
updateCfg := &licenses_model.UpdateStreamConfig{
|
||||
OwnerID: repo.OwnerID,
|
||||
RepoID: repo.ID,
|
||||
Platform: updatePlatform,
|
||||
RequireKey: form.RequireUpdateKey,
|
||||
StreamMode: "joomla", // inherit org default
|
||||
}
|
||||
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
|
||||
log.Error("SaveConfig: %v", err)
|
||||
}
|
||||
|
||||
log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/updateserver"
|
||||
)
|
||||
|
||||
// validateUpdateKey checks for a license key in the request and validates it.
|
||||
// Returns allowed channels (nil = all channels) and whether access is granted.
|
||||
func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool) {
|
||||
rawKey := ctx.FormString("key")
|
||||
if rawKey == "" {
|
||||
rawKey = ctx.FormString("download_key")
|
||||
}
|
||||
|
||||
if rawKey == "" {
|
||||
// Check if this repo requires a key for update feed access.
|
||||
repoCfg, _ := licenses.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
if repoCfg != nil && repoCfg.RequireKey {
|
||||
// Key required but not provided — return empty.
|
||||
return nil, false
|
||||
}
|
||||
// No key required — allow public access (all channels).
|
||||
return nil, true
|
||||
}
|
||||
|
||||
key, pkg, err := licenses.ValidateLicenseKey(ctx, rawKey)
|
||||
if err != nil {
|
||||
log.Debug("License key validation failed: %v", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Record usage.
|
||||
_ = licenses.RecordUsage(ctx, &licenses.LicenseKeyUsage{
|
||||
KeyID: key.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Domain: ctx.FormString("domain"),
|
||||
IPAddress: ctx.RemoteAddr(),
|
||||
UserAgent: ctx.Req.UserAgent(),
|
||||
VersionFrom: ctx.FormString("version"),
|
||||
})
|
||||
|
||||
// Parse allowed channels from the package.
|
||||
if pkg.AllowedChannels != "" {
|
||||
channels := strings.Split(pkg.AllowedChannels, ",")
|
||||
for i := range channels {
|
||||
channels[i] = strings.TrimSpace(channels[i])
|
||||
}
|
||||
// Also try JSON array format.
|
||||
if strings.HasPrefix(pkg.AllowedChannels, "[") {
|
||||
var parsed []string
|
||||
if err := json.Unmarshal([]byte(pkg.AllowedChannels), &parsed); err == nil {
|
||||
channels = parsed
|
||||
}
|
||||
}
|
||||
// Normalize shorthand names to full Joomla convention.
|
||||
for i := range channels {
|
||||
channels[i] = updateserver.NormalizeChannel(channels[i])
|
||||
}
|
||||
return channels, true
|
||||
}
|
||||
|
||||
// Master/internal keys or packages with no channel restriction — all channels.
|
||||
return nil, true
|
||||
}
|
||||
|
||||
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
|
||||
// from the repository's releases.
|
||||
func ServeUpdatesXML(ctx *context.Context) {
|
||||
allowedChannels, ok := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
// Return empty updates XML for invalid keys (Joomla-compatible).
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><updates></updates>`))
|
||||
return
|
||||
}
|
||||
|
||||
xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository, allowedChannels...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateJoomlaXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
|
||||
// ServeDolibarrJSON generates and serves a Dolibarr-compatible update feed
|
||||
// from the repository's releases.
|
||||
func ServeDolibarrJSON(ctx *context.Context) {
|
||||
data, err := updateserver.GenerateDolibarrJSON(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateDolibarrJSON", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
ctx.ServerError("json.Marshal", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(jsonData)
|
||||
}
|
||||
@@ -1057,11 +1057,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("", org.BlockedUsers)
|
||||
m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
|
||||
})
|
||||
|
||||
m.Group("/update-streams", func() {
|
||||
m.Get("", org.SettingsUpdateStreams)
|
||||
m.Post("", org.SettingsUpdateStreamsPost)
|
||||
})
|
||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
||||
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
|
||||
}, reqSignIn)
|
||||
@@ -1104,13 +1099,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
// at the moment, only editing "owner-level projects" need to "mention", maybe in the future we can relax the permission check
|
||||
m.Get("/mentions-in-owner", reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), org.GetMentionsInOwner)
|
||||
|
||||
m.Group("/licenses", func() {
|
||||
m.Get("", org.Licenses)
|
||||
m.Post("/packages", org.LicensesCreatePackage)
|
||||
m.Post("/keys/generate", org.LicensesGenerateKey)
|
||||
m.Post("/keys/{id}/revoke", org.LicensesRevokeKey)
|
||||
})
|
||||
|
||||
m.Get("/repositories", org.Repositories)
|
||||
m.Get("/heatmap", user.DashboardHeatmap)
|
||||
|
||||
@@ -1506,25 +1494,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
|
||||
// end "/{username}/{reponame}": repo releases
|
||||
|
||||
// "/{username}/{reponame}": update server endpoints
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/updates.xml", repo.ServeUpdatesXML)
|
||||
m.Get("/updates/dolibarr.json", repo.ServeDolibarrJSON)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
// "/{username}/{reponame}": licenses page
|
||||
m.Group("/{username}/{reponame}/licenses", func() {
|
||||
m.Get("", repo.Licenses)
|
||||
m.Post("/packages", repo.LicensesCreatePackage)
|
||||
m.Get("/packages/{id}/edit", repo.LicensesEditPackage)
|
||||
m.Post("/packages/{id}/edit", repo.LicensesEditPackagePost)
|
||||
m.Post("/packages/{id}/delete", repo.LicensesDeletePackage)
|
||||
m.Post("/keys/generate", repo.LicensesGenerateKey)
|
||||
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": licenses
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
issues_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
licenses_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
access_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
@@ -606,23 +605,6 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
return
|
||||
}
|
||||
|
||||
// Check if license packages exist for this repo's owner (enables Licenses tab).
|
||||
numLicensePackages, _ := db.Count[licenses_model.LicensePackage](ctx, licenses_model.FindLicensePackageOptions{
|
||||
OwnerID: repo.OwnerID,
|
||||
})
|
||||
ctx.Data["NumLicensePackages"] = numLicensePackages
|
||||
ctx.Data["EnableLicenses"] = numLicensePackages > 0
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
|
||||
|
||||
// Load repo update config for platform-aware UI.
|
||||
repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
|
||||
if repoUpdateCfg != nil {
|
||||
ctx.Data["RepoUpdatePlatform"] = repoUpdateCfg.Platform
|
||||
} else {
|
||||
ctx.Data["RepoUpdatePlatform"] = "joomla"
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
|
||||
ctx.Data["PageTitleCommon"] = repo.Name + " - " + setting.AppName
|
||||
ctx.Data["Repository"] = repo
|
||||
|
||||
@@ -110,14 +110,12 @@ type RepoSettingForm struct {
|
||||
EnablePrune bool
|
||||
|
||||
// Advanced settings
|
||||
EnableCode bool
|
||||
CodeVisibility string
|
||||
EnableCode bool
|
||||
|
||||
EnableWiki bool
|
||||
EnableExternalWiki bool
|
||||
DefaultWikiBranch string
|
||||
ExternalWikiURL string
|
||||
WikiVisibility string
|
||||
EnableWiki bool
|
||||
EnableExternalWiki bool
|
||||
DefaultWikiBranch string
|
||||
ExternalWikiURL string
|
||||
|
||||
EnableIssues bool
|
||||
EnableExternalTracker bool
|
||||
@@ -126,17 +124,13 @@ type RepoSettingForm struct {
|
||||
TrackerIssueStyle string
|
||||
ExternalTrackerRegexpPattern string
|
||||
EnableCloseIssuesViaCommitInAnyBranch bool
|
||||
IssuesVisibility string
|
||||
|
||||
EnableProjects bool
|
||||
ProjectsMode string
|
||||
|
||||
EnableReleases bool
|
||||
ReleasesVisibility string
|
||||
UpdatePlatform string
|
||||
RequireUpdateKey bool
|
||||
EnableReleases bool
|
||||
|
||||
EnablePackages bool
|
||||
EnablePackages bool
|
||||
|
||||
EnablePulls bool
|
||||
PullsIgnoreWhitespace bool
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// DolibarrUpdate represents a single module update entry in Dolibarr format.
|
||||
type DolibarrUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Channel string `json:"channel"`
|
||||
DownloadURL string `json:"url"`
|
||||
ChangelogURL string `json:"changelog"`
|
||||
ReleaseURL string `json:"release_url"`
|
||||
Requires string `json:"requires,omitempty"`
|
||||
Date string `json:"date"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// DolibarrUpdates holds the full update feed response.
|
||||
type DolibarrUpdates struct {
|
||||
Module string `json:"module"`
|
||||
Updates []DolibarrUpdate `json:"updates"`
|
||||
}
|
||||
|
||||
// GenerateDolibarrJSON builds a Dolibarr-compatible update feed from releases.
|
||||
func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository) (*DolibarrUpdates, error) {
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IncludeDrafts: false,
|
||||
IncludeTags: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FindReleases: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := strings.TrimSuffix(setting.AppURL, "/")
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
result := &DolibarrUpdates{
|
||||
Module: repo.Name,
|
||||
}
|
||||
|
||||
// Resolve effective streams.
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Track best release per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
for _, stream := range streams {
|
||||
ch := stream.Name
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var downloadURL string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
suffix := stream.Suffix
|
||||
if suffix == "" {
|
||||
suffix = channelSuffix(ch)
|
||||
}
|
||||
if suffix != "" {
|
||||
version = version + suffix
|
||||
}
|
||||
|
||||
result.Updates = append(result.Updates, DolibarrUpdate{
|
||||
Name: repo.Name,
|
||||
Version: version,
|
||||
Channel: ch,
|
||||
DownloadURL: downloadURL,
|
||||
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
|
||||
ReleaseURL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
||||
Date: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// Joomla-compatible updates.xml structures for XML marshaling.
|
||||
|
||||
type xmlUpdates struct {
|
||||
XMLName xml.Name `xml:"updates"`
|
||||
Updates []xmlUpdate `xml:"update"`
|
||||
}
|
||||
|
||||
type xmlUpdate struct {
|
||||
Name string `xml:"name"`
|
||||
Description string `xml:"description"`
|
||||
Element string `xml:"element"`
|
||||
Type string `xml:"type"`
|
||||
Client string `xml:"client"`
|
||||
Version string `xml:"version"`
|
||||
CreationDate string `xml:"creationDate"`
|
||||
InfoURL xmlInfoURL `xml:"infourl"`
|
||||
Downloads xmlDownloads `xml:"downloads"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
Tags xmlTags `xml:"tags"`
|
||||
ChangelogURL string `xml:"changelogurl,omitempty"`
|
||||
Maintainer string `xml:"maintainer,omitempty"`
|
||||
MaintainerURL string `xml:"maintainerurl,omitempty"`
|
||||
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
|
||||
}
|
||||
|
||||
type xmlInfoURL struct {
|
||||
Title string `xml:"title,attr"`
|
||||
URL string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type xmlDownloads struct {
|
||||
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
|
||||
}
|
||||
|
||||
type xmlDownloadURL struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Format string `xml:"format,attr"`
|
||||
URL string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type xmlTags struct {
|
||||
Tag string `xml:"tag"`
|
||||
}
|
||||
|
||||
type xmlTargetPlat struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
}
|
||||
|
||||
// channelFromTag maps a release tag name to a Joomla update channel.
|
||||
// Joomla update stream names (full convention).
|
||||
const (
|
||||
ChannelStable = "stable"
|
||||
ChannelReleaseCandidate = "release-candidate"
|
||||
ChannelBeta = "beta"
|
||||
ChannelAlpha = "alpha"
|
||||
ChannelDevelopment = "development"
|
||||
)
|
||||
|
||||
// AllChannels in display order (most stable first).
|
||||
var AllChannels = []string{ChannelStable, ChannelReleaseCandidate, ChannelBeta, ChannelAlpha, ChannelDevelopment}
|
||||
|
||||
// channelFromTag maps a release tag name to a Joomla update channel.
|
||||
func channelFromTag(tagName string, isPrerelease bool) string {
|
||||
lower := strings.ToLower(tagName)
|
||||
switch {
|
||||
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
|
||||
return ChannelDevelopment
|
||||
case strings.Contains(lower, "-alpha"):
|
||||
return ChannelAlpha
|
||||
case strings.Contains(lower, "-beta"):
|
||||
return ChannelBeta
|
||||
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
|
||||
return ChannelReleaseCandidate
|
||||
case isPrerelease:
|
||||
return ChannelReleaseCandidate
|
||||
default:
|
||||
return ChannelStable
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeChannel maps shorthand channel names to the full Joomla convention.
|
||||
// Accepts both "rc" and "release-candidate", "dev" and "development", etc.
|
||||
func NormalizeChannel(ch string) string {
|
||||
switch strings.ToLower(ch) {
|
||||
case "rc", "release-candidate":
|
||||
return ChannelReleaseCandidate
|
||||
case "dev", "development":
|
||||
return ChannelDevelopment
|
||||
case "alpha":
|
||||
return ChannelAlpha
|
||||
case "beta":
|
||||
return ChannelBeta
|
||||
case "stable":
|
||||
return ChannelStable
|
||||
default:
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
|
||||
// It returns the raw XML bytes. The element, maintainer, and target platform
|
||||
// are derived from the repo name and owner.
|
||||
// allowedChannels optionally restricts output to specific channels (nil = all).
|
||||
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, allowedChannels ...string) ([]byte, error) {
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IncludeDrafts: false,
|
||||
IncludeTags: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetReleasesByRepoID: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := setting.AppURL
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
element := strings.ToLower(repo.Name)
|
||||
|
||||
// Resolve effective streams (repo override → org default → Joomla default).
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Track best (latest) release per channel to emit one entry per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
// Build allowed channel set for filtering.
|
||||
// Normalize shorthand names so both "rc" and "release-candidate" work.
|
||||
channelAllowed := make(map[string]bool)
|
||||
if len(allowedChannels) > 0 {
|
||||
for _, c := range allowedChannels {
|
||||
channelAllowed[NormalizeChannel(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
var updates xmlUpdates
|
||||
for _, stream := range streams {
|
||||
ch := stream.Name
|
||||
// Skip channels not in the allowed set (when filtering is active).
|
||||
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
||||
continue
|
||||
}
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Load attachments for download URLs.
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the first .zip attachment as the download URL.
|
||||
var downloadURL string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fall back to the release tag archive if no zip attachment.
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
suffix := stream.Suffix
|
||||
if suffix == "" {
|
||||
suffix = channelSuffix(ch) // fallback for Joomla defaults
|
||||
}
|
||||
if suffix != "" {
|
||||
version = version + suffix
|
||||
}
|
||||
|
||||
u := xmlUpdate{
|
||||
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
||||
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
|
||||
Element: element,
|
||||
Type: "component",
|
||||
Client: "site",
|
||||
Version: version,
|
||||
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
||||
InfoURL: xmlInfoURL{
|
||||
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
||||
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
||||
},
|
||||
Downloads: xmlDownloads{
|
||||
DownloadURL: []xmlDownloadURL{
|
||||
{Type: "full", Format: "zip", URL: downloadURL},
|
||||
},
|
||||
},
|
||||
Tags: xmlTags{Tag: ch},
|
||||
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
|
||||
Maintainer: repo.Owner.Name,
|
||||
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
|
||||
TargetPlatform: xmlTargetPlat{
|
||||
Name: "joomla",
|
||||
Version: ".*",
|
||||
},
|
||||
}
|
||||
|
||||
updates.Updates = append(updates.Updates, u)
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(updates, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
|
||||
// extractVersion strips common tag prefixes (v, release-, etc.) to get the version.
|
||||
func extractVersion(tagName string) string {
|
||||
v := tagName
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
v = strings.TrimPrefix(v, "release-")
|
||||
v = strings.TrimPrefix(v, "release/")
|
||||
// Strip channel suffixes to get base version.
|
||||
for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
|
||||
if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
|
||||
v = v[:idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// channelSuffix returns the version suffix for a channel.
|
||||
func channelSuffix(channel string) string {
|
||||
switch channel {
|
||||
case ChannelDevelopment:
|
||||
return "-dev"
|
||||
case ChannelAlpha:
|
||||
return "-alpha"
|
||||
case ChannelBeta:
|
||||
return "-beta"
|
||||
case ChannelReleaseCandidate:
|
||||
return "-rc"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
{{template "admin/config_settings/avatars" .}}
|
||||
{{template "admin/config_settings/repository" .}}
|
||||
{{template "admin/config_settings/landing_page" .}}
|
||||
{{template "admin/config_settings/instance" .}}
|
||||
|
||||
{{template "admin/layout_footer" .}}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "admin.config.instance_landing_page"}}</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form ignore-dirty system-config-form" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{$cfgOpt := $.SystemConfig.Instance.LandingPage}}
|
||||
{{$cfgKey := $cfgOpt.DynKey}}
|
||||
{{$landingPage := $cfgOpt.Value ctx}}
|
||||
<input type="hidden" data-config-dyn-key="{{$cfgKey}}" data-config-value-json="{{JsonUtils.EncodeToString $landingPage}}">
|
||||
<div class="grouped fields">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="{{$cfgKey}}.Mode" type="radio" value="home" {{if or (eq $landingPage.Mode "") (eq $landingPage.Mode "home")}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "admin.config.landing_page.home"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="{{$cfgKey}}.Mode" type="radio" value="explore" {{if eq $landingPage.Mode "explore"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "admin.config.landing_page.explore"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="{{$cfgKey}}.Mode" type="radio" value="organizations" {{if eq $landingPage.Mode "organizations"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "admin.config.landing_page.organizations"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="{{$cfgKey}}.Mode" type="radio" value="login" {{if eq $landingPage.Mode "login"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "admin.config.landing_page.login"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="{{$cfgKey}}.Mode" type="radio" value="custom" {{if eq $landingPage.Mode "custom"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "admin.config.landing_page.custom"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.landing_page.custom_path"}}</label>
|
||||
<input type="text" name="{{$cfgKey}}.CustomPath" value="{{$landingPage.CustomPath}}" placeholder="/MokoConsulting">
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.landing_page.custom_path_help"}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,148 +0,0 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
|
||||
{{if .NewMasterKey}}
|
||||
<div class="ui info message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.master_key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewMasterKey}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .NewKeyCreated}}
|
||||
<div class="ui success message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewKeyCreated}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .IsRepoAdmin}}
|
||||
<details class="tw-mb-4">
|
||||
<summary class="ui primary button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
|
||||
<div class="tw-mt-4">
|
||||
<form class="ui form" method="post" action="{{$.Org.HomeLink}}/-/licenses/packages">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
|
||||
<input name="name" required placeholder="e.g. Pro Annual, Basic Monthly">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
|
||||
<input name="description" placeholder="e.g. Annual pro subscription">
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
|
||||
<input name="duration_days" type="number" value="0" min="0">
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="0" min="0">
|
||||
<p class="help">0 = unlimited</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
<input name="allowed_channels" placeholder="stable,release-candidate">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.duration"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.channels"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.keys_issued"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicensePackages}}
|
||||
<tr>
|
||||
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td>{{if eq .DurationDays 0}}{{ctx.Locale.Tr "repo.licenses.lifetime"}}{{else}}{{.DurationDays}} {{ctx.Locale.Tr "repo.licenses.days"}}{{end}}</td>
|
||||
<td>{{if .AllowedChannels}}<code>{{.AllowedChannels}}</code>{{else}}{{ctx.Locale.Tr "repo.licenses.all_channels"}}{{end}}</td>
|
||||
<td>{{.KeyCount}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/generate" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="package_id" value="{{.ID}}">
|
||||
<button class="ui tiny primary button" type="submit">
|
||||
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-key" 48}}
|
||||
<h2>{{ctx.Locale.Tr "repo.licenses.none"}}</h2>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.none_desc"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .LicenseKeys}}
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.licensee"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.expires"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">Master</span>{{end}}</td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.TimeSince .ExpiresUnix}}{{end}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/revoke" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -25,11 +25,6 @@
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .IsOrganizationMember}}
|
||||
<a class="{{if .IsLicensesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/licenses">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and .IsRepoIndexerEnabled .CanReadCode}}
|
||||
<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
|
||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
{{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsUpdateStreams}}active {{end}}item" href="{{.OrgLink}}/settings/update-streams">
|
||||
{{ctx.Locale.Tr "org.settings.update_streams"}}
|
||||
</a>
|
||||
{{if .EnableActions}}
|
||||
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{{template "org/settings/layout_head" (dict "pageClass" "organization settings")}}
|
||||
<div class="org-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "org.settings.update_streams"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "org.settings.update_streams_desc"}}</p>
|
||||
<form class="ui form" method="post" action="{{.OrgLink}}/settings/update-streams">
|
||||
{{.CsrfTokenHtml}}
|
||||
|
||||
<div class="grouped fields">
|
||||
<label>{{ctx.Locale.Tr "org.settings.stream_mode"}}</label>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="stream_mode" type="radio" value="joomla" {{if or (eq .StreamConfig.StreamMode "") (eq .StreamConfig.StreamMode "joomla")}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "org.settings.stream_mode_joomla"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="stream_mode" type="radio" value="custom" {{if eq .StreamConfig.StreamMode "custom"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "org.settings.stream_mode_custom"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.default_streams"}}</label>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.default_streams_joomla"}}</p>
|
||||
<table class="ui small table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "org.settings.stream_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.stream_suffix"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.description"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .EffectiveStreams}}
|
||||
<tr>
|
||||
<td><code>{{.Name}}</code></td>
|
||||
<td>{{if .Suffix}}<code>{{.Suffix}}</code>{{else}}<em>(no suffix)</em>{{end}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.custom_streams"}}</label>
|
||||
<textarea name="custom_streams" rows="6" placeholder='[{"name":"lts","suffix":"-lts","description":"Long-term support"}]'>{{.StreamConfig.CustomStreams}}</textarea>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.custom_streams_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
||||
@@ -128,15 +128,6 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if or .EnableLicenses .IsRepoAdmin}}
|
||||
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
{{if .NumLicensePackages}}
|
||||
<span class="ui small label">{{CountFmt .NumLicensePackages}}</span>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
{{if and (not ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
|
||||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
|
||||
{{if .NewMasterKey}}
|
||||
<div class="ui info message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.master_key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewMasterKey}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .NewKeyCreated}}
|
||||
<div class="ui success message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewKeyCreated}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ── License Packages ── */}}
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.duration"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.channels"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.keys_issued"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicensePackages}}
|
||||
<tr>
|
||||
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td>{{if eq .DurationDays 0}}{{ctx.Locale.Tr "repo.licenses.lifetime"}}{{else}}{{.DurationDays}} {{ctx.Locale.Tr "repo.licenses.days"}}{{end}}</td>
|
||||
<td>{{if .AllowedChannels}}<code>{{.AllowedChannels}}</code>{{else}}{{ctx.Locale.Tr "repo.licenses.all_channels"}}{{end}}</td>
|
||||
<td>{{.KeyCount}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right tw-flex tw-gap-1 tw-justify-end">
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/keys/generate" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="package_id" value="{{.ID}}">
|
||||
<button class="ui tiny primary button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
|
||||
{{svg "octicon-plus" 14}}
|
||||
</button>
|
||||
</form>
|
||||
<a class="ui tiny button" href="{{$.RepoLink}}/licenses/packages/{{.ID}}/edit" title="{{ctx.Locale.Tr "repo.licenses.edit_package"}}">
|
||||
{{svg "octicon-pencil" 14}}
|
||||
</a>
|
||||
{{if $.IsSiteAdmin}}
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/packages/{{.ID}}/delete" class="tw-inline" onsubmit="return confirm('Delete this package? This action cannot be undone.')">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.delete_package"}}">
|
||||
{{svg "octicon-trash" 14}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-key" 48}}
|
||||
<h2>{{ctx.Locale.Tr "repo.licenses.none"}}</h2>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.none_desc"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* ── Create New License Package ── */}}
|
||||
{{if .IsRepoAdmin}}
|
||||
<div class="tw-mt-4">
|
||||
<details>
|
||||
<summary class="ui primary button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
|
||||
<div class="ui segment tw-mt-2">
|
||||
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
|
||||
<input name="name" required placeholder="e.g. Pro Annual, Basic Monthly">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
|
||||
<input name="description" placeholder="e.g. Annual pro subscription with all channels">
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
|
||||
<input name="duration_days" type="number" value="0" min="0">
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="0" min="0">
|
||||
<p class="help">0 = unlimited</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
<input name="allowed_channels" placeholder="stable,release-candidate">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ── Issued Keys ── */}}
|
||||
{{if .LicenseKeys}}
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.licensee"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.expires"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code></td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.TimeSince .ExpiresUnix}}{{end}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/keys/{{.ID}}/revoke" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.revoke"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ── Update Feed URLs ── */}}
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "repo.licenses.update_feeds"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="field">
|
||||
<label>Joomla updates.xml</label>
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates.xml" onclick="this.select()">
|
||||
<button class="ui button" onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-mt-2">
|
||||
<label>Dolibarr JSON</label>
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" onclick="this.select()">
|
||||
<button class="ui button" onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -1,52 +0,0 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-pencil" 16}} {{ctx.Locale.Tr "repo.licenses.edit_package"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages/{{.Package.ID}}/edit">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
|
||||
<input name="name" required value="{{.Package.Name}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
|
||||
<input name="description" value="{{.Package.Description}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
|
||||
<input name="duration_days" type="number" value="{{.Package.DurationDays}}" min="0">
|
||||
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="{{.Package.MaxSites}}" min="0">
|
||||
<p class="help">0 = unlimited</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
<input name="allowed_channels" value="{{.Package.AllowedChannels}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="is_active" type="checkbox" {{if .Package.IsActive}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.active"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-mt-4">
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
|
||||
<a class="ui button" href="{{.RepoLink}}/licenses">{{ctx.Locale.Tr "cancel"}}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -16,18 +16,6 @@
|
||||
{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "rss_feed"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if not .PageIsTagList}}
|
||||
{{if or (eq .RepoUpdatePlatform "joomla") (eq .RepoUpdatePlatform "both") (eq .RepoUpdatePlatform "")}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/updates.xml" target="_blank">
|
||||
{{svg "octicon-download" 16}} Joomla XML
|
||||
</a>
|
||||
{{end}}
|
||||
{{if or (eq .RepoUpdatePlatform "dolibarr") (eq .RepoUpdatePlatform "both")}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/updates/dolibarr.json" target="_blank">
|
||||
{{svg "octicon-download" 16}} Dolibarr JSON
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if and (not .PageIsTagList) .CanCreateRelease}}
|
||||
<a class="ui small primary button" href="{{$.RepoLink}}/releases/new{{if .PageIsSingleTag}}?tag={{.SingleReleaseTagName}}{{end}}">
|
||||
{{ctx.Locale.Tr "repo.release.new_release"}}
|
||||
|
||||
@@ -330,13 +330,6 @@
|
||||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
|
||||
<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
|
||||
<select name="wiki_visibility" class="ui dropdown">
|
||||
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
|
||||
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
@@ -396,13 +389,6 @@
|
||||
<input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{if .Repository.CloseIssuesViaCommitInAnyBranch}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>
|
||||
</div>
|
||||
<div class="inline field tw-mt-2">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
|
||||
<select name="issues_visibility" class="ui dropdown">
|
||||
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
|
||||
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isExternalTrackerGlobalDisabled}} disabled{{end}}"{{if $isExternalTrackerGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
@@ -501,34 +487,10 @@
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.releases"}}</label>
|
||||
<div class="ui checkbox{{if $isReleasesGlobalDisabled}} disabled{{end}}"{{if $isReleasesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_releases" type="checkbox" data-target="#releases_visibility_box" {{if $isReleasesEnabled}}checked{{end}}>
|
||||
<input class="enable-system" name="enable_releases" type="checkbox" {{if $isReleasesEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.releases_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4{{if not $isReleasesEnabled}} disabled{{end}}" id="releases_visibility_box">
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
|
||||
<select name="releases_visibility" class="ui dropdown">
|
||||
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
|
||||
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.unit_visibility_releases_help"}}</p>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.update_platform"}}</label>
|
||||
<select name="update_platform" class="ui dropdown">
|
||||
<option value="joomla" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.Platform "joomla") (eq .RepoUpdateConfig.Platform "")}}selected{{end}}>Joomla (updates.xml)</option>
|
||||
<option value="dolibarr" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "dolibarr")}}selected{{end}}>Dolibarr (JSON)</option>
|
||||
<option value="both" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "both")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.update_platform_both"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input name="require_update_key" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.RequireKey}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.require_update_key"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}}
|
||||
{{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}
|
||||
|
||||
+32
-32
@@ -1,23 +1,23 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 05.14.00
|
||||
VERSION: 05.04.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea dev build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea dev build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-dev</version>
|
||||
<version>05.04.00-dev</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.05.00-dev.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.04.00-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>dev</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -25,18 +25,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea alpha build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea alpha build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-alpha</version>
|
||||
<version>05.04.00-alpha</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.05.00-alpha.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.04.00-alpha.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -44,18 +44,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea beta build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea beta build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-beta</version>
|
||||
<version>05.04.00-beta</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.05.00-beta.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.04.00-beta.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>beta</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -63,18 +63,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea rc build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea rc build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00-rc</version>
|
||||
<version>05.04.00-rc</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title="MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
|
||||
<infourl title="Application - MokoGitea">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.05.00-rc.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.04.00-rc.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -82,18 +82,18 @@
|
||||
<targetplatform name="go" version=".*"/>
|
||||
</update>
|
||||
<update>
|
||||
<name>MokoGitea</name>
|
||||
<description>MokoGitea stable build.</description>
|
||||
<name>Application - MokoGitea</name>
|
||||
<description>Application - MokoGitea stable build.</description>
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.14.00</version>
|
||||
<creationDate>2026-05-31</creationDate>
|
||||
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<version>05.04.00</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title='Application - MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.14.00.zip</downloadurl>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.04.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>bec4bf5a1a841f8e72d9826451004db5d8afc70144231dfedc7fb01a6695955c</sha256>
|
||||
<sha256>4f2321bc5bab6fd2ef234b53ed5d511437d40a6ee8fb21bf097a4847c16eaf44</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user