Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 789d3c9aa8 | |||
| 700cc77d0b | |||
| c268970505 | |||
| 1f09979c19 | |||
| 7786dee9d4 | |||
| 7959864835 | |||
| 20fef17dac | |||
| 4630327b02 | |||
| 4624385501 | |||
| 9721728b45 | |||
| 38ed8eaeea | |||
| 1608b5c4b9 | |||
| 6654d7605d | |||
| a7e39fa992 | |||
| 7c014dc4da | |||
| d0ca5eff28 | |||
| e946d49bf3 | |||
| c3eb57f124 | |||
| c0662ab118 | |||
| fce8389296 | |||
| eeaf9f036b | |||
| 9957fe56ca | |||
| dcc4a49cdb | |||
| f15a0ed7e4 | |||
| 0e0ce94353 | |||
| 455f12e21b | |||
| 360d0b1b1f | |||
| e8ce4ae60b | |||
| 775766bc64 | |||
| cc61032697 | |||
| 6d9f0d9727 | |||
| 00d862f737 | |||
| 233144e33e | |||
| 38beba655b | |||
| dd6fc4b69c | |||
| 3b79d57d45 | |||
| 871cb495b1 | |||
| d66ca8db83 | |||
| 01f6722ccc | |||
| 7019f08f74 | |||
| aa6c3fc4ed | |||
| b577f8f4f4 | |||
| 5ffe68499d | |||
| 65456962b4 | |||
| 020b160403 | |||
| c633024a9c | |||
| 8ffdbff72a | |||
| d609b8db8c | |||
| bf35e5510d | |||
| 1e1441f8bd | |||
| 3c55a3baca | |||
| b7f9743ade |
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||||
|
<identity>
|
||||||
|
<name>MokoGitea</name>
|
||||||
|
<org>MokoConsulting</org>
|
||||||
|
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
|
||||||
|
<version>01.00.00</version>
|
||||||
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
|
</identity>
|
||||||
|
<governance>
|
||||||
|
<platform>go</platform>
|
||||||
|
<standards-version>05.00.00</standards-version>
|
||||||
|
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
||||||
|
</governance>
|
||||||
|
<build>
|
||||||
|
<language>Go</language>
|
||||||
|
<package-type>application</package-type>
|
||||||
|
<entry-point>./</entry-point>
|
||||||
|
</build>
|
||||||
|
</moko-platform>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
|
# VERSION: 09.02.00
|
||||||
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
|
|
||||||
|
name: "Universal: Auto Version Bump"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
name: Version Bump
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
if [ -d "/opt/moko-platform/cli" ]; then
|
||||||
|
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Bump version
|
||||||
|
run: |
|
||||||
|
BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
|
||||||
|
echo "$BUMP"
|
||||||
|
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
|
||||||
|
[ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; }
|
||||||
|
|
||||||
|
# Propagate to platform manifests with -dev suffix
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
VERSION="${VERSION}-dev"
|
||||||
|
|
||||||
|
# Commit if anything changed
|
||||||
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
|
echo "No version changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push origin dev
|
||||||
|
echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -26,13 +26,20 @@
|
|||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
default: release
|
||||||
|
options:
|
||||||
|
- release
|
||||||
|
- promote-rc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
@@ -44,28 +51,94 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
# ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
|
||||||
name: Build & Release Pipeline
|
promote-rc:
|
||||||
|
name: Promote Pre-Release to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
if: >-
|
||||||
|
(github.event.action == 'opened' && github.event.pull_request.draft == true) ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-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: Promote to release-candidate
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_promote.php \
|
||||||
|
--from auto --to release-candidate \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${API_BASE}" \
|
||||||
|
--branch "${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||||
|
|
||||||
|
- name: Cascade lesser channels
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_cascade.php \
|
||||||
|
--stability release-candidate \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${API_BASE}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git for bot pushes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
# Ensure PHP + Composer are available
|
# Ensure PHP + Composer are available
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
# 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 \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
/tmp/moko-platform-api
|
/tmp/moko-platform-api
|
||||||
@@ -92,20 +165,44 @@ jobs:
|
|||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
# Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20)
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: "Step 1b: Bump version"
|
# -- CHECK FOR RC PROMOTION ------------------------------------------------
|
||||||
id: bump
|
- name: "Check for RC release"
|
||||||
|
id: rc
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
|
||||||
|
RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
|
||||||
|
echo "promote=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
|
||||||
|
else
|
||||||
|
echo "promote=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::No RC release — full build pipeline"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Step 1b: Minor bump version"
|
||||||
|
id: bump
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote != 'true'
|
||||||
run: |
|
run: |
|
||||||
MOKO_API="/tmp/moko-platform-api/cli"
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
|
php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
|
||||||
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
||||||
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
# Strip any pre-release suffix — stable releases have no suffix
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Bumped to: ${VERSION}"
|
echo "Bumped to: ${VERSION}"
|
||||||
|
|
||||||
@@ -135,95 +232,8 @@ jobs:
|
|||||||
steps.check.outputs.already_released != 'true'
|
steps.check.outputs.already_released != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
ERRORS=0
|
php /tmp/moko-platform-api/cli/release_validate.php \
|
||||||
|
--path . --version "$VERSION" --output-summary --github-output || true
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
|
||||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
|
||||||
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- Version drift check (must pass before release) --------
|
|
||||||
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
|
||||||
if [ "$README_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check CHANGELOG version matches
|
|
||||||
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
|
|
||||||
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
|
|
||||||
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check composer.json version if present
|
|
||||||
if [ -f "composer.json" ]; then
|
|
||||||
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
|
|
||||||
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
|
|
||||||
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Common checks
|
|
||||||
if [ ! -f "LICENSE" ]; then
|
|
||||||
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
|
|
||||||
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Platform-specific checks --------
|
|
||||||
case "$PLATFORM" in
|
|
||||||
joomla)
|
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
|
|
||||||
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi ;;
|
|
||||||
dolibarr)
|
|
||||||
if [ -n "$MOD_FILE" ]; then
|
|
||||||
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
if [ ! -f "update.txt" ]; then
|
|
||||||
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi ;;
|
|
||||||
*) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
||||||
# Always runs — every version change on main archives to version/XX.YY
|
# Always runs — every version change on main archives to version/XX.YY
|
||||||
@@ -261,17 +271,21 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
|
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
|
||||||
|
php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
- name: "Step 5: Write update stream"
|
# Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
|
||||||
|
|
||||||
|
- name: "Step 4b: Promote and prune CHANGELOG"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true' &&
|
||||||
steps.platform.outputs.platform == 'joomla'
|
steps.check.outputs.already_released != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
php /tmp/moko-platform-api/cli/updates_xml_build.php \
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
--path . --version "${VERSION}" --stability stable \
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
|
||||||
--github-output
|
php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Commit release changes
|
- name: Commit release changes
|
||||||
if: >-
|
if: >-
|
||||||
@@ -283,21 +297,16 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
# Set push URL with token for branch-protected repos
|
|
||||||
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
git push -u origin HEAD
|
# Detached HEAD on PR merge — push explicitly to main
|
||||||
|
git push origin HEAD:refs/heads/main
|
||||||
|
|
||||||
# -- STEP 6: Create tag ---------------------------------------------------
|
# -- STEP 6: Create tag ---------------------------------------------------
|
||||||
- name: "Step 6: Create git tag"
|
- name: "Step 6: Create git tag"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true'
|
||||||
steps.check.outputs.tag_exists != 'true' &&
|
|
||||||
steps.version.outputs.is_minor == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
# Only create the major release tag if it doesn't exist yet
|
# Only create the major release tag if it doesn't exist yet
|
||||||
@@ -310,374 +319,126 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# -- STEP 7: Create or update Gitea Release --------------------------------
|
# -- STEP 7a: Promote RC to stable (skip build) ----------------------------
|
||||||
- name: "Step 7: Gitea Release"
|
- name: "Step 7a: Promote RC to stable"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true'
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote == 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_promote.php \
|
||||||
|
--from release-candidate --to stable \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${API_BASE}" \
|
||||||
|
--path . --branch main
|
||||||
|
echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 7b: Create or update Gitea Release (full build path) -------------
|
||||||
|
- name: "Step 7b: Gitea Release"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
MAJOR="${{ steps.version.outputs.major }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch main
|
||||||
|
echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# Reuse metadata from Step 5 (single source of truth)
|
# -- STEP 8: Build packages and upload to release ----------------------------
|
||||||
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
- name: "Step 8: Build package and upload"
|
||||||
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
|
id: package
|
||||||
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
|
||||||
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
|
||||||
|
|
||||||
# Fallbacks if Step 5 was skipped
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
|
|
||||||
|
|
||||||
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
|
||||||
|
|
||||||
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
|
|
||||||
TYPE_PREFIX=""
|
|
||||||
case "${EXT_TYPE}" in
|
|
||||||
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
|
||||||
module) TYPE_PREFIX="mod_" ;;
|
|
||||||
component) TYPE_PREFIX="com_" ;;
|
|
||||||
template) TYPE_PREFIX="tpl_" ;;
|
|
||||||
library) TYPE_PREFIX="lib_" ;;
|
|
||||||
package) TYPE_PREFIX="pkg_" ;;
|
|
||||||
esac
|
|
||||||
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
|
|
||||||
|
|
||||||
# Delete existing release if present (overwrite, not append)
|
|
||||||
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$EXISTING_ID" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
|
|
||||||
echo "Deleted previous stable release (id: ${EXISTING_ID})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create fresh release
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/releases" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'tag_name': '${RELEASE_TAG}',
|
|
||||||
'name': '${RELEASE_NAME}',
|
|
||||||
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
|
|
||||||
'target_commitish': '${BRANCH}'
|
|
||||||
}))")"
|
|
||||||
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
|
|
||||||
- name: "Step 8: Build package and update checksum"
|
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true'
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
# All ZIPs upload to the major release tag (vXX)
|
# -- STEP 5: Write update stream (after build so SHA-256 is available) -----
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
- name: "Step 5: Write update stream"
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
|
||||||
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find extension element name from manifest
|
|
||||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
|
||||||
[ -z "$MANIFEST" ] && exit 0
|
|
||||||
|
|
||||||
# Reuse element from Step 5, with same fallback chain
|
|
||||||
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
TYPE_PREFIX=""
|
|
||||||
case "${EXT_TYPE}" in
|
|
||||||
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
|
||||||
module) TYPE_PREFIX="mod_" ;;
|
|
||||||
component) TYPE_PREFIX="com_" ;;
|
|
||||||
template) TYPE_PREFIX="tpl_" ;;
|
|
||||||
library) TYPE_PREFIX="lib_" ;;
|
|
||||||
package) TYPE_PREFIX="pkg_" ;;
|
|
||||||
esac
|
|
||||||
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
|
||||||
|
|
||||||
# -- Build install packages from src/ ----------------------------
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
|
|
||||||
|
|
||||||
# ZIP package (type-aware via moko-platform PHP API)
|
|
||||||
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
|
|
||||||
# Match the expected ZIP_NAME for upload
|
|
||||||
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
|
|
||||||
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
|
|
||||||
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# tar.gz package (flat source archive)
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
|
||||||
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
|
||||||
|
|
||||||
# -- Calculate SHA-256 for both ----------------------------------
|
|
||||||
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# -- Delete existing assets with same name before uploading ------
|
|
||||||
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
|
||||||
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
|
|
||||||
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
|
||||||
import sys,json
|
|
||||||
assets = json.load(sys.stdin)
|
|
||||||
for a in assets:
|
|
||||||
if a['name'] == '${ASSET_NAME}':
|
|
||||||
print(a['id']); break
|
|
||||||
" 2>/dev/null || true)
|
|
||||||
if [ -n "$ASSET_ID" ]; then
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# -- Upload both to release tag ----------------------------------
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${ZIP_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${TAR_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# -- Update updates.xml with both download formats ---------------
|
|
||||||
if [ -f "updates.xml" ]; then
|
|
||||||
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
|
|
||||||
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
|
|
||||||
|
|
||||||
# Use Python to update only the stable entry's downloads + sha256
|
|
||||||
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
|
|
||||||
python3 << 'PYEOF'
|
|
||||||
import re, os
|
|
||||||
|
|
||||||
with open("updates.xml") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
zip_url = os.environ["PY_ZIP_URL"]
|
|
||||||
tar_url = os.environ["PY_TAR_URL"]
|
|
||||||
sha = os.environ["PY_SHA"]
|
|
||||||
|
|
||||||
# Find the stable update block and replace its downloads + sha256
|
|
||||||
def replace_stable(m):
|
|
||||||
block = m.group(0)
|
|
||||||
# Replace downloads block
|
|
||||||
new_downloads = (
|
|
||||||
" <downloads>\n"
|
|
||||||
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
|
|
||||||
" </downloads>"
|
|
||||||
)
|
|
||||||
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
|
|
||||||
# Add or replace sha256
|
|
||||||
if '<sha256>' in block:
|
|
||||||
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
|
|
||||||
else:
|
|
||||||
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
|
|
||||||
return block
|
|
||||||
|
|
||||||
content = re.sub(
|
|
||||||
r' <update>.*?<tag>stable</tag>.*?</update>',
|
|
||||||
replace_stable,
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
with open("updates.xml", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
|
|
||||||
git push || true
|
|
||||||
|
|
||||||
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
|
|
||||||
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
|
||||||
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ]; then
|
|
||||||
CONTENT=$(base64 -w0 updates.xml)
|
|
||||||
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/contents/updates.xml" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg content "$CONTENT" \
|
|
||||||
--arg sha "$FILE_SHA" \
|
|
||||||
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
|
|
||||||
--arg branch "main" \
|
|
||||||
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
|
|
||||||
)" > /dev/null 2>&1 \
|
|
||||||
&& echo "updates.xml synced to main via API" \
|
|
||||||
|| echo "WARNING: failed to sync updates.xml to main"
|
|
||||||
else
|
|
||||||
echo "WARNING: could not get updates.xml SHA from main"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "### Packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 8b: Update release description with changelog + SHA ----------------
|
|
||||||
- name: "Step 8b: Update release body with changelog and SHA"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
# Fetch latest updates.xml from main so preserve logic has all channels
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/contents/updates.xml?ref=main" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
|
||||||
|
> updates.xml 2>/dev/null || true
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php /tmp/moko-platform-api/cli/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability stable \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG} --github-output
|
||||||
|
|
||||||
|
# Commit updates.xml if changed
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push origin HEAD:refs/heads/main 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 8b: Update release description with changelog ----------------------
|
||||||
|
- name: "Step 8b: Update release body"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
php /tmp/moko-platform-api/cli/release_body_update.php \
|
||||||
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
--path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
|
||||||
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
2>&1 || true
|
||||||
# Build TYPE_PREFIX to match Step 8's ZIP naming
|
echo "Release body updated" >> $GITHUB_STEP_SUMMARY
|
||||||
TYPE_PREFIX=""
|
|
||||||
case "${EXT_TYPE}" in
|
|
||||||
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
|
||||||
module) TYPE_PREFIX="mod_" ;;
|
|
||||||
component) TYPE_PREFIX="com_" ;;
|
|
||||||
template) TYPE_PREFIX="tpl_" ;;
|
|
||||||
library) TYPE_PREFIX="lib_" ;;
|
|
||||||
package) TYPE_PREFIX="pkg_" ;;
|
|
||||||
esac
|
|
||||||
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
|
||||||
|
|
||||||
# Get SHA from the built files
|
|
||||||
SHA256_ZIP=""
|
|
||||||
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
SHA256_TAR=""
|
|
||||||
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Extract latest changelog entry (strip the ## header to avoid duplicate)
|
|
||||||
CHANGELOG=""
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
|
|
||||||
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build release body (single header, no duplicate from changelog)
|
|
||||||
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
|
|
||||||
if [ -n "$CHANGELOG" ]; then
|
|
||||||
BODY="${BODY}${CHANGELOG}\n\n"
|
|
||||||
fi
|
|
||||||
BODY="${BODY}---\n\n### Checksums\n\n"
|
|
||||||
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
|
|
||||||
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
|
|
||||||
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
|
|
||||||
|
|
||||||
# Get release ID and update body
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = '''$(printf '%b' "$BODY")'''
|
|
||||||
data = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=data,
|
|
||||||
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
|
|
||||||
method='PATCH'
|
|
||||||
)
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true' &&
|
||||||
steps.version.outputs.stability == 'stable' &&
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
secrets.GH_TOKEN != ''
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
MAJOR="${{ steps.version.outputs.major }}"
|
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
echo "$NOTES" > /tmp/release_notes.md
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
|
--branch main 2>&1 || true
|
||||||
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
if [ -z "$EXISTING" ]; then
|
|
||||||
gh release create "$RELEASE_TAG" \
|
|
||||||
--repo "$GH_REPO" \
|
|
||||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
|
||||||
--notes-file /tmp/release_notes.md \
|
|
||||||
--target "$BRANCH" || true
|
|
||||||
else
|
|
||||||
gh release edit "$RELEASE_TAG" \
|
|
||||||
--repo "$GH_REPO" \
|
|
||||||
--title "v${MAJOR} (latest: ${VERSION})" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload assets to GitHub mirror
|
|
||||||
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
|
|
||||||
if [ -f "$PKG" ]; then
|
|
||||||
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
|
|
||||||
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
- name: "Step 10: Push main to GitHub mirror"
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true' &&
|
||||||
secrets.GH_TOKEN != ''
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
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_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
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 fetch origin main --depth=1
|
||||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
&& echo "main branch pushed to GitHub mirror" \
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
@@ -688,18 +449,20 @@ jobs:
|
|||||||
- name: "Delete lesser pre-release channels"
|
- name: "Delete lesser pre-release channels"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/release_cascade.php \
|
php /tmp/moko-platform-api/cli/release_cascade.php \
|
||||||
--stability stable \
|
--stability stable \
|
||||||
--token "${{ secrets.GA_TOKEN }}" \
|
--version "${VERSION}" \
|
||||||
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--gitea-url "${GITEA_URL}" 2>/dev/null || true
|
--api-base "${API_BASE}" 2>/dev/null || true
|
||||||
|
|
||||||
- name: "Step 11: Delete and recreate dev branch from main"
|
- name: "Step 11: Delete and recreate dev branch from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Delete dev branch
|
# Delete dev branch
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
@@ -713,28 +476,35 @@ jobs:
|
|||||||
|
|
||||||
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: "Step 12: Create version branch from main"
|
||||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
if: steps.version.outputs.skip != 'true'
|
||||||
- name: "Dolibarr: Reset dev version"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.platform.outputs.platform == 'dolibarr' &&
|
|
||||||
steps.platform.outputs.mod_file != ''
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
|
BRANCH_NAME="version/${VERSION}"
|
||||||
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
|
# Delete old version branch if it exists (same version re-release)
|
||||||
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
|
|
||||||
ENCODED=$(echo "$UPDATED" | base64 -w0)
|
# Create version/XX.YY.ZZ from main
|
||||||
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
|
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"
|
||||||
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
|
|
||||||
fi
|
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 --------------------------------------------------------------
|
# -- Summary --------------------------------------------------------------
|
||||||
- name: Pipeline Summary
|
- name: Pipeline Summary
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
- name: Update updates.xml
|
- name: Update updates.xml
|
||||||
if: success()
|
if: success()
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
TAG: ${{ steps.config.outputs.tag }}
|
||||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
||||||
DEPLOY_ENV: ${{ github.event.inputs.environment }}
|
DEPLOY_ENV: ${{ github.event.inputs.environment }}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
- name: Create RC release
|
- name: Create RC release
|
||||||
if: steps.guard.outputs.skip != 'true'
|
if: steps.guard.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
RC_TAG: ${{ steps.version.outputs.tag }}
|
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||||
RC_VERSION: ${{ steps.version.outputs.version }}
|
RC_VERSION: ${{ steps.version.outputs.version }}
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
- name: Commit updates.xml
|
- name: Commit updates.xml
|
||||||
if: steps.guard.outputs.skip != 'true'
|
if: steps.guard.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||||
PR_NUM: ${{ github.event.pull_request.number }}
|
PR_NUM: ${{ github.event.pull_request.number }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -35,56 +39,44 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability }})"
|
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Setup tools
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Update moko-platform CLI tools if available; install PHP if missing
|
if ! command -v composer &> /dev/null; then
|
||||||
if command -v moko-platform-update &> /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
|
||||||
moko-platform-update
|
|
||||||
elif [ -d "/opt/moko-platform" ]; then
|
|
||||||
cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true
|
|
||||||
else
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
fi
|
|
||||||
# Set MOKO_CLI to whichever path exists
|
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
fi
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]')
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
||||||
MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
[ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
[ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
|
||||||
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ inputs.stability }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
@@ -93,60 +85,44 @@ jobs:
|
|||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Patch bump via CLI tool
|
# Read current version (bump already handled by push workflow)
|
||||||
php ${MOKO_CLI}/version_bump.php --path .
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||||
TODAY=$(date +%Y-%m-%d)
|
|
||||||
|
|
||||||
# Update platform-specific manifest
|
# Strip any existing suffix from version before applying stability
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
|
||||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Verify version consistency across all files
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Update VERSION variable with suffix
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
git add -A
|
git add -A
|
||||||
git diff --cached --quiet || {
|
git diff --cached --quiet || {
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
git push origin HEAD 2>&1
|
git push origin HEAD 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-detect element (platform-aware)
|
# Auto-detect element via manifest_element.php
|
||||||
EXT_ELEMENT=""
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
case "$PLATFORM" in
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
joomla)
|
--repo "${GITEA_REPO}" --github-output
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
case "$EXT_ELEMENT" in
|
|
||||||
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
dolibarr)
|
|
||||||
if [ -n "$MOD_FILE" ]; then
|
|
||||||
MOD_BASENAME=$(basename "$MOD_FILE" .class.php)
|
|
||||||
EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]')
|
|
||||||
else
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
# Read back element outputs
|
||||||
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -154,168 +130,50 @@ jobs:
|
|||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
- name: Build package
|
- name: Create release
|
||||||
run: |
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
|
||||||
echo "::error::No src/ or htdocs/ directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
MANIFEST="${{ steps.meta.outputs.manifest }}"
|
|
||||||
EXT_TYPE=""
|
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger"
|
|
||||||
|
|
||||||
mkdir -p build/package
|
|
||||||
|
|
||||||
if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then
|
|
||||||
echo "=== Building Joomla PACKAGE (multi-extension) ==="
|
|
||||||
for ext_dir in "${SOURCE_DIR}"/packages/*/; do
|
|
||||||
[ ! -d "$ext_dir" ] && continue
|
|
||||||
EXT_NAME=$(basename "$ext_dir")
|
|
||||||
echo " Packaging sub-extension: ${EXT_NAME}"
|
|
||||||
cd "$ext_dir"
|
|
||||||
zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
|
|
||||||
cd "$OLDPWD"
|
|
||||||
done
|
|
||||||
for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
|
|
||||||
[ -f "$f" ] && cp "$f" build/package/
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "=== Building standard extension ==="
|
|
||||||
rsync -a \
|
|
||||||
--exclude='sftp-config*' \
|
|
||||||
--exclude='.ftpignore' \
|
|
||||||
--exclude='*.ppk' \
|
|
||||||
--exclude='*.pem' \
|
|
||||||
--exclude='*.key' \
|
|
||||||
--exclude='.env*' \
|
|
||||||
--exclude='*.local' \
|
|
||||||
--exclude='.build-trigger' \
|
|
||||||
"${SOURCE_DIR}/" build/package/
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create ZIP
|
|
||||||
id: zip
|
|
||||||
run: |
|
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
|
||||||
cd build/package
|
|
||||||
zip -r "../${ZIP_NAME}" .
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
|
|
||||||
|
|
||||||
- name: Create or replace Gitea release
|
|
||||||
id: release
|
id: release
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
php ${MOKO_CLI}/release_create.php \
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
BRANCH=$(git branch --show-current)
|
|
||||||
|
|
||||||
BODY="## ${VERSION} ($(date +%Y-%m-%d))
|
- name: Build package and upload
|
||||||
**Channel:** ${STABILITY}
|
id: package
|
||||||
**SHA-256:** \`${SHA256}\`"
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
# Delete existing release
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
|
php ${MOKO_CLI}/release_package.php \
|
||||||
if [ -n "$EXISTING_ID" ]; then
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/tags/${TAG}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create release
|
|
||||||
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/releases" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg tag "$TAG" \
|
|
||||||
--arg target "$BRANCH" \
|
|
||||||
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
|
||||||
--arg body "$BODY" \
|
|
||||||
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
|
|
||||||
)" | jq -r '.id')
|
|
||||||
|
|
||||||
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Upload ZIP
|
|
||||||
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
|
||||||
--data-binary "@build/${ZIP_NAME}"
|
|
||||||
|
|
||||||
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
- name: Update updates.xml
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
if [ ! -f "updates.xml" ]; then
|
||||||
echo "No updates.xml -- skipping"
|
echo "No updates.xml -- skipping"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Map stability to XML tag name
|
SHA_FLAG=""
|
||||||
case "$STABILITY" in
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
development) XML_TAG="development" ;;
|
|
||||||
alpha) XML_TAG="alpha" ;;
|
|
||||||
beta) XML_TAG="beta" ;;
|
|
||||||
release-candidate) XML_TAG="rc" ;;
|
|
||||||
*) XML_TAG="$STABILITY" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}"
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
# Use PHP to update the channel in updates.xml
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
php -r '
|
${SHA_FLAG}
|
||||||
$xml_tag = $argv[1];
|
|
||||||
$version = $argv[2];
|
|
||||||
$sha256 = $argv[3];
|
|
||||||
$url = $argv[4];
|
|
||||||
$date = date("Y-m-d");
|
|
||||||
|
|
||||||
$content = file_get_contents("updates.xml");
|
|
||||||
$pattern = "/(<update>(?:(?!<\/update>).)*?<tag>" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s";
|
|
||||||
|
|
||||||
$content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) {
|
|
||||||
$block = $m[0];
|
|
||||||
$block = preg_replace("/<version>[^<]*<\/version>/", "<version>{$version}</version>", $block);
|
|
||||||
if (strpos($block, "<sha256>") !== false) {
|
|
||||||
$block = preg_replace("/<sha256>[^<]*<\/sha256>/", "<sha256>{$sha256}</sha256>", $block);
|
|
||||||
} else {
|
|
||||||
$block = str_replace("</downloads>", "</downloads>\n <sha256>{$sha256}</sha256>", $block);
|
|
||||||
}
|
|
||||||
$block = preg_replace("/(<downloadurl[^>]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block);
|
|
||||||
return $block;
|
|
||||||
}, $content);
|
|
||||||
|
|
||||||
file_put_contents("updates.xml", $content);
|
|
||||||
echo "Updated {$xml_tag} channel: version={$version}\n";
|
|
||||||
' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL"
|
|
||||||
|
|
||||||
# Commit and push
|
# Commit and push
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
@@ -337,7 +195,7 @@ jobs:
|
|||||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||||
echo "Syncing updates.xml -> ${BRANCH}"
|
echo "Syncing updates.xml -> ${BRANCH}"
|
||||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||||
git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue
|
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
||||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
git add updates.xml
|
git add updates.xml
|
||||||
@@ -351,7 +209,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
--stability "${{ steps.meta.outputs.stability }}" \
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
@@ -364,7 +222,7 @@ jobs:
|
|||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/update-server.yml
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||||
|
#
|
||||||
|
# Thin wrapper around moko-platform CLI tools.
|
||||||
|
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||||
|
#
|
||||||
|
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: "Update Server"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update Server
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||||
|
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve stability and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
# Configure git for bot pushes
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Auto-bump patch version
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||||
|
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Determine stability from branch or manual input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
else
|
||||||
|
STABILITY="development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version suffix per stability stream
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
*) SUFFIX=""; TAG="stable" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Propagate version with stability suffix to all manifest files
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Re-read version (now includes suffix from version_set_platform)
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Commit version bump if changed
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create release and upload package
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Create or update Gitea release
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
# Build package and upload
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push updates.xml
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import base64, json, urllib.request, sys
|
||||||
|
with open('updates.xml', 'rb') as f:
|
||||||
|
content = base64.b64encode(f.read()).decode()
|
||||||
|
payload = json.dumps({
|
||||||
|
'content': content,
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/contents/updates.xml',
|
||||||
|
data=payload, method='PUT',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${GITEA_TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
print('updates.xml synced to main')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Permission check: admin or maintain role required
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
||||||
|
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Sync upstream bugs
|
- name: Sync upstream bugs
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
MOKOGITEA_URL: https://git.mokoconsulting.tech
|
MOKOGITEA_URL: https://git.mokoconsulting.tech
|
||||||
MOKOGITEA_REPO: MokoConsulting/MokoGitea
|
MOKOGITEA_REPO: MokoConsulting/MokoGitea
|
||||||
|
|||||||
@@ -34,16 +34,19 @@ const (
|
|||||||
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
|
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
|
||||||
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
|
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
|
||||||
|
|
||||||
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
|
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
|
||||||
appVerVar = "{{.SwaggerAppVer}}"
|
appVerVar = "{{.SwaggerAppVer}}"
|
||||||
|
appNameVar = "{{.SwaggerAppName}}"
|
||||||
|
|
||||||
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
|
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
|
||||||
appVerPlaceholder = "0.0.0-gitea-placeholder"
|
appVerPlaceholder = "0.0.0-gitea-placeholder"
|
||||||
|
appNamePlaceholder = "GiteaAppNamePlaceholder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
|
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
|
||||||
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
|
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
|
||||||
|
appNameRe = regexp.MustCompile(regexp.QuoteMeta(appNameVar))
|
||||||
|
|
||||||
enumScanDirs = []string{
|
enumScanDirs = []string{
|
||||||
"modules/structs",
|
"modules/structs",
|
||||||
@@ -70,6 +73,7 @@ func main() {
|
|||||||
|
|
||||||
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
|
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
|
||||||
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
|
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
|
||||||
|
cleaned = appNameRe.ReplaceAll(cleaned, []byte(appNamePlaceholder))
|
||||||
|
|
||||||
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
|
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -87,6 +91,7 @@ func main() {
|
|||||||
|
|
||||||
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
|
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
|
||||||
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
|
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
|
||||||
|
result = strings.ReplaceAll(result, appNamePlaceholder, appNameVar)
|
||||||
result = strings.TrimSpace(result)
|
result = strings.TrimSpace(result)
|
||||||
|
|
||||||
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
|
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module git.mokoconsulting.tech/MokoConsulting/MokoGitea
|
module git.mokoconsulting.tech/MokoConsulting/MokoGitea
|
||||||
|
|
||||||
go 1.26.2
|
go 1.26.3
|
||||||
|
|
||||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||||
// But some CAs use negative serial number, just relax the check. related:
|
// But some CAs use negative serial number, just relax the check. related:
|
||||||
@@ -9,6 +9,7 @@ godebug x509negativeserial=1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/actions-proto-go v0.4.1
|
code.gitea.io/actions-proto-go v0.4.1
|
||||||
|
code.gitea.io/gitea v1.26.2
|
||||||
code.gitea.io/sdk/gitea v0.24.1
|
code.gitea.io/sdk/gitea v0.24.1
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||||
connectrpc.com/connect v1.19.1
|
connectrpc.com/connect v1.19.1
|
||||||
@@ -52,8 +53,8 @@ require (
|
|||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-co-op/gocron/v2 v2.19.1
|
github.com/go-co-op/gocron/v2 v2.19.1
|
||||||
github.com/go-enry/go-enry/v2 v2.9.5
|
github.com/go-enry/go-enry/v2 v2.9.5
|
||||||
github.com/go-git/go-billy/v5 v5.8.0
|
github.com/go-git/go-billy/v5 v5.9.0
|
||||||
github.com/go-git/go-git/v5 v5.18.0
|
github.com/go-git/go-git/v5 v5.19.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13
|
github.com/go-ldap/ldap/v3 v3.4.13
|
||||||
github.com/go-redsync/redsync/v4 v4.16.0
|
github.com/go-redsync/redsync/v4 v4.16.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
@@ -242,17 +243,18 @@ require (
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||||
|
github.com/nxadm/tail v1.4.8 // indirect
|
||||||
github.com/oasdiff/yaml v0.0.9 // indirect
|
github.com/oasdiff/yaml v0.0.9 // indirect
|
||||||
github.com/oasdiff/yaml3 v0.0.12 // indirect
|
github.com/oasdiff/yaml3 v0.0.12 // indirect
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
github.com/olekukonko/errors v1.2.0 // indirect
|
github.com/olekukonko/errors v1.2.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.8 // indirect
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/olivere/elastic/v7 v7.0.32 // indirect
|
||||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -265,7 +267,6 @@ require (
|
|||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/smartystreets/assertions v1.1.1 // indirect
|
|
||||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
@@ -277,7 +278,6 @@ require (
|
|||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/zeebo/assert v1.3.0 // indirect
|
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.etcd.io/bbolt v1.4.3 // indirect
|
go.etcd.io/bbolt v1.4.3 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
@@ -287,7 +287,6 @@ require (
|
|||||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
|
||||||
golang.org/x/mod v0.35.0 // indirect
|
golang.org/x/mod v0.35.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.44.0 // indirect
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
|
|||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
|
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
|
||||||
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||||
|
code.gitea.io/gitea v1.26.2 h1:i0oTSOGXnB3WLILa0lRzwi4KFIkKIEZnoyCtYiajtYY=
|
||||||
|
code.gitea.io/gitea v1.26.2/go.mod h1:K2pVuCKcxMzEl/KBD3b4GsWIOu6ZH74g8lJYiACcnsM=
|
||||||
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
||||||
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||||
code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8=
|
code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8=
|
||||||
@@ -269,6 +271,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
|||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||||
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -300,12 +304,12 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5Hql
|
|||||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
||||||
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
@@ -326,7 +330,6 @@ github.com/go-redsync/redsync/v4 v4.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4Y
|
|||||||
github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ=
|
github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
@@ -354,12 +357,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J
|
|||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
@@ -373,9 +370,6 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
|
|||||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -570,7 +564,6 @@ github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlM
|
|||||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||||
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
|
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
|
||||||
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
|
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
|
||||||
@@ -585,14 +578,13 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
|||||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||||
|
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
|
||||||
|
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -608,8 +600,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
|||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -693,7 +685,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -801,8 +792,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss
|
|||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||||
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -820,9 +811,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r
|
|||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -841,7 +830,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -857,16 +845,12 @@ golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -915,7 +899,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
@@ -930,12 +913,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 h1:
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -953,7 +930,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
||||||
)
|
)
|
||||||
@@ -72,3 +73,27 @@ func (err ErrNotExist) Error() string {
|
|||||||
func (err ErrNotExist) Unwrap() error {
|
func (err ErrNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsErrDeadlock checks whether err is a database deadlock.
|
||||||
|
// MySQL returns error 1213 (ER_LOCK_DEADLOCK / SQLSTATE 40001).
|
||||||
|
// PostgreSQL returns SQLSTATE 40P01 with "deadlock detected".
|
||||||
|
// SQLite returns SQLITE_BUSY (error 5) with "database is locked".
|
||||||
|
func IsErrDeadlock(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
// MySQL / MariaDB: "Error 1213 (40001): Deadlock found when trying to get lock"
|
||||||
|
if strings.Contains(msg, "Error 1213") || strings.Contains(msg, "40001") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// PostgreSQL: "deadlock detected"
|
||||||
|
if strings.Contains(msg, "deadlock detected") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// SQLite: "database is locked"
|
||||||
|
if strings.Contains(msg, "database is locked") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsErrDeadlock(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "nil", err: nil, want: false},
|
||||||
|
{name: "unrelated", err: errors.New("connection refused"), want: false},
|
||||||
|
{name: "mysql 1213", err: errors.New("Error 1213 (40001): Deadlock found when trying to get lock; try restarting transaction"), want: true},
|
||||||
|
{name: "mysql sqlstate", err: errors.New("SQLSTATE 40001: serialization failure"), want: true},
|
||||||
|
{name: "postgres", err: errors.New("pq: deadlock detected"), want: true},
|
||||||
|
{name: "sqlite", err: errors.New("database is locked"), want: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, IsErrDeadlock(tt.err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
|
|||||||
}
|
}
|
||||||
} else if ls := asLogStringer(v); ls != nil {
|
} else if ls := asLogStringer(v); ls != nil {
|
||||||
msgArgs[i] = logStringFormatter{v: ls}
|
msgArgs[i] = logStringFormatter{v: ls}
|
||||||
|
} else if str, ok := v.(string); ok {
|
||||||
|
msgArgs[i] = protectSensitiveInfo(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +238,24 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
|
|||||||
l.SendLogEvent(event)
|
l.SendLogEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func protectSensitiveInfo(s string) string {
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
for _, vals := range q {
|
||||||
|
for i := range vals {
|
||||||
|
vals[i] = "_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
masked := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: q.Encode()}
|
||||||
|
if u.User != nil {
|
||||||
|
masked.User = url.User("_masked_")
|
||||||
|
}
|
||||||
|
return masked.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (l *LoggerImpl) GetLevel() Level {
|
func (l *LoggerImpl) GetLevel() Level {
|
||||||
return Level(l.level.Load())
|
return Level(l.level.Load())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs())
|
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProtectSensitiveInfo(t *testing.T) {
|
||||||
|
assert.Empty(t, protectSensitiveInfo(""))
|
||||||
|
assert.Equal(t, "mailto:user@example.com", protectSensitiveInfo("mailto:user@example.com"))
|
||||||
|
assert.Equal(t, "https://example.com", protectSensitiveInfo("https://example.com"))
|
||||||
|
assert.Equal(t, "https://_masked_@example.com/path?k=_", protectSensitiveInfo("https://u:p@example.com/path?k=v#hash"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +68,21 @@ func TestLocaleStore(t *testing.T) {
|
|||||||
assert.Equal(t, "<no-such>", string(res))
|
assert.Equal(t, "<no-such>", string(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocaleAppNameSubstitution(t *testing.T) {
|
||||||
|
setting.AppName = "TestApp"
|
||||||
|
|
||||||
|
ls := NewLocaleStore()
|
||||||
|
assert.NoError(t, ls.AddLocaleByJSON("lang1", "Lang1", []byte(`{"greeting":"Welcome to ${APP_NAME}","plain":"No placeholder here"}`), nil))
|
||||||
|
lang1, _ := ls.Locale("lang1")
|
||||||
|
|
||||||
|
assert.Equal(t, "Welcome to TestApp", lang1.TrString("greeting"))
|
||||||
|
assert.Equal(t, "No placeholder here", lang1.TrString("plain"))
|
||||||
|
|
||||||
|
// Verify it responds to runtime AppName changes
|
||||||
|
setting.AppName = "ChangedApp"
|
||||||
|
assert.Equal(t, "Welcome to ChangedApp", lang1.TrString("greeting"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocaleStoreMoreSource(t *testing.T) {
|
func TestLocaleStoreMoreSource(t *testing.T) {
|
||||||
testData1 := []byte(`
|
testData1 := []byte(`
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import (
|
|||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||||
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file implements the static LocaleStore that will not watch for changes
|
// This file implements the static LocaleStore that will not watch for changes
|
||||||
@@ -142,6 +144,9 @@ func (l *locale) TrString(trKey string, trArgs ...any) string {
|
|||||||
if format == "" { // still missing, use the key itself
|
if format == "" { // still missing, use the key itself
|
||||||
format = html.EscapeString(trKey)
|
format = html.EscapeString(trKey)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(format, "${APP_NAME}") {
|
||||||
|
format = strings.ReplaceAll(format, "${APP_NAME}", setting.AppName)
|
||||||
|
}
|
||||||
msg, err := Format(format, trArgs...)
|
msg, err := Format(format, trArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
|
log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"enable_javascript": "This website requires JavaScript.",
|
"enable_javascript": "This website requires JavaScript.",
|
||||||
"toc": "Table of Contents",
|
"toc": "Table of Contents",
|
||||||
"licenses": "Licenses",
|
"licenses": "Licenses",
|
||||||
"return_to_gitea": "Return to MokoGitea",
|
"return_to_gitea": "Return to ${APP_NAME}",
|
||||||
"more_items": "More items",
|
"more_items": "More items",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"email": "Email Address",
|
"email": "Email Address",
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
"filter.string.asc": "A–Z",
|
"filter.string.asc": "A–Z",
|
||||||
"filter.string.desc": "Z–A",
|
"filter.string.desc": "Z–A",
|
||||||
"error.occurred": "An error occurred",
|
"error.occurred": "An error occurred",
|
||||||
"error.report_message": "If you believe that this is a MokoGitea bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitHub</a> or open a new issue if necessary.",
|
"error.report_message": "If you believe that this is a ${APP_NAME} bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitHub</a> or open a new issue if necessary.",
|
||||||
"error.not_found": "The target couldn't be found.",
|
"error.not_found": "The target couldn't be found.",
|
||||||
"error.permission_denied": "Permission denied.",
|
"error.permission_denied": "Permission denied.",
|
||||||
"error.network_error": "Network error",
|
"error.network_error": "Network error",
|
||||||
@@ -230,16 +230,16 @@
|
|||||||
"startpage.install": "Easy to install",
|
"startpage.install": "Easy to install",
|
||||||
"startpage.install_desc": "Simply <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">run the binary</a> for your platform, ship it with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[2]s\">Docker</a>, or get it <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">packaged</a>.",
|
"startpage.install_desc": "Simply <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">run the binary</a> for your platform, ship it with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[2]s\">Docker</a>, or get it <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">packaged</a>.",
|
||||||
"startpage.platform": "Cross-platform",
|
"startpage.platform": "Cross-platform",
|
||||||
"startpage.platform_desc": "MokoGitea runs anywhere <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!",
|
"startpage.platform_desc": "${APP_NAME} runs anywhere <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!",
|
||||||
"startpage.lightweight": "Lightweight",
|
"startpage.lightweight": "Lightweight",
|
||||||
"startpage.lightweight_desc": "MokoGitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!",
|
"startpage.lightweight_desc": "${APP_NAME} has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!",
|
||||||
"startpage.license": "Open Source",
|
"startpage.license": "Open Source",
|
||||||
"startpage.license_desc": "Go get <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">%[2]s</a>! Join us by <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">contributing</a> to make this project even better. Don't hesitate to contribute!",
|
"startpage.license_desc": "Go get <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">%[2]s</a>! Join us by <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">contributing</a> to make this project even better. Don't hesitate to contribute!",
|
||||||
"install.install": "Installation",
|
"install.install": "Installation",
|
||||||
"install.installing_desc": "Installing now, please wait…",
|
"install.installing_desc": "Installing now, please wait…",
|
||||||
"install.title": "Initial Configuration",
|
"install.title": "Initial Configuration",
|
||||||
"install.docker_helper": "If you run MokoGitea inside Docker, please read the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentation</a> before changing any settings.",
|
"install.docker_helper": "If you run ${APP_NAME} inside Docker, please read the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentation</a> before changing any settings.",
|
||||||
"install.require_db_desc": "MokoGitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).",
|
"install.require_db_desc": "${APP_NAME} requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).",
|
||||||
"install.db_title": "Database Settings",
|
"install.db_title": "Database Settings",
|
||||||
"install.db_type": "Database Type",
|
"install.db_type": "Database Type",
|
||||||
"install.host": "Host",
|
"install.host": "Host",
|
||||||
@@ -250,12 +250,12 @@
|
|||||||
"install.db_schema_helper": "Leave blank for database default (\"public\").",
|
"install.db_schema_helper": "Leave blank for database default (\"public\").",
|
||||||
"install.ssl_mode": "SSL",
|
"install.ssl_mode": "SSL",
|
||||||
"install.path": "Path",
|
"install.path": "Path",
|
||||||
"install.sqlite_helper": "File path for the SQLite3 database.<br>Enter an absolute path if you run MokoGitea as a service.",
|
"install.sqlite_helper": "File path for the SQLite3 database.<br>Enter an absolute path if you run ${APP_NAME} as a service.",
|
||||||
"install.reinstall_error": "You are trying to install into an existing MokoGitea database",
|
"install.reinstall_error": "You are trying to install into an existing ${APP_NAME} database",
|
||||||
"install.reinstall_confirm_message": "Re-installing with an existing MokoGitea database can cause multiple problems. In most cases, you should use your existing \"app.ini\" to run MokoGitea. If you know what you are doing, confirm the following:",
|
"install.reinstall_confirm_message": "Re-installing with an existing ${APP_NAME} database can cause multiple problems. In most cases, you should use your existing \"app.ini\" to run ${APP_NAME}. If you know what you are doing, confirm the following:",
|
||||||
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
|
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
|
||||||
"install.reinstall_confirm_check_2": "The repositories and settings may need to be resynchronized. By checking this box, you confirm that you will resynchronize the hooks for the repositories and authorized_keys file manually. You confirm that you will ensure that repository and mirror settings are correct.",
|
"install.reinstall_confirm_check_2": "The repositories and settings may need to be resynchronized. By checking this box, you confirm that you will resynchronize the hooks for the repositories and authorized_keys file manually. You confirm that you will ensure that repository and mirror settings are correct.",
|
||||||
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this MokoGitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
|
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this ${APP_NAME} is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
|
||||||
"install.err_empty_db_path": "The SQLite3 database path cannot be empty.",
|
"install.err_empty_db_path": "The SQLite3 database path cannot be empty.",
|
||||||
"install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.",
|
"install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.",
|
||||||
"install.err_empty_admin_password": "The administrator password cannot be empty.",
|
"install.err_empty_admin_password": "The administrator password cannot be empty.",
|
||||||
@@ -271,14 +271,14 @@
|
|||||||
"install.lfs_path": "Git LFS Root Path",
|
"install.lfs_path": "Git LFS Root Path",
|
||||||
"install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.",
|
"install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.",
|
||||||
"install.run_user": "Run As Username",
|
"install.run_user": "Run As Username",
|
||||||
"install.run_user_helper": "The operating system username that MokoGitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart MokoGitea under that account.",
|
"install.run_user_helper": "The operating system username that ${APP_NAME} runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart ${APP_NAME} under that account.",
|
||||||
"install.domain": "Server Domain",
|
"install.domain": "Server Domain",
|
||||||
"install.domain_helper": "Domain or host address for the server.",
|
"install.domain_helper": "Domain or host address for the server.",
|
||||||
"install.ssh_port": "SSH Server Port",
|
"install.ssh_port": "SSH Server Port",
|
||||||
"install.ssh_port_helper": "Port number your SSH server listens on. Leave empty to disable.",
|
"install.ssh_port_helper": "Port number your SSH server listens on. Leave empty to disable.",
|
||||||
"install.http_port": "MokoGitea HTTP Listen Port",
|
"install.http_port": "${APP_NAME} HTTP Listen Port",
|
||||||
"install.http_port_helper": "Port number the MokoGitea web server will listen on.",
|
"install.http_port_helper": "Port number the ${APP_NAME} web server will listen on.",
|
||||||
"install.app_url": "MokoGitea Base URL",
|
"install.app_url": "${APP_NAME} Base URL",
|
||||||
"install.app_url_helper": "Base address for HTTP(S) clone URLs and email notifications.",
|
"install.app_url_helper": "Base address for HTTP(S) clone URLs and email notifications.",
|
||||||
"install.log_root_path": "Log Path",
|
"install.log_root_path": "Log Path",
|
||||||
"install.log_root_path_helper": "Log files will be written to this directory.",
|
"install.log_root_path_helper": "Log files will be written to this directory.",
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
"install.smtp_port": "SMTP Port",
|
"install.smtp_port": "SMTP Port",
|
||||||
"install.smtp_from": "Send Email As",
|
"install.smtp_from": "Send Email As",
|
||||||
"install.smtp_from_invalid": "The \"Send Email As\" address is invalid",
|
"install.smtp_from_invalid": "The \"Send Email As\" address is invalid",
|
||||||
"install.smtp_from_helper": "Email address MokoGitea will use. Enter a plain email address or use the \"Name\" <email@example.com> format.",
|
"install.smtp_from_helper": "Email address ${APP_NAME} will use. Enter a plain email address or use the \"Name\" <email@example.com> format.",
|
||||||
"install.mailer_user": "SMTP Username",
|
"install.mailer_user": "SMTP Username",
|
||||||
"install.mailer_password": "SMTP Password",
|
"install.mailer_password": "SMTP Password",
|
||||||
"install.register_confirm": "Require Email Confirmation to Register",
|
"install.register_confirm": "Require Email Confirmation to Register",
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
"install.admin_password": "Password",
|
"install.admin_password": "Password",
|
||||||
"install.confirm_password": "Confirm Password",
|
"install.confirm_password": "Confirm Password",
|
||||||
"install.admin_email": "Email Address",
|
"install.admin_email": "Email Address",
|
||||||
"install.install_btn_confirm": "Install MokoGitea",
|
"install.install_btn_confirm": "Install ${APP_NAME}",
|
||||||
"install.test_git_failed": "Could not test 'git' command: %v",
|
"install.test_git_failed": "Could not test 'git' command: %v",
|
||||||
"install.invalid_db_setting": "The database settings are invalid: %v",
|
"install.invalid_db_setting": "The database settings are invalid: %v",
|
||||||
"install.invalid_db_table": "The database table \"%s\" is invalid: %v",
|
"install.invalid_db_table": "The database table \"%s\" is invalid: %v",
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
"auth.forgot_password_title": "Forgot Password",
|
"auth.forgot_password_title": "Forgot Password",
|
||||||
"auth.forgot_password": "Forgot password?",
|
"auth.forgot_password": "Forgot password?",
|
||||||
"auth.need_account": "Need an account?",
|
"auth.need_account": "Need an account?",
|
||||||
"auth.sign_up_tip": "You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the MokoGitea documentation to recover the account.",
|
"auth.sign_up_tip": "You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the ${APP_NAME} documentation to recover the account.",
|
||||||
"auth.sign_up_now": "Register now.",
|
"auth.sign_up_now": "Register now.",
|
||||||
"auth.sign_up_successful": "Account was successfully created. Welcome!",
|
"auth.sign_up_successful": "Account was successfully created. Welcome!",
|
||||||
"auth.confirmation_mail_sent_prompt_ex": "A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.",
|
"auth.confirmation_mail_sent_prompt_ex": "A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.",
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
"auth.reset_password_helper": "Recover Account",
|
"auth.reset_password_helper": "Recover Account",
|
||||||
"auth.reset_password_wrong_user": "You are signed in as %s, but the account recovery link is meant for %s",
|
"auth.reset_password_wrong_user": "You are signed in as %s, but the account recovery link is meant for %s",
|
||||||
"auth.password_too_short": "Password length cannot be less than %d characters.",
|
"auth.password_too_short": "Password length cannot be less than %d characters.",
|
||||||
"auth.non_local_account": "Non-local users cannot update their password through the MokoGitea web interface.",
|
"auth.non_local_account": "Non-local users cannot update their password through the ${APP_NAME} web interface.",
|
||||||
"auth.verify": "Verify",
|
"auth.verify": "Verify",
|
||||||
"auth.scratch_code": "Scratch code",
|
"auth.scratch_code": "Scratch code",
|
||||||
"auth.use_scratch_code": "Use a scratch code",
|
"auth.use_scratch_code": "Use a scratch code",
|
||||||
@@ -726,7 +726,7 @@
|
|||||||
"settings.retype_new_password": "Confirm New Password",
|
"settings.retype_new_password": "Confirm New Password",
|
||||||
"settings.password_incorrect": "The current password is incorrect.",
|
"settings.password_incorrect": "The current password is incorrect.",
|
||||||
"settings.change_password_success": "Your password has been updated. Sign in using your new password from now on.",
|
"settings.change_password_success": "Your password has been updated. Sign in using your new password from now on.",
|
||||||
"settings.password_change_disabled": "Non-local users cannot update their password through the MokoGitea web interface.",
|
"settings.password_change_disabled": "Non-local users cannot update their password through the ${APP_NAME} web interface.",
|
||||||
"settings.emails": "Email Addresses",
|
"settings.emails": "Email Addresses",
|
||||||
"settings.manage_emails": "Manage Email Addresses",
|
"settings.manage_emails": "Manage Email Addresses",
|
||||||
"settings.manage_themes": "Select default theme",
|
"settings.manage_themes": "Select default theme",
|
||||||
@@ -734,7 +734,7 @@
|
|||||||
"settings.email_desc": "Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.",
|
"settings.email_desc": "Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.",
|
||||||
"settings.theme_desc": "This will be your default theme across the site.",
|
"settings.theme_desc": "This will be your default theme across the site.",
|
||||||
"settings.theme_colorblindness_help": "Color blindness Theme Support",
|
"settings.theme_colorblindness_help": "Color blindness Theme Support",
|
||||||
"settings.theme_colorblindness_prompt": "MokoGitea only has a few themes with basic color blindness support, which only have a few colors defined. The work is still in progress. More improvements could be made by defining more colors in the theme CSS files.",
|
"settings.theme_colorblindness_prompt": "${APP_NAME} only has a few themes with basic color blindness support, which only have a few colors defined. The work is still in progress. More improvements could be made by defining more colors in the theme CSS files.",
|
||||||
"settings.primary": "Primary",
|
"settings.primary": "Primary",
|
||||||
"settings.activated": "Activated",
|
"settings.activated": "Activated",
|
||||||
"settings.requires_activation": "Requires activation",
|
"settings.requires_activation": "Requires activation",
|
||||||
@@ -843,7 +843,7 @@
|
|||||||
"settings.unbind_success": "The social account has been removed successfully.",
|
"settings.unbind_success": "The social account has been removed successfully.",
|
||||||
"settings.manage_access_token": "Manage Access Tokens",
|
"settings.manage_access_token": "Manage Access Tokens",
|
||||||
"settings.generate_new_token": "Generate New Token",
|
"settings.generate_new_token": "Generate New Token",
|
||||||
"settings.tokens_desc": "These tokens grant access to your account using the Gitea API.",
|
"settings.tokens_desc": "These tokens grant access to your account using the ${APP_NAME} API.",
|
||||||
"settings.token_name": "Token Name",
|
"settings.token_name": "Token Name",
|
||||||
"settings.generate_token": "Generate Token",
|
"settings.generate_token": "Generate Token",
|
||||||
"settings.generate_token_success": "Your new token has been generated. Copy it now as it will not be shown again.",
|
"settings.generate_token_success": "Your new token has been generated. Copy it now as it will not be shown again.",
|
||||||
@@ -869,7 +869,7 @@
|
|||||||
"settings.permissions_list": "Permissions:",
|
"settings.permissions_list": "Permissions:",
|
||||||
"settings.manage_oauth2_applications": "Manage OAuth2 Applications",
|
"settings.manage_oauth2_applications": "Manage OAuth2 Applications",
|
||||||
"settings.edit_oauth2_application": "Edit OAuth2 Application",
|
"settings.edit_oauth2_application": "Edit OAuth2 Application",
|
||||||
"settings.oauth2_applications_desc": "OAuth2 applications enable your third-party application to securely authenticate users at this MokoGitea instance.",
|
"settings.oauth2_applications_desc": "OAuth2 applications enable your third-party application to securely authenticate users at this ${APP_NAME} instance.",
|
||||||
"settings.remove_oauth2_application": "Remove OAuth2 Application",
|
"settings.remove_oauth2_application": "Remove OAuth2 Application",
|
||||||
"settings.remove_oauth2_application_desc": "Removing an OAuth2 application will revoke access to all signed access tokens. Continue?",
|
"settings.remove_oauth2_application_desc": "Removing an OAuth2 application will revoke access to all signed access tokens. Continue?",
|
||||||
"settings.remove_oauth2_application_success": "The application has been deleted.",
|
"settings.remove_oauth2_application_success": "The application has been deleted.",
|
||||||
@@ -890,9 +890,9 @@
|
|||||||
"settings.oauth2_application_edit": "Edit",
|
"settings.oauth2_application_edit": "Edit",
|
||||||
"settings.oauth2_application_create_description": "OAuth2 applications give your third-party application access to user accounts on this instance.",
|
"settings.oauth2_application_create_description": "OAuth2 applications give your third-party application access to user accounts on this instance.",
|
||||||
"settings.oauth2_application_remove_description": "Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?",
|
"settings.oauth2_application_remove_description": "Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?",
|
||||||
"settings.oauth2_application_locked": "MokoGitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.",
|
"settings.oauth2_application_locked": "${APP_NAME} pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.",
|
||||||
"settings.authorized_oauth2_applications": "Authorized OAuth2 Applications",
|
"settings.authorized_oauth2_applications": "Authorized OAuth2 Applications",
|
||||||
"settings.authorized_oauth2_applications_description": "You have granted access to your personal MokoGitea account to these third-party applications. Please revoke access for applications you no longer need.",
|
"settings.authorized_oauth2_applications_description": "You have granted access to your personal ${APP_NAME} account to these third-party applications. Please revoke access for applications you no longer need.",
|
||||||
"settings.revoke_key": "Revoke",
|
"settings.revoke_key": "Revoke",
|
||||||
"settings.revoke_oauth2_grant": "Revoke Access",
|
"settings.revoke_oauth2_grant": "Revoke Access",
|
||||||
"settings.revoke_oauth2_grant_description": "Revoking access for this third-party application will prevent this application from accessing your data. Are you sure?",
|
"settings.revoke_oauth2_grant_description": "Revoking access for this third-party application will prevent this application from accessing your data. Are you sure?",
|
||||||
@@ -923,11 +923,11 @@
|
|||||||
"settings.webauthn_key_loss_warning": "If you lose your security keys, you will lose access to your account.",
|
"settings.webauthn_key_loss_warning": "If you lose your security keys, you will lose access to your account.",
|
||||||
"settings.webauthn_alternative_tip": "You may want to configure an additional authentication method.",
|
"settings.webauthn_alternative_tip": "You may want to configure an additional authentication method.",
|
||||||
"settings.manage_account_links": "Manage Linked Accounts",
|
"settings.manage_account_links": "Manage Linked Accounts",
|
||||||
"settings.manage_account_links_desc": "These external accounts are linked to your MokoGitea account.",
|
"settings.manage_account_links_desc": "These external accounts are linked to your ${APP_NAME} account.",
|
||||||
"settings.account_links_not_available": "No external accounts are currently linked to your MokoGitea account.",
|
"settings.account_links_not_available": "No external accounts are currently linked to your ${APP_NAME} account.",
|
||||||
"settings.link_account": "Link Account",
|
"settings.link_account": "Link Account",
|
||||||
"settings.remove_account_link": "Remove Linked Account",
|
"settings.remove_account_link": "Remove Linked Account",
|
||||||
"settings.remove_account_link_desc": "Removing a linked account will revoke its access to your MokoGitea account. Continue?",
|
"settings.remove_account_link_desc": "Removing a linked account will revoke its access to your ${APP_NAME} account. Continue?",
|
||||||
"settings.remove_account_link_success": "The linked account has been removed.",
|
"settings.remove_account_link_success": "The linked account has been removed.",
|
||||||
"settings.hooks.desc": "Add webhooks which will be triggered for <strong>all repositories</strong> that you own.",
|
"settings.hooks.desc": "Add webhooks which will be triggered for <strong>all repositories</strong> that you own.",
|
||||||
"settings.orgs_none": "You are not a member of any organizations.",
|
"settings.orgs_none": "You are not a member of any organizations.",
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
"settings.email_notifications.disable": "Disable Email Notifications",
|
"settings.email_notifications.disable": "Disable Email Notifications",
|
||||||
"settings.email_notifications.submit": "Set Email Preference",
|
"settings.email_notifications.submit": "Set Email Preference",
|
||||||
"settings.email_notifications.andyourown": "And Your Own Notifications",
|
"settings.email_notifications.andyourown": "And Your Own Notifications",
|
||||||
"settings.email_notifications.actions.desc": "Notifications for workflow runs on repositories set up with <a target=\"_blank\" href=\"%s\">Gitea Actions</a>.",
|
"settings.email_notifications.actions.desc": "Notifications for workflow runs on repositories set up with <a target=\"_blank\" href=\"%s\">${APP_NAME} Actions</a>.",
|
||||||
"settings.email_notifications.actions.failure_only": "Only notify for failed workflow runs",
|
"settings.email_notifications.actions.failure_only": "Only notify for failed workflow runs",
|
||||||
"settings.visibility": "User visibility",
|
"settings.visibility": "User visibility",
|
||||||
"settings.visibility.public": "Public",
|
"settings.visibility.public": "Public",
|
||||||
@@ -1125,7 +1125,7 @@
|
|||||||
"repo.migrate.github.description": "Migrate data from github.com or other GitHub instances.",
|
"repo.migrate.github.description": "Migrate data from github.com or other GitHub instances.",
|
||||||
"repo.migrate.git.description": "Migrate a repository only from any Git service.",
|
"repo.migrate.git.description": "Migrate a repository only from any Git service.",
|
||||||
"repo.migrate.gitlab.description": "Migrate data from gitlab.com or other GitLab instances.",
|
"repo.migrate.gitlab.description": "Migrate data from gitlab.com or other GitLab instances.",
|
||||||
"repo.migrate.gitea.description": "Migrate data from gitea.com or other Gitea instances.",
|
"repo.migrate.gitea.description": "Migrate data from other ${APP_NAME} instances.",
|
||||||
"repo.migrate.gogs.description": "Migrate data from notabug.org or other Gogs instances.",
|
"repo.migrate.gogs.description": "Migrate data from notabug.org or other Gogs instances.",
|
||||||
"repo.migrate.onedev.description": "Migrate data from code.onedev.io or other OneDev instances.",
|
"repo.migrate.onedev.description": "Migrate data from code.onedev.io or other OneDev instances.",
|
||||||
"repo.migrate.codebase.description": "Migrate data from codebasehq.com.",
|
"repo.migrate.codebase.description": "Migrate data from codebasehq.com.",
|
||||||
@@ -1891,7 +1891,7 @@
|
|||||||
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
|
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
|
||||||
"repo.pulls.cmd_instruction_checkout_desc": "From your project repository, check out a new branch and test the changes.",
|
"repo.pulls.cmd_instruction_checkout_desc": "From your project repository, check out a new branch and test the changes.",
|
||||||
"repo.pulls.cmd_instruction_merge_title": "Merge",
|
"repo.pulls.cmd_instruction_merge_title": "Merge",
|
||||||
"repo.pulls.cmd_instruction_merge_desc": "Merge the changes and update on MokoGitea.",
|
"repo.pulls.cmd_instruction_merge_desc": "Merge the changes and update on ${APP_NAME}.",
|
||||||
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
|
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
|
||||||
"repo.pulls.clear_merge_message": "Clear merge message",
|
"repo.pulls.clear_merge_message": "Clear merge message",
|
||||||
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By…\".",
|
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By…\".",
|
||||||
@@ -2199,11 +2199,11 @@
|
|||||||
"repo.settings.trust_model.collaborator.long": "Collaborator: Trust signatures by collaborators",
|
"repo.settings.trust_model.collaborator.long": "Collaborator: Trust signatures by collaborators",
|
||||||
"repo.settings.trust_model.collaborator.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\", whether they match the committer or not. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" if not.",
|
"repo.settings.trust_model.collaborator.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\", whether they match the committer or not. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" if not.",
|
||||||
"repo.settings.trust_model.committer": "Committer",
|
"repo.settings.trust_model.committer": "Committer",
|
||||||
"repo.settings.trust_model.committer.long": "Committer: Trust signatures that match committers. This matches GitHub's behavior and will force commits signed by MokoGitea to have MokoGitea as the committer.",
|
"repo.settings.trust_model.committer.long": "Committer: Trust signatures that match committers. This matches GitHub's behavior and will force commits signed by ${APP_NAME} to have ${APP_NAME} as the committer.",
|
||||||
"repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces MokoGitea to be the committer on signed commits, with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default MokoGitea key must match a user in the database.",
|
"repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces ${APP_NAME} to be the committer on signed commits, with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default ${APP_NAME} key must match a user in the database.",
|
||||||
"repo.settings.trust_model.collaboratorcommitter": "Collaborator+Committer",
|
"repo.settings.trust_model.collaboratorcommitter": "Collaborator+Committer",
|
||||||
"repo.settings.trust_model.collaboratorcommitter.long": "Collaborator+Committer: Trust signatures by collaborators which match the committer",
|
"repo.settings.trust_model.collaboratorcommitter.long": "Collaborator+Committer: Trust signatures by collaborators which match the committer",
|
||||||
"repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force MokoGitea to be marked as the committer on signed commits, with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default MokoGitea key must match a user in the database.",
|
"repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force ${APP_NAME} to be marked as the committer on signed commits, with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default ${APP_NAME} key must match a user in the database.",
|
||||||
"repo.settings.wiki_delete": "Delete Wiki Data",
|
"repo.settings.wiki_delete": "Delete Wiki Data",
|
||||||
"repo.settings.wiki_delete_desc": "Deleting repository wiki data is permanent and cannot be undone.",
|
"repo.settings.wiki_delete_desc": "Deleting repository wiki data is permanent and cannot be undone.",
|
||||||
"repo.settings.wiki_delete_notices_1": "- This will permanently delete and disable the repository wiki for %s.",
|
"repo.settings.wiki_delete_notices_1": "- This will permanently delete and disable the repository wiki for %s.",
|
||||||
@@ -2240,7 +2240,7 @@
|
|||||||
"repo.settings.remove_team_success": "The team's access to the repository has been removed.",
|
"repo.settings.remove_team_success": "The team's access to the repository has been removed.",
|
||||||
"repo.settings.add_webhook": "Add Webhook",
|
"repo.settings.add_webhook": "Add Webhook",
|
||||||
"repo.settings.add_webhook.invalid_channel_name": "Webhook channel name cannot be empty and cannot contain only a # character.",
|
"repo.settings.add_webhook.invalid_channel_name": "Webhook channel name cannot be empty and cannot contain only a # character.",
|
||||||
"repo.settings.hooks_desc": "Webhooks automatically make HTTP POST requests to a server when certain MokoGitea events trigger. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
|
"repo.settings.hooks_desc": "Webhooks automatically make HTTP POST requests to a server when certain ${APP_NAME} events trigger. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
|
||||||
"repo.settings.webhook_deletion": "Remove Webhook",
|
"repo.settings.webhook_deletion": "Remove Webhook",
|
||||||
"repo.settings.webhook_deletion_desc": "Removing a webhook deletes its settings and delivery history. Continue?",
|
"repo.settings.webhook_deletion_desc": "Removing a webhook deletes its settings and delivery history. Continue?",
|
||||||
"repo.settings.webhook_deletion_success": "The webhook has been removed.",
|
"repo.settings.webhook_deletion_success": "The webhook has been removed.",
|
||||||
@@ -2258,7 +2258,7 @@
|
|||||||
"repo.settings.githooks_desc": "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations.",
|
"repo.settings.githooks_desc": "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations.",
|
||||||
"repo.settings.githook_edit_desc": "If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.",
|
"repo.settings.githook_edit_desc": "If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.",
|
||||||
"repo.settings.update_githook": "Update Hook",
|
"repo.settings.update_githook": "Update Hook",
|
||||||
"repo.settings.add_webhook_desc": "MokoGitea will send <code>POST</code> requests with a specified content type to the target URL. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
|
"repo.settings.add_webhook_desc": "${APP_NAME} will send <code>POST</code> requests with a specified content type to the target URL. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
|
||||||
"repo.settings.payload_url": "Target URL",
|
"repo.settings.payload_url": "Target URL",
|
||||||
"repo.settings.http_method": "HTTP Method",
|
"repo.settings.http_method": "HTTP Method",
|
||||||
"repo.settings.content_type": "POST Content Type",
|
"repo.settings.content_type": "POST Content Type",
|
||||||
@@ -2326,9 +2326,9 @@
|
|||||||
"repo.settings.event_pull_request_merge": "Pull Request Merge",
|
"repo.settings.event_pull_request_merge": "Pull Request Merge",
|
||||||
"repo.settings.event_header_workflow": "Workflow Events",
|
"repo.settings.event_header_workflow": "Workflow Events",
|
||||||
"repo.settings.event_workflow_run": "Workflow Run",
|
"repo.settings.event_workflow_run": "Workflow Run",
|
||||||
"repo.settings.event_workflow_run_desc": "Gitea Actions Workflow run queued, waiting, in progress, or completed.",
|
"repo.settings.event_workflow_run_desc": "${APP_NAME} Actions Workflow run queued, waiting, in progress, or completed.",
|
||||||
"repo.settings.event_workflow_job": "Workflow Jobs",
|
"repo.settings.event_workflow_job": "Workflow Jobs",
|
||||||
"repo.settings.event_workflow_job_desc": "Gitea Actions Workflow job queued, waiting, in progress, or completed.",
|
"repo.settings.event_workflow_job_desc": "${APP_NAME} Actions Workflow job queued, waiting, in progress, or completed.",
|
||||||
"repo.settings.event_package": "Package",
|
"repo.settings.event_package": "Package",
|
||||||
"repo.settings.event_package_desc": "Package created or deleted in a repository.",
|
"repo.settings.event_package_desc": "Package created or deleted in a repository.",
|
||||||
"repo.settings.branch_filter": "Branch filter",
|
"repo.settings.branch_filter": "Branch filter",
|
||||||
@@ -2349,7 +2349,7 @@
|
|||||||
"repo.settings.slack_domain": "Domain",
|
"repo.settings.slack_domain": "Domain",
|
||||||
"repo.settings.slack_channel": "Channel",
|
"repo.settings.slack_channel": "Channel",
|
||||||
"repo.settings.add_web_hook_desc": "Integrate <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">%s</a> into your repository.",
|
"repo.settings.add_web_hook_desc": "Integrate <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">%s</a> into your repository.",
|
||||||
"repo.settings.web_hook_name_gitea": "MokoGitea",
|
"repo.settings.web_hook_name_gitea": "${APP_NAME}",
|
||||||
"repo.settings.web_hook_name_gogs": "Gogs",
|
"repo.settings.web_hook_name_gogs": "Gogs",
|
||||||
"repo.settings.web_hook_name_slack": "Slack",
|
"repo.settings.web_hook_name_slack": "Slack",
|
||||||
"repo.settings.web_hook_name_discord": "Discord",
|
"repo.settings.web_hook_name_discord": "Discord",
|
||||||
@@ -2634,7 +2634,7 @@
|
|||||||
"repo.release.delete_release": "Delete Release",
|
"repo.release.delete_release": "Delete Release",
|
||||||
"repo.release.delete_tag": "Delete Tag",
|
"repo.release.delete_tag": "Delete Tag",
|
||||||
"repo.release.deletion": "Delete Release",
|
"repo.release.deletion": "Delete Release",
|
||||||
"repo.release.deletion_desc": "Deleting a release only removes it from MokoGitea. It will not affect the Git tag, the contents of your repository or its history. Continue?",
|
"repo.release.deletion_desc": "Deleting a release only removes it from ${APP_NAME}. It will not affect the Git tag, the contents of your repository or its history. Continue?",
|
||||||
"repo.release.deletion_success": "The release has been deleted.",
|
"repo.release.deletion_success": "The release has been deleted.",
|
||||||
"repo.release.deletion_tag_desc": "Will delete this tag from repository. Repository contents and history remain unchanged. Continue?",
|
"repo.release.deletion_tag_desc": "Will delete this tag from repository. Repository contents and history remain unchanged. Continue?",
|
||||||
"repo.release.deletion_tag_success": "The tag has been deleted.",
|
"repo.release.deletion_tag_success": "The tag has been deleted.",
|
||||||
@@ -2913,7 +2913,7 @@
|
|||||||
"admin.last_page": "Last",
|
"admin.last_page": "Last",
|
||||||
"admin.total": "Total: %d",
|
"admin.total": "Total: %d",
|
||||||
"admin.settings": "Admin Settings",
|
"admin.settings": "Admin Settings",
|
||||||
"admin.dashboard.new_version_hint": "MokoGitea %s is now available, you are running %s. Check <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">the blog</a> for more details.",
|
"admin.dashboard.new_version_hint": "${APP_NAME} %s is now available, you are running %s. Check <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">the blog</a> for more details.",
|
||||||
"admin.dashboard.statistic": "Summary",
|
"admin.dashboard.statistic": "Summary",
|
||||||
"admin.dashboard.maintenance_operations": "Maintenance Operations",
|
"admin.dashboard.maintenance_operations": "Maintenance Operations",
|
||||||
"admin.dashboard.system_status": "System Status",
|
"admin.dashboard.system_status": "System Status",
|
||||||
@@ -2949,8 +2949,8 @@
|
|||||||
"admin.dashboard.deleted_branches_cleanup": "Clean up deleted branches",
|
"admin.dashboard.deleted_branches_cleanup": "Clean up deleted branches",
|
||||||
"admin.dashboard.update_migration_poster_id": "Update migration poster IDs",
|
"admin.dashboard.update_migration_poster_id": "Update migration poster IDs",
|
||||||
"admin.dashboard.git_gc_repos": "Garbage-collect all repositories",
|
"admin.dashboard.git_gc_repos": "Garbage-collect all repositories",
|
||||||
"admin.dashboard.resync_all_sshkeys": "Update the '.ssh/authorized_keys' file with MokoGitea SSH keys",
|
"admin.dashboard.resync_all_sshkeys": "Update the '.ssh/authorized_keys' file with ${APP_NAME} SSH keys",
|
||||||
"admin.dashboard.resync_all_sshprincipals": "Update the '.ssh/authorized_principals' file with MokoGitea SSH principals",
|
"admin.dashboard.resync_all_sshprincipals": "Update the '.ssh/authorized_principals' file with ${APP_NAME} SSH principals",
|
||||||
"admin.dashboard.resync_all_hooks": "Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)",
|
"admin.dashboard.resync_all_hooks": "Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)",
|
||||||
"admin.dashboard.reinit_missing_repos": "Reinitialize all missing Git repositories for which records exist",
|
"admin.dashboard.reinit_missing_repos": "Reinitialize all missing Git repositories for which records exist",
|
||||||
"admin.dashboard.sync_external_users": "Synchronize external user data",
|
"admin.dashboard.sync_external_users": "Synchronize external user data",
|
||||||
@@ -3030,7 +3030,7 @@
|
|||||||
"admin.users.is_admin": "Is Administrator",
|
"admin.users.is_admin": "Is Administrator",
|
||||||
"admin.users.is_restricted": "Is Restricted",
|
"admin.users.is_restricted": "Is Restricted",
|
||||||
"admin.users.allow_git_hook": "May Create Git Hooks",
|
"admin.users.allow_git_hook": "May Create Git Hooks",
|
||||||
"admin.users.allow_git_hook_tooltip": "Git Hooks are executed as the OS user running MokoGitea and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all MokoGitea repositories as well as the database used by MokoGitea. Consequently they are also able to gain MokoGitea administrator privileges.",
|
"admin.users.allow_git_hook_tooltip": "Git Hooks are executed as the OS user running ${APP_NAME} and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all ${APP_NAME} repositories as well as the database used by ${APP_NAME}. Consequently they are also able to gain ${APP_NAME} administrator privileges.",
|
||||||
"admin.users.allow_import_local": "May Import Local Repositories",
|
"admin.users.allow_import_local": "May Import Local Repositories",
|
||||||
"admin.users.allow_create_organization": "May Create Organizations",
|
"admin.users.allow_create_organization": "May Create Organizations",
|
||||||
"admin.users.update_profile": "Update User Account",
|
"admin.users.update_profile": "Update User Account",
|
||||||
@@ -3100,11 +3100,11 @@
|
|||||||
"admin.packages.size": "Size",
|
"admin.packages.size": "Size",
|
||||||
"admin.packages.published": "Published",
|
"admin.packages.published": "Published",
|
||||||
"admin.defaulthooks": "Default Webhooks",
|
"admin.defaulthooks": "Default Webhooks",
|
||||||
"admin.defaulthooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain MokoGitea events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
|
"admin.defaulthooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain ${APP_NAME} events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
|
||||||
"admin.defaulthooks.add_webhook": "Add Default Webhook",
|
"admin.defaulthooks.add_webhook": "Add Default Webhook",
|
||||||
"admin.defaulthooks.update_webhook": "Update Default Webhook",
|
"admin.defaulthooks.update_webhook": "Update Default Webhook",
|
||||||
"admin.systemhooks": "System Webhooks",
|
"admin.systemhooks": "System Webhooks",
|
||||||
"admin.systemhooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain MokoGitea events trigger. Webhooks defined here will act on all repositories on the system, so please consider any performance implications this may have. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
|
"admin.systemhooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain ${APP_NAME} events trigger. Webhooks defined here will act on all repositories on the system, so please consider any performance implications this may have. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
|
||||||
"admin.systemhooks.add_webhook": "Add System Webhook",
|
"admin.systemhooks.add_webhook": "Add System Webhook",
|
||||||
"admin.systemhooks.update_webhook": "Update System Webhook",
|
"admin.systemhooks.update_webhook": "Update System Webhook",
|
||||||
"admin.auths.auth_manage_panel": "Authentication Source Management",
|
"admin.auths.auth_manage_panel": "Authentication Source Management",
|
||||||
@@ -3125,7 +3125,7 @@
|
|||||||
"admin.auths.user_base": "User Search Base",
|
"admin.auths.user_base": "User Search Base",
|
||||||
"admin.auths.user_dn": "User DN",
|
"admin.auths.user_dn": "User DN",
|
||||||
"admin.auths.attribute_username": "Username Attribute",
|
"admin.auths.attribute_username": "Username Attribute",
|
||||||
"admin.auths.attribute_username_placeholder": "Leave empty to use the username entered in MokoGitea.",
|
"admin.auths.attribute_username_placeholder": "Leave empty to use the username entered in ${APP_NAME}.",
|
||||||
"admin.auths.attribute_name": "First Name Attribute",
|
"admin.auths.attribute_name": "First Name Attribute",
|
||||||
"admin.auths.attribute_surname": "Surname Attribute",
|
"admin.auths.attribute_surname": "Surname Attribute",
|
||||||
"admin.auths.attribute_mail": "Email Attribute",
|
"admin.auths.attribute_mail": "Email Attribute",
|
||||||
@@ -3232,7 +3232,7 @@
|
|||||||
"admin.auths.invalid_openIdConnectAutoDiscoveryURL": "Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://)",
|
"admin.auths.invalid_openIdConnectAutoDiscoveryURL": "Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://)",
|
||||||
"admin.config.server_config": "Server Configuration",
|
"admin.config.server_config": "Server Configuration",
|
||||||
"admin.config.app_name": "Site Title",
|
"admin.config.app_name": "Site Title",
|
||||||
"admin.config.app_ver": "MokoGitea Version",
|
"admin.config.app_ver": "${APP_NAME} Version",
|
||||||
"admin.config.custom_conf": "Configuration File Path",
|
"admin.config.custom_conf": "Configuration File Path",
|
||||||
"admin.config.custom_file_root_path": "Custom File Root Path",
|
"admin.config.custom_file_root_path": "Custom File Root Path",
|
||||||
"admin.config.disable_router_log": "Disable Router Log",
|
"admin.config.disable_router_log": "Disable Router Log",
|
||||||
@@ -3272,7 +3272,7 @@
|
|||||||
"admin.config.service_config": "Service Configuration",
|
"admin.config.service_config": "Service Configuration",
|
||||||
"admin.config.register_email_confirm": "Require Email Confirmation to Register",
|
"admin.config.register_email_confirm": "Require Email Confirmation to Register",
|
||||||
"admin.config.disable_register": "Disable Self-Registration",
|
"admin.config.disable_register": "Disable Self-Registration",
|
||||||
"admin.config.allow_only_internal_registration": "Allow Registration Only Through MokoGitea itself",
|
"admin.config.allow_only_internal_registration": "Allow Registration Only Through ${APP_NAME} itself",
|
||||||
"admin.config.allow_only_external_registration": "Allow Registration Only Through External Services",
|
"admin.config.allow_only_external_registration": "Allow Registration Only Through External Services",
|
||||||
"admin.config.enable_openid_signup": "Enable OpenID Self-Registration",
|
"admin.config.enable_openid_signup": "Enable OpenID Self-Registration",
|
||||||
"admin.config.enable_openid_signin": "Enable OpenID Sign-In",
|
"admin.config.enable_openid_signin": "Enable OpenID Sign-In",
|
||||||
@@ -3414,11 +3414,11 @@
|
|||||||
"admin.self_check.no_problem_found": "No problem found yet.",
|
"admin.self_check.no_problem_found": "No problem found yet.",
|
||||||
"admin.self_check.startup_warnings": "Startup warnings:",
|
"admin.self_check.startup_warnings": "Startup warnings:",
|
||||||
"admin.self_check.database_collation_mismatch": "Expect database to use collation: %s",
|
"admin.self_check.database_collation_mismatch": "Expect database to use collation: %s",
|
||||||
"admin.self_check.database_collation_case_insensitive": "Database is using collation %s, which is a case-insensitive collation. Although MokoGitea could work with it, there might be some rare cases which don't work as expected.",
|
"admin.self_check.database_collation_case_insensitive": "Database is using collation %s, which is a case-insensitive collation. Although ${APP_NAME} could work with it, there might be some rare cases which don't work as expected.",
|
||||||
"admin.self_check.database_inconsistent_collation_columns": "Database is using collation %s, but these columns are using mismatched collations. This might cause some unexpected problems.",
|
"admin.self_check.database_inconsistent_collation_columns": "Database is using collation %s, but these columns are using mismatched collations. This might cause some unexpected problems.",
|
||||||
"admin.self_check.database_fix_mysql": "For MySQL/MariaDB users, you could use the \"gitea doctor convert\" command to fix the collation problems, or you could also fix the problem manually with \"ALTER ... COLLATE ...\" SQL queries.",
|
"admin.self_check.database_fix_mysql": "For MySQL/MariaDB users, you could use the \"gitea doctor convert\" command to fix the collation problems, or you could also fix the problem manually with \"ALTER ... COLLATE ...\" SQL queries.",
|
||||||
"admin.self_check.database_fix_mssql": "For MSSQL users, you could only fix the problem manually with \"ALTER ... COLLATE ...\" SQL queries at the moment.",
|
"admin.self_check.database_fix_mssql": "For MSSQL users, you could only fix the problem manually with \"ALTER ... COLLATE ...\" SQL queries at the moment.",
|
||||||
"admin.self_check.location_origin_mismatch": "Current URL (%[1]s) doesn't match the URL seen by MokoGitea (%[2]s). If you are using a reverse proxy, please make sure the \"Host\" and \"X-Forwarded-Proto\" headers are set correctly.",
|
"admin.self_check.location_origin_mismatch": "Current URL (%[1]s) doesn't match the URL seen by ${APP_NAME} (%[2]s). If you are using a reverse proxy, please make sure the \"Host\" and \"X-Forwarded-Proto\" headers are set correctly.",
|
||||||
"action.create_repo": "created repository <a href=\"%s\">%s</a>",
|
"action.create_repo": "created repository <a href=\"%s\">%s</a>",
|
||||||
"action.rename_repo": "renamed repository from <code>%[1]s</code> to <a href=\"%[2]s\">%[3]s</a>",
|
"action.rename_repo": "renamed repository from <code>%[1]s</code> to <a href=\"%[2]s\">%[3]s</a>",
|
||||||
"action.commit_repo": "pushed to <a href=\"%[2]s\">%[3]s</a> at <a href=\"%[1]s\">%[4]s</a>",
|
"action.commit_repo": "pushed to <a href=\"%[2]s\">%[3]s</a> at <a href=\"%[1]s\">%[4]s</a>",
|
||||||
@@ -3771,8 +3771,8 @@
|
|||||||
"actions.runs.status_no_select": "All status",
|
"actions.runs.status_no_select": "All status",
|
||||||
"actions.runs.no_results": "No results matched.",
|
"actions.runs.no_results": "No results matched.",
|
||||||
"actions.runs.no_workflows": "There are no workflows yet.",
|
"actions.runs.no_workflows": "There are no workflows yet.",
|
||||||
"actions.runs.no_workflows.quick_start": "Don't know how to start with Gitea Actions? See <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the quick start guide</a>.",
|
"actions.runs.no_workflows.quick_start": "Don't know how to start with ${APP_NAME} Actions? See <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the quick start guide</a>.",
|
||||||
"actions.runs.no_workflows.documentation": "For more information on Gitea Actions, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
|
"actions.runs.no_workflows.documentation": "For more information on ${APP_NAME} Actions, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
|
||||||
"actions.runs.no_runs": "The workflow has no runs yet.",
|
"actions.runs.no_runs": "The workflow has no runs yet.",
|
||||||
"actions.runs.empty_commit_message": "(empty commit message)",
|
"actions.runs.empty_commit_message": "(empty commit message)",
|
||||||
"actions.runs.expire_log_message": "Logs have been purged because they were too old.",
|
"actions.runs.expire_log_message": "Logs have been purged because they were too old.",
|
||||||
|
|||||||
@@ -253,6 +253,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID)
|
authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetSourceByID", err)
|
ctx.ServerError("GetSourceByID", err)
|
||||||
|
|||||||
+37
-15
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||||
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||||
@@ -301,21 +302,42 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us
|
|||||||
ctx.Redirect(setting.AppSubURL + "/user/link_account")
|
ctx.Redirect(setting.AppSubURL + "/user/link_account")
|
||||||
}
|
}
|
||||||
|
|
||||||
func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
|
var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
|
|
||||||
resp, err := http.Get(url)
|
func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) {
|
||||||
if err == nil {
|
if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 {
|
||||||
defer func() {
|
return
|
||||||
_ = resp.Body.Close()
|
}
|
||||||
}()
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil)
|
||||||
}
|
if err != nil {
|
||||||
// ignore any error
|
log.Warn("invalid avatar URL %q: %v", avatarURL, err)
|
||||||
if err == nil && resp.StatusCode == http.StatusOK {
|
return
|
||||||
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
|
}
|
||||||
if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
|
// Some hosts (e.g. Wikimedia) reject Go's default User-Agent.
|
||||||
_ = user_service.UploadAvatar(ctx, u, data)
|
req.Header.Set("User-Agent", "Gitea "+setting.AppVer)
|
||||||
}
|
|
||||||
}
|
resp, err := oauth2AvatarHTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("fetch %q failed: %v", avatarURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Warn("fetch %q returned status %d", avatarURL, resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("read body from %q failed: %v", avatarURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if int64(len(data)) > setting.Avatar.MaxFileSize {
|
||||||
|
log.Warn("avatar from %q exceeds max size %d", avatarURL, setting.Avatar.MaxFileSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := user_service.UploadAvatar(ctx, u, data); err != nil {
|
||||||
|
log.Warn("UploadAvatar for user %q failed: %v", u.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,7 @@ func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.Action
|
|||||||
var runByID, runByIndex *actions_model.ActionRun
|
var runByID, runByIndex *actions_model.ActionRun
|
||||||
var targetJobByIndex *actions_model.ActionRunJob
|
var targetJobByIndex *actions_model.ActionRunJob
|
||||||
|
|
||||||
// Each run must have at least one job, so a valid job ID in the same run cannot be smaller than the run ID.
|
if !byIndex {
|
||||||
if !byIndex && jobNum >= runNum {
|
|
||||||
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
|
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
|
||||||
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
|
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
|
||||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
|||||||
if !ctx.IsSigned {
|
if !ctx.IsSigned {
|
||||||
// TODO: support digit auth - which would be Authorization header with digit
|
// TODO: support digit auth - which would be Authorization header with digit
|
||||||
if setting.OAuth2.Enabled {
|
if setting.OAuth2.Enabled {
|
||||||
// `Basic realm="Gitea"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
|
// `Basic realm="<AppName>"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, setting.AppName))
|
||||||
} else {
|
} else {
|
||||||
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
|
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea (Basic Auth)"`)
|
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s (Basic Auth)"`, setting.AppName))
|
||||||
}
|
}
|
||||||
ctx.HTTPError(http.StatusUnauthorized)
|
ctx.HTTPError(http.StatusUnauthorized)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
// SwaggerV1Json render swagger v1 json
|
// SwaggerV1Json render swagger v1 json
|
||||||
func SwaggerV1Json(ctx *context.Context) {
|
func SwaggerV1Json(ctx *context.Context) {
|
||||||
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
|
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
|
||||||
|
ctx.Data["SwaggerAppName"] = template.HTML(template.JSEscapeString(setting.AppName))
|
||||||
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
|
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
|
||||||
ctx.JSONTemplate("swagger/v1_json")
|
ctx.JSONTemplate("swagger/v1_json")
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ func SwaggerV1Json(ctx *context.Context) {
|
|||||||
// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0)
|
// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0)
|
||||||
func OpenAPI3Json(ctx *context.Context) {
|
func OpenAPI3Json(ctx *context.Context) {
|
||||||
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
|
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
|
||||||
|
ctx.Data["SwaggerAppName"] = template.HTML(template.JSEscapeString(setting.AppName))
|
||||||
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
|
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
|
||||||
ctx.JSONTemplate("swagger/v1_openapi3_json")
|
ctx.JSONTemplate("swagger/v1_openapi3_json")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/actions"
|
actions_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/actions"
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||||
@@ -344,7 +345,7 @@ func handleWorkflows(
|
|||||||
|
|
||||||
run.NeedApproval = need
|
run.NeedApproval = need
|
||||||
|
|
||||||
if err := PrepareRunAndInsert(ctx, dwf.Content, run, nil); err != nil {
|
if err := prepareRunAndInsertWithRetry(ctx, dwf.Content, run); err != nil {
|
||||||
log.Error("PrepareRunAndInsert: %v", err)
|
log.Error("PrepareRunAndInsert: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -352,6 +353,54 @@ func handleWorkflows(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareRunAndInsertWithRetry wraps PrepareRunAndInsert with retries on
|
||||||
|
// database deadlocks. When multiple workflow runs are inserted for the same
|
||||||
|
// event (e.g. several workflows triggered by a single pull_request), each
|
||||||
|
// InsertRun transaction acquires an X-lock on the repository row (via
|
||||||
|
// UpdateRepoRunsNumbers) and an index lock on action_run. Two concurrent
|
||||||
|
// transactions can deadlock when each holds one lock and waits for the other.
|
||||||
|
// InnoDB resolves this by killing the lighter transaction, but handleWorkflows
|
||||||
|
// only logged the error and moved on — silently dropping the workflow run.
|
||||||
|
// Retrying the insert is safe because the rolled-back transaction left no
|
||||||
|
// partial state.
|
||||||
|
func prepareRunAndInsertWithRetry(ctx context.Context, content []byte, run *actions_model.ActionRun) error {
|
||||||
|
const maxRetries = 3
|
||||||
|
backoff := 50 * time.Millisecond
|
||||||
|
|
||||||
|
// Save original values that InsertRun mutates inside its transaction.
|
||||||
|
// On deadlock rollback these become stale and must be reset before retry.
|
||||||
|
origTitle := run.Title
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for attempt := range maxRetries {
|
||||||
|
if err = PrepareRunAndInsert(ctx, content, run, nil); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !db.IsErrDeadlock(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Warn("PrepareRunAndInsert deadlock (attempt %d/%d) for workflow %s in repo %d, retrying: %v",
|
||||||
|
attempt+1, maxRetries, run.WorkflowID, run.RepoID, err)
|
||||||
|
|
||||||
|
// Reset fields that InsertRun sets inside the (now rolled-back) transaction
|
||||||
|
// so the next attempt starts clean.
|
||||||
|
run.ID = 0
|
||||||
|
run.Index = 0
|
||||||
|
run.Status = actions_model.StatusWaiting
|
||||||
|
run.Title = origTitle
|
||||||
|
run.ConcurrencyGroup = ""
|
||||||
|
run.ConcurrencyCancel = false
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(backoff):
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
}
|
||||||
|
return fmt.Errorf("deadlock persisted after %d retries: %w", maxRetries, err)
|
||||||
|
}
|
||||||
|
|
||||||
func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
|
func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
|
||||||
return newNotifyInput(issue.Repo, issue.Poster, event)
|
return newNotifyInput(issue.Repo, issue.Poster, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname,
|
|||||||
|
|
||||||
// VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic
|
// VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic
|
||||||
func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) {
|
func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) {
|
||||||
// get oauth2 token's user's ID
|
// get oauth2 token's user's ID and access scope
|
||||||
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
|
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
|
||||||
if uid != 0 {
|
if uid != 0 {
|
||||||
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
||||||
|
|||||||
@@ -150,16 +150,18 @@ func WebPathFromRequest(s string) WebPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
|
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
|
||||||
var nonAlphanumRe = regexp.MustCompile(`[^a-zA-Z0-9\-]`)
|
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
|
||||||
|
|
||||||
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
|
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
|
||||||
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
|
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
|
||||||
|
// Preserves: letters, digits, hyphens, plus signs (+), and dots (.)
|
||||||
func sanitizeWikiTitle(title string) string {
|
func sanitizeWikiTitle(title string) string {
|
||||||
title = strings.TrimSpace(title)
|
title = strings.TrimSpace(title)
|
||||||
title = strings.ReplaceAll(title, " ", "-")
|
title = strings.ReplaceAll(title, " ", "-")
|
||||||
title = nonAlphanumRe.ReplaceAllString(title, "-")
|
title = nonSlugRe.ReplaceAllString(title, "-")
|
||||||
title = multiHyphenRe.ReplaceAllString(title, "-")
|
title = multiHyphenRe.ReplaceAllString(title, "-")
|
||||||
title = strings.Trim(title, "-")
|
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
|
||||||
|
title = strings.Trim(title, "-+.")
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="admin-setting-content">
|
<div class="admin-setting-content">
|
||||||
{{if .NeedUpdate}}
|
{{if .NeedUpdate}}
|
||||||
<div class="ui positive message">
|
<div class="ui positive message">
|
||||||
<div class="header">{{svg "octicon-info"}} MokoGitea Update Available</div>
|
<div class="header">{{svg "octicon-info"}} {{AppName}} Update Available</div>
|
||||||
<p>A new version <strong>{{.LatestVersion}}</strong> is available{{if .UpdateChannel}} ({{.UpdateChannel}} channel){{end}}.
|
<p>A new version <strong>{{.LatestVersion}}</strong> is available{{if .UpdateChannel}} ({{.UpdateChannel}} channel){{end}}.
|
||||||
{{if .ReleaseURL}}<a href="{{.ReleaseURL}}" target="_blank" rel="noopener noreferrer">View release notes</a>{{end}}</p>
|
{{if .ReleaseURL}}<a href="{{.ReleaseURL}}" target="_blank" rel="noopener noreferrer">View release notes</a>{{end}}</p>
|
||||||
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
|
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
|
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
|
||||||
<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
|
<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
|
||||||
{{if ShowFooterPoweredBy}}
|
{{if ShowFooterPoweredBy}}
|
||||||
<a target="_blank" href="https://git.mokoconsulting.tech/MokoConsulting/MokoGitea">{{ctx.Locale.Tr "powered_by" "MokoGitea"}}</a>
|
<a target="_blank" href="https://git.mokoconsulting.tech/MokoConsulting/MokoGitea">{{ctx.Locale.Tr "powered_by" AppName}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if (or .ShowFooterVersion .PageIsAdmin)}}
|
{{if (or .ShowFooterVersion .PageIsAdmin)}}
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{{ctx.HeadMetaContentSecurityPolicy}}
|
{{ctx.HeadMetaContentSecurityPolicy}}
|
||||||
<title>Gitea API</title>
|
<title>{{AppName}} API</title>
|
||||||
<link rel="stylesheet" href="{{ctx.CurrentWebTheme.PublicAssetURI}}">
|
<link rel="stylesheet" href="{{ctx.CurrentWebTheme.PublicAssetURI}}">
|
||||||
{{/* HINT: SWAGGER-CSS-IMPORT: import swagger styles ahead to avoid UI flicker (e.g.: the swagger-back-link element) */}}
|
{{/* HINT: SWAGGER-CSS-IMPORT: import swagger styles ahead to avoid UI flicker (e.g.: the swagger-back-link element) */}}
|
||||||
<link rel="stylesheet" href="{{AssetURI "css/swagger.css"}}">
|
<link rel="stylesheet" href="{{AssetURI "css/swagger.css"}}">
|
||||||
|
|||||||
Generated
+2
-2
@@ -11,8 +11,8 @@
|
|||||||
],
|
],
|
||||||
"swagger": "2.0",
|
"swagger": "2.0",
|
||||||
"info": {
|
"info": {
|
||||||
"description": "This documentation describes the Gitea API.",
|
"description": "This documentation describes the {{.SwaggerAppName}} API.",
|
||||||
"title": "Gitea API",
|
"title": "{{.SwaggerAppName}} API",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "MIT",
|
"name": "MIT",
|
||||||
"url": "http://opensource.org/licenses/MIT"
|
"url": "http://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -10588,12 +10588,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"description": "This documentation describes the Gitea API.",
|
"description": "This documentation describes the {{.SwaggerAppName}} API.",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "MIT",
|
"name": "MIT",
|
||||||
"url": "http://opensource.org/licenses/MIT"
|
"url": "http://opensource.org/licenses/MIT"
|
||||||
},
|
},
|
||||||
"title": "Gitea API",
|
"title": "{{.SwaggerAppName}} API",
|
||||||
"version": "{{.SwaggerAppVer}}"
|
"version": "{{.SwaggerAppVer}}"
|
||||||
},
|
},
|
||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
|
|||||||
@@ -160,6 +160,10 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
|
|||||||
collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", collisionRun.CommitSHA)
|
collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", collisionRun.CommitSHA)
|
||||||
collisionJobIdx1 := mkJob(2601, collisionRun.ID, "legacy-collision-job-2", collisionRun.CommitSHA)
|
collisionJobIdx1 := mkJob(2601, collisionRun.ID, "legacy-collision-job-2", collisionRun.CommitSHA)
|
||||||
|
|
||||||
|
// A run whose job has a smaller ID than the run itself (job_id < run_id)
|
||||||
|
jobSmallerThanRunRun := mkRun(5000, 5500, "legacy route job before run", "aaa007")
|
||||||
|
jobSmallerThanRunJob := mkJob(4500, jobSmallerThanRunRun.ID, "legacy-job-before-run-job", jobSmallerThanRunRun.CommitSHA)
|
||||||
|
|
||||||
// A small ID-based run/job pair that collides with a different legacy run/job index pair.
|
// A small ID-based run/job pair that collides with a different legacy run/job index pair.
|
||||||
ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005")
|
ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005")
|
||||||
ambiguousIDJob := mkJob(4, ambiguousIDRun.ID, "legacy-ambiguous-id-job", ambiguousIDRun.CommitSHA)
|
ambiguousIDJob := mkJob(4, ambiguousIDRun.ID, "legacy-ambiguous-id-job", ambiguousIDRun.CommitSHA)
|
||||||
@@ -182,11 +186,12 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
|
|||||||
targetAmbiguousLegacyJob := ambiguousLegacyJobs[int(ambiguousIDJob.ID)]
|
targetAmbiguousLegacyJob := ambiguousLegacyJobs[int(ambiguousIDJob.ID)]
|
||||||
|
|
||||||
insertBeansWithExplicitIDs(t, "action_run",
|
insertBeansWithExplicitIDs(t, "action_run",
|
||||||
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun,
|
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, jobSmallerThanRunRun,
|
||||||
)
|
)
|
||||||
insertBeansWithExplicitIDs(t, "action_run_job",
|
insertBeansWithExplicitIDs(t, "action_run_job",
|
||||||
smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1,
|
smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1,
|
||||||
ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5,
|
ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5,
|
||||||
|
jobSmallerThanRunJob,
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Run("OnlyRunID", func(t *testing.T) {
|
t.Run("OnlyRunID", func(t *testing.T) {
|
||||||
@@ -220,6 +225,9 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
|
|||||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, normalRun.ID, normalRunJob.ID))
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, normalRun.ID, normalRunJob.ID))
|
||||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
// URL must resolve even when job_id < run_id.
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, jobSmallerThanRunRun.ID, jobSmallerThanRunJob.ID))
|
||||||
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("RunIndexAndJobIndex", func(t *testing.T) {
|
t.Run("RunIndexAndJobIndex", func(t *testing.T) {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
auth_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||||
|
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||||
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
|
||||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
||||||
@@ -20,6 +23,7 @@ import (
|
|||||||
func TestGitSmartHTTP(t *testing.T) {
|
func TestGitSmartHTTP(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
testGitSmartHTTP(t, u)
|
testGitSmartHTTP(t, u)
|
||||||
|
testGitSmartHTTPTokenScopes(t)
|
||||||
testRenamedRepoRedirect(t)
|
testRenamedRepoRedirect(t)
|
||||||
testGitArchiveRemote(t, u)
|
testGitArchiveRemote(t, u)
|
||||||
})
|
})
|
||||||
@@ -80,6 +84,42 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testGitSmartHTTPTokenScopes(t *testing.T) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerName: "user2", Name: "repo2"})
|
||||||
|
require.True(t, repo.IsPrivate)
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
badToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification)
|
||||||
|
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
|
||||||
|
t.Run("upload-pack requires read repository scope", func(t *testing.T) {
|
||||||
|
path := "/user2/repo2/info/refs?service=git-upload-pack"
|
||||||
|
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(badToken, "x-oauth-basic"), http.StatusForbidden)
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(badToken), http.StatusForbidden)
|
||||||
|
|
||||||
|
resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "refs/heads/master")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("receive-pack requires write repository scope", func(t *testing.T) {
|
||||||
|
path := "/user2/repo2/info/refs?service=git-receive-pack"
|
||||||
|
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(readToken, "x-oauth-basic"), http.StatusForbidden)
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusForbidden)
|
||||||
|
|
||||||
|
resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(writeToken), http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "refs/heads/master")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public-only scope rejects private repo", func(t *testing.T) {
|
||||||
|
path := "/user2/repo2/info/refs?service=git-upload-pack"
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testRenamedRepoRedirect(t *testing.T) {
|
func testRenamedRepoRedirect(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/routers/web/auth"
|
||||||
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/markbates/goth"
|
||||||
|
"github.com/markbates/goth/gothic"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOAuth2AvatarFromPicture(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.OAuth2Client.UpdateAvatar, true)()
|
||||||
|
|
||||||
|
mockServer := createOAuth2MockProvider()
|
||||||
|
defer mockServer.Close()
|
||||||
|
addOAuth2Source(t, "test-oidc-avatar", oauth2.Source{
|
||||||
|
Provider: "openidConnect",
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
|
||||||
|
})
|
||||||
|
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "test-oidc-avatar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
providerName := authSource.Cfg.(*oauth2.Source).Provider
|
||||||
|
|
||||||
|
t.Run("AutoRegister", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.OAuth2Client.Username, "")()
|
||||||
|
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
||||||
|
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
|
||||||
|
return goth.User{
|
||||||
|
Provider: providerName,
|
||||||
|
UserID: "oidc-user-ua-pic",
|
||||||
|
Email: "oidc-user-ua-pic@example.com",
|
||||||
|
Name: "OIDC UA Pic",
|
||||||
|
AvatarURL: mockServer.URL + "/avatar.png",
|
||||||
|
}, nil
|
||||||
|
})()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user/oauth2/test-oidc-avatar/callback?code=XYZ&state=XYZ")
|
||||||
|
emptyTestSession(t).MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "oidc-user-ua-pic"})
|
||||||
|
assert.True(t, user.UseCustomAvatar, "avatar must sync (requires Gitea UA)")
|
||||||
|
assert.NotEmpty(t, user.Avatar)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LinkAccountRegister", func(t *testing.T) {
|
||||||
|
const newUserName = "oidc-link-register"
|
||||||
|
defer web.RouteMockReset()
|
||||||
|
web.RouteMock(web.MockAfterMiddlewares, func(ctx *context.Context) {
|
||||||
|
require.NoError(t, auth.Oauth2SetLinkAccountData(ctx, auth.LinkAccountData{
|
||||||
|
AuthSourceID: authSource.ID,
|
||||||
|
GothUser: goth.User{
|
||||||
|
Provider: providerName,
|
||||||
|
UserID: "oidc-link-register-sub",
|
||||||
|
Email: "oidc-link-register-a@example.com",
|
||||||
|
Name: "OIDC Link Register",
|
||||||
|
AvatarURL: mockServer.URL + "/avatar.png",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{
|
||||||
|
"user_name": newUserName,
|
||||||
|
"email": "oidc-link-register-b@example.com",
|
||||||
|
"password": "AVeryStrongPassword!1",
|
||||||
|
"retype": "AVeryStrongPassword!1",
|
||||||
|
})
|
||||||
|
emptyTestSession(t).MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: newUserName})
|
||||||
|
require.Equal(t, auth_model.OAuth2, user.LoginType)
|
||||||
|
assert.True(t, user.UseCustomAvatar, "register-link flow must sync avatar from `picture` claim")
|
||||||
|
assert.NotEmpty(t, user.Avatar)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -1131,10 +1133,17 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMockServer() *httptest.Server {
|
func createOAuth2MockProvider() *httptest.Server {
|
||||||
var mockServer *httptest.Server
|
var mockServer *httptest.Server
|
||||||
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
|
case "/avatar.png":
|
||||||
|
if !strings.HasPrefix(r.Header.Get("User-Agent"), "Gitea ") {
|
||||||
|
http.Error(w, "user agent doesn't match", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/png")
|
||||||
|
_ = png.Encode(w, image.NewRGBA(image.Rect(0, 0, 8, 8)))
|
||||||
case "/.well-known/openid-configuration":
|
case "/.well-known/openid-configuration":
|
||||||
_, _ = w.Write([]byte(`{
|
_, _ = w.Write([]byte(`{
|
||||||
"issuer": "` + mockServer.URL + `",
|
"issuer": "` + mockServer.URL + `",
|
||||||
@@ -1153,7 +1162,7 @@ func createMockServer() *httptest.Server {
|
|||||||
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
|
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
mockServer := createMockServer()
|
mockServer := createOAuth2MockProvider()
|
||||||
defer mockServer.Close()
|
defer mockServer.Close()
|
||||||
|
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -1233,7 +1242,7 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
|
|||||||
// Checks if an OAuth provider with spaces within the name does work,
|
// Checks if an OAuth provider with spaces within the name does work,
|
||||||
// with the encoding of its names in the URL (PR#37327)
|
// with the encoding of its names in the URL (PR#37327)
|
||||||
func testOAuthSourceSpecialChars(t *testing.T) {
|
func testOAuthSourceSpecialChars(t *testing.T) {
|
||||||
mockServer := createMockServer()
|
mockServer := createOAuth2MockProvider()
|
||||||
defer mockServer.Close()
|
defer mockServer.Close()
|
||||||
|
|
||||||
addOAuth2Source(t, "test space", oauth2.Source{
|
addOAuth2Source(t, "test space", oauth2.Source{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {showErrorToast} from '../modules/toast.ts';
|
|||||||
import {sleep} from '../utils.ts';
|
import {sleep} from '../utils.ts';
|
||||||
import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
|
import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
|
||||||
import {createApp} from 'vue';
|
import {createApp} from 'vue';
|
||||||
import {toOriginUrl} from '../utils/url.ts';
|
|
||||||
import {createTippy} from '../modules/tippy.ts';
|
import {createTippy} from '../modules/tippy.ts';
|
||||||
import {localUserSettings} from '../modules/user-settings.ts';
|
import {localUserSettings} from '../modules/user-settings.ts';
|
||||||
|
|
||||||
@@ -79,7 +78,8 @@ function initCloneSchemeUrlSelection(parent: Element) {
|
|||||||
const isTea = scheme === 'tea';
|
const isTea = scheme === 'tea';
|
||||||
|
|
||||||
if (tabHttps) {
|
if (tabHttps) {
|
||||||
tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS"
|
const link = tabHttps.getAttribute('data-link')!;
|
||||||
|
tabHttps.textContent = link.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS"
|
||||||
tabHttps.classList.toggle('active', isHttps);
|
tabHttps.classList.toggle('active', isHttps);
|
||||||
}
|
}
|
||||||
if (tabSsh) {
|
if (tabSsh) {
|
||||||
@@ -99,7 +99,7 @@ function initCloneSchemeUrlSelection(parent: Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
const link = toOriginUrl(tab.getAttribute('data-link')!);
|
const link = tab.getAttribute('data-link')!;
|
||||||
|
|
||||||
for (const el of document.querySelectorAll('.js-clone-url')) {
|
for (const el of document.querySelectorAll('.js-clone-url')) {
|
||||||
if (el.nodeName === 'INPUT') {
|
if (el.nodeName === 'INPUT') {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {linkifyURLs, pathEscape, pathEscapeSegments, toOriginUrl, urlQueryEscape} from './url.ts';
|
import {linkifyURLs, pathEscape, pathEscapeSegments, urlQueryEscape} from './url.ts';
|
||||||
|
|
||||||
describe('escape', () => {
|
describe('escape', () => {
|
||||||
const queryNonAscii = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
|
const queryNonAscii = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
|
||||||
@@ -45,19 +45,3 @@ test('linkifyURLs', () => {
|
|||||||
expect(linkifyURLs('https://evil.com/\nonclick=alert(1)')).toEqual(`${link('https://evil.com/')}\nonclick=alert(1)`);
|
expect(linkifyURLs('https://evil.com/\nonclick=alert(1)')).toEqual(`${link('https://evil.com/')}\nonclick=alert(1)`);
|
||||||
expect(linkifyURLs('https://evil.com/"onmouseover=alert(1)')).toEqual(`${link('https://evil.com/"onmouseover=alert')}(1)`);
|
expect(linkifyURLs('https://evil.com/"onmouseover=alert(1)')).toEqual(`${link('https://evil.com/"onmouseover=alert')}(1)`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toOriginUrl', () => {
|
|
||||||
const oldLocation = String(window.location);
|
|
||||||
for (const origin of ['https://example.com', 'https://example.com:3000']) {
|
|
||||||
window.location.assign(`${origin}/`);
|
|
||||||
expect(toOriginUrl('/')).toEqual(`${origin}/`);
|
|
||||||
expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`);
|
|
||||||
expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`);
|
|
||||||
expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`);
|
|
||||||
expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`);
|
|
||||||
expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`);
|
|
||||||
expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`);
|
|
||||||
expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`);
|
|
||||||
}
|
|
||||||
window.location.assign(oldLocation);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -57,19 +57,3 @@ export function linkifyURLs(html: string): string {
|
|||||||
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`; // eslint-disable-line github/unescaped-html-literal
|
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`; // eslint-disable-line github/unescaped-html-literal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert an absolute or relative URL to an absolute URL with the current origin. It only
|
|
||||||
* processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. */
|
|
||||||
export function toOriginUrl(urlStr: string) {
|
|
||||||
try {
|
|
||||||
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
|
|
||||||
const {origin, protocol, hostname, port} = window.location;
|
|
||||||
const url = new URL(urlStr, origin);
|
|
||||||
url.protocol = protocol;
|
|
||||||
url.hostname = hostname;
|
|
||||||
url.port = port || (protocol === 'https:' ? '443' : '80');
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return urlStr;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user