Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b0ec5196a | |||
| 8741096fb4 | |||
| e5aa0c343d | |||
| ab3a65abdf | |||
| ba0d180e39 | |||
| 44107d6485 | |||
| 82c3c11053 | |||
| ff6d1bf3c9 | |||
| 9832f8a7bb | |||
| 9506a19ab8 | |||
| d0e3b3dfd8 | |||
| 3231ac2707 | |||
| 963fa6d384 | |||
| 48ff05d4b3 | |||
| 70699b4f2a | |||
| 99f5833c25 | |||
| 241596361e | |||
| 6405163e60 | |||
| 01011f6115 | |||
| ea10e8500c | |||
| 92bd3f7dc0 | |||
| 89fcbda623 | |||
| dd6ee750f0 | |||
| ffb9363e3e | |||
| a1ceac6396 | |||
| a22fa57ab1 | |||
| 68736c78a1 | |||
| cfea80d3ca | |||
| e2c738a8d8 | |||
| 6c7a6e4061 | |||
| 95d93da2bc | |||
| 02424c3f75 | |||
| 449af83e2b | |||
| 3ad37e48e1 | |||
| 021a054348 | |||
| ead620daf9 | |||
| 0add8bda72 | |||
| bd81616432 | |||
| 02f3ed88f1 | |||
| 0fb0aea719 | |||
| eca929f680 | |||
| b65b155446 | |||
| de52ad0fbc | |||
| 1dfa5d8079 | |||
| 70793075fc | |||
| 2799558040 | |||
| d85ae6aa21 | |||
| 1b9b82d59a | |||
| 37322e4212 | |||
| 2f9097a254 | |||
| ce3af35c40 | |||
| 0a3cd3115f | |||
| 3e31b662a6 | |||
| 774ea3842b | |||
| 0e7d3c4a34 |
@@ -0,0 +1,283 @@
|
||||
# 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: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found — aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- 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
|
||||
+277
-236
@@ -1,236 +1,277 @@
|
||||
# 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://code.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://code.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
|
||||
# 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: 09.23.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: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found in source files"
|
||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- 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
|
||||
|
||||
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||
report-issues:
|
||||
name: Report Issues
|
||||
runs-on: ubuntu-latest
|
||||
needs: [branch-policy, validate]
|
||||
if: >-
|
||||
always() &&
|
||||
needs.validate.result == 'failure'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: automation/ci-issue-reporter.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: "File issue for PR validation failure"
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
chmod +x automation/ci-issue-reporter.sh
|
||||
./automation/ci-issue-reporter.sh \
|
||||
--gate "PR Validation" \
|
||||
--workflow "PR Check" \
|
||||
--severity error \
|
||||
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||
|
||||
@@ -0,0 +1,711 @@
|
||||
# ============================================================================
|
||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# This file is part of a Moko Consulting project.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Validation
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||
# ============================================================================
|
||||
|
||||
name: "Generic: Repo Health"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
description: 'Validation profile: all, scripts, or repo'
|
||||
required: true
|
||||
default: all
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- scripts
|
||||
- repo
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# Scripts governance policy
|
||||
SCRIPTS_REQUIRED_DIRS:
|
||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||
|
||||
# Repo health policy
|
||||
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
|
||||
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
||||
REPO_DISALLOWED_DIRS:
|
||||
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||
|
||||
# Extended checks toggles
|
||||
EXTENDED_CHECKS: "true"
|
||||
|
||||
# File / directory variables
|
||||
DOCS_INDEX: docs/docs-index.md
|
||||
SCRIPT_DIR: scripts
|
||||
WORKFLOWS_DIR: .mokogitea/workflows
|
||||
SHELLCHECK_PATTERN: '*.sh'
|
||||
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
access_check:
|
||||
name: Access control
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
outputs:
|
||||
allowed: ${{ steps.perm.outputs.allowed }}
|
||||
permission: ${{ steps.perm.outputs.permission }}
|
||||
|
||||
steps:
|
||||
- name: Check actor permission (admin only)
|
||||
id: perm
|
||||
env:
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ALLOWED=false
|
||||
PERMISSION=unknown
|
||||
METHOD=""
|
||||
|
||||
# Hardcoded authorized users — always allowed
|
||||
case "$ACTOR" in
|
||||
jmiller|gitea-actions[bot])
|
||||
ALLOWED=true
|
||||
PERMISSION=admin
|
||||
METHOD="hardcoded allowlist"
|
||||
;;
|
||||
*)
|
||||
# Detect platform and check permissions via API
|
||||
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
||||
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
||||
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
|
||||
ALLOWED=true
|
||||
fi
|
||||
METHOD="collaborator API"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "## Access Authorization"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|-------|-------|"
|
||||
echo "| **Actor** | \`${ACTOR}\` |"
|
||||
echo "| **Repository** | \`${REPO}\` |"
|
||||
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||
echo "| **Method** | ${METHOD} |"
|
||||
echo "| **Authorized** | ${ALLOWED} |"
|
||||
echo ""
|
||||
if [ "$ALLOWED" = "true" ]; then
|
||||
echo "${ACTOR} authorized (${METHOD})"
|
||||
else
|
||||
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
|
||||
fi
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Deny execution when not permitted
|
||||
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
|
||||
scripts_governance:
|
||||
name: Scripts governance
|
||||
needs: access_check
|
||||
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scripts folder checks
|
||||
env:
|
||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
profile="${PROFILE_RAW:-all}"
|
||||
case "${profile}" in
|
||||
all|scripts|repo) ;;
|
||||
*)
|
||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${profile}" = 'repo' ]; then
|
||||
{
|
||||
printf '%s\n' '### Scripts governance'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
printf '%s\n' 'Status: SKIPPED'
|
||||
printf '%s\n' 'Reason: profile excludes scripts governance'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -d "${SCRIPT_DIR}" ]; then
|
||||
{
|
||||
printf '%s\n' '### Scripts governance'
|
||||
printf '%s\n' 'Status: OK (advisory)'
|
||||
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
|
||||
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||
|
||||
missing_dirs=()
|
||||
unapproved_dirs=()
|
||||
|
||||
for d in "${required_dirs[@]}"; do
|
||||
req="${d%/}"
|
||||
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
|
||||
done
|
||||
|
||||
while IFS= read -r d; do
|
||||
allowed=false
|
||||
for a in "${allowed_dirs[@]}"; do
|
||||
a_norm="${a%/}"
|
||||
[ "${d%/}" = "${a_norm}" ] && allowed=true
|
||||
done
|
||||
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
|
||||
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
|
||||
|
||||
{
|
||||
printf '%s\n' '### Scripts governance'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
printf '%s\n' '| Area | Status | Notes |'
|
||||
printf '%s\n' '|---|---|---|'
|
||||
|
||||
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
|
||||
else
|
||||
printf '%s\n' '| Required directories | OK | All required subfolders present |'
|
||||
fi
|
||||
|
||||
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
|
||||
else
|
||||
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
|
||||
fi
|
||||
|
||||
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
|
||||
printf '\n'
|
||||
|
||||
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||
printf '%s\n' 'Missing required script directories:'
|
||||
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '\n'
|
||||
else
|
||||
printf '%s\n' 'Missing required script directories: none.'
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||
printf '%s\n' 'Unapproved script directories detected:'
|
||||
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '\n'
|
||||
else
|
||||
printf '%s\n' 'Unapproved script directories detected: none.'
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
printf '%s\n' 'Scripts governance completed in advisory mode.'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
repo_health:
|
||||
name: Repository health
|
||||
needs: access_check
|
||||
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Repository health checks
|
||||
env:
|
||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
profile="${PROFILE_RAW:-all}"
|
||||
case "${profile}" in
|
||||
all|scripts|repo) ;;
|
||||
*)
|
||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${profile}" = 'scripts' ]; then
|
||||
{
|
||||
printf '%s\n' '### Repository health'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
printf '%s\n' 'Status: SKIPPED'
|
||||
printf '%s\n' 'Reason: profile excludes repository health'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
|
||||
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
|
||||
|
||||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||
SOURCE_DIR=""
|
||||
if [ -d "src" ]; then
|
||||
SOURCE_DIR="src"
|
||||
elif [ -d "htdocs" ]; then
|
||||
SOURCE_DIR="htdocs"
|
||||
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||
# Platform/tooling repos don't need src/
|
||||
SOURCE_DIR=""
|
||||
else
|
||||
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||
fi
|
||||
|
||||
for item in "${required_artifacts[@]}"; do
|
||||
if printf '%s' "${item}" | grep -q '/$'; then
|
||||
d="${item%/}"
|
||||
[ ! -d "${d}" ] && missing_required+=("${item}")
|
||||
else
|
||||
[ ! -f "${item}" ] && missing_required+=("${item}")
|
||||
fi
|
||||
done
|
||||
|
||||
for f in "${optional_files[@]}"; do
|
||||
if printf '%s' "${f}" | grep -q '/$'; then
|
||||
d="${f%/}"
|
||||
[ ! -d "${d}" ] && missing_optional+=("${f}")
|
||||
else
|
||||
[ ! -f "${f}" ] && missing_optional+=("${f}")
|
||||
fi
|
||||
done
|
||||
|
||||
for d in "${disallowed_dirs[@]}"; do
|
||||
d_norm="${d%/}"
|
||||
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
|
||||
done
|
||||
|
||||
for f in "${disallowed_files[@]}"; do
|
||||
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
|
||||
done
|
||||
|
||||
git fetch origin --prune
|
||||
|
||||
dev_paths=()
|
||||
dev_branches=()
|
||||
|
||||
while IFS= read -r b; do
|
||||
name="${b#origin/}"
|
||||
if [ "${name}" = 'dev' ]; then
|
||||
dev_branches+=("${name}")
|
||||
else
|
||||
dev_paths+=("${name}")
|
||||
fi
|
||||
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||
|
||||
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||
missing_required+=("dev or dev/* branch")
|
||||
fi
|
||||
|
||||
content_warnings=()
|
||||
|
||||
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
|
||||
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
|
||||
fi
|
||||
|
||||
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
|
||||
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
|
||||
fi
|
||||
|
||||
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
|
||||
content_warnings+=("LICENSE does not look like a GPL text")
|
||||
fi
|
||||
|
||||
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
|
||||
content_warnings+=("README.md missing expected brand keyword")
|
||||
fi
|
||||
|
||||
export PROFILE_RAW="${profile}"
|
||||
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
|
||||
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
||||
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||
|
||||
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
|
||||
|
||||
{
|
||||
printf '%s\n' '### Repository health'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
printf '%s\n' '| Metric | Value |'
|
||||
printf '%s\n' '|---|---|'
|
||||
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
|
||||
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
|
||||
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
|
||||
printf '\n'
|
||||
|
||||
printf '%s\n' '### Guardrails report (JSON)'
|
||||
printf '%s\n' '```json'
|
||||
printf '%s\n' "${report_json}"
|
||||
printf '%s\n' '```'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
if [ "${#missing_required[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Missing required repo artifacts'
|
||||
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Missing optional repo artifacts'
|
||||
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
if [ "${#content_warnings[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Repo content warnings'
|
||||
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
# -- Joomla-specific checks --
|
||||
joomla_findings=()
|
||||
|
||||
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
||||
if [ -z "${MANIFEST}" ]; then
|
||||
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
||||
else
|
||||
if ! grep -qP '<version>' "${MANIFEST}"; then
|
||||
joomla_findings+=("XML manifest: <version> tag missing")
|
||||
fi
|
||||
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
||||
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
||||
fi
|
||||
if ! grep -qP '<name>' "${MANIFEST}"; then
|
||||
joomla_findings+=("XML manifest: <name> tag missing")
|
||||
fi
|
||||
if ! grep -qP '<author>' "${MANIFEST}"; then
|
||||
joomla_findings+=("XML manifest: <author> tag missing")
|
||||
fi
|
||||
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
||||
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
||||
fi
|
||||
fi
|
||||
|
||||
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
||||
if [ "${INI_COUNT}" -eq 0 ]; then
|
||||
joomla_findings+=("No .ini language files found")
|
||||
fi
|
||||
|
||||
if [ ! -f 'updates.xml' ]; then
|
||||
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||
fi
|
||||
|
||||
if [ -n "${SOURCE_DIR}" ]; then
|
||||
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||
for dir in "${INDEX_DIRS[@]}"; do
|
||||
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Joomla extension checks'
|
||||
printf '%s\n' '| Check | Status |'
|
||||
printf '%s\n' '|---|---|'
|
||||
for f in "${joomla_findings[@]}"; do
|
||||
printf '%s\n' "| ${f} | Warning |"
|
||||
done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
{
|
||||
printf '%s\n' '### Joomla extension checks'
|
||||
printf '%s\n' 'All Joomla-specific checks passed.'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
extended_enabled="${EXTENDED_CHECKS:-true}"
|
||||
extended_findings=()
|
||||
|
||||
if [ "${extended_enabled}" = 'true' ]; then
|
||||
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
||||
:
|
||||
else
|
||||
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
||||
fi
|
||||
|
||||
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
||||
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
||||
if [ -n "${bad_refs}" ]; then
|
||||
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
|
||||
{
|
||||
printf '%s\n' '### Workflow pinning advisory'
|
||||
printf '%s\n' 'Found uses: entries pinned to main/master:'
|
||||
printf '%s\n' '```'
|
||||
printf '%s\n' "${bad_refs}"
|
||||
printf '%s\n' '```'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${DOCS_INDEX}" ]; then
|
||||
missing_links=""
|
||||
while IFS= read -r docline; do
|
||||
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
|
||||
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
|
||||
linkpath="${link%%#*}"
|
||||
linkpath="${linkpath%%\?*}"
|
||||
[ -z "$linkpath" ] && continue
|
||||
if [ "${linkpath:0:1}" = "/" ]; then
|
||||
testpath="${linkpath#/}"
|
||||
else
|
||||
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
|
||||
fi
|
||||
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
|
||||
done
|
||||
done < "${DOCS_INDEX}"
|
||||
if [ -n "${missing_links}" ]; then
|
||||
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||
{
|
||||
printf '%s\n' '### Docs index link integrity'
|
||||
printf '%s\n' 'Broken relative links:'
|
||||
for bl in ${missing_links}; do
|
||||
printf '%s\n' "- ${bl}"
|
||||
done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d "${SCRIPT_DIR}" ]; then
|
||||
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y shellcheck >/dev/null
|
||||
fi
|
||||
|
||||
sc_out=''
|
||||
while IFS= read -r shf; do
|
||||
[ -z "${shf}" ] && continue
|
||||
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
|
||||
if [ -n "${out_one}" ]; then
|
||||
sc_out="${sc_out}${out_one}\n"
|
||||
fi
|
||||
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
|
||||
|
||||
if [ -n "${sc_out}" ]; then
|
||||
extended_findings+=("ShellCheck warnings detected (advisory)")
|
||||
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
|
||||
{
|
||||
printf '%s\n' '### ShellCheck (advisory)'
|
||||
printf '%s\n' '```'
|
||||
printf '%s\n' "${sc_head}"
|
||||
printf '%s\n' '```'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
fi
|
||||
|
||||
spdx_missing=()
|
||||
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
||||
spdx_args=()
|
||||
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
|
||||
|
||||
while IFS= read -r f; do
|
||||
[ -z "${f}" ] && continue
|
||||
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
|
||||
spdx_missing+=("${f}")
|
||||
fi
|
||||
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
|
||||
|
||||
if [ "${#spdx_missing[@]}" -gt 0 ]; then
|
||||
extended_findings+=("SPDX header missing in some tracked files (advisory)")
|
||||
{
|
||||
printf '%s\n' '### SPDX header advisory'
|
||||
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
|
||||
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
stale_cutoff_days=180
|
||||
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
|
||||
if [ -n "${stale_branches}" ]; then
|
||||
extended_findings+=("Stale remote branches detected (advisory)")
|
||||
{
|
||||
printf '%s\n' '### Git hygiene advisory'
|
||||
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
|
||||
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
printf '%s\n' '### Guardrails coverage matrix'
|
||||
printf '%s\n' '| Domain | Status | Notes |'
|
||||
printf '%s\n' '|---|---|---|'
|
||||
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
|
||||
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||
if [ "${extended_enabled}" = 'true' ]; then
|
||||
if [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
|
||||
else
|
||||
printf '%s\n' '| Extended checks | OK | No findings |'
|
||||
fi
|
||||
else
|
||||
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
|
||||
fi
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Extended findings (advisory)'
|
||||
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
|
||||
site-health:
|
||||
name: Site Health
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
|
||||
- name: Uptime check
|
||||
if: env.URLS != ''
|
||||
run: |
|
||||
echo "$URLS" > /tmp/urls.txt
|
||||
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
|
||||
rm -f /tmp/urls.txt
|
||||
env:
|
||||
URLS: ${{ vars.MONITORED_URLS }}
|
||||
|
||||
- name: SSL certificate check
|
||||
if: env.DOMAINS != ''
|
||||
run: |
|
||||
echo "$DOMAINS" > /tmp/domains.txt
|
||||
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
|
||||
rm -f /tmp/domains.txt
|
||||
env:
|
||||
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Issue Reporter — file issues for failed gates
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
report-issues:
|
||||
name: "Report Issues"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [access_check, scripts_governance, repo_health]
|
||||
if: >-
|
||||
always() &&
|
||||
(needs.scripts_governance.result == 'failure' ||
|
||||
needs.repo_health.result == 'failure')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: automation/ci-issue-reporter.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: "File issues for failed gates"
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
chmod +x automation/ci-issue-reporter.sh
|
||||
REPORTER="./automation/ci-issue-reporter.sh"
|
||||
WF="Repo Health"
|
||||
|
||||
report_gate() {
|
||||
local gate="$1" result="$2" details="$3"
|
||||
if [ "$result" = "failure" ]; then
|
||||
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||
fi
|
||||
}
|
||||
|
||||
report_gate "Scripts Governance" \
|
||||
"${{ needs.scripts_governance.result }}" \
|
||||
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||
|
||||
report_gate "Repository Health" \
|
||||
"${{ needs.repo_health.result }}" \
|
||||
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||
+36
-6
@@ -10,11 +10,11 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Package archiving with soft-delete and collapsible archived section
|
||||
* Search keys by customer, domain, key number, email, or payment ref
|
||||
* Download gating (none/prerelease/all modes)
|
||||
* Feed visibility (public/no-download/hidden modes)
|
||||
* Domain lock grace period (DomainLockHours)
|
||||
* RepoScope enforcement — packages scoped to specific repos
|
||||
* Configurable license key prefix per organization
|
||||
* Manual release-to-stream mapping with UI selector
|
||||
* Joomla changelog XML endpoint (/changelog.xml)
|
||||
* WordPress PUC-compatible update feed (/updates/wordpress.json)
|
||||
* SHA256 checksums from sidecar files in Joomla updates.xml
|
||||
* Joomla-standard tag values (dev/alpha/beta/rc/stable)
|
||||
* Double confirmation modals for permanent deletion
|
||||
@@ -23,12 +23,42 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* API: package CRUD, key revoke, key renew, settings GET/PUT
|
||||
* API: purchase webhook with PaymentRef idempotency
|
||||
* API: public validation endpoint (no auth)
|
||||
* Migration v340: all new columns synced
|
||||
* feat(updates): infourl defaults to release listing page
|
||||
* Migration v340-v342: all new columns synced
|
||||
* feat(updates): 7 platform update feeds
|
||||
* Joomla XML with downloadkey, SHA256, changelog URL
|
||||
* Dolibarr JSON with channel filtering
|
||||
* WordPress PUC-compatible JSON (plugin-update-checker)
|
||||
* Composer packages.json
|
||||
* PrestaShop module update XML
|
||||
* Drupal update status XML
|
||||
* WHMCS module update JSON
|
||||
* feat(updates): feed always public — downloads gated separately
|
||||
* feat(updates): stream-name tags supported alongside version tags
|
||||
* feat(updates): version extraction via regex from release titles
|
||||
* feat(updates): infourl defaults to release listing / support URL
|
||||
* feat(updates): downloadkey prefix matches Akeeba pattern (dlid=)
|
||||
* feat(orgs): enterprise sub-org hierarchy with parent-child relationships
|
||||
* feat(repos): three-level visibility — Public (200), Private (403), Hidden (404)
|
||||
* feat(settings): separate licensing settings page (/settings/licensing)
|
||||
* feat(settings): advanced settings on dedicated page (/settings/advanced)
|
||||
* feat(settings): section headers with dividers and icons
|
||||
* feat(ui): icons on all settings navbars (repo, org, user, admin)
|
||||
* feat(ui): styled 403 Access Denied page with inline login form
|
||||
* feat(ui): open-in-new-tab button on feed URLs
|
||||
* SECURITY
|
||||
* fix(security): ownership guards on all API handlers (cross-org prevention)
|
||||
* fix(security): RepoScope JSON parsing (substring matching bug)
|
||||
* fix(security): CSRF tokens in delete confirmation modals
|
||||
* fix(security): XSS escaping in WordPress changelog HTML
|
||||
* fix(security): require login for licenses and actions pages
|
||||
* fix(security): 403 for all users on private repos (not 404)
|
||||
* fix(security): licensed private repos allow release viewing for signed-in users
|
||||
* fix(security): anonymous download access respects download_gating setting
|
||||
* FIXES
|
||||
* fix(licenses): expanded delete permissions to org owners + site admins
|
||||
* fix(licenses): no-download mode shows release notes but hides files
|
||||
* fix(licenses): releases require login in hidden feed visibility mode
|
||||
* fix(licenses): explicit xorm column names for UpdateStreamConfig fields
|
||||
* fix(licenses): feed always public when licensing enabled
|
||||
* fix(build): permanent fixes for AI migration, feed/file.go, unused imports
|
||||
|
||||
## [v1.26.1-moko.05.15.00] - 2026-05-31
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Automation.CI
|
||||
# INGROUP: moko-platform.Automation
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /automation/ci-issue-reporter.sh
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||
# Deduplicates by searching open issues with the "ci-auto" label
|
||||
# whose title matches the gate. If a matching issue exists, a comment
|
||||
# is appended instead of opening a duplicate.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||
REPO="${GITHUB_REPOSITORY:-}"
|
||||
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||
LABEL_NAME="ci-auto"
|
||||
LABEL_COLOR="#e11d48"
|
||||
|
||||
GATE=""
|
||||
DETAILS=""
|
||||
SEVERITY="error"
|
||||
WORKFLOW=""
|
||||
|
||||
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||
|
||||
Required:
|
||||
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||
--details Human-readable failure description
|
||||
|
||||
Optional:
|
||||
--severity "error" (default) or "warning"
|
||||
--workflow Workflow name for the issue title
|
||||
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||
--run-url URL to the CI run (auto-detected from env)
|
||||
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||
--url Gitea base URL (default: \$GITEA_URL)
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--gate) GATE="$2"; shift 2 ;;
|
||||
--details) DETAILS="$2"; shift 2 ;;
|
||||
--severity) SEVERITY="$2"; shift 2 ;;
|
||||
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||
--repo) REPO="$2"; shift 2 ;;
|
||||
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||
--url) GITEA_URL="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||
|
||||
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||
|
||||
# ── Build title ─────────────────────────────────────────────────────────────
|
||||
if [[ -n "$WORKFLOW" ]]; then
|
||||
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||
else
|
||||
TITLE="[CI] ${GATE} failed"
|
||||
fi
|
||||
|
||||
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||
ensure_label() {
|
||||
local exists
|
||||
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/labels" 2>/dev/null || echo "000")
|
||||
|
||||
if [[ "$exists" == "200" ]]; then
|
||||
# Check if label already exists
|
||||
local found
|
||||
found=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/labels" 2>/dev/null \
|
||||
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||
|
||||
if [[ -z "$found" ]]; then
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/labels" \
|
||||
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||
> /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Search for existing open issue ──────────────────────────────────────────
|
||||
find_existing_issue() {
|
||||
# URL-encode the gate name for the query
|
||||
local query
|
||||
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||
|
||||
local response
|
||||
response=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||
2>/dev/null || echo "[]")
|
||||
|
||||
# Extract the first matching issue number
|
||||
echo "$response" \
|
||||
| grep -oP '"number":\s*\K[0-9]+' \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# ── Build issue body ────────────────────────────────────────────────────────
|
||||
build_body() {
|
||||
local severity_badge
|
||||
if [[ "$SEVERITY" == "error" ]]; then
|
||||
severity_badge="**Severity:** Error"
|
||||
else
|
||||
severity_badge="**Severity:** Warning"
|
||||
fi
|
||||
|
||||
cat <<BODY
|
||||
## CI Gate Failure: ${GATE}
|
||||
|
||||
${severity_badge}
|
||||
**Workflow:** ${WORKFLOW:-unknown}
|
||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||
**Run:** [View CI run](${RUN_URL})
|
||||
|
||||
### Details
|
||||
|
||||
${DETAILS}
|
||||
|
||||
### Resolution
|
||||
|
||||
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||
|
||||
---
|
||||
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||
BODY
|
||||
}
|
||||
|
||||
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||
build_comment() {
|
||||
cat <<COMMENT
|
||||
### CI failure recurrence
|
||||
|
||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||
**Run:** [View CI run](${RUN_URL})
|
||||
|
||||
${DETAILS}
|
||||
COMMENT
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
ensure_label
|
||||
|
||||
EXISTING=$(find_existing_issue)
|
||||
|
||||
if [[ -n "$EXISTING" ]]; then
|
||||
# Append comment to existing issue
|
||||
COMMENT_BODY=$(build_comment)
|
||||
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||
import sys, json
|
||||
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||
|
||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${EXISTING}/comments" \
|
||||
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||
|
||||
if [[ "$HTTP" == "201" ]]; then
|
||||
echo "Commented on existing issue #${EXISTING}"
|
||||
else
|
||||
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||
fi
|
||||
else
|
||||
# Create new issue
|
||||
ISSUE_BODY=$(build_body)
|
||||
ISSUE_JSON=$(python3 -c "
|
||||
import sys, json
|
||||
body = sys.stdin.read()
|
||||
print(json.dumps({
|
||||
'title': sys.argv[1],
|
||||
'body': body,
|
||||
'labels': []
|
||||
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||
|
||||
# Create the issue
|
||||
RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues" \
|
||||
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||
|
||||
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||
|
||||
if [[ -n "$ISSUE_NUM" ]]; then
|
||||
# Apply label (separate call — more reliable across Gitea versions)
|
||||
LABEL_ID=$(curl -sf \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/labels" 2>/dev/null \
|
||||
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||
| head -1 || true)
|
||||
|
||||
if [[ -n "$LABEL_ID" ]]; then
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||
> /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||
else
|
||||
echo "WARNING: Failed to create issue"
|
||||
echo "Response: ${RESPONSE}"
|
||||
fi
|
||||
fi
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
@@ -31,7 +33,7 @@ type LicenseKey struct {
|
||||
LicenseeEmail string `xorm:""` // customer email
|
||||
DomainRestriction string `xorm:"TEXT"` // comma-separated allowed domains
|
||||
MaxSites int `xorm:"NOT NULL DEFAULT 0"` // 0 = use package default
|
||||
PaymentRef string `xorm:"UNIQUE"` // idempotency key from payment system
|
||||
PaymentRef string `xorm:""` // idempotency key from payment system
|
||||
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
|
||||
@@ -46,14 +48,18 @@ func (LicenseKey) TableName() string {
|
||||
return "license_key"
|
||||
}
|
||||
|
||||
// GenerateKeyString creates a random license key in MOKO-XXXX-XXXX-XXXX-XXXX format.
|
||||
func GenerateKeyString() (string, error) {
|
||||
// GenerateKeyString creates a random license key in PREFIX-XXXX-XXXX-XXXX-XXXX format.
|
||||
// If prefix is empty, defaults to "MOKO".
|
||||
func GenerateKeyString(prefix string) (string, error) {
|
||||
if prefix == "" {
|
||||
prefix = "MOKO"
|
||||
}
|
||||
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
|
||||
h := strings.ToUpper(hex.EncodeToString(b))
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s", prefix, h[0:4], h[4:8], h[8:12], h[12:16]), nil
|
||||
}
|
||||
|
||||
// HashKey returns the SHA-256 hash of a raw key string.
|
||||
@@ -63,8 +69,14 @@ func HashKey(rawKey string) string {
|
||||
}
|
||||
|
||||
// CreateLicenseKey generates a new key, stores it in plaintext and hashed, and returns the raw key.
|
||||
// The prefix is looked up from the org's update stream config.
|
||||
func CreateLicenseKey(ctx context.Context, key *LicenseKey) (rawKey string, err error) {
|
||||
rawKey, err = GenerateKeyString()
|
||||
prefix := ""
|
||||
cfg := GetEffectiveConfig(ctx, key.OwnerID, 0)
|
||||
if cfg != nil && cfg.KeyPrefix != "" {
|
||||
prefix = cfg.KeyPrefix
|
||||
}
|
||||
rawKey, err = GenerateKeyString(prefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GenerateKeyString: %w", err)
|
||||
}
|
||||
@@ -332,21 +344,30 @@ func ValidateLicenseKeyForRepo(ctx context.Context, rawKey, domain string, repoI
|
||||
|
||||
// Check repo scope.
|
||||
if pkg.RepoScope != "" && pkg.RepoScope != "all" {
|
||||
// RepoScope is either a single repo ID or a JSON array of IDs.
|
||||
scopeStr := pkg.RepoScope
|
||||
repoIDStr := fmt.Sprintf("%d", repoID)
|
||||
allowed := false
|
||||
|
||||
if strings.HasPrefix(scopeStr, "[") {
|
||||
// JSON array format: ["1","2","3"]
|
||||
if !strings.Contains(scopeStr, repoIDStr) {
|
||||
return nil, nil, fmt.Errorf("license key not valid for this repository")
|
||||
// JSON array format: parse properly to avoid substring matching bugs.
|
||||
var ids []int64
|
||||
if err := json.Unmarshal([]byte(scopeStr), &ids); err == nil {
|
||||
for _, id := range ids {
|
||||
if id == repoID {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single repo ID.
|
||||
if scopeStr != repoIDStr {
|
||||
return nil, nil, fmt.Errorf("license key not valid for this repository")
|
||||
// Single repo ID string.
|
||||
if id, err := strconv.ParseInt(scopeStr, 10, 64); err == nil {
|
||||
allowed = id == repoID
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
return nil, nil, fmt.Errorf("license key not valid for this repository")
|
||||
}
|
||||
}
|
||||
|
||||
return key, pkg, nil
|
||||
|
||||
@@ -5,8 +5,10 @@ package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
org_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
@@ -79,6 +81,32 @@ func ListLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage,
|
||||
return pkgs, db.GetEngine(ctx).Where("owner_id = ? AND is_archived = ?", ownerID, false).Find(&pkgs)
|
||||
}
|
||||
|
||||
// ListLicensePackagesWithAncestors returns packages from the org and all parent orgs.
|
||||
func ListLicensePackagesWithAncestors(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
|
||||
ancestorIDs := org_model.GetAncestorOrgIDs(ctx, ownerID)
|
||||
pkgs := make([]*LicensePackage, 0, 10)
|
||||
return pkgs, db.GetEngine(ctx).In("owner_id", ancestorIDs).Where("is_archived = ?", false).Find(&pkgs)
|
||||
}
|
||||
|
||||
// ListLicenseKeysWithAncestors returns keys from the org and all parent orgs.
|
||||
func ListLicenseKeysWithAncestors(ctx context.Context, ownerID int64) ([]*LicenseKey, error) {
|
||||
ancestorIDs := org_model.GetAncestorOrgIDs(ctx, ownerID)
|
||||
keys := make([]*LicenseKey, 0, 20)
|
||||
return keys, db.GetEngine(ctx).In("owner_id", ancestorIDs).Find(&keys)
|
||||
}
|
||||
|
||||
// SearchLicenseKeysWithAncestors searches keys across the org and all parent orgs.
|
||||
func SearchLicenseKeysWithAncestors(ctx context.Context, ownerID int64, query string) ([]*LicenseKey, error) {
|
||||
ancestorIDs := org_model.GetAncestorOrgIDs(ctx, ownerID)
|
||||
keys := make([]*LicenseKey, 0, 20)
|
||||
like := "%" + strings.ToLower(query) + "%"
|
||||
return keys, db.GetEngine(ctx).
|
||||
In("owner_id", ancestorIDs).
|
||||
And("(LOWER(key_prefix) LIKE ? OR LOWER(key_raw) LIKE ? OR LOWER(licensee_name) LIKE ? OR LOWER(licensee_email) LIKE ? OR LOWER(domain_restriction) LIKE ? OR LOWER(payment_ref) LIKE ?)",
|
||||
like, like, like, like, like, like).
|
||||
Find(&keys)
|
||||
}
|
||||
|
||||
// ListArchivedLicensePackages returns archived packages for the given owner.
|
||||
func ListArchivedLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
|
||||
pkgs := make([]*LicensePackage, 0, 10)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ReleaseStreamMap))
|
||||
}
|
||||
|
||||
// ReleaseStreamMap manually assigns a release to an update stream.
|
||||
// When present, overrides automatic stream detection from tag names.
|
||||
type ReleaseStreamMap struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ReleaseID int64 `xorm:"UNIQUE NOT NULL INDEX"` // FK to release
|
||||
RepoID int64 `xorm:"NOT NULL INDEX"` // for fast repo-scoped queries
|
||||
StreamName string `xorm:"NOT NULL"` // e.g. "stable", "release-candidate"
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
func (ReleaseStreamMap) TableName() string {
|
||||
return "release_stream_map"
|
||||
}
|
||||
|
||||
// SetReleaseStream assigns or updates the stream for a release.
|
||||
func SetReleaseStream(ctx context.Context, releaseID, repoID int64, streamName string) error {
|
||||
existing := new(ReleaseStreamMap)
|
||||
has, err := db.GetEngine(ctx).Where("release_id = ?", releaseID).Get(existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
existing.StreamName = streamName
|
||||
_, err = db.GetEngine(ctx).ID(existing.ID).Cols("stream_name").Update(existing)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).Insert(&ReleaseStreamMap{
|
||||
ReleaseID: releaseID,
|
||||
RepoID: repoID,
|
||||
StreamName: streamName,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetReleaseStream returns the manually assigned stream for a release, or empty string.
|
||||
func GetReleaseStream(ctx context.Context, releaseID int64) string {
|
||||
m := new(ReleaseStreamMap)
|
||||
has, err := db.GetEngine(ctx).Where("release_id = ?", releaseID).Get(m)
|
||||
if err != nil || !has {
|
||||
return ""
|
||||
}
|
||||
return m.StreamName
|
||||
}
|
||||
|
||||
// GetStreamMapForRepo returns all manual stream assignments for a repo.
|
||||
func GetStreamMapForRepo(ctx context.Context, repoID int64) (map[int64]string, error) {
|
||||
var maps []ReleaseStreamMap
|
||||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&maps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[int64]string, len(maps))
|
||||
for _, m := range maps {
|
||||
result[m.ReleaseID] = m.StreamName
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ResolveReleaseStream returns the stream for a release: manual mapping first, auto-detect fallback.
|
||||
func ResolveReleaseStream(ctx context.Context, releaseID int64, tagName string, isPrerelease bool, streams []StreamDef) string {
|
||||
if manual := GetReleaseStream(ctx, releaseID); manual != "" {
|
||||
return manual
|
||||
}
|
||||
return MatchStreamFromTag(tagName, isPrerelease, streams)
|
||||
}
|
||||
|
||||
// DeleteReleaseStream removes the manual stream assignment for a release.
|
||||
func DeleteReleaseStream(ctx context.Context, releaseID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("release_id = ?", releaseID).Delete(new(ReleaseStreamMap))
|
||||
return err
|
||||
}
|
||||
@@ -23,26 +23,27 @@ 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
|
||||
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla' 'stream_mode'"` // joomla, custom
|
||||
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both, wordpress, prestashop, drupal
|
||||
LicensingEnabled bool `xorm:"NOT NULL DEFAULT false"` // master toggle for licensing system
|
||||
RequireKey bool `xorm:"NOT NULL DEFAULT false"` // require license key for update feed
|
||||
FeedVisibility string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'public'"` // public, no-download, hidden
|
||||
DownloadGating string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'none'"` // none, all, prerelease
|
||||
SupportURL string `xorm:"TEXT"` // wiki or external support page URL
|
||||
LicensingEnabled bool `xorm:"NOT NULL DEFAULT false 'licensing_enabled'"` // master toggle
|
||||
RequireKey bool `xorm:"NOT NULL DEFAULT false 'require_key'"` // require license key for update feed
|
||||
FeedVisibility string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'public' 'feed_visibility'"` // public, no-download, hidden
|
||||
DownloadGating string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'none' 'download_gating'"` // none, all, prerelease
|
||||
SupportURL string `xorm:"TEXT 'support_url'"` // wiki or external support page URL
|
||||
KeyPrefix string `xorm:"VARCHAR(20) 'key_prefix'"` // org-specific license key prefix (e.g. "ACME")
|
||||
// Extension metadata — used in update feed generation.
|
||||
ExtensionName string `xorm:"TEXT"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
|
||||
DisplayName string `xorm:"TEXT"` // human-readable name (e.g. "Package - MokoWaaS")
|
||||
ExtensionName string `xorm:"TEXT 'extension_name'"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
|
||||
DisplayName string `xorm:"TEXT 'display_name'"` // human-readable name (e.g. "Package - MokoWaaS")
|
||||
Description string `xorm:"TEXT"` // short description for update feeds
|
||||
ExtensionType string `xorm:"VARCHAR(50)"` // component, module, plugin, package, template, library
|
||||
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library
|
||||
Maintainer string `xorm:"TEXT"` // maintainer/author name
|
||||
MaintainerURL string `xorm:"TEXT"` // maintainer website
|
||||
InfoURL string `xorm:"TEXT"` // extension info/product page URL
|
||||
TargetVersion string `xorm:"TEXT"` // target platform version regex (e.g. "(5|6)\..*")
|
||||
PHPMinimum string `xorm:"VARCHAR(20)"` // minimum PHP version (e.g. "8.1")
|
||||
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
|
||||
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
|
||||
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex (e.g. "(5|6)\..*")
|
||||
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version (e.g. "8.1")
|
||||
// CustomStreams is a JSON array of stream definitions.
|
||||
// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
|
||||
CustomStreams string `xorm:"TEXT"`
|
||||
CustomStreams string `xorm:"TEXT 'custom_streams'"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
@@ -171,10 +172,20 @@ func SaveConfig(ctx context.Context, cfg *UpdateStreamConfig) error {
|
||||
}
|
||||
|
||||
// MatchStreamFromTag determines which stream a tag belongs to based on the given stream definitions.
|
||||
// Supports two conventions:
|
||||
// - Stream-name tags: tag IS the stream name (e.g. "stable", "release-candidate", "development")
|
||||
// - Version tags: tag contains a version + optional suffix (e.g. "v1.0.0", "v1.0.0-rc1")
|
||||
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").
|
||||
// First: check if the tag name directly matches a stream name (stream-name convention).
|
||||
for _, s := range streams {
|
||||
if strings.EqualFold(s.Name, tagName) {
|
||||
return s.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Second: check suffixes in the tag (version-tag convention, longest match first).
|
||||
var bestMatch string
|
||||
bestLen := 0
|
||||
for _, s := range streams {
|
||||
|
||||
@@ -416,8 +416,10 @@ func prepareMigrationTasks() []*migration {
|
||||
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),
|
||||
newMigration(339, "Add AI assistant tables", v1_27.AddAITables),
|
||||
newMigration(339, "Placeholder for AI tables", noopMigration),
|
||||
newMigration(340, "Sync license system columns (key_raw, payment_ref, heartbeat, archive, metadata)", v1_27.SyncLicenseSystemColumns),
|
||||
newMigration(341, "Add parent_org_id to user table for enterprise sub-org hierarchy", v1_27.AddParentOrgIDToUser),
|
||||
newMigration(342, "Add is_hidden to repository for three-level visibility", v1_27.AddIsHiddenToRepository),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type licenseKey340 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
KeyRaw string `xorm:"TEXT"`
|
||||
PaymentRef string `xorm:"UNIQUE"`
|
||||
PaymentRef string `xorm:""`
|
||||
LastHeartbeatUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"`
|
||||
FirstUsedUnix timeutil.TimeStamp `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
@@ -49,18 +49,32 @@ type updateStreamConfig340 struct {
|
||||
InfoURL string `xorm:"TEXT"`
|
||||
TargetVersion string `xorm:"TEXT"`
|
||||
PHPMinimum string `xorm:"VARCHAR(20)"`
|
||||
KeyPrefix string `xorm:"VARCHAR(20)"`
|
||||
}
|
||||
|
||||
func (updateStreamConfig340) TableName() string {
|
||||
return "update_stream_config"
|
||||
}
|
||||
|
||||
// SyncLicenseSystemColumns adds missing columns to license_key,
|
||||
// license_package, and update_stream_config tables.
|
||||
// releaseStreamMap340 creates the release-to-stream manual mapping table.
|
||||
type releaseStreamMap340 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ReleaseID int64 `xorm:"UNIQUE NOT NULL INDEX"`
|
||||
RepoID int64 `xorm:"NOT NULL INDEX"`
|
||||
StreamName string `xorm:"NOT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
func (releaseStreamMap340) TableName() string {
|
||||
return "release_stream_map"
|
||||
}
|
||||
|
||||
// SyncLicenseSystemColumns adds missing columns and creates new tables.
|
||||
func SyncLicenseSystemColumns(x *xorm.Engine) error {
|
||||
return x.Sync(
|
||||
new(licenseKey340),
|
||||
new(licensePackage340),
|
||||
new(updateStreamConfig340),
|
||||
new(releaseStreamMap340),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
type userParentOrg341 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"`
|
||||
}
|
||||
|
||||
func (userParentOrg341) TableName() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
// AddParentOrgIDToUser adds the parent_org_id column to the user table
|
||||
// for enterprise sub-org hierarchy support.
|
||||
func AddParentOrgIDToUser(x *xorm.Engine) error {
|
||||
return x.Sync(new(userParentOrg341))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
type repoHidden342 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IsHidden bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
func (repoHidden342) TableName() string {
|
||||
return "repository"
|
||||
}
|
||||
|
||||
// AddIsHiddenToRepository adds the is_hidden column for three-level repo visibility.
|
||||
func AddIsHiddenToRepository(x *xorm.Engine) error {
|
||||
return x.Sync(new(repoHidden342))
|
||||
}
|
||||
@@ -97,6 +97,48 @@ func (org *Organization) IsOrgAdmin(ctx context.Context, uid int64) (bool, error
|
||||
return IsOrganizationAdmin(ctx, org.ID, uid)
|
||||
}
|
||||
|
||||
// HasParentOrg returns true if this org has a parent.
|
||||
func (org *Organization) HasParentOrg() bool {
|
||||
return org.ParentOrgID > 0
|
||||
}
|
||||
|
||||
// GetParentOrg returns the parent organization, or nil if none.
|
||||
func (org *Organization) GetParentOrg(ctx context.Context) (*Organization, error) {
|
||||
if org.ParentOrgID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return GetOrgByID(ctx, org.ParentOrgID)
|
||||
}
|
||||
|
||||
// GetChildOrgs returns all direct child organizations.
|
||||
func GetChildOrgs(ctx context.Context, parentOrgID int64) ([]*Organization, error) {
|
||||
var orgs []*Organization
|
||||
return orgs, db.GetEngine(ctx).
|
||||
Where("type = ? AND parent_org_id = ?", user_model.UserTypeOrganization, parentOrgID).
|
||||
Find(&orgs)
|
||||
}
|
||||
|
||||
// GetAncestorOrgIDs returns all org IDs in the parent chain (including self).
|
||||
// Used for license validation — a key from any ancestor org is valid.
|
||||
func GetAncestorOrgIDs(ctx context.Context, orgID int64) []int64 {
|
||||
ids := []int64{orgID}
|
||||
currentID := orgID
|
||||
seen := map[int64]bool{orgID: true}
|
||||
for i := 0; i < 10; i++ { // max 10 levels to prevent infinite loops
|
||||
org, err := GetOrgByID(ctx, currentID)
|
||||
if err != nil || org.ParentOrgID == 0 {
|
||||
break
|
||||
}
|
||||
if seen[org.ParentOrgID] {
|
||||
break // cycle detected
|
||||
}
|
||||
seen[org.ParentOrgID] = true
|
||||
ids = append(ids, org.ParentOrgID)
|
||||
currentID = org.ParentOrgID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// IsOrgMember returns true if given user is member of organization.
|
||||
func (org *Organization) IsOrgMember(ctx context.Context, uid int64) (bool, error) {
|
||||
return IsOrganizationMember(ctx, org.ID, uid)
|
||||
|
||||
@@ -185,6 +185,7 @@ type Repository struct {
|
||||
NumOpenActionRuns int `xorm:"-"`
|
||||
|
||||
IsPrivate bool `xorm:"INDEX"`
|
||||
IsHidden bool `xorm:"INDEX NOT NULL DEFAULT false"` // hidden repos return 404, private repos return 403
|
||||
IsEmpty bool `xorm:"INDEX"`
|
||||
IsArchived bool `xorm:"INDEX"`
|
||||
IsMirror bool `xorm:"INDEX"`
|
||||
|
||||
@@ -152,6 +152,7 @@ type User struct {
|
||||
NumMembers int
|
||||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
|
||||
|
||||
// Preferences
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
"loading": "Loading…",
|
||||
"files": "Files",
|
||||
"error_title": "Error",
|
||||
"error403": "You do not have permission to access this resource. If you believe this is an error, contact the repository owner.",
|
||||
"error404": "The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.",
|
||||
"error503": "The server could not complete your request. Please try again later.",
|
||||
"go_back": "Go Back",
|
||||
@@ -2677,6 +2678,7 @@
|
||||
"repo.licenses.feed_joomla_updates": "Joomla updates.xml",
|
||||
"repo.licenses.feed_dolibarr_updates": "Dolibarr JSON",
|
||||
"repo.licenses.feed_wordpress_updates": "WordPress (PUC JSON)",
|
||||
"repo.licenses.open_feed": "Open in new tab",
|
||||
"repo.licenses.feed_changelog_xml": "Changelog XML (Joomla)",
|
||||
"repo.licenses.master_label": "Master",
|
||||
"repo.licenses.unlimited": "unlimited",
|
||||
@@ -2714,6 +2716,19 @@
|
||||
"repo.settings.download_gating": "Download Gating",
|
||||
"repo.settings.support_url": "Support / Product Page URL",
|
||||
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
|
||||
"repo.settings.features": "Features",
|
||||
"repo.settings.features_units": "Units",
|
||||
"repo.settings.change_visibility": "Change Visibility",
|
||||
"repo.settings.visibility.warning": "Changing repository visibility affects who can access code, releases, and update feeds.",
|
||||
"repo.settings.visibility.public.label": "Public",
|
||||
"repo.settings.visibility.public.desc": "Visible to everyone. Anyone can clone and view.",
|
||||
"repo.settings.visibility.private.label": "Private",
|
||||
"repo.settings.visibility.private.desc": "Members only. Non-members see Access Denied (403). Licensed update feeds still work.",
|
||||
"repo.settings.visibility.hidden.label": "Hidden",
|
||||
"repo.settings.visibility.hidden.desc": "Members only. Non-members see Not Found (404). Hides the repo's existence entirely.",
|
||||
"repo.release.update_stream": "Update Stream",
|
||||
"repo.release.update_stream_auto": "(auto-detect from tag name)",
|
||||
"repo.release.update_stream_help": "Assign this release to an update stream. The update feed will serve the latest release per stream.",
|
||||
"repo.release.downloads_require_login": "Sign in to download release files.",
|
||||
"repo.settings.extension_metadata": "Extension Metadata",
|
||||
"repo.settings.extension_metadata_desc": "Override the org-level extension metadata for this repository. Empty fields inherit from the organization settings.",
|
||||
@@ -2905,6 +2920,11 @@
|
||||
"org.settings.streams_tag_help": "When licensing is active, release tags with prerelease suffixes must match one of the streams above (e.g. v1.0.0-rc1 matches the -rc stream).",
|
||||
"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.key_prefix": "License Key Prefix",
|
||||
"org.settings.key_prefix_help": "Custom prefix for license keys generated in this org (e.g. ACME, CLIENT). Leave empty for default (MOKO). Max 20 chars, auto-uppercased.",
|
||||
"org.settings.parent_org": "Parent Organization",
|
||||
"org.settings.parent_org_none": "(none — top-level organization)",
|
||||
"org.settings.parent_org_help": "Set a parent org for enterprise hierarchy. Child orgs inherit license packages and master keys from parent orgs.",
|
||||
"org.settings.update_streams_saved": "Settings saved.",
|
||||
"org.settings.full_name": "Full Name",
|
||||
"org.settings.email": "Contact Email Address",
|
||||
|
||||
@@ -69,6 +69,24 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, form)
|
||||
}
|
||||
|
||||
// verifyPackageOwnership checks that a package belongs to the current repo's owner.
|
||||
func verifyPackageOwnership(ctx *context.APIContext, pkg *licenses.LicensePackage) bool {
|
||||
if pkg.OwnerID != ctx.Repo.Repository.OwnerID {
|
||||
ctx.APIErrorNotFound(nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// verifyKeyOwnership checks that a key belongs to the current repo's owner.
|
||||
func verifyKeyOwnership(ctx *context.APIContext, key *licenses.LicenseKey) bool {
|
||||
if key.OwnerID != ctx.Repo.Repository.OwnerID {
|
||||
ctx.APIErrorNotFound(nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
|
||||
return &structs.LicensePackage{
|
||||
ID: pkg.ID,
|
||||
@@ -165,6 +183,9 @@ func EditLicensePackage(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyPackageOwnership(ctx, pkg) {
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
ctx.APIError(http.StatusForbidden, "master package cannot be edited")
|
||||
@@ -210,6 +231,9 @@ func DeleteLicensePackage(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyPackageOwnership(ctx, pkg) {
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
ctx.APIError(http.StatusForbidden, "master package cannot be deleted")
|
||||
@@ -233,6 +257,9 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyPackageOwnership(ctx, pkg) {
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
ctx.APIError(http.StatusForbidden, "master package cannot be archived")
|
||||
@@ -249,7 +276,16 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
|
||||
|
||||
// UnarchiveLicensePackage restores an archived license package via API.
|
||||
func UnarchiveLicensePackage(ctx *context.APIContext) {
|
||||
if err := licenses.UnarchiveLicensePackage(ctx, ctx.PathParamInt64("id")); err != nil {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyPackageOwnership(ctx, pkg) {
|
||||
return
|
||||
}
|
||||
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -331,6 +367,9 @@ func EditLicenseKey(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyKeyOwnership(ctx, key) {
|
||||
return
|
||||
}
|
||||
|
||||
if key.IsInternal {
|
||||
ctx.APIError(http.StatusForbidden, "master keys cannot be edited")
|
||||
@@ -386,6 +425,9 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyPackageOwnership(ctx, pkg) {
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
PackageID: form.PackageID,
|
||||
@@ -423,6 +465,9 @@ func RenewLicenseKey(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyKeyOwnership(ctx, key) {
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
@@ -453,6 +498,9 @@ func RevokeLicenseKey(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyKeyOwnership(ctx, key) {
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
@@ -465,7 +513,16 @@ func RevokeLicenseKey(ctx *context.APIContext) {
|
||||
|
||||
// DeleteLicenseKey deletes a license key.
|
||||
func DeleteLicenseKey(ctx *context.APIContext) {
|
||||
if err := licenses.DeleteLicenseKey(ctx, ctx.PathParamInt64("id")); err != nil {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !verifyKeyOwnership(ctx, key) {
|
||||
return
|
||||
}
|
||||
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
|
||||
}
|
||||
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: ctx.Repo.RefFullName.String()
|
||||
Revision: ctx.Repo.RefFullName.String(),
|
||||
File: fileName,
|
||||
Page: 1,
|
||||
})
|
||||
|
||||
@@ -89,7 +89,7 @@ func Licenses(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
pkgs, err := licenses.ListLicensePackagesWithAncestors(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
@@ -112,9 +112,9 @@ func Licenses(ctx *context.Context) {
|
||||
|
||||
var keys []*licenses.LicenseKey
|
||||
if searchQuery != "" {
|
||||
keys, err = licenses.SearchLicenseKeys(ctx, ownerID, searchQuery)
|
||||
keys, err = licenses.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
|
||||
} else {
|
||||
keys, err = licenses.ListLicenseKeys(ctx, ownerID)
|
||||
keys, err = licenses.ListLicenseKeysWithAncestors(ctx, ownerID)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
org_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
packages_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/packages"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
@@ -47,6 +48,22 @@ func Settings(ctx *context.Context) {
|
||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
ctx.Data["ParentOrgID"] = ctx.Org.Organization.ParentOrgID
|
||||
|
||||
// Load available parent orgs (all orgs the current user belongs to, excluding self).
|
||||
if ctx.Doer.IsAdmin || ctx.Org.IsOwner {
|
||||
orgs, _ := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
|
||||
UserID: ctx.Doer.ID,
|
||||
IncludeVisibility: structs.VisibleTypePrivate,
|
||||
})
|
||||
var parentCandidates []*org_model.Organization
|
||||
for _, o := range orgs {
|
||||
if o.ID != ctx.Org.Organization.ID {
|
||||
parentCandidates = append(parentCandidates, o)
|
||||
}
|
||||
}
|
||||
ctx.Data["ParentOrgCandidates"] = parentCandidates
|
||||
}
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
@@ -98,6 +115,17 @@ func SettingsPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save parent org (enterprise hierarchy).
|
||||
parentOrgID := ctx.FormInt64("parent_org_id")
|
||||
if parentOrgID != org.ParentOrgID {
|
||||
user := org.AsUser()
|
||||
user.ParentOrgID = parentOrgID
|
||||
if err := user_model.UpdateUserCols(ctx, user, "parent_org_id"); err != nil {
|
||||
ctx.ServerError("UpdateUserCols", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Organization setting updated: %s", org.Name)
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
|
||||
@@ -5,6 +5,7 @@ package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
@@ -47,6 +48,7 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
|
||||
FeedVisibility: ctx.FormString("feed_visibility"),
|
||||
DownloadGating: ctx.FormString("download_gating"),
|
||||
SupportURL: ctx.FormString("support_url"),
|
||||
KeyPrefix: strings.ToUpper(strings.TrimSpace(ctx.FormString("key_prefix"))),
|
||||
ExtensionName: ctx.FormString("extension_name"),
|
||||
DisplayName: ctx.FormString("display_name"),
|
||||
Description: ctx.FormString("feed_description"),
|
||||
|
||||
@@ -182,8 +182,11 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
}
|
||||
|
||||
if !perm.CanRead(unitType) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
// Allow access for licensed read-only mode (private repo with valid license key).
|
||||
if ctx.Data["LicensedReadOnly"] != true {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if requiredScope, ok := attachmentReadScope(unitType); ok {
|
||||
|
||||
@@ -77,6 +77,12 @@ func ServeChangelogXML(ctx *context.Context) {
|
||||
}
|
||||
|
||||
version := extractVersionFromTag(rel.TagName)
|
||||
// If the tag is a stream name, try the release title for the version.
|
||||
if version == rel.TagName && (version == "stable" || version == "release-candidate" || version == "beta" || version == "alpha" || version == "development") {
|
||||
if titleVer := extractVersionFromTag(rel.Title); titleVer != "" {
|
||||
version = titleVer
|
||||
}
|
||||
}
|
||||
cl := xmlChangelog{
|
||||
Element: element,
|
||||
Type: extType,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
@@ -355,6 +356,17 @@ func newReleaseCommon(ctx *context.Context) {
|
||||
|
||||
upload.AddUploadContext(ctx, "release")
|
||||
|
||||
// Load available streams for the stream selector (when licensing enabled).
|
||||
if ctx.Data["LicensingEnabled"] == true {
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, ownerID)
|
||||
if orgCfg != nil {
|
||||
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
ctx.Data["AvailableStreams"] = licenses_model.DefaultJoomlaStreams()
|
||||
}
|
||||
}
|
||||
|
||||
PrepareBranchList(ctx) // for New Release page
|
||||
}
|
||||
|
||||
@@ -520,6 +532,10 @@ func NewReleasePost(ctx *context.Context) {
|
||||
handleTagReleaseError(err)
|
||||
return
|
||||
}
|
||||
// Save manual stream assignment if specified.
|
||||
if streamName := form.UpdateStream; streamName != "" {
|
||||
_ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||
return
|
||||
}
|
||||
@@ -580,6 +596,7 @@ func EditRelease(ctx *context.Context) {
|
||||
ctx.Data["content"] = rel.Note
|
||||
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||
ctx.Data["IsDraft"] = rel.IsDraft
|
||||
ctx.Data["ReleaseStream"] = licenses_model.GetReleaseStream(ctx, rel.ID)
|
||||
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
@@ -660,6 +677,12 @@ func EditReleasePost(ctx *context.Context) {
|
||||
ctx.ServerError("UpdateRelease", err)
|
||||
return
|
||||
}
|
||||
// Save manual stream assignment.
|
||||
if streamName := form.UpdateStream; streamName != "" {
|
||||
_ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
|
||||
} else {
|
||||
_ = licenses_model.DeleteReleaseStream(ctx, rel.ID)
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsAdvanced templates.TplName = "repo/settings/advanced"
|
||||
|
||||
// AdvancedSettings displays the advanced (feature units) settings page.
|
||||
func AdvancedSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.advanced_settings")
|
||||
ctx.Data["PageIsSettingsAdvanced"] = true
|
||||
ctx.HTML(http.StatusOK, tplSettingsAdvanced)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsLicensing templates.TplName = "repo/settings/licensing"
|
||||
|
||||
// LicensingSettings displays the licensing settings page.
|
||||
func LicensingSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.licensing_section")
|
||||
ctx.Data["PageIsSettingsLicensing"] = true
|
||||
|
||||
repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
ctx.Data["RepoUpdateConfig"] = repoCfg
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsLicensing)
|
||||
}
|
||||
|
||||
// LicensingSettingsPost saves the licensing settings.
|
||||
func LicensingSettingsPost(ctx *context.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
updatePlatform := ctx.FormString("update_platform")
|
||||
if updatePlatform == "" {
|
||||
updatePlatform = "joomla"
|
||||
}
|
||||
|
||||
updateCfg := &licenses_model.UpdateStreamConfig{
|
||||
OwnerID: repo.OwnerID,
|
||||
RepoID: repo.ID,
|
||||
Platform: updatePlatform,
|
||||
LicensingEnabled: ctx.FormString("enable_licensing") == "on",
|
||||
RequireKey: ctx.FormString("require_update_key") == "on",
|
||||
DownloadGating: ctx.FormString("download_gating"),
|
||||
SupportURL: ctx.FormString("support_url"),
|
||||
ExtensionName: ctx.FormString("extension_name"),
|
||||
DisplayName: ctx.FormString("display_name"),
|
||||
ExtensionType: ctx.FormString("extension_type"),
|
||||
TargetVersion: ctx.FormString("target_version"),
|
||||
Maintainer: ctx.FormString("maintainer"),
|
||||
PHPMinimum: ctx.FormString("php_minimum"),
|
||||
StreamMode: "joomla",
|
||||
}
|
||||
|
||||
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
|
||||
log.Error("SaveConfig: %v", err)
|
||||
ctx.ServerError("SaveConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/licensing")
|
||||
}
|
||||
@@ -1055,26 +1055,40 @@ func handleSettingsPostVisibility(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
|
||||
visibility := ctx.FormString("visibility")
|
||||
// Backward compat: if old "private" field is sent instead of "visibility"
|
||||
if visibility == "" {
|
||||
private := ctx.FormOptionalBool("private").ValueOrDefault(true)
|
||||
if private {
|
||||
visibility = "private"
|
||||
} else {
|
||||
visibility = "public"
|
||||
}
|
||||
}
|
||||
|
||||
// System repos (dot-prefixed) cannot be made public, regardless of user role.
|
||||
if !private && repo.IsSystemRepo() {
|
||||
isPrivate := visibility == "private" || visibility == "hidden"
|
||||
isHidden := visibility == "hidden"
|
||||
|
||||
if !isPrivate && repo.IsSystemRepo() {
|
||||
ctx.JSONError(ctx.Tr("repo.settings.visibility.system_repo_private"))
|
||||
return
|
||||
}
|
||||
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
|
||||
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
|
||||
if !isPrivate && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
|
||||
ctx.JSONError(ctx.Tr("form.repository_force_private"))
|
||||
return
|
||||
}
|
||||
if private && repo.FullName() != ctx.FormString("confirm_repo_name") {
|
||||
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
|
||||
|
||||
err := repo_service.MakeRepoPrivate(ctx, repo, isPrivate)
|
||||
if err != nil {
|
||||
log.Error("Tried to change the visibility of the repo: %s", err)
|
||||
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
|
||||
return
|
||||
}
|
||||
|
||||
err := repo_service.MakeRepoPrivate(ctx, repo, private)
|
||||
if err != nil {
|
||||
log.Error("Tried to change the visibility of the repo: %s", err)
|
||||
// Update IsHidden separately.
|
||||
repo.IsHidden = isHidden
|
||||
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "is_hidden"); err != nil {
|
||||
log.Error("Failed to update is_hidden: %s", err)
|
||||
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,30 +27,8 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
|
||||
}
|
||||
|
||||
if rawKey == "" {
|
||||
cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
feedVis := "public"
|
||||
requireKey := false
|
||||
if cfg != nil {
|
||||
requireKey = cfg.RequireKey
|
||||
if cfg.FeedVisibility != "" {
|
||||
feedVis = cfg.FeedVisibility
|
||||
}
|
||||
}
|
||||
|
||||
if requireKey {
|
||||
switch feedVis {
|
||||
case "hidden":
|
||||
// Fully hidden — return empty feed.
|
||||
return nil, false, false
|
||||
case "no-download":
|
||||
// Show versions but strip download URLs.
|
||||
return nil, true, true
|
||||
default:
|
||||
// "public" with RequireKey — still hide feed (backward compat).
|
||||
return nil, false, false
|
||||
}
|
||||
}
|
||||
// No key required — allow public access (all channels).
|
||||
// Feed is always public — shows versions and download URLs.
|
||||
// Actual file downloads are gated by CheckDownloadGating, not the feed.
|
||||
return nil, true, false
|
||||
}
|
||||
|
||||
@@ -58,7 +36,7 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
|
||||
key, pkg, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
log.Debug("License key validation failed: %v", err)
|
||||
return nil, false
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
// Update heartbeat and record usage.
|
||||
@@ -74,17 +52,18 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
|
||||
|
||||
// Parse allowed channels from the package.
|
||||
if pkg.AllowedChannels != "" {
|
||||
channels := strings.Split(pkg.AllowedChannels, ",")
|
||||
var channels []string
|
||||
if strings.HasPrefix(pkg.AllowedChannels, "[") {
|
||||
// JSON array format — parse first to avoid substring issues.
|
||||
if err := json.Unmarshal([]byte(pkg.AllowedChannels), &channels); err != nil {
|
||||
channels = strings.Split(pkg.AllowedChannels, ",")
|
||||
}
|
||||
} else {
|
||||
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])
|
||||
@@ -210,3 +189,133 @@ func ServeWordPressJSON(ctx *context.Context) {
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(jsonData)
|
||||
}
|
||||
|
||||
// ServeComposerJSON generates and serves a Composer packages.json feed.
|
||||
func ServeComposerJSON(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "composer" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`{"packages":{}}`))
|
||||
return
|
||||
}
|
||||
|
||||
licenseKey := ctx.FormString("key")
|
||||
if licenseKey == "" {
|
||||
licenseKey = ctx.FormString("dlid")
|
||||
}
|
||||
|
||||
data, err := updateserver.GenerateComposerJSON(ctx, ctx.Repo.Repository, licenseKey)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateComposerJSON", 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)
|
||||
}
|
||||
|
||||
// ServePrestaShopXML generates and serves a PrestaShop module update XML.
|
||||
func ServePrestaShopXML(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "prestashop" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
allowedChannels, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
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"?><modules></modules>`))
|
||||
return
|
||||
}
|
||||
|
||||
xmlData, err := updateserver.GeneratePrestaShopXML(ctx, ctx.Repo.Repository, allowedChannels...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GeneratePrestaShopXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
|
||||
// ServeDrupalXML generates and serves a Drupal update status XML.
|
||||
func ServeDrupalXML(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "drupal" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
allowedChannels, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
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"?><project></project>`))
|
||||
return
|
||||
}
|
||||
|
||||
xmlData, err := updateserver.GenerateDrupalXML(ctx, ctx.Repo.Repository, allowedChannels...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateDrupalXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
|
||||
// ServeWHMCSJSON generates and serves a WHMCS module update JSON.
|
||||
func ServeWHMCSJSON(ctx *context.Context) {
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform != "whmcs" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok, _ := validateUpdateKey(ctx)
|
||||
if !ok {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write([]byte(`{"name":"","version":"0.0.0"}`))
|
||||
return
|
||||
}
|
||||
|
||||
licenseKey := ctx.FormString("key")
|
||||
if licenseKey == "" {
|
||||
licenseKey = ctx.FormString("dlid")
|
||||
}
|
||||
|
||||
data, err := updateserver.GenerateWHMCSJSON(ctx, ctx.Repo.Repository, licenseKey)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateWHMCSJSON", 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)
|
||||
}
|
||||
|
||||
+11
-3
@@ -1183,6 +1183,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar)
|
||||
|
||||
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
|
||||
m.Group("", func() {
|
||||
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
|
||||
}, repo_setting.SettingsCtxData)
|
||||
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
|
||||
|
||||
m.Group("/collaboration", func() {
|
||||
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
|
||||
@@ -1519,8 +1523,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/updates.xml", repo.ServeUpdatesXML)
|
||||
m.Get("/updates/dolibarr.json", repo.ServeDolibarrJSON)
|
||||
m.Get("/updates/wordpress.json", repo.ServeWordPressJSON)
|
||||
m.Get("/updates/packages.json", repo.ServeComposerJSON)
|
||||
m.Get("/updates/prestashop.xml", repo.ServePrestaShopXML)
|
||||
m.Get("/updates/drupal.xml", repo.ServeDrupalXML)
|
||||
m.Get("/updates/whmcs.json", repo.ServeWHMCSJSON)
|
||||
m.Get("/changelog.xml", repo.ServeChangelogXML)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
}, context.RepoAssignmentPublicFeed())
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
// "/{username}/{reponame}": licenses page
|
||||
@@ -1540,7 +1548,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
|
||||
m.Post("/keys/{id}/renew", repo.LicensesRenewKey)
|
||||
m.Post("/keys/{id}/delete", repo.LicensesDeleteKey)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
}, reqSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": licenses
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
@@ -1620,7 +1628,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Group("/workflows/{workflow_name}", func() {
|
||||
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
|
||||
})
|
||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
|
||||
}, reqSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
|
||||
// end "/{username}/{reponame}/actions"
|
||||
|
||||
m.Group("/{username}/{reponame}/wiki", func() {
|
||||
|
||||
@@ -169,6 +169,27 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
ctx.HTML(http.StatusNotFound, "status/404")
|
||||
}
|
||||
|
||||
// Forbidden displays a styled 403 (Access Denied) page, matching the 404 page layout.
|
||||
func (ctx *Context) Forbidden() {
|
||||
showHTML := false
|
||||
for _, part := range ctx.Req.Header["Accept"] {
|
||||
if strings.Contains(part, "text/html") {
|
||||
showHTML = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !showHTML {
|
||||
ctx.plainTextInternal(3, http.StatusForbidden, []byte("Access denied.\n"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Access Denied"
|
||||
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
|
||||
ctx.HTML(http.StatusForbidden, "status/403")
|
||||
}
|
||||
|
||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||||
// If the error is controlled by our error system, a related 404 page can be displayed instead.
|
||||
func (ctx *Context) ServerError(logMsg string, logErr error) {
|
||||
|
||||
@@ -78,6 +78,10 @@ func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
// RequireUnitReader returns a middleware for requiring repository write to one of the unit permission
|
||||
func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
// Licensed read-only mode grants read access to all units.
|
||||
if ctx.Data["LicensedReadOnly"] == true {
|
||||
return
|
||||
}
|
||||
for _, unitType := range unitTypes {
|
||||
if ctx.Repo.Permission.CanRead(unitType) {
|
||||
return
|
||||
|
||||
@@ -435,8 +435,53 @@ func repoAssignmentLegacy(ctx *Context, data *repoAssignmentPrepareDataStruct) {
|
||||
EarlyResponseForGoGetMeta(ctx)
|
||||
return
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
|
||||
// Check if licensing is enabled — licensed repos allow access to
|
||||
// releases and downloads via license key, even without membership.
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
|
||||
repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
|
||||
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
|
||||
(repoCfg != nil && repoCfg.LicensingEnabled)
|
||||
|
||||
if licensingEnabled {
|
||||
// Check if a license key is provided in query params (for Joomla/WP clients).
|
||||
hasKey := ctx.FormString("dlid") != "" || ctx.FormString("key") != "" || ctx.FormString("download_key") != ""
|
||||
|
||||
// Check if downloads are set to public (download_gating=none means no key needed).
|
||||
// Only apply to release/download paths, not the main repo page.
|
||||
downloadsPublic := false
|
||||
reqPath := ctx.Req.URL.Path
|
||||
isDownloadPath := strings.Contains(reqPath, "/releases/") || strings.Contains(reqPath, "/archive/")
|
||||
if isDownloadPath {
|
||||
// Allow anonymous access to download paths — the actual gating
|
||||
// is done by CheckDownloadGating in the handler, which checks
|
||||
// the stream (stable vs prerelease) and validates the key.
|
||||
// RepoAssignment just needs to let the request through.
|
||||
downloadsPublic = true
|
||||
}
|
||||
|
||||
if ctx.IsSigned || hasKey || downloadsPublic {
|
||||
// Grant read-only access — downloads gated by CheckDownloadGating handler.
|
||||
ctx.Data["LicensingEnabled"] = licensingEnabled
|
||||
ctx.Data["HideReleaseDownloads"] = !hasKey && !ctx.IsSigned
|
||||
ctx.Data["LicensedReadOnly"] = true
|
||||
// Continue — don't block access.
|
||||
} else if repo.IsHidden {
|
||||
// Hidden repo: 404 — pretend it doesn't exist.
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
} else {
|
||||
// Private repo: 403 — access denied with styled page.
|
||||
ctx.Forbidden()
|
||||
return
|
||||
}
|
||||
} else if repo.IsHidden {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
} else {
|
||||
ctx.Forbidden()
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Data["Permission"] = &ctx.Repo.Permission
|
||||
|
||||
@@ -628,10 +673,29 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
feedVis = repoUpdateCfg.FeedVisibility
|
||||
}
|
||||
ctx.Data["FeedVisibility"] = feedVis
|
||||
// Only "hidden" mode requires login. "no-download" shows page but hides files.
|
||||
// Only "hidden" mode requires login for the page itself.
|
||||
ctx.Data["ReleasesRequireLogin"] = licensingEnabled && feedVis == "hidden"
|
||||
// Hide download attachments for anonymous users in "no-download" mode.
|
||||
ctx.Data["HideReleaseDownloads"] = licensingEnabled && feedVis == "no-download" && !ctx.IsSigned
|
||||
|
||||
// Determine download gating mode.
|
||||
downloadGating := "none"
|
||||
if orgCfg != nil && orgCfg.DownloadGating != "" {
|
||||
downloadGating = orgCfg.DownloadGating
|
||||
}
|
||||
if repoUpdateCfg != nil && repoUpdateCfg.DownloadGating != "" {
|
||||
downloadGating = repoUpdateCfg.DownloadGating
|
||||
}
|
||||
ctx.Data["DownloadGating"] = downloadGating
|
||||
|
||||
// Hide download links on release page when:
|
||||
// - licensing enabled AND feed visibility is "no-download" (anonymous only), OR
|
||||
// - licensing enabled AND download gating is active AND user not signed in
|
||||
hideDownloads := false
|
||||
if licensingEnabled && !ctx.IsSigned {
|
||||
if feedVis == "no-download" || feedVis == "hidden" || downloadGating != "none" {
|
||||
hideDownloads = true
|
||||
}
|
||||
}
|
||||
ctx.Data["HideReleaseDownloads"] = hideDownloads
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
)
|
||||
|
||||
// RepoAssignmentPublicFeed is a lightweight repo loader for update feed endpoints.
|
||||
// It loads the repo and owner without checking user permissions — the feed
|
||||
// handlers gate access via license keys instead of repo membership.
|
||||
// This allows private repos to serve update feeds to anonymous clients.
|
||||
func RepoAssignmentPublicFeed() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
ownerName := ctx.PathParam("username")
|
||||
repoName := ctx.PathParam("reponame")
|
||||
|
||||
owner, err := user_model.GetUserByName(ctx, ownerName)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("GetRepositoryByOwnerAndName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
repo.Owner = owner
|
||||
ctx.Repo.Repository = repo
|
||||
|
||||
// Load update config for platform-aware routing.
|
||||
repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
|
||||
if repoUpdateCfg != nil {
|
||||
ctx.Data["RepoUpdatePlatform"] = repoUpdateCfg.Platform
|
||||
} else {
|
||||
ctx.Data["RepoUpdatePlatform"] = "joomla"
|
||||
}
|
||||
|
||||
log.Trace("Public feed access: %s/%s", ownerName, repoName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,8 +670,9 @@ type NewReleaseForm struct {
|
||||
Draft bool
|
||||
TagOnly bool
|
||||
Prerelease bool
|
||||
AddTagMsg bool
|
||||
Files []string
|
||||
AddTagMsg bool
|
||||
Files []string
|
||||
UpdateStream string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
@@ -695,11 +696,12 @@ func (f *GenerateReleaseNotesForm) Validate(req *http.Request, errs binding.Erro
|
||||
|
||||
// EditReleaseForm form for changing release
|
||||
type EditReleaseForm struct {
|
||||
Title string `form:"title" binding:"Required;MaxSize(255)"`
|
||||
Content string `form:"content"`
|
||||
Draft string `form:"draft"`
|
||||
Prerelease bool `form:"prerelease"`
|
||||
Files []string
|
||||
Title string `form:"title" binding:"Required;MaxSize(255)"`
|
||||
Content string `form:"content"`
|
||||
Draft string `form:"draft"`
|
||||
Prerelease bool `form:"prerelease"`
|
||||
Files []string
|
||||
UpdateStream string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// ComposerPackage represents the packages.json response for Composer/Packagist.
|
||||
type ComposerPackage struct {
|
||||
Packages map[string]map[string]ComposerVersion `json:"packages"`
|
||||
}
|
||||
|
||||
// ComposerVersion represents a single version entry.
|
||||
type ComposerVersion struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
License []string `json:"license,omitempty"`
|
||||
Authors []ComposerAuthor `json:"authors,omitempty"`
|
||||
Dist *ComposerDist `json:"dist,omitempty"`
|
||||
Require map[string]string `json:"require,omitempty"`
|
||||
Time string `json:"time,omitempty"`
|
||||
}
|
||||
|
||||
// ComposerAuthor represents a package author.
|
||||
type ComposerAuthor struct {
|
||||
Name string `json:"name"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
}
|
||||
|
||||
// ComposerDist represents a distribution archive.
|
||||
type ComposerDist struct {
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
Shasum string `json:"shasum,omitempty"`
|
||||
Reference string `json:"reference,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateComposerJSON builds a Composer packages.json from repo releases.
|
||||
func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*ComposerPackage, 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)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Composer package name: vendor/package
|
||||
packageName := fmt.Sprintf("%s/%s", strings.ToLower(repo.Owner.Name), strings.ToLower(repo.Name))
|
||||
if cfg != nil && cfg.ExtensionName != "" {
|
||||
packageName = cfg.ExtensionName
|
||||
}
|
||||
|
||||
maintainer := repo.Owner.Name
|
||||
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
||||
if cfg != nil && cfg.Maintainer != "" {
|
||||
maintainer = cfg.Maintainer
|
||||
}
|
||||
if cfg != nil && cfg.MaintainerURL != "" {
|
||||
maintainerURL = cfg.MaintainerURL
|
||||
}
|
||||
|
||||
description := ""
|
||||
if cfg != nil && cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
|
||||
phpMin := ""
|
||||
if cfg != nil && cfg.PHPMinimum != "" {
|
||||
phpMin = ">=" + cfg.PHPMinimum
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
versions := make(map[string]ComposerVersion)
|
||||
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if ch != "stable" {
|
||||
continue // Composer only serves stable versions
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
if isStreamName(version, streams) {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
if version == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var downloadURL string
|
||||
var sha256Hash string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
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)
|
||||
}
|
||||
if licenseKey != "" {
|
||||
downloadURL += "?dlid=" + licenseKey
|
||||
}
|
||||
|
||||
// Look for SHA256 sidecar
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require := make(map[string]string)
|
||||
if phpMin != "" {
|
||||
require["php"] = phpMin
|
||||
}
|
||||
|
||||
v := ComposerVersion{
|
||||
Name: packageName,
|
||||
Version: version,
|
||||
Description: description,
|
||||
Homepage: repoLink,
|
||||
Authors: []ComposerAuthor{
|
||||
{Name: maintainer, Homepage: maintainerURL},
|
||||
},
|
||||
Dist: &ComposerDist{
|
||||
URL: downloadURL,
|
||||
Type: "zip",
|
||||
Shasum: sha256Hash,
|
||||
Reference: rel.TagName,
|
||||
},
|
||||
Require: require,
|
||||
Time: time.Unix(int64(rel.CreatedUnix), 0).Format(time.RFC3339),
|
||||
}
|
||||
|
||||
versions[version] = v
|
||||
}
|
||||
|
||||
return &ComposerPackage{
|
||||
Packages: map[string]map[string]ComposerVersion{
|
||||
packageName: versions,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allo
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
@@ -108,6 +108,12 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allo
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
if version == "" || isStreamName(rel.TagName, streams) {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = rel.TagName
|
||||
}
|
||||
suffix := stream.Suffix
|
||||
if suffix == "" {
|
||||
suffix = channelSuffix(ch)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// Drupal update status XML structures.
|
||||
// See: https://www.drupal.org/docs/drupal-apis/update-status-module/update-status-xml-format
|
||||
|
||||
type drupalProject struct {
|
||||
XMLName xml.Name `xml:"project"`
|
||||
Title string `xml:"title"`
|
||||
ShortName string `xml:"short_name"`
|
||||
APIVersion string `xml:"api_version"`
|
||||
RecommendedMaj string `xml:"recommended_major"`
|
||||
DefaultMajor string `xml:"default_major"`
|
||||
ProjectStatus string `xml:"project_status"`
|
||||
Link string `xml:"link"`
|
||||
Releases drupalReleases `xml:"releases"`
|
||||
}
|
||||
|
||||
type drupalReleases struct {
|
||||
Releases []drupalRelease `xml:"release"`
|
||||
}
|
||||
|
||||
type drupalRelease struct {
|
||||
Name string `xml:"name"`
|
||||
Version string `xml:"version"`
|
||||
Tag string `xml:"tag"`
|
||||
Status string `xml:"status"`
|
||||
ReleaseLink string `xml:"release_link"`
|
||||
DownloadURL string `xml:"download_link"`
|
||||
Date string `xml:"date"`
|
||||
FileHash string `xml:"mdhash,omitempty"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateDrupalXML builds a Drupal update status-compatible XML feed.
|
||||
func GenerateDrupalXML(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("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)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
shortName := strings.ToLower(repo.Name)
|
||||
title := repo.Name
|
||||
if cfg != nil {
|
||||
if cfg.ExtensionName != "" {
|
||||
shortName = cfg.ExtensionName
|
||||
}
|
||||
if cfg.DisplayName != "" {
|
||||
title = cfg.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
channelAllowed := make(map[string]bool)
|
||||
if len(allowedChannels) > 0 {
|
||||
for _, c := range allowedChannels {
|
||||
channelAllowed[NormalizeChannel(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
project := drupalProject{
|
||||
Title: title,
|
||||
ShortName: shortName,
|
||||
APIVersion: "7.x", // default API version
|
||||
RecommendedMaj: "1",
|
||||
DefaultMajor: "1",
|
||||
ProjectStatus: "published",
|
||||
Link: repoLink,
|
||||
}
|
||||
|
||||
if cfg != nil && cfg.TargetVersion != "" {
|
||||
project.APIVersion = cfg.TargetVersion
|
||||
}
|
||||
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var downloadURL, sha256Hash string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
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)
|
||||
}
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
if isStreamName(version, streams) || version == "" {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = rel.TagName
|
||||
}
|
||||
|
||||
status := "published"
|
||||
if rel.IsPrerelease {
|
||||
status = "insecure" // Drupal uses this for non-stable
|
||||
}
|
||||
|
||||
project.Releases.Releases = append(project.Releases.Releases, drupalRelease{
|
||||
Name: fmt.Sprintf("%s %s", shortName, version),
|
||||
Version: version,
|
||||
Tag: rel.TagName,
|
||||
Status: status,
|
||||
ReleaseLink: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
||||
DownloadURL: downloadURL,
|
||||
Date: fmt.Sprintf("%d", rel.CreatedUnix),
|
||||
SHA256: sha256Hash,
|
||||
})
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(project, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -107,6 +108,17 @@ func channelFromTag(tagName string, isPrerelease bool) string {
|
||||
}
|
||||
}
|
||||
|
||||
// isStreamName checks if a string matches any stream name (indicating the tag
|
||||
// is a stream name, not a version number).
|
||||
func isStreamName(s string, streams []licenses.StreamDef) bool {
|
||||
for _, st := range streams {
|
||||
if strings.EqualFold(st.Name, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// joomlaTagName maps internal stream names to Joomla-standard tag values.
|
||||
// Joomla recognizes: dev, alpha, beta, rc, stable.
|
||||
func joomlaTagName(channel string) string {
|
||||
@@ -190,10 +202,12 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
maintainer = cfg.Maintainer
|
||||
}
|
||||
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
||||
if cfg != nil && cfg.MaintainerURL != "" {
|
||||
if cfg != nil && cfg.SupportURL != "" {
|
||||
maintainerURL = cfg.SupportURL
|
||||
} else if cfg != nil && cfg.MaintainerURL != "" {
|
||||
maintainerURL = cfg.MaintainerURL
|
||||
}
|
||||
targetVersion := ".*"
|
||||
targetVersion := "(5|6)\\..*"
|
||||
if cfg != nil && cfg.TargetVersion != "" {
|
||||
targetVersion = cfg.TargetVersion
|
||||
}
|
||||
@@ -215,7 +229,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
@@ -273,6 +287,14 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
// If the tag is a stream name (not a version), try the release title instead.
|
||||
if version == "" || isStreamName(rel.TagName, streams) {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
// Last resort: use the tag name as-is.
|
||||
if version == "" {
|
||||
version = rel.TagName
|
||||
}
|
||||
suffix := stream.Suffix
|
||||
if suffix == "" {
|
||||
suffix = channelSuffix(ch) // fallback for Joomla defaults
|
||||
@@ -287,9 +309,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
}
|
||||
|
||||
infoURL := fmt.Sprintf("%s/releases", repoLink)
|
||||
if cfg != nil && cfg.SupportURL != "" {
|
||||
infoURL = cfg.SupportURL
|
||||
} else if cfg != nil && cfg.InfoURL != "" {
|
||||
if cfg != nil && cfg.InfoURL != "" {
|
||||
infoURL = cfg.InfoURL
|
||||
}
|
||||
|
||||
@@ -340,20 +360,35 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
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
|
||||
// versionRegex matches semantic version patterns like 1.0.0, 02.29.04, etc.
|
||||
var versionRegex = regexp.MustCompile(`(\d+\.\d+(?:\.\d+)?)`)
|
||||
|
||||
// extractVersion finds a version number from a tag name or release title.
|
||||
// Tries: (1) strip common prefixes for version-style tags, (2) regex match for embedded versions.
|
||||
func extractVersion(s string) string {
|
||||
// Try prefix stripping first (works for "v1.0.0", "release-1.0.0").
|
||||
v := s
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
v = strings.TrimPrefix(v, "release-")
|
||||
v = strings.TrimPrefix(v, "release/")
|
||||
// Strip channel suffixes to get base version.
|
||||
// Strip channel suffixes.
|
||||
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
|
||||
// If result looks like a version (starts with digit), use it.
|
||||
if len(v) > 0 && v[0] >= '0' && v[0] <= '9' {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
// Fallback: extract version pattern from anywhere in the string.
|
||||
if m := versionRegex.FindString(s); m != "" {
|
||||
return m
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// channelSuffix returns the version suffix for a channel.
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// PrestaShop module update XML structures.
|
||||
|
||||
type psUpdates struct {
|
||||
XMLName xml.Name `xml:"modules"`
|
||||
Modules []psModule `xml:"module"`
|
||||
}
|
||||
|
||||
type psModule struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
DisplayName string `xml:"displayName"`
|
||||
Description string `xml:"description,omitempty"`
|
||||
Author string `xml:"author"`
|
||||
Tab string `xml:"tab,omitempty"`
|
||||
Download string `xml:"download"`
|
||||
Date string `xml:"date"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// GeneratePrestaShopXML builds a PrestaShop-compatible module update XML.
|
||||
func GeneratePrestaShopXML(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("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)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
moduleName := strings.ToLower(repo.Name)
|
||||
displayName := repo.Name
|
||||
maintainer := repo.Owner.Name
|
||||
description := ""
|
||||
if cfg != nil {
|
||||
if cfg.ExtensionName != "" {
|
||||
moduleName = cfg.ExtensionName
|
||||
}
|
||||
if cfg.DisplayName != "" {
|
||||
displayName = cfg.DisplayName
|
||||
}
|
||||
if cfg.Maintainer != "" {
|
||||
maintainer = cfg.Maintainer
|
||||
}
|
||||
if cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Channel filtering.
|
||||
channelAllowed := make(map[string]bool)
|
||||
if len(allowedChannels) > 0 {
|
||||
for _, c := range allowedChannels {
|
||||
channelAllowed[NormalizeChannel(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Track best release per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
var result psUpdates
|
||||
for _, stream := range streams {
|
||||
ch := stream.Name
|
||||
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
||||
continue
|
||||
}
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok || ch != "stable" {
|
||||
continue // PrestaShop typically only serves stable
|
||||
}
|
||||
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var downloadURL, sha256Hash string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
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)
|
||||
}
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
if isStreamName(version, streams) || version == "" {
|
||||
version = extractVersion(rel.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = rel.TagName
|
||||
}
|
||||
|
||||
result.Modules = append(result.Modules, psModule{
|
||||
Name: moduleName,
|
||||
Version: version,
|
||||
DisplayName: displayName,
|
||||
Description: description,
|
||||
Author: maintainer,
|
||||
Download: downloadURL,
|
||||
Date: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
||||
SHA256: sha256Hash,
|
||||
})
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// WHMCS module update JSON structures.
|
||||
// WHMCS marketplace modules check for updates via a simple JSON endpoint.
|
||||
|
||||
type WHMCSUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
Changelog string `json:"changelog,omitempty"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Author string `json:"author,omitempty"`
|
||||
AuthorURL string `json:"author_url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateWHMCSJSON builds a WHMCS-compatible module update response.
|
||||
func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*WHMCSUpdate, 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)
|
||||
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
displayName := repo.Name
|
||||
maintainer := repo.Owner.Name
|
||||
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
||||
description := ""
|
||||
if cfg != nil {
|
||||
if cfg.DisplayName != "" {
|
||||
displayName = cfg.DisplayName
|
||||
}
|
||||
if cfg.Maintainer != "" {
|
||||
maintainer = cfg.Maintainer
|
||||
}
|
||||
if cfg.MaintainerURL != "" {
|
||||
maintainerURL = cfg.MaintainerURL
|
||||
}
|
||||
if cfg.Description != "" {
|
||||
description = cfg.Description
|
||||
}
|
||||
}
|
||||
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Find latest stable release.
|
||||
var latestStable *repo_model.Release
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if ch == "stable" {
|
||||
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
|
||||
latestStable = rel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestStable == nil {
|
||||
return &WHMCSUpdate{
|
||||
Name: displayName,
|
||||
Version: "0.0.0",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := latestStable.LoadAttributes(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadAttributes: %w", err)
|
||||
}
|
||||
|
||||
var downloadURL, sha256Hash string
|
||||
for _, att := range latestStable.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, latestStable.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, latestStable.TagName)
|
||||
}
|
||||
if licenseKey != "" {
|
||||
downloadURL += "?dlid=" + licenseKey
|
||||
}
|
||||
for _, att := range latestStable.Attachments {
|
||||
if strings.HasSuffix(att.Name, ".sha256") {
|
||||
sha256Hash = readSHA256FromSidecar(ctx, att)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version := extractVersion(latestStable.TagName)
|
||||
if isStreamName(version, streams) || version == "" {
|
||||
version = extractVersion(latestStable.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = latestStable.TagName
|
||||
}
|
||||
|
||||
changelog := ""
|
||||
if latestStable.Note != "" {
|
||||
changelog = latestStable.Note
|
||||
}
|
||||
|
||||
return &WHMCSUpdate{
|
||||
Name: displayName,
|
||||
Version: version,
|
||||
Description: description,
|
||||
DownloadURL: downloadURL,
|
||||
Changelog: changelog,
|
||||
ReleaseDate: time.Unix(int64(latestStable.CreatedUnix), 0).Format("2006-01-02"),
|
||||
Author: maintainer,
|
||||
AuthorURL: maintainerURL,
|
||||
SHA256: sha256Hash,
|
||||
}, nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package updateserver
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -95,7 +96,7 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if ch == "stable" {
|
||||
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
|
||||
latestStable = rel
|
||||
@@ -138,15 +139,21 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
|
||||
}
|
||||
|
||||
version := extractVersion(latestStable.TagName)
|
||||
if version == "" || isStreamName(latestStable.TagName, streams) {
|
||||
version = extractVersion(latestStable.Title)
|
||||
}
|
||||
if version == "" {
|
||||
version = latestStable.TagName
|
||||
}
|
||||
lastUpdated := time.Unix(int64(latestStable.CreatedUnix), 0).Format("2006-01-02 3:04pm MST")
|
||||
|
||||
// Build sections from release notes.
|
||||
sections := map[string]string{}
|
||||
if latestStable.Note != "" {
|
||||
sections["changelog"] = buildWordPressChangelog(releases, streams)
|
||||
sections["changelog"] = buildWordPressChangelog(ctx, releases, streams)
|
||||
}
|
||||
if cfg != nil && cfg.Description != "" {
|
||||
sections["description"] = "<p>" + cfg.Description + "</p>"
|
||||
sections["description"] = "<p>" + html.EscapeString(cfg.Description) + "</p>"
|
||||
}
|
||||
|
||||
// Build icon/banner URLs from repo assets.
|
||||
@@ -170,14 +177,14 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
|
||||
}
|
||||
|
||||
// buildWordPressChangelog builds an HTML changelog from multiple releases.
|
||||
func buildWordPressChangelog(releases []*repo_model.Release, streams []licenses.StreamDef) string {
|
||||
func buildWordPressChangelog(ctx context.Context, releases []*repo_model.Release, streams []licenses.StreamDef) string {
|
||||
var b strings.Builder
|
||||
count := 0
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag || rel.Note == "" {
|
||||
continue
|
||||
}
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
|
||||
if ch != "stable" {
|
||||
continue
|
||||
}
|
||||
@@ -191,7 +198,7 @@ func buildWordPressChangelog(releases []*repo_model.Release, streams []licenses.
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") {
|
||||
b.WriteString(fmt.Sprintf("<li>%s</li>\n", strings.TrimLeft(trimmed[2:], " ")))
|
||||
b.WriteString(fmt.Sprintf("<li>%s</li>\n", html.EscapeString(strings.TrimLeft(trimmed[2:], " "))))
|
||||
}
|
||||
}
|
||||
b.WriteString("</ul>\n")
|
||||
|
||||
@@ -25,14 +25,16 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
|
||||
<details id="new-package-details">
|
||||
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
|
||||
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
|
||||
{{if .IsRepoAdmin}}
|
||||
<summary class="ui primary small button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
|
||||
{{end}}
|
||||
</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">
|
||||
<div class="tw-mb-4">
|
||||
<form class="ui form" method="post" action="{{$.Org.HomeLink}}/-/licenses/packages">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
@@ -80,9 +82,9 @@
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{{end}}
|
||||
</details>
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
@@ -149,8 +151,10 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form tw-mb-4" method="get" action="{{$.Org.HomeLink}}/-/licenses">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" name="q" value="{{.SearchQuery}}" placeholder="{{ctx.Locale.Tr "repo.licenses.search_placeholder"}}">
|
||||
<div class="tw-flex tw-gap-2 tw-items-center tw-max-w-lg">
|
||||
<div class="ui input tw-flex-1">
|
||||
<input type="text" name="q" value="{{.SearchQuery}}" placeholder="{{ctx.Locale.Tr "repo.licenses.search_placeholder"}}">
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{svg "octicon-search" 14}}</button>
|
||||
{{if .SearchQuery}}<a class="ui button" href="{{$.Org.HomeLink}}/-/licenses">{{ctx.Locale.Tr "repo.licenses.clear_search"}}</a>{{end}}
|
||||
</div>
|
||||
@@ -261,6 +265,7 @@
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.confirm_delete_package_typed"}}</p>
|
||||
</div>
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.type_name_to_confirm"}}</label>
|
||||
<input name="confirm_name" required>
|
||||
@@ -277,6 +282,7 @@
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.confirm_delete_key_typed"}}</p>
|
||||
</div>
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.licenses.delete_key"))}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<div class="flex-container-nav">
|
||||
<div class="ui fluid vertical menu">
|
||||
<div class="header item">{{ctx.Locale.Tr "org.settings"}}</div>
|
||||
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.settings"}}</div>
|
||||
<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.OrgLink}}/settings">
|
||||
{{ctx.Locale.Tr "org.settings.options"}}
|
||||
{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.settings.options"}}
|
||||
</a>
|
||||
{{if not DisableWebhooks}}
|
||||
<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{.OrgLink}}/settings/hooks">
|
||||
{{ctx.Locale.Tr "repo.settings.hooks"}}
|
||||
{{svg "octicon-webhook"}} {{ctx.Locale.Tr "repo.settings.hooks"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsOrgSettingsLabels}}active {{end}}item" href="{{.OrgLink}}/settings/labels">
|
||||
{{ctx.Locale.Tr "repo.labels"}}
|
||||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.labels"}}
|
||||
</a>
|
||||
{{if .EnableOAuth2}}
|
||||
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{.OrgLink}}/settings/applications">
|
||||
{{ctx.Locale.Tr "settings.applications"}}
|
||||
{{svg "octicon-apps"}} {{ctx.Locale.Tr "settings.applications"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
|
||||
{{ctx.Locale.Tr "user.block.list"}}
|
||||
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block.list"}}
|
||||
</a>
|
||||
{{if .EnablePackages}}
|
||||
<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
|
||||
{{ctx.Locale.Tr "packages.title"}}
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsUpdateStreams}}active {{end}}item" href="{{.OrgLink}}/settings/update-streams">
|
||||
@@ -30,7 +30,7 @@
|
||||
</a>
|
||||
{{if .EnableActions}}
|
||||
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
<div class="menu">
|
||||
<a class="{{if .PageIsOrgSettingsActionsGeneral}}active {{end}}item" href="{{.OrgLink}}/settings/actions">
|
||||
{{ctx.Locale.Tr "settings.general"}}
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
<input id="location" name="location" value="{{.Org.Location}}" maxlength="50">
|
||||
</div>
|
||||
|
||||
{{if .ParentOrgCandidates}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.parent_org"}}</label>
|
||||
<select name="parent_org_id" class="ui dropdown">
|
||||
<option value="0">{{ctx.Locale.Tr "org.settings.parent_org_none"}}</option>
|
||||
{{range .ParentOrgCandidates}}
|
||||
<option value="{{.ID}}" {{if eq $.ParentOrgID .ID}}selected{{end}}>{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.parent_org_help"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="field" id="permission_box">
|
||||
<label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
|
||||
<div class="field">
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.feed_visibility_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.key_prefix"}}</label>
|
||||
<input name="key_prefix" value="{{.StreamConfig.KeyPrefix}}" placeholder="MOKO" maxlength="20">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.key_prefix_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.download_gating"}}</label>
|
||||
<select name="download_gating" class="ui dropdown">
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}}
|
||||
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo) .IsSigned}}
|
||||
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions">
|
||||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}
|
||||
{{if .Repository.NumOpenActionRuns}}
|
||||
@@ -128,7 +128,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .LicensingEnabled}}
|
||||
{{if and .LicensingEnabled .IsSigned}}
|
||||
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
{{if .NumLicensePackages}}
|
||||
|
||||
@@ -26,8 +26,12 @@
|
||||
{{end}}
|
||||
|
||||
{{/* ── License Packages ── */}}
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
|
||||
<details id="new-package-details">
|
||||
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
|
||||
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
|
||||
{{if .IsRepoAdmin}}
|
||||
<summary class="ui primary small button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
|
||||
{{end}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .LicensePackages}}
|
||||
@@ -90,12 +94,9 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* ── Create New License Package ── */}}
|
||||
{{/* ── Create New License Package (form panel, toggled by header button) ── */}}
|
||||
{{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">
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
@@ -143,9 +144,8 @@
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
|
||||
{{/* ── Issued Keys ── */}}
|
||||
@@ -155,8 +155,10 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form tw-mb-4" method="get" action="{{.RepoLink}}/licenses">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" name="q" value="{{.SearchQuery}}" placeholder="{{ctx.Locale.Tr "repo.licenses.search_placeholder"}}">
|
||||
<div class="tw-flex tw-gap-2 tw-items-center tw-max-w-lg">
|
||||
<div class="ui input tw-flex-1">
|
||||
<input type="text" name="q" value="{{.SearchQuery}}" placeholder="{{ctx.Locale.Tr "repo.licenses.search_placeholder"}}">
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{svg "octicon-search" 14}}</button>
|
||||
{{if .SearchQuery}}<a class="ui button" href="{{.RepoLink}}/licenses">{{ctx.Locale.Tr "repo.licenses.clear_search"}}</a>{{end}}
|
||||
</div>
|
||||
@@ -270,6 +272,7 @@
|
||||
<div class="ui action input tw-w-full">
|
||||
<input class="js-feed-url-joomla" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates.xml" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-feed-url-joomla" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates.xml" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -279,6 +282,7 @@
|
||||
<div class="ui action input tw-w-full">
|
||||
<input class="js-feed-url-dolibarr" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-feed-url-dolibarr" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -288,6 +292,7 @@
|
||||
<div class="ui action input tw-w-full">
|
||||
<input class="js-feed-url-wordpress" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/wordpress.json" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-feed-url-wordpress" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates/wordpress.json" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -296,6 +301,7 @@
|
||||
<div class="ui action input tw-w-full">
|
||||
<input class="js-feed-url-changelog" type="text" readonly value="{{.Repository.HTMLURL ctx}}/changelog.xml" onclick="this.select()">
|
||||
<button class="ui button" data-clipboard-target=".js-feed-url-changelog" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
|
||||
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/changelog.xml" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,6 +317,7 @@
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.confirm_delete_package_typed"}}</p>
|
||||
</div>
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.type_name_to_confirm"}}</label>
|
||||
<input name="confirm_name" required>
|
||||
@@ -328,6 +335,7 @@
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.confirm_delete_key_typed"}}</p>
|
||||
</div>
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.licenses.delete_key"))}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -117,6 +117,19 @@
|
||||
<div class="help tw-block tw-ml-[21px]">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</div>
|
||||
</div>
|
||||
|
||||
{{if .LicensingEnabled}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.release.update_stream"}}</label>
|
||||
<select name="update_stream" class="ui dropdown">
|
||||
<option value="">{{ctx.Locale.Tr "repo.release.update_stream_auto"}}</option>
|
||||
{{range .AvailableStreams}}
|
||||
<option value="{{.Name}}" {{if eq $.ReleaseStream .Name}}selected{{end}}>{{.Name}}{{if .Description}} — {{.Description}}{{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<div class="help">{{ctx.Locale.Tr "repo.release.update_stream_help"}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="flex-text-block tw-justify-end">
|
||||
{{if .PageIsEditRelease}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/releases">
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings advanced")}}
|
||||
<div class="user-main-content twelve wide column">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "repo.settings.advanced_settings"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post">
|
||||
<input type="hidden" name="action" value="advanced">
|
||||
{{/* ── Code ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-code" 16}} {{ctx.Locale.Tr "repo.code"}}</h5>
|
||||
{{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
|
||||
{{$isCodeGlobalDisabled := ctx.Consts.RepoUnitTypeCode.UnitGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox{{if $isCodeGlobalDisabled}} disabled{{end}}"{{if $isCodeGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.code.desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/* ── Wiki ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-book" 16}} {{ctx.Locale.Tr "repo.wiki"}}</h5>
|
||||
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
|
||||
{{$isExternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalWiki}}
|
||||
{{$isWikiEnabled := or $isInternalWikiEnabled $isExternalWikiEnabled}}
|
||||
{{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}}
|
||||
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}}
|
||||
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox{{if $isBothWikiGlobalDisabled}} disabled{{end}}"{{if $isBothWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if $isWikiEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.wiki_desc"}}</label>
|
||||
</div>
|
||||
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isInternalWikiEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="internal_wiki_box" class="field tw-pl-4 {{if not $isInternalWikiEnabled}}disabled{{end}}">
|
||||
<div class="inline field">
|
||||
<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}}>
|
||||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isExternalWikiEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="external_wiki_box" class="field tw-pl-4 {{if not $isExternalWikiEnabled}}disabled{{end}}">
|
||||
<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
|
||||
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/* ── Issues ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-issue-opened" 16}} {{ctx.Locale.Tr "repo.issues"}}</h5>
|
||||
{{$isIssuesEnabled := or (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues) (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
|
||||
{{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
|
||||
{{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}}
|
||||
{{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.issues"}}</label>
|
||||
<div class="ui checkbox{{if $isIssuesAndExternalGlobalDisabled}} disabled{{end}}"{{if $isIssuesAndExternalGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if $isIssuesEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.issues_desc"}}</label>
|
||||
</div>
|
||||
<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isIssuesGlobalDisabled}} disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
|
||||
{{if .Repository.CanEnableTimetracker}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_timetracker" class="enable-system" data-target="#only_contributors" type="checkbox" {{if .Repository.IsTimetrackerEnabled ctx}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.enable_timetracker"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if not (.Repository.IsTimetrackerEnabled ctx)}}disabled{{end}}" id="only_contributors">
|
||||
<div class="ui checkbox">
|
||||
<input name="allow_only_contributors_to_track_time" type="checkbox" {{if .Repository.AllowOnlyContributorsToTrackTime ctx}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.allow_only_contributors_to_track_time"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_issue_dependencies" type="checkbox" {{if (.Repository.IsDependenciesEnabled ctx)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.issues.dependency.setting"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
<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}}>
|
||||
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
|
||||
<div class="field">
|
||||
<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
|
||||
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label>
|
||||
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
|
||||
</div>
|
||||
<div class="inline fields">
|
||||
<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
{{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
|
||||
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="numeric" {{if eq $externalTrackerStyle "numeric"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
|
||||
<label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
|
||||
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/* ── Projects ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-project" 16}} {{ctx.Locale.Tr "repo.projects"}}</h5>
|
||||
{{$isProjectsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
|
||||
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.projects"}}</label>
|
||||
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
|
||||
</div>
|
||||
<div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box">
|
||||
<p>
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
|
||||
</p>
|
||||
<div class="ui dropdown selection">
|
||||
<select name="projects_mode">
|
||||
<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
|
||||
<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
|
||||
<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
|
||||
</select>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="default text">
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
|
||||
{{end}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
|
||||
{{end}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
|
||||
<div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
|
||||
<div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/* ── Releases ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-tag" 16}} {{ctx.Locale.Tr "repo.releases"}}</h5>
|
||||
{{$isReleasesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeReleases}}
|
||||
{{$isReleasesGlobalDisabled := ctx.Consts.RepoUnitTypeReleases.UnitGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<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}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.releases_desc"}}</label>
|
||||
</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>
|
||||
{{/* ── Packages ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-package" 16}} {{ctx.Locale.Tr "repo.packages"}}</h5>
|
||||
{{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}}
|
||||
{{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox{{if $isPackagesGlobalDisabled}} disabled{{end}}"{{if $isPackagesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_packages" type="checkbox" {{if $isPackagesEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.packages_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{if not .IsMirror}}
|
||||
{{/* ── Pull Requests ── */}}
|
||||
<div class="tw-mb-4">
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls"}}</h5>
|
||||
{{$pullRequestEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}}
|
||||
{{$pullRequestGlobalDisabled := ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled}}
|
||||
{{$prUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypePullRequests}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.pulls"}}</label>
|
||||
<div class="ui checkbox{{if $pullRequestGlobalDisabled}} disabled{{end}}"{{if $pullRequestGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_pulls" type="checkbox" data-target="#pull_box" {{if $pullRequestEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field{{if not $pullRequestEnabled}} disabled{{end}}" id="pull_box">
|
||||
<div class="field">
|
||||
<p>
|
||||
{{ctx.Locale.Tr "repo.settings.merge_style_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_rebase" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebase)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_rebase_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_squash" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowSquash)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.merge_manually"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p>
|
||||
{{ctx.Locale.Tr "repo.settings.default_merge_style_desc"}}
|
||||
</p>
|
||||
<div class="ui dropdown selection">
|
||||
<select name="pulls_default_merge_style">
|
||||
<option value="merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</option>
|
||||
<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option>
|
||||
<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option>
|
||||
<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option>
|
||||
<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option>
|
||||
</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="default text">
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div>
|
||||
<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
|
||||
<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
|
||||
<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div>
|
||||
<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch"}}</label>
|
||||
<div class="ui search selection dropdown">
|
||||
<input type="hidden" name="default_target_branch" value="{{$prUnit.PullRequestsConfig.DefaultTargetBranch}}">
|
||||
<div class="default text"></div>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="item" data-value="">{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch_default" $.Repository.DefaultBranch}}</div>
|
||||
{{range $branchName := $.Branches}}
|
||||
<div class="item" data-value="{{$branchName}}">{{$branchName}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="default_allow_maintainer_edit" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultAllowMaintainerEdit)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_allow_edits_from_maintainers"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_rebase_update" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseUpdate)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_rebase_update"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_delete_branch_after_merge"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_autodetect_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AutodetectManualMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_ignore_whitespace" type="checkbox" {{if and $pullRequestEnabled ($prUnit.PullRequestsConfig.IgnoreWhitespaceConflicts)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.ignore_whitespace"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="field tw-mt-4">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||
@@ -0,0 +1,110 @@
|
||||
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings licensing")}}
|
||||
<div class="user-main-content twelve wide column">
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post" action="{{.Link}}">
|
||||
<input type="hidden" name="action" value="licensing">
|
||||
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_licensing" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
|
||||
<label><strong>{{ctx.Locale.Tr "repo.settings.enable_licensing"}}</strong></label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.licensing_section_desc"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></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="wordpress" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "wordpress")}}selected{{end}}>WordPress (JSON)</option>
|
||||
<option value="composer" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "composer")}}selected{{end}}>Composer (packages.json)</option>
|
||||
<option value="prestashop" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "prestashop")}}selected{{end}}>PrestaShop</option>
|
||||
<option value="drupal" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "drupal")}}selected{{end}}>Drupal</option>
|
||||
<option value="both" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "both")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.update_platform_both"}}</option>
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.update_platform_help"}}</p>
|
||||
</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>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.require_update_key_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.download_gating"}}</label>
|
||||
<select name="download_gating" class="ui dropdown">
|
||||
<option value="none" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.DownloadGating "") (eq .RepoUpdateConfig.DownloadGating "none")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_none"}}</option>
|
||||
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_prerelease"}}</option>
|
||||
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_all"}}</option>
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.download_gating_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.support_url"}}</label>
|
||||
<input name="support_url" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.SupportURL}}{{end}}" placeholder="https://mokoconsulting.tech/support">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.support_url_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<h5>{{ctx.Locale.Tr "repo.settings.extension_metadata"}}</h5>
|
||||
<p class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.extension_metadata_desc"}}</p>
|
||||
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.extension_name"}}</label>
|
||||
<input name="extension_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.ExtensionName}}{{end}}" placeholder="pkg_myextension">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.extension_name_help"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.display_name"}}</label>
|
||||
<input name="display_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.DisplayName}}{{end}}" placeholder="Package - My Extension">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.display_name_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.extension_type"}}</label>
|
||||
<select name="extension_type" class="ui dropdown">
|
||||
<option value="">{{ctx.Locale.Tr "repo.settings.inherit_org"}}</option>
|
||||
<option value="package" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "package")}}selected{{end}}>Package</option>
|
||||
<option value="component" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "component")}}selected{{end}}>Component</option>
|
||||
<option value="module" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "module")}}selected{{end}}>Module</option>
|
||||
<option value="plugin" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "plugin")}}selected{{end}}>Plugin</option>
|
||||
<option value="template" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "template")}}selected{{end}}>Template</option>
|
||||
<option value="library" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "library")}}selected{{end}}>Library</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.target_version"}}</label>
|
||||
<input name="target_version" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.TargetVersion}}{{end}}" placeholder="(5|6)\..*">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.target_version_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.maintainer"}}</label>
|
||||
<input name="maintainer" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.Maintainer}}{{end}}" placeholder="Moko Consulting">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.php_minimum"}}</label>
|
||||
<input name="php_minimum" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.PHPMinimum}}{{end}}" placeholder="8.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||
@@ -1,45 +1,53 @@
|
||||
<div class="flex-container-nav">
|
||||
<div class="ui fluid vertical menu">
|
||||
<div class="header item">{{ctx.Locale.Tr "repo.settings"}}</div>
|
||||
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "repo.settings"}}</div>
|
||||
<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.RepoLink}}/settings">
|
||||
{{ctx.Locale.Tr "repo.settings.options"}}
|
||||
{{svg "octicon-gear"}} {{ctx.Locale.Tr "repo.settings.options"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced">
|
||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}}
|
||||
</a>
|
||||
{{if .LicensingEnabled}}
|
||||
<a class="{{if .PageIsSettingsLicensing}}active {{end}}item" href="{{.RepoLink}}/settings/licensing">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
|
||||
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
|
||||
{{ctx.Locale.Tr "repo.settings.public_access"}}
|
||||
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsCollaboration}}active {{end}}item" href="{{.RepoLink}}/settings/collaboration">
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration"}}
|
||||
{{svg "octicon-people"}} {{ctx.Locale.Tr "repo.settings.collaboration"}}
|
||||
</a>
|
||||
{{if not DisableWebhooks}}
|
||||
<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{.RepoLink}}/settings/hooks">
|
||||
{{ctx.Locale.Tr "repo.settings.hooks"}}
|
||||
{{svg "octicon-webhook"}} {{ctx.Locale.Tr "repo.settings.hooks"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
|
||||
<a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches">
|
||||
{{ctx.Locale.Tr "repo.settings.branches"}}
|
||||
{{svg "octicon-git-branch"}} {{ctx.Locale.Tr "repo.settings.branches"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags">
|
||||
{{ctx.Locale.Tr "repo.settings.tags"}}
|
||||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.settings.tags"}}
|
||||
</a>
|
||||
{{if .SignedUser.CanEditGitHook}}
|
||||
<a class="{{if .PageIsSettingsGitHooks}}active {{end}}item" href="{{.RepoLink}}/settings/hooks/git">
|
||||
{{ctx.Locale.Tr "repo.settings.githooks"}}
|
||||
{{svg "octicon-terminal"}} {{ctx.Locale.Tr "repo.settings.githooks"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{.RepoLink}}/settings/keys">
|
||||
{{ctx.Locale.Tr "repo.settings.deploy_keys"}}
|
||||
{{svg "octicon-key-asterisk"}} {{ctx.Locale.Tr "repo.settings.deploy_keys"}}
|
||||
</a>
|
||||
{{if .LFSStartServer}}
|
||||
<a class="{{if .PageIsSettingsLFS}}active {{end}}item" href="{{.RepoLink}}/settings/lfs">
|
||||
{{ctx.Locale.Tr "repo.settings.lfs"}}
|
||||
{{svg "octicon-file-binary"}} {{ctx.Locale.Tr "repo.settings.lfs"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
<div class="menu">
|
||||
<a class="{{if .PageIsActionsSettingsGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/actions/general">
|
||||
{{ctx.Locale.Tr "actions.general"}}
|
||||
|
||||
@@ -287,481 +287,6 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* FIXME: need to split the "Advance Settings" by units, there are too many options here */}}
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "repo.settings.advanced_settings"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post">
|
||||
<input type="hidden" name="action" value="advanced">
|
||||
|
||||
{{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
|
||||
{{$isCodeGlobalDisabled := ctx.Consts.RepoUnitTypeCode.UnitGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.code"}}</label>
|
||||
<div class="ui checkbox{{if $isCodeGlobalDisabled}} disabled{{end}}"{{if $isCodeGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.code.desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
|
||||
{{$isExternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalWiki}}
|
||||
{{$isWikiEnabled := or $isInternalWikiEnabled $isExternalWikiEnabled}}
|
||||
{{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}}
|
||||
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}}
|
||||
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.wiki"}}</label>
|
||||
<div class="ui checkbox{{if $isBothWikiGlobalDisabled}} disabled{{end}}"{{if $isBothWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if $isWikiEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.wiki_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isInternalWikiEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="internal_wiki_box" class="field tw-pl-4 {{if not $isInternalWikiEnabled}}disabled{{end}}">
|
||||
<div class="inline field">
|
||||
<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}}>
|
||||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isExternalWikiEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="external_wiki_box" class="field tw-pl-4 {{if not $isExternalWikiEnabled}}disabled{{end}}">
|
||||
<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
|
||||
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{{$isIssuesEnabled := or (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues) (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
|
||||
{{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
|
||||
{{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}}
|
||||
{{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.issues"}}</label>
|
||||
<div class="ui checkbox{{if $isIssuesAndExternalGlobalDisabled}} disabled{{end}}"{{if $isIssuesAndExternalGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if $isIssuesEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.issues_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isIssuesGlobalDisabled}} disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
|
||||
{{if .Repository.CanEnableTimetracker}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_timetracker" class="enable-system" data-target="#only_contributors" type="checkbox" {{if .Repository.IsTimetrackerEnabled ctx}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.enable_timetracker"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if not (.Repository.IsTimetrackerEnabled ctx)}}disabled{{end}}" id="only_contributors">
|
||||
<div class="ui checkbox">
|
||||
<input name="allow_only_contributors_to_track_time" type="checkbox" {{if .Repository.AllowOnlyContributorsToTrackTime ctx}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.allow_only_contributors_to_track_time"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_issue_dependencies" type="checkbox" {{if (.Repository.IsDependenciesEnabled ctx)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.issues.dependency.setting"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
<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}}>
|
||||
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
|
||||
<div class="field">
|
||||
<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
|
||||
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label>
|
||||
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
|
||||
</div>
|
||||
<div class="inline fields">
|
||||
<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
{{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
|
||||
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="numeric" {{if eq $externalTrackerStyle "numeric"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
|
||||
<label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
|
||||
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{{$isProjectsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
|
||||
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.projects"}}</label>
|
||||
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box">
|
||||
<p>
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
|
||||
</p>
|
||||
<div class="ui dropdown selection">
|
||||
<select name="projects_mode">
|
||||
<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
|
||||
<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
|
||||
<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
|
||||
</select>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="default text">
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
|
||||
{{end}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
|
||||
{{end}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
|
||||
<div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
|
||||
<div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{{$isReleasesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeReleases}}
|
||||
{{$isReleasesGlobalDisabled := ctx.Consts.RepoUnitTypeReleases.UnitGlobalDisabled}}
|
||||
<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}}>
|
||||
<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>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{{/* ── Licensing & Update Feeds ── */}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.licensing_section"}}</label>
|
||||
<div class="ui checkbox">
|
||||
<input class="enable-system" name="enable_licensing" type="checkbox" data-target="#licensing_options_box" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.enable_licensing"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4{{if not (and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled)}} disabled{{end}}" id="licensing_options_box">
|
||||
<p class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.licensing_section_desc"}}</p>
|
||||
<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>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.update_platform_help"}}</p>
|
||||
</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>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.require_update_key_help"}}</p>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.download_gating"}}</label>
|
||||
<select name="download_gating" class="ui dropdown">
|
||||
<option value="none" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.DownloadGating "") (eq .RepoUpdateConfig.DownloadGating "none")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_none"}}</option>
|
||||
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_prerelease"}}</option>
|
||||
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_all"}}</option>
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.download_gating_help"}}</p>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.support_url"}}</label>
|
||||
<input name="support_url" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.SupportURL}}{{end}}" placeholder="https://mokoconsulting.tech/support">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.support_url_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<h6>{{ctx.Locale.Tr "repo.settings.extension_metadata"}}</h6>
|
||||
<p class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.extension_metadata_desc"}}</p>
|
||||
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.extension_name"}}</label>
|
||||
<input name="extension_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.ExtensionName}}{{end}}" placeholder="pkg_myextension">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.extension_name_help"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.display_name"}}</label>
|
||||
<input name="display_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.DisplayName}}{{end}}" placeholder="Package - My Extension">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.display_name_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.extension_type"}}</label>
|
||||
<select name="extension_type" class="ui dropdown">
|
||||
<option value="">{{ctx.Locale.Tr "repo.settings.inherit_org"}}</option>
|
||||
<option value="package" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "package")}}selected{{end}}>Package</option>
|
||||
<option value="component" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "component")}}selected{{end}}>Component</option>
|
||||
<option value="module" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "module")}}selected{{end}}>Module</option>
|
||||
<option value="plugin" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "plugin")}}selected{{end}}>Plugin</option>
|
||||
<option value="template" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "template")}}selected{{end}}>Template</option>
|
||||
<option value="library" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "library")}}selected{{end}}>Library</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.target_version"}}</label>
|
||||
<input name="target_version" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.TargetVersion}}{{end}}" placeholder="(5|6)\..*">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.target_version_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.maintainer"}}</label>
|
||||
<input name="maintainer" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.Maintainer}}{{end}}" placeholder="Moko Consulting">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.php_minimum"}}</label>
|
||||
<input name="php_minimum" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.PHPMinimum}}{{end}}" placeholder="8.1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}}
|
||||
{{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.packages"}}</label>
|
||||
<div class="ui checkbox{{if $isPackagesGlobalDisabled}} disabled{{end}}"{{if $isPackagesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_packages" type="checkbox" {{if $isPackagesEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.packages_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if not .IsMirror}}
|
||||
<div class="divider"></div>
|
||||
{{$pullRequestEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}}
|
||||
{{$pullRequestGlobalDisabled := ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled}}
|
||||
{{$prUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypePullRequests}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.pulls"}}</label>
|
||||
<div class="ui checkbox{{if $pullRequestGlobalDisabled}} disabled{{end}}"{{if $pullRequestGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_pulls" type="checkbox" data-target="#pull_box" {{if $pullRequestEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field{{if not $pullRequestEnabled}} disabled{{end}}" id="pull_box">
|
||||
<div class="field">
|
||||
<p>
|
||||
{{ctx.Locale.Tr "repo.settings.merge_style_desc"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_rebase" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebase)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_rebase_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_squash" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowSquash)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.pulls.merge_manually"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<p>
|
||||
{{ctx.Locale.Tr "repo.settings.default_merge_style_desc"}}
|
||||
</p>
|
||||
<div class="ui dropdown selection">
|
||||
<select name="pulls_default_merge_style">
|
||||
<option value="merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</option>
|
||||
<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option>
|
||||
<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option>
|
||||
<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option>
|
||||
<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option>
|
||||
</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="default text">
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}
|
||||
{{end}}
|
||||
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}
|
||||
{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div>
|
||||
<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
|
||||
<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
|
||||
<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div>
|
||||
<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch"}}</label>
|
||||
<div class="ui search selection dropdown">
|
||||
<input type="hidden" name="default_target_branch" value="{{$prUnit.PullRequestsConfig.DefaultTargetBranch}}">
|
||||
<div class="default text"></div>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="item" data-value="">{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch_default" $.Repository.DefaultBranch}}</div>
|
||||
{{range $branchName := $.Branches}}
|
||||
<div class="item" data-value="{{$branchName}}">{{$branchName}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="default_allow_maintainer_edit" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultAllowMaintainerEdit)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_allow_edits_from_maintainers"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_allow_rebase_update" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseUpdate)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_rebase_update"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_delete_branch_after_merge"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="enable_autodetect_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AutodetectManualMerge)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pulls_ignore_whitespace" type="checkbox" {{if and $pullRequestEnabled ($prUnit.PullRequestsConfig.IgnoreWhitespaceConflicts)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.ignore_whitespace"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "repo.settings.signing_settings"}}
|
||||
@@ -879,23 +404,34 @@
|
||||
{{if not .Repository.IsFork}}
|
||||
<div class="item tw-items-center">
|
||||
<div class="item-main">
|
||||
<div class="item-title">{{ctx.Locale.Tr "repo.visibility"}}</div>
|
||||
{{if .IsSystemRepo}}
|
||||
<div class="item-body">This is a system repository (dot-prefixed name). System repositories are always private and cannot be made public.</div>
|
||||
{{else if .Repository.IsPrivate}}
|
||||
<div class="item-body">{{ctx.Locale.Tr "repo.settings.visibility.public.text"}}</div>
|
||||
{{else}}
|
||||
<div class="item-body">{{ctx.Locale.Tr "repo.settings.visibility.private.text"}}</div>
|
||||
{{end}}
|
||||
<div class="item-title tw-flex tw-items-center tw-justify-between">
|
||||
<span>{{ctx.Locale.Tr "repo.visibility"}}</span>
|
||||
{{if .IsSystemRepo}}
|
||||
<span class="ui grey label">System</span>
|
||||
{{else if .Repository.IsHidden}}
|
||||
<span class="ui red label">{{ctx.Locale.Tr "repo.settings.visibility.hidden.label"}}</span>
|
||||
{{else if .Repository.IsPrivate}}
|
||||
<span class="ui orange label">{{ctx.Locale.Tr "repo.settings.visibility.private.label"}}</span>
|
||||
{{else}}
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.settings.visibility.public.label"}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="item-body">
|
||||
{{if .IsSystemRepo}}
|
||||
This is a system repository. System repositories are always private.
|
||||
{{else if .Repository.IsHidden}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.hidden.desc"}}
|
||||
{{else if .Repository.IsPrivate}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.private.desc"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.public.desc"}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if not .IsSystemRepo}}
|
||||
<div class="item-trailing">
|
||||
<button class="ui basic red show-modal button" data-modal="#visibility-repo-modal">
|
||||
{{if .Repository.IsPrivate}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.public.button"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.private.button"}}
|
||||
{{end}}
|
||||
{{ctx.Locale.Tr "repo.settings.change_visibility"}}
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -1078,43 +614,32 @@
|
||||
{{ctx.Locale.Tr "repo.visibility"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
{{if .Repository.IsPrivate}}
|
||||
<p>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_title"}}</p>
|
||||
<ul>
|
||||
<li>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_one"}}</li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_title"}}</p>
|
||||
<ul>
|
||||
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_one"}}</li>
|
||||
<li>
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_two"}}
|
||||
</li>
|
||||
{{if or .Repository.NumStars .Repository.NumWatches .Repository.NumForks}}
|
||||
<ul class="tw-my-0 tw-pl-4">
|
||||
{{if .Repository.NumStars}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_stars" .Repository.NumStars}}</li>{{end}}
|
||||
{{if .Repository.NumWatches}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_watchers" .Repository.NumWatches}}</li>{{end}}
|
||||
{{if .Repository.NumForks}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_forks" .Repository.NumForks}}</li>{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<form class="ui form tw-mt-5 form-fetch-action" action="{{.Link}}" method="post">
|
||||
<div class="ui warning message">
|
||||
<p>{{ctx.Locale.Tr "repo.settings.visibility.warning"}}</p>
|
||||
</div>
|
||||
<form class="ui form tw-mt-4 form-fetch-action" action="{{.Link}}" method="post">
|
||||
<input type="hidden" name="action" value="visibility">
|
||||
<input type="hidden" name="private" value="{{not .Repository.IsPrivate}}">
|
||||
{{if not .Repository.IsPrivate}}
|
||||
<div class="grouped fields">
|
||||
<div class="field">
|
||||
<label>
|
||||
{{ctx.Locale.Tr "repo.settings.enter_repo_full_name_to_confirm"}}
|
||||
<span class="tw-text-red">{{.Repository.FullName}}</span>
|
||||
</label>
|
||||
<div class="ui radio checkbox">
|
||||
<input name="visibility" type="radio" value="public" {{if and (not .Repository.IsPrivate) (not .Repository.IsHidden)}}checked{{end}}>
|
||||
<label><strong>{{ctx.Locale.Tr "repo.settings.visibility.public.label"}}</strong> — {{ctx.Locale.Tr "repo.settings.visibility.public.desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
|
||||
<input name="confirm_repo_name" required maxlength="200">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="visibility" type="radio" value="private" {{if and .Repository.IsPrivate (not .Repository.IsHidden)}}checked{{end}}>
|
||||
<label><strong>{{ctx.Locale.Tr "repo.settings.visibility.private.label"}}</strong> — {{ctx.Locale.Tr "repo.settings.visibility.private.desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (Iif .Repository.IsPrivate (ctx.Locale.Tr "repo.settings.visibility.public.button") (ctx.Locale.Tr "repo.settings.visibility.private.button")))}}
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input name="visibility" type="radio" value="hidden" {{if .Repository.IsHidden}}checked{{end}}>
|
||||
<label><strong>{{ctx.Locale.Tr "repo.settings.visibility.hidden.label"}}</strong> — {{ctx.Locale.Tr "repo.settings.visibility.hidden.desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.change_visibility"))}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
|
||||
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="status-page-error">
|
||||
<div class="status-page-error-title">403 Access Denied</div>
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-my-4">{{if .AccessDeniedPrompt}}{{.AccessDeniedPrompt}}{{else}}{{ctx.Locale.Tr "error403"}}{{end}}</div>
|
||||
</div>
|
||||
{{if not .IsSigned}}
|
||||
<div class="tw-max-w-sm tw-mx-auto tw-mt-4">
|
||||
<form class="ui form" action="{{AppSubUrl}}/user/login" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="redirect_to" value="{{.CurrentURL}}">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "home.uname_holder"}}</label>
|
||||
<input type="text" name="user_name" required autofocus>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "password"}}</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -1,41 +1,41 @@
|
||||
<div class="flex-container-nav">
|
||||
<div class="ui fluid vertical menu">
|
||||
<div class="header item">{{ctx.Locale.Tr "user.settings"}}</div>
|
||||
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "user.settings"}}</div>
|
||||
<a class="{{if .PageIsSettingsProfile}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
|
||||
{{ctx.Locale.Tr "settings.profile"}}
|
||||
{{svg "octicon-person"}} {{ctx.Locale.Tr "settings.profile"}}
|
||||
</a>
|
||||
{{if not ($.UserDisabledFeatures.Contains "manage_credentials" "deletion")}}
|
||||
<a class="{{if .PageIsSettingsAccount}}active {{end}}item" href="{{AppSubUrl}}/user/settings/account">
|
||||
{{ctx.Locale.Tr "settings.account"}}
|
||||
{{svg "octicon-shield-lock"}} {{ctx.Locale.Tr "settings.account"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if $.EnableNotifyMail}}
|
||||
<a class="{{if .PageIsSettingsNotifications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/notifications">
|
||||
{{ctx.Locale.Tr "notifications"}}
|
||||
{{svg "octicon-bell"}} {{ctx.Locale.Tr "notifications"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsAppearance}}active {{end}}item" href="{{AppSubUrl}}/user/settings/appearance">
|
||||
{{ctx.Locale.Tr "settings.appearance"}}
|
||||
{{svg "octicon-paintbrush"}} {{ctx.Locale.Tr "settings.appearance"}}
|
||||
</a>
|
||||
{{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
|
||||
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
|
||||
{{ctx.Locale.Tr "settings.security"}}
|
||||
{{svg "octicon-lock"}} {{ctx.Locale.Tr "settings.security"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
|
||||
{{ctx.Locale.Tr "user.block.list"}}
|
||||
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block.list"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
|
||||
{{ctx.Locale.Tr "settings.applications"}}
|
||||
{{svg "octicon-apps"}} {{ctx.Locale.Tr "settings.applications"}}
|
||||
</a>
|
||||
{{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys" "manage_gpg_keys")}}
|
||||
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{AppSubUrl}}/user/settings/keys">
|
||||
{{ctx.Locale.Tr "settings.ssh_gpg_keys"}}
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "settings.ssh_gpg_keys"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .EnableActions}}
|
||||
<details class="item toggleable-item" {{if or .PageIsUserSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
<div class="menu">
|
||||
<a class="{{if .PageIsUserSettingsActionsGeneral}}active {{end}}item" href="{{AppSubUrl}}/user/settings/actions/general">
|
||||
{{ctx.Locale.Tr "actions.general"}}
|
||||
@@ -54,19 +54,19 @@
|
||||
{{end}}
|
||||
{{if .EnablePackages}}
|
||||
<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{AppSubUrl}}/user/settings/packages">
|
||||
{{ctx.Locale.Tr "packages.title"}}
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if not DisableWebhooks}}
|
||||
<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{AppSubUrl}}/user/settings/hooks">
|
||||
{{ctx.Locale.Tr "repo.settings.hooks"}}
|
||||
{{svg "octicon-webhook"}} {{ctx.Locale.Tr "repo.settings.hooks"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsOrganization}}active {{end}}item" href="{{AppSubUrl}}/user/settings/organization">
|
||||
{{ctx.Locale.Tr "settings.organization"}}
|
||||
{{svg "octicon-organization"}} {{ctx.Locale.Tr "settings.organization"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
|
||||
{{ctx.Locale.Tr "settings.repos"}}
|
||||
{{svg "octicon-repo"}} {{ctx.Locale.Tr "settings.repos"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
<?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.17.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
@@ -87,15 +87,15 @@
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.14.00</version>
|
||||
<creationDate>2026-05-31</creationDate>
|
||||
<infourl title='MokoGitea'>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<version>05.17.00</version>
|
||||
<creationDate>2026-06-03</creationDate>
|
||||
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://code.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.17.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>bec4bf5a1a841f8e72d9826451004db5d8afc70144231dfedc7fb01a6695955c</sha256>
|
||||
<sha256>7f50295f58e207f1c2d2be92a172f4d077a4115ad1337c663e6f33e065e0cff9</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name="go" version=".*" />
|
||||
|
||||
@@ -426,10 +426,11 @@ The update feed system currently supports:
|
||||
| **Joomla** | `/{repo}/updates.xml` | XML with `<downloadkey>` | Production |
|
||||
| **Dolibarr** | `/{repo}/updates/dolibarr.json` | JSON | Production |
|
||||
| **WordPress** | `/{repo}/updates/wordpress.json` | PUC-compatible JSON | Production |
|
||||
| **Drupal** | Planned | XML/JSON | Planned (#353) |
|
||||
| **PrestaShop** | Planned | XML | Planned (#352) |
|
||||
| **Composer** | Planned | packages.json | Planned (#354) |
|
||||
| **WHMCS** | Planned | Custom | Planned (#355) |
|
||||
| **Composer** | `/{repo}/updates/packages.json` | packages.json | Production |
|
||||
| **PrestaShop** | `/{repo}/updates/prestashop.xml` | Module update XML | Production |
|
||||
| **Drupal** | `/{repo}/updates/drupal.xml` | Update status XML | Production |
|
||||
| **WHMCS** | `/{repo}/updates/whmcs.json` | Module update JSON | Production |
|
||||
| **Changelog** | `/{repo}/changelog.xml` | Joomla changelog XML | Production |
|
||||
|
||||
All platforms share the same licensing backend — the same keys, packages, and validation work across all feed formats.
|
||||
|
||||
@@ -444,3 +445,4 @@ All platforms share the same licensing backend — the same keys, packages, and
|
||||
| 1.2 | 2026-05-31 | Jonathan Miller (@jmiller) | Add permissions (TypeLicenses unit), renewal, auto-domain, custom keys, UI/UX cleanup |
|
||||
| 1.3 | 2026-06-01 | Jonathan Miller (@jmiller) | Add package archiving, expanded delete permissions, migration v340, API renew, step-by-step guides |
|
||||
| 1.4 | 2026-06-02 | Jonathan Miller (@jmiller) | WordPress feed, feed visibility modes, download gating, RepoScope enforcement, API package CRUD, settings API, combolist channel picker, double confirmation modals, extension metadata in repo settings, domain lock timer, Joomla-standard tags, SHA256 in XML, changelog XML, no-download release page mode |
|
||||
| 1.5 | 2026-06-02 | Jonathan Miller (@jmiller) | All 7 platform feeds (Composer, PrestaShop, Drupal, WHMCS), enterprise sub-org hierarchy, three-level repo visibility (Public/Private/Hidden), styled 403 page with login form, separate licensing/advanced settings pages, icons on all navbars, manual stream mapping, configurable key prefix, feed always public, xorm column name fixes, security hardening |
|
||||
|
||||
Reference in New Issue
Block a user