diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 10a7e519f4..fb9dc8271b 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -16,6 +16,9 @@ on: push: branches: - dev + - rc + - 'feature/**' + - 'patch/**' env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -37,7 +40,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 - name: Setup moko-platform tools @@ -49,7 +52,7 @@ jobs: 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" \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ /tmp/moko-platform-api cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" @@ -57,28 +60,7 @@ jobs: - name: Bump version run: | - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true - echo "$BUMP" - - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true - [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } - - # Propagate to platform manifests - 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 + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 8012b68ccb..1227ff8d83 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -51,58 +51,64 @@ permissions: contents: write jobs: - # ── Draft PR → Promote highest pre-release to RC ───────────────────────────── + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── promote-rc: - name: Promote Pre-Release to RC + name: Promote to RC runs-on: release if: >- - (github.event.action == 'opened' && github.event.pull_request.draft == true) || + (github.event.action == 'opened' && github.event.pull_request.merged != true) || (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 - name: Setup moko-platform tools env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ /tmp/moko-platform-api cd /tmp/moko-platform-api composer install --no-dev --no-interaction --quiet - - name: Promote to release-candidate + - name: Rename branch to rc 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 || 'dev' }}" + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" - - name: Cascade lesser channels - continue-on-error: true + - name: Checkout rc and configure git 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}" + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" - name: Summary if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: @@ -116,19 +122,27 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 0 + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + - name: Setup moko-platform tools env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' run: | # Ensure PHP + Composer are available if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ /tmp/moko-platform-api @@ -136,264 +150,17 @@ jobs: composer install --no-dev --no-interaction --quiet - # -- PLATFORM DETECTION --------------------------------------------------- - - name: Detect platform - id: platform + - name: "Publish stable release" run: | - php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" - - - name: "Step 1: Read version" - id: version - run: | - VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) - if [ -z "$VERSION" ]; then - echo "::error::No VERSION in README.md" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - MAJOR=$(echo "$VERSION" | cut -d. -f1) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$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: Minor bump version" - id: bump - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - MOKO_API="/tmp/moko-platform-api/cli" - php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true - VERSION=$(php ${MOKO_API}/version_read.php --path .) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Bumped to: ${VERSION}" - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - # Tag and branch may persist across patch releases — never skip - echo "already_released=false" >> "$GITHUB_OUTPUT" - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.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 - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: steps.version.outputs.skip != 'true' - 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 - - # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum - - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - # Set push URL with token for branch-protected repos - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push -u origin HEAD - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - 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 - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- - - name: "Step 7a: Promote RC to stable" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote == 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from release-candidate --to stable \ - --token "${{ secrets.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 }}" - 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 - - # -- STEP 8: Build packages and upload to release ---------------------------- - - name: "Step 8: Build package and upload" - id: package - 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 }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_package.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- - - name: "Step 5: Write update stream" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - # Fetch latest updates.xml from main so preserve logic has all channels - 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 - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php /tmp/moko-platform-api/cli/updates_xml_build.php \ - --path . --version "${VERSION}" --stability stable \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} --github-output - - # Commit updates.xml if changed - if ! git diff --quiet updates.xml 2>/dev/null; then - git 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 updates.xml - git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push origin HEAD 2>&1 || true - fi - - # -- STEP 8b: Update release description with changelog ---------------------- - - name: "Step 8b: Update release body" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - php /tmp/moko-platform-api/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>&1 || true - echo "Release body updated" >> $GITHUB_STEP_SUMMARY + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" if: >- steps.version.outputs.skip != 'true' && - secrets.GH_TOKEN != '' + secrets.GH_MIRROR_TOKEN != '' continue-on-error: true run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" @@ -402,8 +169,8 @@ jobs: 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" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ --branch main 2>&1 || true echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY @@ -411,41 +178,30 @@ jobs: - name: "Step 10: Push main to GitHub mirror" if: >- steps.version.outputs.skip != 'true' && - secrets.GH_TOKEN != '' + secrets.GH_MIRROR_TOKEN != '' continue-on-error: true run: | GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" git fetch origin main --depth=1 git push github origin/main:refs/heads/main --force 2>/dev/null \ && echo "main branch pushed to GitHub mirror" \ || echo "WARNING: GitHub mirror push failed" - git push github origin/main:refs/heads/version --force 2>/dev/null \ - && echo "version branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror version push failed" - # -- Clean up lesser pre-releases (cascade) --------------------------------- - # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - - 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 }}" \ - --api-base "${API_BASE}" 2>/dev/null || true - - - name: "Step 11: Delete and recreate dev branch from main" + - name: "Step 11: Delete rc branch and recreate dev from main" if: steps.version.outputs.skip != 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" # Delete dev branch curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ @@ -457,28 +213,27 @@ jobs: "${API_BASE}/branches" \ -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - name: "Step 12: Update version branch from main" + - name: "Step 12: Create version branch from main" if: steps.version.outputs.skip != 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" MAIN_SHA=$(git rev-parse HEAD) - # Try delete + recreate (handles protected branch edge cases) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/version" 2>/dev/null && echo "Deleted old version branch" + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"version","old_branch_name":"main"}' 2>/dev/null \ - && echo "Created version branch from main (${MAIN_SHA})" \ - || echo "WARNING: version branch update failed" - echo "Version branch updated to main (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY # -- Dolibarr post-release: Reset dev version ----------------------------- - name: "Post-release: Reset dev version" @@ -487,7 +242,7 @@ jobs: run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --branch dev --path . 2>&1 || true # -- Summary -------------------------------------------------------------- diff --git a/.mokogitea/workflows/deploy-mokogitea.yml b/.mokogitea/workflows/deploy-mokogitea.yml index 0754e78ebe..84e4583eee 100644 --- a/.mokogitea/workflows/deploy-mokogitea.yml +++ b/.mokogitea/workflows/deploy-mokogitea.yml @@ -143,7 +143,7 @@ jobs: - name: Update updates.xml if: success() env: - GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} TAG: ${{ steps.config.outputs.tag }} INSTANCE_URL: ${{ steps.config.outputs.instance_url }} DEPLOY_ENV: ${{ github.event.inputs.environment }} diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c2b02a6f91..f084fe1b0a 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Create branch and comment run: | - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_TITLE="${{ github.event.issue.title }}" diff --git a/.mokogitea/workflows/pr-branch-check.yml b/.mokogitea/workflows/pr-branch-check.yml index b8d9742bbb..debdd13b30 100644 --- a/.mokogitea/workflows/pr-branch-check.yml +++ b/.mokogitea/workflows/pr-branch-check.yml @@ -1,90 +1,90 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# Enforces branch merge policy: -# feature/* → dev only -# fix/* → dev only -# hotfix/* → dev or main (emergency) -# dev → main only -# alpha/* → dev only -# beta/* → dev only -# rc/* → main only - -name: Branch Policy Check - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -jobs: - check-target: - name: Verify merge target - runs-on: ubuntu-latest - steps: - - name: Check branch policy - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - alpha/*|beta/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Pre-release branches must target 'dev', not '${BASE}'" - fi - ;; - rc/*) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Release candidate branches must target 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Enforces branch merge policy: +# feature/* → dev only +# fix/* → dev only +# hotfix/* → dev or main (emergency) +# dev → main only +# alpha/* → dev only +# beta/* → dev only +# rc/* → main only + +name: Branch Policy Check + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + check-target: + name: Verify merge target + runs-on: ubuntu-latest + steps: + - name: Check branch policy + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/pr-rc-release.yml b/.mokogitea/workflows/pr-rc-release.yml index 300c4d42be..596eeb39fd 100644 --- a/.mokogitea/workflows/pr-rc-release.yml +++ b/.mokogitea/workflows/pr-rc-release.yml @@ -108,7 +108,7 @@ jobs: - name: Create RC release if: steps.guard.outputs.skip != 'true' env: - GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} RC_TAG: ${{ steps.version.outputs.tag }} RC_VERSION: ${{ steps.version.outputs.version }} PR_TITLE: ${{ github.event.pull_request.title }} @@ -155,7 +155,7 @@ jobs: - name: Commit updates.xml if: steps.guard.outputs.skip != 'true' env: - GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} HEAD_REF: ${{ github.event.pull_request.head.ref }} PR_NUM: ${{ github.event.pull_request.number }} run: | diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index f0b1d883c1..162b08fab3 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,223 +1,233 @@ -# 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: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} - - - 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 - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - STABILITY="${{ inputs.stability || 'development' }}" - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Read current version (bump already handled by push workflow) - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) - [ -z "$VERSION" ] && VERSION="00.00.01" - - 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]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - name: Update updates.xml - if: steps.platform.outputs.platform == 'joomla' - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml -- skipping" - exit 0 - fi - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php ${MOKO_CLI}/updates_xml_build.php \ - --path . --version "${VERSION}" --stability "${STABILITY}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} - - # Commit and push - if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push origin HEAD 2>&1 || echo "WARNING: push failed" - fi - - - name: "Sync updates.xml to all branches" - if: steps.platform.outputs.platform == 'joomla' - run: | - CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - echo "Syncing updates.xml -> ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +# 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: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + STABILITY="${{ inputs.stability || 'development' }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read current version (bump already handled by push workflow) + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) + [ -z "$VERSION" ] && VERSION="00.00.01" + + # Strip any existing suffix from version before applying stability + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + + # Verify version consistency across all files + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Update VERSION variable with suffix + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml -- skipping" + exit 0 + fi + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php ${MOKO_CLI}/updates_xml_build.php \ + --path . --version "${VERSION}" --stability "${STABILITY}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} + + # Commit and push + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + echo "Syncing updates.xml -> ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index 8660a43134..339d3f5c6f 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -1,660 +1,312 @@ -# 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 (ZIP + tar.gz) -------------------- - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ -d "$SOURCE_DIR" ]; then - EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" - TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" - - cd "$SOURCE_DIR" - zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES - cd .. - tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ - --exclude='.ftpignore' --exclude='sftp-config*' \ - --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . - - SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) - - # Ensure release exists on Gitea - 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 - # Create release - RELEASE_JSON=$(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_TAG} (${DISPLAY_VERSION})', - 'body': '${STABILITY} release', - 'prerelease': True, - 'target_commitish': 'main' - }))")" 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) - fi - - if [ -n "$RELEASE_ID" ]; then - # Delete existing assets with same name before uploading - ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") - for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do - ASSET_ID=$(echo "$ASSETS" | python3 -c " - import sys,json - assets = json.load(sys.stdin) - for a in assets: - if a['name'] == '${ASSET_FILE}': - print(a['id']); break - " 2>/dev/null || true) - if [ -n "$ASSET_ID" ]; then - curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true - fi - done - - # Upload both formats - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${PACKAGE_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_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 - fi - - echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY - else - SHA256="" - fi - - # -- 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 +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/update-server.yml +# VERSION: 05.00.00 +# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches +# +# Thin wrapper around moko-platform CLI tools. +# Builds packages, updates updates.xml, and optionally deploys via SFTP. +# +# Joomla filters update entries by the user's "Minimum Stability" setting. + +name: "Update Server" + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update Server + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform 2>/dev/null || true + if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then + cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV" + + - name: Detect platform + id: platform + run: php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve stability and bump version + id: meta + run: | + BRANCH="${{ github.ref_name }}" + + # Configure git for bot pushes + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + # Auto-bump patch version + php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true + + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Strip any existing suffix before applying stability + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + # Determine stability from branch or manual input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + else + STABILITY="development" + fi + + # Version suffix per stability stream + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + rc) SUFFIX="-rc"; TAG="release-candidate" ;; + *) SUFFIX=""; TAG="stable" ;; + esac + + # Propagate version with stability suffix to all manifest files + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Re-read version (now includes suffix from version_set_platform) + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT" + + # Commit version bump if changed + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + - name: Create release and upload package + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Create or update Gitea release + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease + + # Build package and upload + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php ${MOKO_CLI}/updates_xml_build.php \ + --path . --version "${VERSION}" --stability "${STABILITY}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} + + # Commit and push updates.xml + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push + } + + - name: Sync updates.xml to main + if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + python3 -c " + import base64, json, urllib.request, sys + with open('updates.xml', 'rb') as f: + content = base64.b64encode(f.read()).decode() + payload = json.dumps({ + 'content': content, + 'sha': '${FILE_SHA}', + 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', + 'branch': 'main' + }).encode() + req = urllib.request.Request( + '${API_BASE}/contents/updates.xml', + data=payload, method='PUT', + headers={ + 'Authorization': 'token ${GITEA_TOKEN}', + 'Content-Type': 'application/json' + }) + try: + urllib.request.urlopen(req) + print('updates.xml synced to main') + except Exception as e: + print(f'WARNING: sync to main failed: {e}', file=sys.stderr) + " + fi + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # Permission check: admin or maintain role required + ACTOR="${{ github.actor }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then + php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then + php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + DISPLAY="${{ steps.meta.outputs.display_version }}" + echo "## Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/upstream-bug-sync.yml b/.mokogitea/workflows/upstream-bug-sync.yml index 8fbf590806..df21bdaa38 100644 --- a/.mokogitea/workflows/upstream-bug-sync.yml +++ b/.mokogitea/workflows/upstream-bug-sync.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Sync upstream bugs env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} + GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} MOKOGITEA_URL: https://git.mokoconsulting.tech MOKOGITEA_REPO: MokoConsulting/MokoGitea diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index b96da67b35..1edb05205e 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -48,6 +48,9 @@ type ProtectedBranch struct { ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` + WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` StatusCheckContexts []string `xorm:"JSON TEXT"` EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` @@ -124,6 +127,11 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use return false } + // Allow the actions bot user if explicitly whitelisted. + if user.IsActions() && protectBranch.WhitelistActionsUser { + return true + } + if !protectBranch.EnableWhitelist { if err := protectBranch.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) @@ -161,6 +169,11 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user return false } + // Allow the actions bot user if explicitly whitelisted. + if user.IsActions() && protectBranch.ForcePushAllowlistActionsUser { + return protectBranch.CanUserPush(ctx, user) + } + if !protectBranch.EnableForcePushAllowlist { return protectBranch.CanUserPush(ctx, user) } @@ -183,6 +196,11 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user // IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool { + // Allow the actions bot user if explicitly whitelisted. + if userID == user_model.ActionsUserID && protectBranch.MergeWhitelistActionsUser { + return true + } + if !protectBranch.EnableMergeWhitelist { // Then we need to fall back on whether the user has write permission return permissionInRepo.CanWrite(unit.TypeCode) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e362332ca5..bb5774a26f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -411,6 +411,7 @@ func prepareMigrationTasks() []*migration { newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel), newMigration(332, "Add org-level branch protection rulesets", v1_27.AddOrgProtectedBranchTable), newMigration(333, "Add require_2fa to user table for org enforcement", v1_27.AddRequire2FAToUser), + newMigration(334, "Add actions user whitelist to protected branches", v1_27.AddActionsUserWhitelistToProtectedBranch), } return preparedMigrations } diff --git a/models/migrations/v1_27/v334.go b/models/migrations/v1_27/v334.go new file mode 100644 index 0000000000..aeb8ed8efa --- /dev/null +++ b/models/migrations/v1_27/v334.go @@ -0,0 +1,17 @@ +// Copyright 2026 The MokoGitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import "xorm.io/xorm" + +// AddActionsUserWhitelistToProtectedBranch adds toggle fields that allow +// the built-in actions bot user to bypass branch protection rules. +func AddActionsUserWhitelistToProtectedBranch(x *xorm.Engine) error { + type ProtectedBranch struct { + WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + } + return x.Sync(new(ProtectedBranch)) +} diff --git a/models/user/avatar.go b/models/user/avatar.go index 291d1467bb..c691357509 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -54,9 +54,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { - // ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user + // ghost user was deleted, actions bot is a system user, 0 means the user should be a virtual user // which comes from git configure information - if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 { + if u.IsGhost() || u.IsActions() || u.ID <= 0 { return avatars.DefaultAvatarLink() } diff --git a/models/user/user.go b/models/user/user.go index f7dcc407e8..435b7af250 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -510,9 +510,9 @@ func (u *User) GitName() string { } // IsMailable checks if a user is eligible to receive emails. -// System users like Ghost and Gitea Actions are excluded. +// System users like Ghost and the actions bot are excluded. func (u *User) IsMailable() bool { - return u.IsActive && !u.IsGiteaActions() && !u.IsGhost() + return u.IsActive && !u.IsActions() && !u.IsGhost() } // IsUserExist checks if given username exist, @@ -627,8 +627,10 @@ var ( "swagger.v1.json", "openapi3.v1.json", - "ghost", // reserved name for deleted users (id: -1) - "gitea-actions", // gitea builtin user (id: -2) + "ghost", // reserved name for deleted users (id: -1) + "mokogitea-actions", // actions bot user (id: -2) + "gitea-actions", // legacy actions bot name + "github-actions", // legacy actions bot name } // These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc. diff --git a/models/user/user_system.go b/models/user/user_system.go index 5671e45487..09703acdab 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -34,8 +34,12 @@ func (u *User) IsGhost() bool { const ( ActionsUserID int64 = -2 - ActionsUserName = "gitea-actions" - ActionsUserEmail = "teabot@gitea.io" + ActionsUserName = "mokogitea-actions" + ActionsUserEmail = "mokogitea-actions[bot]@mokoconsulting.tech" + + // Legacy names recognized as aliases for the actions bot user. + ActionsUserNameLegacyGitea = "gitea-actions" + ActionsUserNameLegacyGitHub = "github-actions" ) // NewActionsUser creates and returns a fake user for running the actions. @@ -45,7 +49,7 @@ func NewActionsUser() *User { Name: ActionsUserName, LowerName: ActionsUserName, IsActive: true, - FullName: "Gitea Actions", + FullName: "MokoGitea Actions", Email: ActionsUserEmail, KeepEmailPrivate: true, LoginName: ActionsUserName, @@ -75,15 +79,30 @@ func GetActionsUserTaskID(u *User) (int64, bool) { return 0, false } -func (u *User) IsGiteaActions() bool { +// IsActions checks whether this user is the built-in actions bot. +func (u *User) IsActions() bool { return u != nil && u.ID == ActionsUserID } +// IsGiteaActions is a deprecated alias for IsActions. +func (u *User) IsGiteaActions() bool { + return u.IsActions() +} + +// isActionsName returns true if the given name (case-insensitive, with +// optional "[bot]" suffix stripped) matches any known actions bot name. +func isActionsName(name string) bool { + clean := strings.TrimSuffix(name, "[bot]") + return strings.EqualFold(clean, ActionsUserName) || + strings.EqualFold(clean, ActionsUserNameLegacyGitea) || + strings.EqualFold(clean, ActionsUserNameLegacyGitHub) +} + func GetSystemUserByName(name string) *User { if strings.EqualFold(name, GhostUserName) { return NewGhostUser() } - if strings.EqualFold(name, ActionsUserName) { + if isActionsName(name) { return NewActionsUser() } return nil diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go index 3ae9c6e366..ae3eec81fd 100644 --- a/models/user/user_system_test.go +++ b/models/user/user_system_test.go @@ -25,13 +25,39 @@ func TestSystemUser(t *testing.T) { uid, u, err = GetPossibleUserByID(t.Context(), -2) require.NoError(t, err) assert.Equal(t, int64(-2), uid) - assert.Equal(t, "gitea-actions", u.Name) - assert.Equal(t, "gitea-actions", u.LowerName) - assert.True(t, u.IsGiteaActions()) + assert.Equal(t, "mokogitea-actions", u.Name) + assert.Equal(t, "mokogitea-actions", u.LowerName) + assert.True(t, u.IsActions()) + assert.True(t, u.IsGiteaActions()) // deprecated alias + // canonical name lookup + u = GetSystemUserByName("mokogitea-actions") + require.NotNil(t, u) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + // legacy name lookups u = GetSystemUserByName("Gitea-actionS") require.NotNil(t, u) - assert.Equal(t, "Gitea Actions", u.FullName) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + u = GetSystemUserByName("github-actions") + require.NotNil(t, u) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + // [bot] suffix lookups + u = GetSystemUserByName("mokogitea-actions[bot]") + require.NotNil(t, u) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + u = GetSystemUserByName("gitea-actions[bot]") + require.NotNil(t, u) + + u = GetSystemUserByName("github-actions[bot]") + require.NotNil(t, u) + + // unknown name returns nil + u = GetSystemUserByName("unknown-bot") + assert.Nil(t, u) uid, u, err = GetPossibleUserByID(t.Context(), 999999) require.NoError(t, err) diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index dee9490998..77b82a11a4 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -42,14 +42,17 @@ type BranchProtection struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"` EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"` + ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` @@ -84,14 +87,17 @@ type CreateBranchProtectionOption struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"` EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"` + ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` @@ -117,14 +123,17 @@ type EditBranchProtectionOption struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"` + PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"` EnableForcePush *bool `json:"enable_force_push"` EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"` ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` ForcePushAllowlistDeployKeys *bool `json:"force_push_allowlist_deploy_keys"` + ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"` EnableMergeWhitelist *bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"` EnableStatusCheck *bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals *int64 `json:"required_approvals"` diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 5c780182f0..cf91027ca8 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2404,15 +2404,18 @@ "repo.settings.protect_whitelist_committers": "Allowlist Restricted Push", "repo.settings.protect_whitelist_committers_desc": "Only allowlisted users or teams will be allowed to push to this branch (but not force push).", "repo.settings.protect_whitelist_deploy_keys": "Allowlist deploy keys with write access to push.", + "repo.settings.protect_whitelist_actions_user": "Allowlist actions bot user to push.", "repo.settings.protect_whitelist_users": "Allowlisted users for pushing:", "repo.settings.protect_whitelist_teams": "Allowlisted teams for pushing:", "repo.settings.protect_force_push_allowlist_users": "Allowlisted users for force pushing:", "repo.settings.protect_force_push_allowlist_teams": "Allowlisted teams for force pushing:", "repo.settings.protect_force_push_allowlist_deploy_keys": "Allowlist deploy keys with push access to force push.", + "repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.", "repo.settings.protect_merge_whitelist_committers": "Enable Merge Allowlist", "repo.settings.protect_merge_whitelist_committers_desc": "Allow only allowlisted users or teams to merge pull requests into this branch.", "repo.settings.protect_merge_whitelist_users": "Allowlisted users for merging:", "repo.settings.protect_merge_whitelist_teams": "Allowlisted teams for merging:", + "repo.settings.protect_merge_whitelist_actions_user": "Allowlist actions bot user to merge.", "repo.settings.protect_check_status_contexts": "Enable Status Check", "repo.settings.protect_status_check_patterns": "Status check patterns:", "repo.settings.protect_status_check_patterns_desc": "Enter patterns to specify which status checks must pass before branches can be merged into a branch that matches this rule. Each line specifies a pattern. Patterns cannot be empty.", diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 4bbc5fc411..5aa5f5813a 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -758,10 +758,13 @@ func CreateBranchProtection(ctx *context.APIContext) { CanPush: form.EnablePush, EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, + WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser, CanForcePush: form.EnablePush && form.EnableForcePush, EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist, ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys, + ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser, EnableMergeWhitelist: form.EnableMergeWhitelist, + MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser, EnableStatusCheck: form.EnableStatusCheck, StatusCheckContexts: form.StatusCheckContexts, EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, @@ -861,17 +864,22 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.CanPush = false protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false } else { protectBranch.CanPush = true if form.EnablePushWhitelist != nil { if !*form.EnablePushWhitelist { protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false } else { protectBranch.EnableWhitelist = true if form.PushWhitelistDeployKeys != nil { protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys } + if form.PushWhitelistActionsUser != nil { + protectBranch.WhitelistActionsUser = *form.PushWhitelistActionsUser + } } } } @@ -882,17 +890,22 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.CanForcePush = false protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false } else { protectBranch.CanForcePush = true if form.EnableForcePushAllowlist != nil { if !*form.EnableForcePushAllowlist { protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false } else { protectBranch.EnableForcePushAllowlist = true if form.ForcePushAllowlistDeployKeys != nil { protectBranch.ForcePushAllowlistDeployKeys = *form.ForcePushAllowlistDeployKeys } + if form.ForcePushAllowlistActionsUser != nil { + protectBranch.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser + } } } } @@ -904,6 +917,12 @@ func EditBranchProtection(ctx *context.APIContext) { if form.EnableMergeWhitelist != nil { protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist + if !*form.EnableMergeWhitelist { + protectBranch.MergeWhitelistActionsUser = false + } + } + if form.MergeWhitelistActionsUser != nil && protectBranch.EnableMergeWhitelist { + protectBranch.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser } if form.EnableStatusCheck != nil { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index c73ecc0814..2e2c3f3b01 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -162,7 +162,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { return nil } - if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() { + if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsActions() { _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID) if err == nil { // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index d983a08c19..14f37a9bad 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -168,10 +168,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanPush = true protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false case "whitelist": protectBranch.CanPush = true protectBranch.EnableWhitelist = true protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys + protectBranch.WhitelistActionsUser = f.WhitelistActionsUser if strings.TrimSpace(f.WhitelistUsers) != "" { whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) } @@ -182,6 +184,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanPush = false protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false } switch f.EnableForcePush { @@ -189,10 +192,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanForcePush = true protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false case "whitelist": protectBranch.CanForcePush = true protectBranch.EnableForcePushAllowlist = true protectBranch.ForcePushAllowlistDeployKeys = f.ForcePushAllowlistDeployKeys + protectBranch.ForcePushAllowlistActionsUser = f.ForcePushAllowlistActionsUser if strings.TrimSpace(f.ForcePushAllowlistUsers) != "" { forcePushAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.ForcePushAllowlistUsers, ",")) } @@ -203,9 +208,11 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanForcePush = false protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false } protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist + protectBranch.MergeWhitelistActionsUser = f.EnableMergeWhitelist && f.MergeWhitelistActionsUser if f.EnableMergeWhitelist { if strings.TrimSpace(f.MergeWhitelistUsers) != "" { mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index f528ca36d0..1a055ced6e 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -118,7 +118,7 @@ func (input *notifyInput) Notify(ctx context.Context) { func notify(ctx context.Context, input *notifyInput) error { shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch - if input.Doer.IsGiteaActions() { + if input.Doer.IsActions() { // avoiding triggering cyclically, for example: // a comment of an issue will trigger the runner to add a new comment as reply, // and the new comment will trigger the runner again. diff --git a/services/convert/convert.go b/services/convert/convert.go index d543c1a731..2bc181de9a 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -173,14 +173,17 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo PushWhitelistUsernames: pushWhitelistUsernames, PushWhitelistTeams: pushWhitelistTeams, PushWhitelistDeployKeys: bp.WhitelistDeployKeys, + PushWhitelistActionsUser: bp.WhitelistActionsUser, EnableForcePush: bp.CanForcePush, EnableForcePushAllowlist: bp.EnableForcePushAllowlist, ForcePushAllowlistUsernames: forcePushAllowlistUsernames, ForcePushAllowlistTeams: forcePushAllowlistTeams, ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys, + ForcePushAllowlistActionsUser: bp.ForcePushAllowlistActionsUser, EnableMergeWhitelist: bp.EnableMergeWhitelist, MergeWhitelistUsernames: mergeWhitelistUsernames, MergeWhitelistTeams: mergeWhitelistTeams, + MergeWhitelistActionsUser: bp.MergeWhitelistActionsUser, EnableStatusCheck: bp.EnableStatusCheck, StatusCheckContexts: bp.StatusCheckContexts, RequiredApprovals: bp.RequiredApprovals, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 5899af498b..20aff24682 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -172,13 +172,16 @@ type ProtectBranchForm struct { WhitelistUsers string WhitelistTeams string WhitelistDeployKeys bool + WhitelistActionsUser bool EnableForcePush string ForcePushAllowlistUsers string ForcePushAllowlistTeams string ForcePushAllowlistDeployKeys bool + ForcePushAllowlistActionsUser bool EnableMergeWhitelist bool MergeWhitelistUsers string MergeWhitelistTeams string + MergeWhitelistActionsUser bool EnableStatusCheck bool StatusCheckContexts string RequiredApprovals int64 diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index fc26c3b517..01803890c3 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -88,6 +88,12 @@ +
+
+ + +
+
@@ -158,6 +164,12 @@
+
+
+ + +
+
{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}
@@ -303,6 +315,12 @@ {{end}} +
+
+ + +
+
diff --git a/tests/integration/actions_job_token_test.go b/tests/integration/actions_job_token_test.go index 66bfec5bf4..1071eaa426 100644 --- a/tests/integration/actions_job_token_test.go +++ b/tests/integration/actions_job_token_test.go @@ -149,7 +149,7 @@ func TestActionsJobTokenPermissiveAccess(t *testing.T) { require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request")) testURL := *u - testURL.User = url.UserPassword("gitea-actions", task.Token) + testURL.User = url.UserPassword("mokogitea-actions", task.Token) t.Run("ReadGitContent", func(t *testing.T) { testURL.Path = "/user5/repo4.git/HEAD" @@ -443,7 +443,7 @@ jobs: // but it should not have write access req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}). SetHeader("Accept", lfs.MediaType). - AddBasicAuth("gitea-actions", task1Token) + AddBasicAuth("mokogitea-actions", task1Token) MakeRequest(t, req, http.StatusUnauthorized) // set repo1&repo2 max permission to "write" so that the actions token can access code @@ -465,11 +465,11 @@ jobs: // now task1 has write access to repo1, but still only read access to repo2 (different repo) req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}). SetHeader("Accept", lfs.MediaType). - AddBasicAuth("gitea-actions", task1Token) + AddBasicAuth("mokogitea-actions", task1Token) MakeRequest(t, req, http.StatusOK) req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}). SetHeader("Accept", lfs.MediaType). - AddBasicAuth("gitea-actions", task1Token) + AddBasicAuth("mokogitea-actions", task1Token) MakeRequest(t, req, http.StatusUnauthorized) }) } diff --git a/tests/integration/actions_settings_test.go b/tests/integration/actions_settings_test.go index dea36a3831..2c8429bc1e 100644 --- a/tests/integration/actions_settings_test.go +++ b/tests/integration/actions_settings_test.go @@ -57,7 +57,7 @@ jobs: // prepare for clone dstPath := t.TempDir() u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow") - u.User = url.UserPassword("gitea-actions", taskToken) + u.User = url.UserPassword("mokogitea-actions", taskToken) // the git clone will fail doGitCloneFail(u)(t) diff --git a/tests/integration/api_actions_permission_test.go b/tests/integration/api_actions_permission_test.go index 27ced97bd0..37d3ccb13e 100644 --- a/tests/integration/api_actions_permission_test.go +++ b/tests/integration/api_actions_permission_test.go @@ -21,7 +21,7 @@ func testActionUserSignIn(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) u := DecodeJSON(t, resp, &api.User{}) - assert.Equal(t, "gitea-actions", u.UserName) + assert.Equal(t, "mokogitea-actions", u.UserName) } func testActionUserAccessPublicRepo(t *testing.T) {