From 5ffe68499d433d72a0d21bb7717feaaf351078a2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 19:35:33 +0000 Subject: [PATCH 01/14] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 340 +++++++++++++------------- 1 file changed, 168 insertions(+), 172 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index ecf2de559a..eca25d8900 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -26,12 +26,10 @@ name: "Universal: Build & Release" on: - push: + pull_request: + types: [opened, closed] branches: - main - paths: - - 'src/**' - - 'htdocs/**' workflow_dispatch: env: @@ -44,10 +42,65 @@ permissions: contents: write jobs: + # ── Draft PR → Promote highest pre-release to RC ───────────────────────────── + promote-rc: + name: Promote Pre-Release to RC + runs-on: release + if: >- + github.event.action == 'opened' && + github.event.pull_request.draft == true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + 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.GA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --branch "${{ github.event.pull_request.head.ref }}" + + - 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.GA_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_name == 'push' || github.event_name == 'workflow_dispatch' + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' steps: - name: Checkout repository @@ -94,13 +147,34 @@ jobs: fi MAJOR=$(echo "$VERSION" | cut -d. -f1) echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT" - echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "branch=main" >> "$GITHUB_OUTPUT" + + # -- CHECK FOR RC PROMOTION ------------------------------------------------ + - name: "Check for RC release" + id: rc + 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.GA_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: Bump version" id: bump - if: steps.version.outputs.skip != 'true' + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' run: | MOKO_API="/tmp/moko-platform-api/cli" BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor) @@ -261,6 +335,7 @@ jobs: run: | 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/version_check.php --path . --fix 2>/dev/null || true - name: "Step 5: Write update stream" if: >- @@ -268,6 +343,15 @@ jobs: steps.platform.outputs.platform == 'joomla' run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + + # Fetch latest updates.xml from main so preserve logic has all channels + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + curl -sf -H "Authorization: token ${GA_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 + 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}" \ @@ -295,9 +379,7 @@ jobs: # -- STEP 6: Create tag --------------------------------------------------- - name: "Step 6: Create git tag" if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' && - steps.version.outputs.is_minor == 'true' + steps.version.outputs.skip != 'true' run: | RELEASE_TAG="${{ steps.version.outputs.release_tag }}" # Only create the major release tag if it doesn't exist yet @@ -310,10 +392,26 @@ jobs: fi echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - # -- STEP 7: Create or update Gitea Release -------------------------------- - - name: "Step 7: Gitea Release" + # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- + - name: "Step 7a: Promote RC to stable" if: >- - steps.version.outputs.skip != 'true' + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote == 'true' + run: | + 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.GA_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: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" @@ -337,6 +435,8 @@ jobs: [ -z "$NOTES" ] && NOTES="Release ${VERSION}" # Build release name: "Pretty Name VERSION (type_element-VERSION)" + # Strip existing type prefix to prevent duplication + EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') TYPE_PREFIX="" case "${EXT_TYPE}" in plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; @@ -376,7 +476,8 @@ jobs: # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ - name: "Step 8: Build package and update checksum" 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 }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" @@ -407,6 +508,13 @@ jobs: # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + # For packages, prefer over filename-derived element + if [ "$EXT_TYPE" = "package" ]; then + PKG_NAME=$(sed -n 's/.*\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME" + fi + # Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas) + EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') TYPE_PREFIX="" case "${EXT_TYPE}" in plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; @@ -442,110 +550,35 @@ jobs: 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 ------ + # -- Get existing assets for cleanup -------------------------------- 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 + + # -- Create per-file .sha256 checksum files ------------------------- + echo "${SHA256_ZIP} ${ZIP_NAME}" > "/tmp/${ZIP_NAME}.sha256" + echo "${SHA256_TAR} ${TAR_NAME}" > "/tmp/${TAR_NAME}.sha256" + + # -- Upload packages + checksums to release tag -------------------- + for ASSET in "${ZIP_NAME}" "${TAR_NAME}" "${ZIP_NAME}.sha256" "${TAR_NAME}.sha256"; do + [ ! -f "/tmp/${ASSET}" ] && continue + # Delete existing asset with same name ASSET_ID=$(echo "$ASSETS" | python3 -c " import sys,json assets = json.load(sys.stdin) for a in assets: - if a['name'] == '${ASSET_NAME}': + if a['name'] == '${ASSET}': 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 + [ -n "$ASSET_ID" ] && curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + # Upload + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ASSET}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ASSET}" > /dev/null 2>&1 || true 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 = ( - " \n" - f" {zip_url}\n" - " " - ) - block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) - # Add or replace sha256 - if '' in block: - block = re.sub(r' .*?', f' {sha}', block) - else: - block = block.replace('', f'\n {sha}') - return block - - content = re.sub( - r' .*?stable.*?', - 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] " || 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 + # updates.xml already handled by Step 5 (updates_xml_build.php with preserve logic) echo "### Packages" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -556,72 +589,33 @@ jobs: 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" + # -- STEP 8b: Update release description with changelog ---------------------- + - name: "Step 8b: Update release body" if: steps.version.outputs.skip != 'true' run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - EXT_TYPE="${{ steps.updates.outputs.ext_type }}" - EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + MOKO_CLI="/tmp/moko-platform-api/cli" - # Build TYPE_PREFIX to match Step 8's ZIP naming - 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 + php ${MOKO_CLI}/release_body_update.php \ + --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ + --token "${{ secrets.GA_TOKEN }}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + 2>/dev/null || { + # Fallback: simple body update if CLI not available + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + 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 + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\nChecksum files attached as \`*.sha256\` assets." + curl -sf -X PATCH -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases/${RELEASE_ID}" \ + -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 + fi + } + echo "Release body updated" >> $GITHUB_STEP_SUMMARY # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" @@ -688,11 +682,13 @@ jobs: - name: "Delete lesser pre-release channels" continue-on-error: 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_cascade.php \ --stability stable \ + --version "${VERSION}" \ --token "${{ secrets.GA_TOKEN }}" \ - --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - --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" if: steps.version.outputs.skip != 'true' From aa6c3fc4edadd7a0783df94f71ef27e6a77f069f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 19:35:48 +0000 Subject: [PATCH 02/14] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 0980e61109..698251d1da 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -13,6 +13,10 @@ name: "Universal: Pre-Release" on: + pull_request: + types: [closed] + branches: + - dev workflow_dispatch: inputs: stability: @@ -35,8 +39,11 @@ env: jobs: build: - name: "Build Pre-Release (${{ inputs.stability }})" + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') steps: - name: Checkout @@ -84,7 +91,7 @@ jobs: - name: Resolve metadata and bump version id: meta run: | - STABILITY="${{ inputs.stability }}" + STABILITY="${{ inputs.stability || 'development' }}" case "$STABILITY" in development) SUFFIX="-dev"; TAG="development" ;; @@ -107,6 +114,9 @@ jobs: php ${MOKO_CLI}/version_set_platform.php \ --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true + # Verify version consistency across all files + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + # Commit version bump git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" From 01f6722cccaa915e6d053f0983a5e5ad6e288750 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 19:56:42 +0000 Subject: [PATCH 03/14] chore(ci): add update-server.yml universal workflow [skip ci] --- .mokogitea/workflows/update-server.yml | 597 +++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 .mokogitea/workflows/update-server.yml diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml new file mode 100644 index 0000000000..fd6407f7ba --- /dev/null +++ b/.mokogitea/workflows/update-server.yml @@ -0,0 +1,597 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/update-server.yml +# VERSION: 04.07.00 +# BRIEF: Update server XML feed with stable/rc/beta/alpha/dev entries (universal) +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev or dev/** +# +# Joomla filters by 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 updates.xml + 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.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_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 + if [ -d "/tmp/moko-platform" ]; then + echo "moko-platform already available — skipping clone" + else + 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 + fi + 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 + + - name: Generate updates.xml entry + id: update + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on all branches (dev, alpha, beta, rc) + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + BUMPED=$(php /tmp/moko-platform/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " 2>/dev/null || true + git push 2>/dev/null || true + fi + + # Determine stability from branch or 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" + elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + + # Parse manifest (portable — no grep -P) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found — skipping" + exit 0 + fi + + # Extract fields using sed (works on all runners) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: try XML filename, then repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" + + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + # Joomla requires on ALL extension types for update matching + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + else + CLIENT_TAG="site" + fi + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" + + # -- Build install packages and upload to release -------------------- + php /tmp/moko-platform/cli/release_package.php \ + --path . --version "${DISPLAY_VERSION}" --tag "${RELEASE_TAG}" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ + --repo "${GITEA_REPO}" --output /tmp 2>&1 || true + + echo "Package built and uploaded for ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- Build the new entry (canonical format matching release.yml) -- + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n" + NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" + NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" + NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" + NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n" + NEW_ENTRY="${NEW_ENTRY} " + + # -- Write new entry to temp file -------------------------------- + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # -- Merge into updates.xml ---------------------------------------- + # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev + CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" + TARGETS="" + for entry in $CASCADE_MAP; do + key="${entry%%:*}" + vals="${entry#*:}" + if [ "$key" = "${STABILITY}" ]; then + TARGETS="$vals" + break + fi + done + [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + + echo "Cascade: ${STABILITY} → ${TARGETS}" + + # Create updates.xml if missing + if [ ! -f "updates.xml" ]; then + printf '%s\n' "" > updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + fi + + # Update existing blocks or create missing ones + export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" + python3 << 'PYEOF' + import re, os + + targets = os.environ["PY_TARGETS"].split(",") + version = os.environ["PY_VERSION"] + date = os.environ["PY_DATE"] + + with open("updates.xml") as f: + content = f.read() + with open("/tmp/new_entry.xml") as f: + new_entry_template = f.read() + + for tag in targets: + tag = tag.strip() + # Build entry with this tag's name + new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) + + # Try to find existing block (handles both single-line and multi-line ) + block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if match: + # Update in place — replace entire block + content = content.replace(match.group(1), new_entry.strip()) + print(f" UPDATED: {tag} → {version}") + else: + # Create — insert before + content = content.replace("", "\n" + new_entry.strip() + "\n\n") + print(f" CREATED: {tag} → {version}") + + # Clean up excessive blank lines + content = re.sub(r"\n{3,}", "\n\n", content) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Sync updates.xml to main + if: github.ref_name != 'main' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_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 ${STABILITY} [skip ci]', + 'branch': 'main' + }).encode() + req = urllib.request.Request( + '${API_BASE}/contents/updates.xml', + data=payload, method='PUT', + headers={ + 'Authorization': 'token ${GA_TOKEN}', + 'Content-Type': 'application/json' + }) + try: + urllib.request.urlopen(req) + print('updates.xml synced to main') + except Exception as e: + print(f'ERROR: failed to sync updates.xml to main: {e}', file=sys.stderr) + sys.exit(1) + " \ + && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ + || echo "::error::failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY + else + echo "::error::could not get updates.xml SHA from main — file may not exist on main yet" >> $GITHUB_STEP_SUMMARY + 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 }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_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 /tmp/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then + php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then + php /tmp/moko-platform/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: Validate updates.xml integrity + run: | + ERRORS=0 + + if [ ! -f "updates.xml" ]; then + echo "::error::updates.xml not found" + exit 1 + fi + + # Well-formed XML + if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then + echo "::error::updates.xml is not valid XML" + ERRORS=$((ERRORS+1)) + fi + + python3 << 'PYEOF' + import xml.etree.ElementTree as ET, sys, re, os + + tree = ET.parse("updates.xml") + root = tree.getroot() + updates = root.findall("update") + errors = 0 + warnings = 0 + seen_tags = set() + + # All 5 channels MUST be present + REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"} + VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias + REPO = os.environ.get("GITEA_REPO", "") + ORG = os.environ.get("GITEA_ORG", "MokoConsulting") + REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/" + + # Gitea release tag names per channel (Moko standard) + RELEASE_TAG_MAP = { + "stable": "stable", + "rc": "release-candidate", + "beta": "beta", + "alpha": "alpha", + "dev": "development", + "development": "development", + } + + # Joomla update XML required fields per + # https://docs.joomla.org/Deploying_an_Update_Server + REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"] + + for i, u in enumerate(updates): + tag_el = u.find("tags/tag") + tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None + label = f"Entry {i+1} ({tag or '?'})" + + # -- Required Joomla fields -- + for field in REQUIRED_FIELDS: + el = u.find(field) + if el is None or not (el.text or "").strip(): + print(f"::error::{label}: missing required <{field}>") + errors += 1 + + # -- -- + dl = u.find("downloads/downloadurl") + if dl is None or not (dl.text or "").strip(): + print(f"::error::{label}: missing ") + errors += 1 + else: + dl_url = dl.text.strip() + # Must point to org repo + if REPO_BASE not in dl_url: + print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}") + errors += 1 + # Must end in .zip + if not dl_url.endswith(".zip"): + print(f"::error::{label}: download URL must end in .zip: {dl_url}") + errors += 1 + # Must use correct Gitea release tag in path + if tag and tag in RELEASE_TAG_MAP: + expected_tag = RELEASE_TAG_MAP[tag] + if f"/download/{expected_tag}/" not in dl_url: + print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}") + errors += 1 + + # -- (required for Joomla to match update) -- + client = u.find("client") + if client is None or not (client.text or "").strip(): + print(f"::error::{label}: missing (required for Joomla update matching)") + errors += 1 + + # -- -- + tp = u.find("targetplatform") + if tp is None: + print(f"::error::{label}: missing ") + errors += 1 + else: + tp_name = tp.get("name", "") + tp_ver = tp.get("version", "") + if tp_name != "joomla": + print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'") + errors += 1 + if not tp_ver: + print(f"::error::{label}: targetplatform missing version regex") + errors += 1 + elif "5" not in tp_ver or "6" not in tp_ver: + print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}") + warnings += 1 + + # -- must be valid Joomla type -- + type_el = u.find("type") + if type_el is not None and type_el.text: + valid_types = {"component", "module", "plugin", "template", "library", "package", "file"} + if type_el.text.strip() not in valid_types: + print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})") + errors += 1 + + # -- format (XX.YY.ZZ with optional suffix) -- + ver_el = u.find("version") + if ver_el is not None and ver_el.text: + if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()): + print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format") + warnings += 1 + + # -- and -- + for field in ["maintainer", "maintainerurl"]: + el = u.find(field) + if el is None or not (el.text or "").strip(): + print(f"::warning::{label}: missing <{field}>") + warnings += 1 + + # -- Valid stability tag -- + if tag is None: + print(f"::error::{label}: missing ") + errors += 1 + elif tag not in VALID_TAGS: + print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})") + errors += 1 + + # -- Duplicate tag check -- + norm_tag = "dev" if tag == "development" else tag + if norm_tag in seen_tags: + print(f"::error::{label}: duplicate channel '{tag}'") + errors += 1 + if norm_tag: + seen_tags.add(norm_tag) + + # -- All 5 channels must exist -- + missing = REQUIRED_CHANNELS - seen_tags + if missing: + print(f"::error::Missing required update channels: {', '.join(sorted(missing))}") + errors += 1 + + # -- Version ordering: higher stability must not exceed dev version -- + channel_versions = {} + for u in updates: + tag_el = u.find("tags/tag") + ver_el = u.find("version") + if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text: + norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip() + # Strip suffix for comparison (01.00.18-dev -> 01.00.18) + base_ver = re.sub(r"-\w+$", "", ver_el.text.strip()) + channel_versions[norm] = base_ver + + # Cascade check: dev >= alpha >= beta >= rc >= stable + ORDER = ["dev", "alpha", "beta", "rc", "stable"] + for j in range(1, len(ORDER)): + current = ORDER[j] + previous = ORDER[j - 1] + if current in channel_versions and previous in channel_versions: + if channel_versions[current] > channel_versions[previous]: + print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})") + errors += 1 + + # -- Summary -- + print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)") + if errors > 0: + sys.exit(1) + PYEOF + + - name: Summary + if: always() + run: | + echo "## Joomla 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_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY From dd6fc4b69cc675374ca5ff0cd830dd4f4b0a3122 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 26 May 2026 15:16:30 -0500 Subject: [PATCH 04/14] fix(actions): retry workflow insertion on database deadlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple workflows are triggered by a single event (e.g. a pull_request with several matching workflow files), 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 kills the lighter transaction, but handleWorkflows only logged the error and silently dropped the workflow run — making it appear as though pull_request events were never fired. This was the root cause of API-created PRs appearing to not trigger Actions workflows: the notification pipeline was correct, but the DB insert was lost to an unretried deadlock. The fix wraps PrepareRunAndInsert in a retry loop (up to 3 attempts with exponential backoff) that detects deadlock errors across MySQL, PostgreSQL, and SQLite. On deadlock, the rolled-back run fields are reset before the next attempt. Also adds db.IsErrDeadlock() for cross-engine deadlock detection and unit tests for the same. Closes #220 Co-Authored-By: Claude Opus 4.6 (1M context) --- models/db/error.go | 25 ++++++++++++++ models/db/error_test.go | 31 ++++++++++++++++++ services/actions/notifier_helper.go | 51 ++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 models/db/error_test.go diff --git a/models/db/error.go b/models/db/error.go index a2f6dfc1c2..afab17f0af 100644 --- a/models/db/error.go +++ b/models/db/error.go @@ -5,6 +5,7 @@ package db import ( "fmt" + "strings" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util" ) @@ -72,3 +73,27 @@ func (err ErrNotExist) Error() string { func (err ErrNotExist) Unwrap() error { 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 +} diff --git a/models/db/error_test.go b/models/db/error_test.go new file mode 100644 index 0000000000..f328ba34dd --- /dev/null +++ b/models/db/error_test.go @@ -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)) + }) + } +} diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 4b0817aac1..f528ca36d0 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -9,6 +9,7 @@ import ( "fmt" "slices" "strings" + "time" actions_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/actions" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" @@ -344,7 +345,7 @@ func handleWorkflows( 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) continue } @@ -352,6 +353,54 @@ func handleWorkflows( 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 { return newNotifyInput(issue.Repo, issue.Poster, event) } From 38beba655bf26e88a384deba1b1bf97c753bfccd Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Sun, 17 May 2026 00:42:20 -0600 Subject: [PATCH 05/14] fix(actions): wrong assumption that run id always >= job id (#37737) (#37742) Backport #37737 Fix #37734 Follow up #37008 The `jobNum >= runNum` check is useless. Removed it to support `job_id < run_id` --- routers/web/repo/actions/view.go | 3 +-- tests/integration/actions_route_test.go | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 44b13213b4..7a1fcaee9e 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -138,8 +138,7 @@ func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.Action var runByID, runByIndex *actions_model.ActionRun 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 && jobNum >= runNum { + if !byIndex { // 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) if err != nil && !errors.Is(err, util.ErrNotExist) { diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go index 1b585ef1d0..b73fc21d1b 100644 --- a/tests/integration/actions_route_test.go +++ b/tests/integration/actions_route_test.go @@ -160,6 +160,10 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) { collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", 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. ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005") 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)] insertBeansWithExplicitIDs(t, "action_run", - smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, + smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, jobSmallerThanRunRun, ) insertBeansWithExplicitIDs(t, "action_run_job", smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1, ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5, + jobSmallerThanRunJob, ) t.Run("OnlyRunID", func(t *testing.T) { @@ -220,6 +225,9 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) { 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)) 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) { From 233144e33e1c90fd799e76ea55236fda661e5ed2 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sat, 16 May 2026 07:15:53 -0700 Subject: [PATCH 06/14] fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register (#37564) (#37588) (#37726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport #37588 by @pandareen ## Summary Fixes [go-gitea/gitea#37564](https://github.com/go-gitea/gitea/issues/37564): when an OIDC provider returns a `picture` claim, Gitea is supposed to download that image as the user's avatar (if `[oauth2_client] UPDATE_AVATAR = true`). Two latent bugs prevented this from working consistently: 1. **Default Go User-Agent rejected by some image hosts.** `oauth2UpdateAvatarIfNeed` used `http.Get`, which sends `User-Agent: Go-http-client/1.1`. Hosts like `upload.wikimedia.org` reject that UA with `403`, and every error path silently returned, so the user was left with an identicon and **no log line** to diagnose the issue. 2. **Link-account *register* path skipped avatar sync.** First-time OIDC sign-ins where auto-registration is disabled (or required a username/password retype) go through `LinkAccountPostRegister`, which created the user but never called `oauth2SignInSync`. So the avatar / full name / SSH keys from the IdP were dropped on the floor for those users, even though the existing-account-link path (`oauth2LinkAccount`) and the auto-register path (`handleOAuth2SignIn`) both already did the sync. ## Changes - `routers/web/auth/oauth.go` — `oauth2UpdateAvatarIfNeed` now uses `http.NewRequest` + `http.DefaultClient.Do`, sets `User-Agent: Gitea `, and logs every failure path at `Warn` (invalid URL, fetch error, non-200, body read error, oversize body, upload error). No silent failures. - `routers/web/auth/linkaccount.go` — `LinkAccountPostRegister` now calls `oauth2SignInSync` after a successful user creation, mirroring the auto-register and link-existing-account flows. - `tests/integration/oauth_avatar_test.go` — new `TestOAuth2AvatarFromPicture` integration test with five sub-cases: - `AutoRegister_FetchesAvatarFromPictureWithGiteaUA` — happy path, asserts `use_custom_avatar=true`, an avatar hash is set, exactly one HTTP request was made, and the request carried a `Gitea ` UA. The mock server enforces the UA prefix to mirror real-world hosts that reject Go's default UA. - `AutoRegister_NonOK_DoesNotUpdateAvatar` — server returns 403; user's avatar must remain unset. - `AutoRegister_EmptyPicture_NoFetch` — empty `picture` claim must not trigger any HTTP request. - `AutoRegister_UpdateAvatarFalse_NoFetch` — `UPDATE_AVATAR=false` must not trigger any HTTP request. - `LinkAccountRegister_FetchesAvatarFromPicture` — guards the `linkaccount.go` fix; without the new `oauth2SignInSync` call this assertion fails. ## Test plan - [x] `go test -tags 'sqlite sqlite_unlock_notify' -run '^TestOAuth2AvatarFromPicture$' ./tests/integration/ -v` — 5/5 sub-tests pass. - [x] Manual: log in as a Keycloak user with `picture` claim pointing at `https://avatars.githubusercontent.com/u/9919?v=4` — Gitea avatar is replaced with the GitHub picture. - [x] Manual: same flow with `https://upload.wikimedia.org/...` — request now succeeds (or returns a clearly logged `Warn` line if rate-limited with `429`); previously it silently 403'd. - [x] Manual: `UPDATE_AVATAR=false` — user keeps the identicon, no outbound request in container logs. - [ ] Reviewer: please double-check that no other call sites of `oauth2UpdateAvatarIfNeed` rely on the old `http.Get` behaviour. ## Related - Upstream issue: go-gitea/gitea#37564 -------------------------------------------- AI Editor was used in this PR --------- Signed-off-by: silverwind Co-authored-by: pandareen <7270563+pandareen@users.noreply.github.com> Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: wxiaoguang Co-authored-by: Nicolas --- modules/log/logger_impl.go | 21 ++++++ modules/log/logger_test.go | 7 ++ routers/web/auth/linkaccount.go | 5 ++ routers/web/auth/oauth.go | 52 ++++++++++----- tests/integration/oauth_avatar_test.go | 92 ++++++++++++++++++++++++++ tests/integration/oauth_test.go | 15 ++++- 6 files changed, 174 insertions(+), 18 deletions(-) create mode 100644 tests/integration/oauth_avatar_test.go diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go index 9ca5a7d00d..acff767633 100644 --- a/modules/log/logger_impl.go +++ b/modules/log/logger_impl.go @@ -5,6 +5,7 @@ package log import ( "context" + "net/url" "reflect" "runtime" "strings" @@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any) } } else if ls := asLogStringer(v); ls != nil { 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) } +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 { return Level(l.level.Load()) } diff --git a/modules/log/logger_test.go b/modules/log/logger_test.go index a74139dc51..6e38610ddd 100644 --- a/modules/log/logger_test.go +++ b/modules/log/logger_test.go @@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) { 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")) +} diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index c070c50acf..9c0deb3ecd 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -253,6 +253,11 @@ func LinkAccountPostRegister(ctx *context.Context) { return } + oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser) + if ctx.Written() { + return + } + authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID) if err != nil { ctx.ServerError("GetSourceByID", err) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index d1e220ca69..37be3f5466 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -13,6 +13,7 @@ import ( "net/url" "sort" "strings" + "time" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth" 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") } -func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) { - if setting.OAuth2Client.UpdateAvatar && len(url) > 0 { - resp, err := http.Get(url) - if err == nil { - defer func() { - _ = resp.Body.Close() - }() - } - // ignore any error - if err == nil && resp.StatusCode == http.StatusOK { - data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1)) - if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize { - _ = user_service.UploadAvatar(ctx, u, data) - } - } +var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second} + +func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) { + if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 { + return + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil) + if err != nil { + log.Warn("invalid avatar URL %q: %v", avatarURL, err) + return + } + // Some hosts (e.g. Wikimedia) reject Go's default User-Agent. + 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) } } diff --git a/tests/integration/oauth_avatar_test.go b/tests/integration/oauth_avatar_test.go new file mode 100644 index 0000000000..d86f2f2cbe --- /dev/null +++ b/tests/integration/oauth_avatar_test.go @@ -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) + }) +} diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 90f606656b..a55336c419 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -8,6 +8,8 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "image" + "image/png" "io" "net/http" "net/http/httptest" @@ -1131,10 +1133,17 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) { require.NoError(t, err) } -func createMockServer() *httptest.Server { +func createOAuth2MockProvider() *httptest.Server { var mockServer *httptest.Server mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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": _, _ = w.Write([]byte(`{ "issuer": "` + mockServer.URL + `", @@ -1153,7 +1162,7 @@ func createMockServer() *httptest.Server { func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { defer tests.PrepareTestEnv(t)() - mockServer := createMockServer() + mockServer := createOAuth2MockProvider() defer mockServer.Close() ctx := t.Context() @@ -1233,7 +1242,7 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { // Checks if an OAuth provider with spaces within the name does work, // with the encoding of its names in the URL (PR#37327) func testOAuthSourceSpecialChars(t *testing.T) { - mockServer := createMockServer() + mockServer := createOAuth2MockProvider() defer mockServer.Close() addOAuth2Source(t, "test space", oauth2.Source{ From 00d862f737da7349d79a7c22f930b38f7901ea56 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 8 May 2026 14:04:04 -0700 Subject: [PATCH 07/14] fix: make clone URL respect public URL detection setting (#37615) (#37617) Backport #37615 by @wxiaoguang Fix #37614 Co-authored-by: wxiaoguang --- web_src/js/features/repo-common.ts | 6 +++--- web_src/js/utils/url.test.ts | 18 +----------------- web_src/js/utils/url.ts | 16 ---------------- 3 files changed, 4 insertions(+), 36 deletions(-) diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 65b89cf261..50d0db3673 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -5,7 +5,6 @@ import {showErrorToast} from '../modules/toast.ts'; import {sleep} from '../utils.ts'; import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue'; import {createApp} from 'vue'; -import {toOriginUrl} from '../utils/url.ts'; import {createTippy} from '../modules/tippy.ts'; import {localUserSettings} from '../modules/user-settings.ts'; @@ -79,7 +78,8 @@ function initCloneSchemeUrlSelection(parent: Element) { const isTea = scheme === 'tea'; 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); } if (tabSsh) { @@ -99,7 +99,7 @@ function initCloneSchemeUrlSelection(parent: Element) { } 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')) { if (el.nodeName === 'INPUT') { diff --git a/web_src/js/utils/url.test.ts b/web_src/js/utils/url.test.ts index a2e7339375..0a7e27ca61 100644 --- a/web_src/js/utils/url.test.ts +++ b/web_src/js/utils/url.test.ts @@ -1,4 +1,4 @@ -import {linkifyURLs, pathEscape, pathEscapeSegments, toOriginUrl, urlQueryEscape} from './url.ts'; +import {linkifyURLs, pathEscape, pathEscapeSegments, urlQueryEscape} from './url.ts'; describe('escape', () => { 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/"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); -}); diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index 927f0937a4..84328faf24 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -57,19 +57,3 @@ export function linkifyURLs(html: string): string { return `${cleanUrl}${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; -} From 6d9f0d97272b980a8af876558e4799b5bb6c5c2b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 2 May 2026 12:58:40 +0200 Subject: [PATCH 08/14] Fix basic auth bug (#37503) Backport for #37486 --- services/auth/basic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/auth/basic.go b/services/auth/basic.go index caf47af4f6..75a6345876 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -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 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) if uid != 0 { log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid) From cc61032697e324cc92d8b015c75bb18f888eaa92 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 8 May 2026 09:14:41 -0700 Subject: [PATCH 09/14] fix(git): Fix smart http request scope bug (#37583) (#37605) Backport #37583 by @lunny Co-authored-by: Lunny Xiao Co-authored-by: Nicolas Co-authored-by: Claude Opus 4.7 Co-authored-by: silverwind --- tests/integration/git_smart_http_test.go | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index 33e90c7dfa..b07e047091 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -9,6 +9,9 @@ import ( "net/url" "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/test" "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util" @@ -20,6 +23,7 @@ import ( func TestGitSmartHTTP(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { testGitSmartHTTP(t, u) + testGitSmartHTTPTokenScopes(t) testRenamedRepoRedirect(t) 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) { defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() From 775766bc649243eb159ff03456754fe0eb6cf06f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 26 May 2026 16:49:33 -0500 Subject: [PATCH 10/14] chore(deps): bump go-git/go-git/v5 to 5.19.0 (security) Addresses security fixes in the go-git library. Upstream backport of go-gitea/gitea#37608. Co-Authored-By: Claude Opus 4.6 (1M context) --- go.mod | 15 +++++++-------- go.sum | 52 ++++++++++++++-------------------------------------- 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index 6c415897bf..5dc8bf4261 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ 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." // But some CAs use negative serial number, just relax the check. related: @@ -9,6 +9,7 @@ godebug x509negativeserial=1 require ( code.gitea.io/actions-proto-go v0.4.1 + code.gitea.io/gitea v1.26.2 code.gitea.io/sdk/gitea v0.24.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 connectrpc.com/connect v1.19.1 @@ -52,8 +53,8 @@ require ( github.com/go-chi/cors v1.2.2 github.com/go-co-op/gocron/v2 v2.19.1 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-git/v5 v5.18.0 + github.com/go-git/go-billy/v5 v5.9.0 + github.com/go-git/go-git/v5 v5.19.0 github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-redsync/redsync/v4 v4.16.0 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/ncruces/go-strftime v0.1.9 // 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/yaml3 v0.0.12 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/ll v0.1.8 // 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/philhofer/fwd v1.2.0 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/sirupsen/logrus v1.9.4 // 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/spf13/afero v1.15.0 // 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/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // 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 go.etcd.io/bbolt v1.4.3 // 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/v3 v3.0.4 // 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/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect diff --git a/go.sum b/go.sum index cbca6c940f..38d76a2b40 100644 --- a/go.sum +++ b/go.sum @@ -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= 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/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/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= 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/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= 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.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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-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/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= -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 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +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/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.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +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/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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-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-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/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 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/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.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/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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 v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= 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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= 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/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 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/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= 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.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/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.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/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 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/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= 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.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= 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-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +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/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= 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-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-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-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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-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-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-20220722155255-886fb9371eb4/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-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-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-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-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-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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= 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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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.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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 455f12e21bd0bc4a7bcc614c09ad969885b94494 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:12:18 +0000 Subject: [PATCH 11/14] chore(ci): add auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/workflows/auto-bump.yml diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml new file mode 100644 index 0000000000..ef0aedc5c8 --- /dev/null +++ b/.mokogitea/workflows/auto-bump.yml @@ -0,0 +1,82 @@ +# Copyright (C) 2026 Moko Consulting +# +# 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 + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Patch Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_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.GA_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: Patch 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 + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch dev 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # 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://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push origin dev + echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY From f15a0ed7e489201846ab55b5d6d095537145002e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:13:29 +0000 Subject: [PATCH 12/14] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 104 +++++---------------------- 1 file changed, 16 insertions(+), 88 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 698251d1da..83443c7699 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -78,15 +78,7 @@ jobs: - name: Detect platform id: platform run: | - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/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" + php ${MOKO_CLI}/manifest_read.php --path . --github-output - name: Resolve metadata and bump version id: meta @@ -100,16 +92,9 @@ jobs: release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; esac - # Patch bump via CLI tool - php ${MOKO_CLI}/version_bump.php --path . + # Read current version (bump already handled by push workflow) VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) [ -z "$VERSION" ] && VERSION="00.00.01" - TODAY=$(date +%Y-%m-%d) - - # Update platform-specific manifest - PLATFORM="${{ steps.platform.outputs.platform }}" - MANIFEST="${{ steps.platform.outputs.manifest }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" php ${MOKO_CLI}/version_set_platform.php \ --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true @@ -127,36 +112,16 @@ jobs: git push origin HEAD 2>&1 } - # Auto-detect element (platform-aware) - EXT_ELEMENT="" - case "$PLATFORM" in - joomla) - if [ -n "$MANIFEST" ]; then - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/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 + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output - 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}${SUFFIX}.zip" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" @@ -278,54 +243,17 @@ jobs: - name: Update updates.xml if: steps.platform.outputs.platform == 'joomla' run: | - STABILITY="${{ steps.meta.outputs.stability }}" VERSION="${{ steps.meta.outputs.version }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - TAG="${{ steps.meta.outputs.tag }}" + STABILITY="${{ steps.meta.outputs.stability }}" if [ ! -f "updates.xml" ]; then echo "No updates.xml -- skipping" exit 0 fi - # Map stability to XML tag name - case "$STABILITY" in - 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}" - - # Use PHP to update the channel in updates.xml - php -r ' - $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>).)*?" . 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}", $block); - if (strpos($block, "") !== false) { - $block = preg_replace("/[^<]*<\/sha256>/", "{$sha256}", $block); - } else { - $block = str_replace("", "\n {$sha256}", $block); - } - $block = preg_replace("/(]*>)[^<]*(<\/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" + php ${MOKO_CLI}/updates_xml_build.php \ + --path . --version "${VERSION}" --stability "${STABILITY}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" # Commit and push if ! git diff --quiet updates.xml 2>/dev/null; then @@ -347,7 +275,7 @@ jobs: [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue echo "Syncing updates.xml -> ${BRANCH}" 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 if ! git diff --quiet updates.xml 2>/dev/null; then git add updates.xml From 9957fe56ca2701e0cb170322964c486e9328efcf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:24:08 +0000 Subject: [PATCH 13/14] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 380 ++++---------------------- 1 file changed, 51 insertions(+), 329 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index eca25d8900..d665ce7500 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -31,6 +31,15 @@ on: branches: - main workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -47,8 +56,8 @@ jobs: name: Promote Pre-Release to RC runs-on: release if: >- - github.event.action == 'opened' && - github.event.pull_request.draft == true + (github.event.action == 'opened' && github.event.pull_request.draft == true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') steps: - name: Checkout repository @@ -100,7 +109,8 @@ jobs: name: Build & Release Pipeline runs-on: release if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') steps: - name: Checkout repository @@ -170,18 +180,7 @@ jobs: echo "::notice::No RC release — full build pipeline" fi - - name: "Step 1b: Bump version" - id: bump - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - MOKO_API="/tmp/moko-platform-api/cli" - BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor) - VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) - [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Bumped to: ${VERSION}" + # Version bump handled by auto-bump.yml (minor on main, patch on dev) - name: Check if already released if: steps.version.outputs.skip != 'true' @@ -208,96 +207,9 @@ jobs: steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - ERRORS=0 - - 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>.*/\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/.*]*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 + VERSION="${{ steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/release_validate.php \ + --path . --version "$VERSION" --output-summary --github-output || true # -- STEP 2: Create or update version/XX.YY archive branch --------------- # Always runs — every version change on main archives to version/XX.YY @@ -306,7 +218,7 @@ jobs: run: | BRANCH="${{ steps.version.outputs.branch }}" IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH="${{ steps.version.outputs.version }}" PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') # Check if branch exists @@ -325,7 +237,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" php /tmp/moko-platform-api/cli/version_set_platform.php \ --path . --version "$VERSION" --branch main @@ -333,7 +245,7 @@ jobs: - name: "Step 4: Update version badges" if: steps.version.outputs.skip != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + 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/version_check.php --path . --fix 2>/dev/null || true @@ -342,7 +254,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.platform.outputs.platform == 'joomla' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" # Fetch latest updates.xml from main so preserve logic has all channels GA_TOKEN="${{ secrets.GA_TOKEN }}" @@ -366,7 +278,7 @@ jobs: echo "No changes to commit" exit 0 fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + 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 @@ -413,187 +325,34 @@ jobs: steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" 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}" + php /tmp/moko-platform-api/cli/release_create.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.GA_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) - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - EXT_NAME="${{ steps.updates.outputs.ext_name }}" - 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)" - # Strip existing type prefix to prevent duplication - EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') - 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" + # -- STEP 8: Build packages and upload to release ---------------------------- + - name: "Step 8: Build package and upload" if: >- steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - REPO="${{ github.repository }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # All ZIPs upload to the major release tag (vXX) - RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${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 '/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>.*/\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/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - # For packages, prefer over filename-derived element - if [ "$EXT_TYPE" = "package" ]; then - PKG_NAME=$(sed -n 's/.*\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME" - fi - # Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas) - EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') - 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) - - # -- Get existing assets for cleanup -------------------------------- - ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") - - # -- Create per-file .sha256 checksum files ------------------------- - echo "${SHA256_ZIP} ${ZIP_NAME}" > "/tmp/${ZIP_NAME}.sha256" - echo "${SHA256_TAR} ${TAR_NAME}" > "/tmp/${TAR_NAME}.sha256" - - # -- Upload packages + checksums to release tag -------------------- - for ASSET in "${ZIP_NAME}" "${TAR_NAME}" "${ZIP_NAME}.sha256" "${TAR_NAME}.sha256"; do - [ ! -f "/tmp/${ASSET}" ] && continue - # Delete existing asset with same name - ASSET_ID=$(echo "$ASSETS" | python3 -c " - import sys,json - assets = json.load(sys.stdin) - for a in assets: - if a['name'] == '${ASSET}': - print(a['id']); break - " 2>/dev/null || true) - [ -n "$ASSET_ID" ] && curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true - # Upload - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${ASSET}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ASSET}" > /dev/null 2>&1 || true - done - - # updates.xml already handled by Step 5 (updates_xml_build.php with preserve logic) - - 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 + php /tmp/moko-platform-api/cli/release_package.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true # -- STEP 8b: Update release description with changelog ---------------------- - name: "Step 8b: Update release body" if: steps.version.outputs.skip != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" MOKO_CLI="/tmp/moko-platform-api/cli" @@ -621,44 +380,19 @@ jobs: - name: "Step 9: Mirror release to GitHub" if: >- steps.version.outputs.skip != 'true' && - steps.version.outputs.stability == 'stable' && secrets.GH_TOKEN != '' continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" 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 }}" - - NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - echo "$NOTES" > /tmp/release_notes.md - - 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) - - 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 + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - name: "Step 10: Push main to GitHub mirror" @@ -682,7 +416,7 @@ jobs: - name: "Delete lesser pre-release channels" continue-on-error: true run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + 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 \ --stability stable \ @@ -711,32 +445,20 @@ jobs: # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Dolibarr: Reset dev version" - if: >- - steps.version.outputs.skip != 'true' && - steps.platform.outputs.platform == 'dolibarr' && - steps.platform.outputs.mod_file != '' + - 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}" - TOKEN="${{ secrets.GA_TOKEN }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") - FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) - 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) - if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then - UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") - ENCODED=$(echo "$UPDATED" | base64 -w0) - curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ - -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 + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true # -- Summary -------------------------------------------------------------- - name: Pipeline Summary if: always() run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" PLATFORM="${{ steps.platform.outputs.platform }}" if [ "${{ steps.version.outputs.skip }}" = "true" ]; then echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY From fce8389296890088b1d193a244ff80efb7ef3f02 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:25:24 +0000 Subject: [PATCH 14/14] chore(ci): update auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index ef0aedc5c8..d1dedf1a86 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -16,6 +16,7 @@ on: push: branches: - dev + - main env: GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} @@ -53,9 +54,20 @@ jobs: echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" fi - - name: Patch bump version + - name: Bump version run: | - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true + BRANCH="${{ github.ref_name }}" + + # main = minor bump, dev = patch bump + if [ "$BRANCH" = "main" ]; then + BUMP_TYPE="--minor" + BUMP_LABEL="minor" + else + BUMP_TYPE="" + BUMP_LABEL="patch" + fi + + BUMP=$(php ${MOKO_CLI}/version_bump.php --path . $BUMP_TYPE 2>&1) || true echo "$BUMP" VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true @@ -63,7 +75,7 @@ jobs: # Propagate to platform manifests php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch dev 2>/dev/null || true + --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true # Commit if anything changed @@ -76,7 +88,7 @@ jobs: 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 add -A - git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \ + git commit -m "chore(version): ${BUMP_LABEL} bump to ${VERSION} [skip ci]" \ --author="gitea-actions[bot] " - git push origin dev - echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY + git push origin "$BRANCH" + echo "Bumped to ${VERSION} (${BUMP_LABEL})" >> $GITHUB_STEP_SUMMARY