From 108b19dcc8f492e615775a3865620550b7a491ec Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 11:12:11 +0000 Subject: [PATCH 01/26] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 21676fabcf..c834bf5f8b 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -47,15 +47,15 @@ jobs: fi ;; fix/*|bugfix/*) - if [ "$BASE" != "main" ] && [ "$BASE" != "dev" ]; then + if [ "$BASE" != "dev" ]; then ALLOWED=false - REASON="Fix branches must target 'main' or 'dev', not '${BASE}'" + REASON="Fix branches must target 'dev', not '${BASE}'" fi ;; patch/*) - if [ "$BASE" != "main" ] && [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then ALLOWED=false - REASON="Patch branches must target 'main', 'dev', or 'rc', not '${BASE}'" + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" fi ;; hotfix/*) @@ -86,11 +86,10 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`main\` or \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`patch/*\` → \`main\`, \`dev\`, or \`rc\`" >> $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 + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY exit 1 fi @@ -147,21 +146,12 @@ jobs: - name: Detect platform id: platform - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - REPO: ${{ github.repository }} run: | - # Query metadata API for platform (manifest.xml is deprecated) - PLATFORM="" - if [ -n "$MOKOGITEA_TOKEN" ]; then - PLATFORM=$(curl -sf -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - "${MOKOGITEA_URL}/api/v1/repos/${REPO}/metadata" 2>/dev/null \ - | sed -n 's/.*"platform"\s*:\s*"\([^"]*\)".*/\1/p' | head -1) || true - fi + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') [ -z "$PLATFORM" ] && PLATFORM="generic" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Detected platform: $PLATFORM" - name: Setup PHP if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' -- 2.52.0 From 11b2195bdf37d89882f50b8889e49445de2031ec Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 28 Jun 2026 20:07:10 +0000 Subject: [PATCH 02/26] chore: sync auto-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/auto-release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 5865324d5a..4489ae06e0 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -7,7 +7,7 @@ # INGROUP: mokocli.Release # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 +# VERSION: 05.01.00 # BRIEF: Universal build & release � detects platform from manifest.xml # # +=======================================================================+ @@ -75,6 +75,7 @@ jobs: with: token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 + submodules: recursive - name: Setup mokocli tools env: @@ -173,6 +174,7 @@ jobs: with: token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 0 + submodules: recursive - name: Configure git for bot pushes run: | -- 2.52.0 From 84c6a94333d5d42b352f1678e1dcc68ceab8f555 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 30 Jun 2026 18:07:04 +0000 Subject: [PATCH 03/26] fix: remove orphaned deploy-manual workflow [skip ci] --- .mokogitea/workflows/deploy-manual.yml | 126 ------------------------- 1 file changed, 126 deletions(-) delete mode 100644 .mokogitea/workflows/deploy-manual.yml diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml deleted file mode 100644 index 1af323c4ea..0000000000 --- a/.mokogitea/workflows/deploy-manual.yml +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API -# PATH: /templates/workflows/joomla/deploy-manual.yml.template -# VERSION: 04.07.00 -# BRIEF: Manual SFTP deploy to dev server for Joomla repos - -name: "Universal: Deploy to Dev (Manual)" - -on: - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all remote files before uploading' - required: false - default: 'false' - type: boolean - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: read - -jobs: - deploy: - name: SFTP Deploy to Dev - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - run: | - php -v && composer --version - - - name: Setup MokoStandards tools - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api 2>/dev/null || true - if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then - cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Check FTP configuration - id: check - env: - HOST: ${{ vars.DEV_FTP_HOST }} - PATH_VAR: ${{ vars.DEV_FTP_PATH }} - PORT: ${{ vars.DEV_FTP_PORT }} - run: | - if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then - echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "host=$HOST" >> "$GITHUB_OUTPUT" - - REMOTE="${PATH_VAR%/}" - echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" - - [ -z "$PORT" ] && PORT="22" - echo "port=$PORT" >> "$GITHUB_OUTPUT" - - - name: Deploy via SFTP - if: steps.check.outputs.skip != 'true' - env: - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; } - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ - > /tmp/sftp-config.json - - if [ -n "$SFTP_KEY" ]; then - echo "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json - fi - - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) - - PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" - else - php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - fi - - rm -f /tmp/deploy_key /tmp/sftp-config.json - - - name: Summary - if: always() - run: | - if [ "${{ steps.check.outputs.skip }}" = "true" ]; then - echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY - else - echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY - fi -- 2.52.0 From 982e45a56edf898780905f7db6eb47c2ddd33119 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 01:20:29 -0500 Subject: [PATCH 04/26] fix: support radio inputs in admin system config form The system config form JS (config.ts) only mapped checkbox, text, textarea, and datetime-local elements. The fork landing_page.tmpl uses radio inputs for the Mode field, so fillFromSystemConfig() hit unsupportedElement() and threw, aborting all JS init on the admin settings page. Add radio handling in both directions: fill checks the option whose value matches the config value; collect returns the checked option's value and skips/nulls unchecked radios so a group resolves to exactly one value. Adds a radio-group test case. --- CHANGELOG.md | 1 + web_src/js/features/admin/config.test.ts | 7 +++++++ web_src/js/features/admin/config.ts | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aea9e753f..3e69540f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185) ### Fixed +- Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page - PR check: platform detection now queries metadata API instead of removed manifest.xml - Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783) - Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622) diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts index b8468610ca..b7763ee6b7 100644 --- a/web_src/js/features/admin/config.test.ts +++ b/web_src/js/features/admin/config.test.ts @@ -21,6 +21,12 @@ test('ConfigFormValueMapper', () => { + + + + + + `; @@ -44,5 +50,6 @@ test('ConfigFormValueMapper', () => { 'k-flipped-true': 'true', 'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend 'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}', + 'landing': '{"Mode":"explore"}', }); }); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 96c7dcf84b..02bced6fa2 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -99,6 +99,9 @@ export class ConfigFormValueMapper { if (el.matches('[type="checkbox"]')) { if (valType !== 'boolean') requireExplicitValueType(el); el.checked = Boolean(val ?? el.checked); + } else if (el.matches('[type="radio"]')) { + // a radio group shares one name; check only the option whose value equals the config value + el.checked = el.value === String(val); } else if (el.matches('[type="datetime-local"]')) { if (valType !== 'timestamp') requireExplicitValueType(el); if (val) el.value = toDatetimeLocalValue(val); @@ -120,6 +123,9 @@ export class ConfigFormValueMapper { // it needs to iterate the "namedElems" to find all the checkboxes with the same name and collect values accordingly, // and set the namedElems[matchedIdx] to null to avoid duplicate processing. val = collectCheckboxBooleanValue(el); + } else if (el.matches('[type="radio"]')) { + // only the checked radio of a group reaches here (callers skip unchecked ones); its value is the selection + val = el.value; } else if (el.matches('[type="datetime-local"]')) { if (valType !== 'timestamp') requireExplicitValueType(el); val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null. @@ -139,6 +145,12 @@ export class ConfigFormValueMapper { if (!el) continue; const subKey = extractElemConfigSubKey(el, dynKey); if (!subKey) continue; // if not match, skip + // a radio group has N elements sharing the same name; only the checked one carries the value. + // drop the unchecked ones so they neither overwrite the selection here nor leak into the fallback loop. + if (el.matches('[type="radio"]') && !el.checked) { + namedElems[idx] = null; + continue; + } cfgVal[subKey] = this.collectConfigValueFromElement(el); namedElems[idx] = null; } @@ -194,6 +206,7 @@ export class ConfigFormValueMapper { // "foo.enabled" => "true" for (const el of namedElems) { if (!el) continue; + if (el.matches('[type="radio"]') && !el.checked) continue; // skip unchecked radios of a top-level group const dynKey = el.name; const newVal = this.collectConfigValueFromElement(el); formData.append('key', dynKey); -- 2.52.0 From efb04334123e8afdcd6afad8310d36b3426f36c1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 13:15:10 -0500 Subject: [PATCH 05/26] fix: preserve server-rendered radio default when config value is empty LandingPageType.Mode defaults to "" (Go zero value), and the template renders the home radio as checked for an empty Mode. The initial radio fill would evaluate home.checked = ("home" === "") = false, unchecking the default on a fresh install. Skip assignment when the config value is empty so the server-rendered selection is preserved. Adds a test for the empty-value case. --- web_src/js/features/admin/config.test.ts | 6 ++++++ web_src/js/features/admin/config.ts | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts index b7763ee6b7..d633aa9502 100644 --- a/web_src/js/features/admin/config.test.ts +++ b/web_src/js/features/admin/config.test.ts @@ -27,6 +27,11 @@ test('ConfigFormValueMapper', () => { + + + + + `; @@ -51,5 +56,6 @@ test('ConfigFormValueMapper', () => { 'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend 'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}', 'landing': '{"Mode":"explore"}', + 'landingDefault': '{"Mode":"home"}', }); }); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 02bced6fa2..64cba73213 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -100,8 +100,9 @@ export class ConfigFormValueMapper { if (valType !== 'boolean') requireExplicitValueType(el); el.checked = Boolean(val ?? el.checked); } else if (el.matches('[type="radio"]')) { - // a radio group shares one name; check only the option whose value equals the config value - el.checked = el.value === String(val); + // a radio group shares one name; check only the option whose value equals the config value. + // when the value is empty (unset), leave the server-rendered default selection untouched. + if (String(val) !== '') el.checked = el.value === String(val); } else if (el.matches('[type="datetime-local"]')) { if (valType !== 'timestamp') requireExplicitValueType(el); if (val) el.value = toDatetimeLocalValue(val); -- 2.52.0 From b252e9569fd1231eec521fb206bc8bc44368f583 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 13:36:47 -0500 Subject: [PATCH 06/26] ci: allow fix/patch branches to target main and guard missing manifest Branch policy in pr-check.yml only allowed fix/* and patch/* to target dev/rc, blocking fix/* PRs to main despite the documented policy. Allow fix/* -> main and patch/* -> main. Also guard the Detect platform step for a missing .mokogitea/manifest.xml (removed in favor of the metadata API) so it no longer aborts the Validate PR job under set -e. --- .mokogitea/workflows/pr-check.yml | 23 ++++++++++++++--------- CHANGELOG.md | 2 ++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index c834bf5f8b..a789ecca58 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -47,15 +47,15 @@ jobs: fi ;; fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" + REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" fi ;; patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'" fi ;; hotfix/*) @@ -86,7 +86,8 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY @@ -147,10 +148,14 @@ jobs: - name: Detect platform id: platform run: | - # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" + # Read platform from XML manifest ( tag) if present; otherwise default to generic. + # manifest.xml was removed in favor of the metadata API, so guard for its absence to avoid + # aborting under `set -e` when the file is missing. + PLATFORM="generic" + if [ -f .mokogitea/manifest.xml ]; then + DETECTED=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml | head -1) + [ -n "$DETECTED" ] && PLATFORM="$DETECTED" + fi echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - name: Setup PHP diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e69540f09..7bbb61c6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ ### Fixed - Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page +- PR check branch policy: allow `fix/*` → `main` and `patch/*` → `main` to match documented policy (was rejecting fix/patch PRs to main) +- PR check platform detection: guard for missing `.mokogitea/manifest.xml` so the Validate PR job no longer aborts under `set -e` (manifest replaced by metadata API) - PR check: platform detection now queries metadata API instead of removed manifest.xml - Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783) - Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622) -- 2.52.0 From fc234bc91170a410fcecf611c3694d049a4a5241 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 13:46:35 -0500 Subject: [PATCH 07/26] fix: remove dangling mcp-mokogitea-api submodule gitlink The tree carried a gitlink at mcp-mokogitea-api (mode 160000) with no .gitmodules entry, so git submodule update --init --recursive failed with exit 128 at checkout, breaking every PR build/release job. mcp-mokogitea-api is a separate repo, not a submodule; remove the gitlink from the index (keeping the local working-tree clone) and gitignore the path so it can't be re-added. --- .gitignore | 3 +++ CHANGELOG.md | 1 + mcp-mokogitea-api | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) delete mode 160000 mcp-mokogitea-api diff --git a/.gitignore b/.gitignore index 76a7578646..d2f1993ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,6 @@ prime/ # A Makefile for custom make targets Makefile.local + +# Local clone of the MCP server (separate repo, not a submodule of this project) +/mcp-mokogitea-api/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbb61c6ef..422c646056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ - Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page - PR check branch policy: allow `fix/*` → `main` and `patch/*` → `main` to match documented policy (was rejecting fix/patch PRs to main) - PR check platform detection: guard for missing `.mokogitea/manifest.xml` so the Validate PR job no longer aborts under `set -e` (manifest replaced by metadata API) +- Remove dangling `mcp-mokogitea-api` submodule gitlink (no `.gitmodules` entry) that broke `submodule update --init` at checkout, failing all PR build/release jobs; ignore the local clone path - PR check: platform detection now queries metadata API instead of removed manifest.xml - Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783) - Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622) diff --git a/mcp-mokogitea-api b/mcp-mokogitea-api deleted file mode 160000 index dbaf91546e..0000000000 --- a/mcp-mokogitea-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dbaf91546e0fb44614cf8d914b7a78e679dc1efe -- 2.52.0 From aea43708456bc41de5e49c7f6fef5dd9fef16918 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 13:53:15 -0500 Subject: [PATCH 08/26] ci: no-op PR RC Release when updates.xml is absent The RC release workflow drives a Joomla-style updates.xml update stream. On a generic repo with no updates.xml, the Determine RC version step ran sed on a missing file and aborted under set -e (exit 2). Detect updates.xml presence and gate the update-stream steps (edit/create-release/commit) on it so the job succeeds and no-ops when there is nothing to package. --- .mokogitea/workflows/custom/pr-rc-release.yml | 15 ++++++++++++--- CHANGELOG.md | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/custom/pr-rc-release.yml b/.mokogitea/workflows/custom/pr-rc-release.yml index 998a25a7f7..f6b9f905f6 100644 --- a/.mokogitea/workflows/custom/pr-rc-release.yml +++ b/.mokogitea/workflows/custom/pr-rc-release.yml @@ -47,6 +47,15 @@ jobs: env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | + # This RC flow drives a Joomla-style update stream (updates.xml). Repos that don't ship + # one (e.g. generic Go/TS) have nothing to package here, so no-op cleanly instead of + # aborting under `set -e` when the file is absent. + if [ ! -f updates.xml ]; then + echo "has_updates=false" >> "$GITHUB_OUTPUT" + echo "No updates.xml in this repo — skipping RC update-stream packaging" + exit 0 + fi + echo "has_updates=true" >> "$GITHUB_OUTPUT" BASE_VERSION=$(sed -n 's/.*\(.*\)<\/version>.*/\1/p' updates.xml | head -1) [ -z "$BASE_VERSION" ] && BASE_VERSION="04.00.00" RC_VERSION="${BASE_VERSION}-rc.${PR_NUMBER}" @@ -56,7 +65,7 @@ jobs: echo "RC version: $RC_VERSION (tag: $RC_TAG)" - name: Update updates.xml RC channel - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true' env: RC_VERSION: ${{ steps.version.outputs.version }} RC_TAG: ${{ steps.version.outputs.tag }} @@ -106,7 +115,7 @@ jobs: PYEOF - name: Create RC release - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true' env: GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} RC_TAG: ${{ steps.version.outputs.tag }} @@ -153,7 +162,7 @@ jobs: PYEOF - name: Commit updates.xml - if: steps.guard.outputs.skip != 'true' + if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true' env: GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} HEAD_REF: ${{ github.event.pull_request.head.ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 422c646056..64ac781fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ - PR check branch policy: allow `fix/*` → `main` and `patch/*` → `main` to match documented policy (was rejecting fix/patch PRs to main) - PR check platform detection: guard for missing `.mokogitea/manifest.xml` so the Validate PR job no longer aborts under `set -e` (manifest replaced by metadata API) - Remove dangling `mcp-mokogitea-api` submodule gitlink (no `.gitmodules` entry) that broke `submodule update --init` at checkout, failing all PR build/release jobs; ignore the local clone path +- PR RC Release workflow: no-op cleanly when `updates.xml` is absent (generic repos) instead of aborting the "Determine RC version" step under `set -e` - PR check: platform detection now queries metadata API instead of removed manifest.xml - Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783) - Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622) -- 2.52.0 From 844b4b6e81e9dbf796374b6393acc43cb423b11f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:04:10 +0000 Subject: [PATCH 09/26] chore: sync auto-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/auto-release.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 4489ae06e0..8337c71a68 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -64,10 +64,14 @@ jobs: promote-rc: name: Promote to RC runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event.action == 'synchronize' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + !startsWith(github.event.repository.name, 'Template-') && + ( + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event.action == 'synchronize' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + ) steps: - name: Checkout repository @@ -164,9 +168,13 @@ jobs: release: name: Build & Release Pipeline runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + !startsWith(github.event.repository.name, 'Template-') && + ( + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + ) steps: - name: Checkout repository -- 2.52.0 From 8fc2700c16c5f0c4b69a0bc9b3341dd14cb99531 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:04:20 +0000 Subject: [PATCH 10/26] chore: sync ci-generic.yml from Template-Generic [skip ci] --- .mokogitea/workflows/ci-generic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index 92d26853d1..3a942ad23b 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -6,7 +6,7 @@ # DEFGROUP: Gitea.Workflow # INGROUP: MokoStandards.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.gitea/workflows/ci-generic.yml +# PATH: /.mokogitea/workflows/ci-generic.yml # VERSION: 01.00.00 # BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js) -- 2.52.0 From 51332a587d877e9ff082c5b7be97e646b7b4280c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:04:27 +0000 Subject: [PATCH 11/26] chore: sync cleanup.yml from Template-Generic [skip ci] --- .mokogitea/workflows/cleanup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml index 002386274d..2e6b6a2829 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -6,7 +6,7 @@ # DEFGROUP: Gitea.Workflow # INGROUP: MokoStandards.Maintenance # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/cleanup.yml +# PATH: /.mokogitea/workflows/cleanup.yml # VERSION: 01.00.00 # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs -- 2.52.0 From 296df7792aa53a3cdf40738be25f5b84652cc87b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:04:37 +0000 Subject: [PATCH 12/26] chore: sync notify.yml from Template-Generic [skip ci] --- .mokogitea/workflows/notify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index 51dfcb5dd3..9dfa047cf9 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -6,7 +6,7 @@ # DEFGROUP: Gitea.Workflow # INGROUP: MokoStandards.Notifications # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/notify.yml +# PATH: /.mokogitea/workflows/notify.yml # VERSION: 01.00.00 # BRIEF: Push notifications via ntfy on release success or workflow failure -- 2.52.0 From adf0e923dfbda2a43b0b0dd4d211f707f9da6fe4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:04:47 +0000 Subject: [PATCH 13/26] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index a789ecca58..1652713df7 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# INGROUP: mokocli.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # PATH: /templates/workflows/universal/pr-check.yml.template # VERSION: 09.23.00 # BRIEF: PR gate — branch policy + code validation before merge @@ -47,15 +47,15 @@ jobs: fi ;; fix/*|bugfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + if [ "$BASE" != "dev" ]; then ALLOWED=false - REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" + REASON="Fix branches must target 'dev', not '${BASE}'" fi ;; patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then ALLOWED=false - REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'" + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" fi ;; hotfix/*) @@ -86,8 +86,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $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 @@ -148,14 +147,10 @@ jobs: - name: Detect platform id: platform run: | - # Read platform from XML manifest ( tag) if present; otherwise default to generic. - # manifest.xml was removed in favor of the metadata API, so guard for its absence to avoid - # aborting under `set -e` when the file is missing. - PLATFORM="generic" - if [ -f .mokogitea/manifest.xml ]; then - DETECTED=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml | head -1) - [ -n "$DETECTED" ] && PLATFORM="$DETECTED" - fi + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - name: Setup PHP -- 2.52.0 From 8680fd1cab3ea0871219c731776f2135854d8889 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:05:10 +0000 Subject: [PATCH 14/26] chore: sync pre-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pre-release.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index efb3d1b4f6..cc40025b9e 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -48,9 +48,13 @@ jobs: build: name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. if: >- - github.event_name == 'workflow_dispatch' || - github.event_name == 'push' + !startsWith(github.event.repository.name, 'Template-') && + ( + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' + ) steps: - name: Checkout -- 2.52.0 From 0c45a2fda34b3bd314aad42f2591e84ddfd4647e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:05:30 +0000 Subject: [PATCH 15/26] chore: sync sync-on-merge.yml from Template-Generic [skip ci] --- .mokogitea/workflows/sync-on-merge.yml | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .mokogitea/workflows/sync-on-merge.yml diff --git a/.mokogitea/workflows/sync-on-merge.yml b/.mokogitea/workflows/sync-on-merge.yml new file mode 100644 index 0000000000..1b882bcd41 --- /dev/null +++ b/.mokogitea/workflows/sync-on-merge.yml @@ -0,0 +1,31 @@ +name: Sync Workflows to Repos + +on: + push: + branches: + - main + paths: + - '.mokogitea/workflows/**' + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout mokocli + uses: actions/checkout@v4 + with: + repository: MokoConsulting/mokocli + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Setup PHP + uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1 + with: + php-version: '8.1' + + - name: Install dependencies + run: composer install --no-dev --no-interaction + + - name: Sync workflows to generic repos + run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}" + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} -- 2.52.0 From 05763eb661d412abb71f63d968d6d078ffddd2a1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:37:58 +0000 Subject: [PATCH 16/26] chore: sync branch-cleanup.yml from Template-Generic [skip ci] --- .mokogitea/workflows/branch-cleanup.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml index 9d884e70a3..dd5ff5695e 100644 --- a/.mokogitea/workflows/branch-cleanup.yml +++ b/.mokogitea/workflows/branch-cleanup.yml @@ -33,7 +33,8 @@ jobs: run: | BRANCH="${{ github.event.pull_request.head.ref }}" API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" - ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") + # URL-encode the branch name's slashes (no PHP dependency on the runner) + ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ -- 2.52.0 From 1256f57d40713a9e53a3aac4b26789d59bea4d9d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:38:11 +0000 Subject: [PATCH 17/26] chore: sync ci-generic.yml from Template-Generic [skip ci] --- .mokogitea/workflows/ci-generic.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index 3a942ad23b..f55616242b 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -32,6 +32,8 @@ jobs: lint: name: Lint & Validate runs-on: ubuntu-latest + # Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout @@ -130,6 +132,8 @@ jobs: name: Tests runs-on: ubuntu-latest needs: lint + # Skip on template repos (Template-*) — see lint job. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout -- 2.52.0 From 22b0b14ea8cb6dbf5b2a287cea1e773166a8f1d1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 19:38:21 +0000 Subject: [PATCH 18/26] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 1652713df7..941e436d82 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -126,6 +126,8 @@ jobs: validate: name: Validate PR runs-on: ubuntu-latest + # Skip on template repos (Template-*) — no real manifest/source/changelog to validate. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout -- 2.52.0 From d77b82a8af54bd5a0dbf9548618b6f0ba4e16aa3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 20:22:34 +0000 Subject: [PATCH 19/26] chore: sync ci-generic.yml from Template-Generic [skip ci] --- .mokogitea/workflows/ci-generic.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index f55616242b..72650d2430 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -132,8 +132,9 @@ jobs: name: Tests runs-on: ubuntu-latest needs: lint - # Skip on template repos (Template-*) — see lint job. - if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + # Run only when lint succeeded; always() forces evaluation so a skipped + # lint (e.g. template repos) skips this job cleanly instead of hanging. + if: ${{ always() && needs.lint.result == 'success' }} steps: - name: Checkout -- 2.52.0 From ea87b3db979c452480bbf8fb4a639ae7ad9ca863 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 20:22:43 +0000 Subject: [PATCH 20/26] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 941e436d82..c13b15ecb0 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -494,6 +494,9 @@ jobs: name: Build RC Package runs-on: ubuntu-latest needs: [branch-policy, validate] + # Run only when both gates succeeded; always() forces evaluation so a skipped + # validate (e.g. template repos) skips this job cleanly instead of hanging. + if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }} steps: - name: Trigger RC pre-release -- 2.52.0 From 4ed45a5916e8e78f0b9dfaa32a2c2a98e1e6afdf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 20:27:57 +0000 Subject: [PATCH 21/26] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index c13b15ecb0..019371d9e6 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -47,15 +47,15 @@ jobs: fi ;; fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" + REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" fi ;; patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'" fi ;; hotfix/*) @@ -86,7 +86,8 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY @@ -149,11 +150,12 @@ jobs: - name: Detect platform id: platform run: | - # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + # Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. + API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" + PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" [ -z "$PLATFORM" ] && PLATFORM="generic" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Detected platform: $PLATFORM" - name: Setup PHP if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' -- 2.52.0 From 0cc569aef6f1c3e143af05dbb9bb28715c228593 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 16:27:32 -0500 Subject: [PATCH 22/26] fix: render org teams list and make issue type editable (#720, #721) #720: org Teams page wrote ctx.Data["OrgListTeams"] but the template iterates .Teams, so no teams rendered. Use the canonical Teams key (matches org/home.go). #721: issue type sidebar gated editing on a FieldEditFlags data key that no handler sets (always nil -> always read-only). Use HasIssuesOrPullsWritePermission like the priority field; the /custom-type endpoint is already protected by reqRepoIssuesOrPullsWriter. --- CHANGELOG.md | 2 ++ routers/web/org/teams.go | 2 +- templates/repo/issue/sidebar/issue_type.tmpl | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ac781fde..e45eaa9105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ - Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185) ### Fixed +- Org Teams page: list now renders — the handler wrote `ctx.Data["OrgListTeams"]` but the template reads `.Teams`, so the page showed header/nav but no teams (#720) +- Issue type: now editable after creation for users with issue write permission — the sidebar gated editing on a `FieldEditFlags` data key that was never populated (always read-only); now uses `HasIssuesOrPullsWritePermission` like the priority field (#721) - Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page - PR check branch policy: allow `fix/*` → `main` and `patch/*` → `main` to match documented policy (was rejecting fix/patch PRs to main) - PR check platform detection: guard for missing `.mokogitea/manifest.xml` so the Validate PR job no longer aborts under `set -e` (manifest replaced by metadata API) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index fc6e82231f..819c9a793b 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -98,7 +98,7 @@ func Teams(ctx *context.Context) { } } - ctx.Data["OrgListTeams"] = teams + ctx.Data["Teams"] = teams ctx.Data["Keyword"] = keyword pager := context.NewPagination(count, setting.UI.MembersPagingNum, page, 5) pager.AddParamFromRequest(ctx.Req) diff --git a/templates/repo/issue/sidebar/issue_type.tmpl b/templates/repo/issue/sidebar/issue_type.tmpl index 42fb18277f..bb41078ff9 100644 --- a/templates/repo/issue/sidebar/issue_type.tmpl +++ b/templates/repo/issue/sidebar/issue_type.tmpl @@ -2,7 +2,7 @@
{{ctx.Locale.Tr "repo.issues.type"}} - {{$canModify := and .FieldEditFlags .FieldEditFlags.CustomFields}} + {{$canModify := .HasIssuesOrPullsWritePermission}} {{if $canModify}}
{{$.CsrfTokenHtml}} -- 2.52.0 From 377dd019bee8063b881280a265ee276e25fbcc77 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 4 Jul 2026 23:27:10 +0000 Subject: [PATCH 23/26] chore: sync auto-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/auto-release.yml | 42 ++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 8337c71a68..9b908d02ad 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -95,7 +95,7 @@ jobs: 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 rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli cd /tmp/mokocli composer install --no-dev --no-interaction --quiet @@ -104,11 +104,39 @@ jobs: - name: Rename branch to rc run: | - php ${MOKO_CLI}/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" + FROM="${{ github.event.pull_request.head.ref || 'dev' }}" + PR="${{ github.event.pull_request.number }}" + + # Resolve the source branch HEAD commit. + SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \ + || { echo "::error::Source branch ${FROM} not found"; exit 1; } + SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true) + [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; } + + # Point rc at the source commit. If rc already exists (a protected branch that + # cannot be deleted), force-update its ref in place instead of delete+recreate: + # deleting a protected branch fails, which then makes the recreate return HTTP 409. + if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then + echo "rc exists - force-updating to ${FROM} (${SRC_SHA})" + curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \ + || { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; } + else + echo "Creating rc from ${FROM}" + curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \ + || { echo "::error::Failed to create rc from ${FROM}"; exit 1; } + fi + + # Repoint the PR at rc, then delete the old source branch (non-fatal). + if [ -n "$PR" ]; then + curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true + fi + curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true + echo "Renamed ${FROM} -> rc" - name: Checkout rc and configure git run: | @@ -218,7 +246,7 @@ jobs: 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 rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli cd /tmp/mokocli composer install --no-dev --no-interaction --quiet -- 2.52.0 From 24b3516c1d39ce9cfec60151857cf779e039ece1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 19:42:08 -0500 Subject: [PATCH 24/26] fix(org): layer org-level branch protection with repo rules, most-restrictive wins (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Org-level branch protection was already consulted at the single enforcement choke point `GetFirstMatchProtectedBranchRule`, but only as a FALLBACK: if any repo-level rule matched the branch, the org rule was ignored entirely. That let a repo define a looser rule for a pattern and effectively opt out of the org's protection. Make the choke point LAYER the two rules instead: when both an org rule and a repo rule match a branch, return their most-restrictive (fail-closed) combination, so the org rule is a mandatory floor a repo can only tighten. - models/git/protected_branch_merge.go: mergeMostRestrictive + helpers. Allow flags AND'd; gate/require/block flags OR'd; RequiredApprovals max'd; required sets (status contexts, protected files) unioned; allow sets (whitelists, unprotected files) intersected. A disabled allowlist means "everyone", so it only constrains when enabled. - models/git/protected_branch_list.go: GetFirstMatchProtectedBranchRule now fetches both the repo rule and the org rule and merges when both match; returns whichever exists when only one matches. Org lookup factored into getFirstMatchOrgProtectedBranchRule. Supersedes the materialization approach previously proposed for this issue — the org fallback already existed, so only this one function needed to change. Fail-closed by design: any merge edge errs toward MORE protection (over-restrict) rather than less, so it cannot open a hole. Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here — relying on CI to validate build, formatting, and tests. Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT --- CHANGELOG.md | 1 + models/git/protected_branch_list.go | 35 ++++-- models/git/protected_branch_merge.go | 178 +++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 models/git/protected_branch_merge.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e45eaa9105..64a1d05706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185) ### Fixed +- Org-level branch protection now **layers** with per-repo rules instead of being ignored whenever a repo rule exists. When both an org rule and a repo rule match a branch, the effective rule is the most-restrictive (fail-closed) combination — the org rule is a mandatory floor a repo cannot weaken: allow flags AND'd, gate/require/block flags OR'd, required approvals max'd, status checks and protected-file patterns unioned, whitelists intersected. Previously a repo rule shadowed the org rule entirely at the enforcement choke point (`GetFirstMatchProtectedBranchRule`), letting a repo opt out of org protection (#727) - Org Teams page: list now renders — the handler wrote `ctx.Data["OrgListTeams"]` but the template reads `.Teams`, so the page showed header/nav but no teams (#720) - Issue type: now editable after creation for users with issue write permission — the sidebar gated editing on a `FieldEditFlags` data key that was never populated (always read-only); now uses `HasIssuesOrPullsWritePermission` like the priority field (#721) - Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index 4875857db6..fa3acf37d7 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -85,19 +85,40 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) return results, nil } -// GetFirstMatchProtectedBranchRule returns the first matched rule. -// It checks repo-level rules first; if none match, it falls back to org-level rules -// (if the repo belongs to an organization). +// GetFirstMatchProtectedBranchRule returns the effective protected-branch rule for a +// branch. It combines the matching repo-level rule with the matching org-level rule +// (when the repo belongs to an organization): if both match they are layered with +// mergeMostRestrictive so the org rule acts as a floor the repo cannot weaken; if +// only one matches that one is returned; if neither matches, nil. func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) { rules, err := FindRepoProtectedBranchRules(ctx, repoID) if err != nil { return nil, err } - if matched := rules.GetFirstMatched(branchName); matched != nil { - return matched, nil + repoRule := rules.GetFirstMatched(branchName) + + orgRule, err := getFirstMatchOrgProtectedBranchRule(ctx, repoID, branchName) + if err != nil { + return nil, err } - // Fall back to org-level rules + switch { + case repoRule == nil && orgRule == nil: + return nil, nil + case orgRule == nil: + return repoRule, nil + case repoRule == nil: + return orgRule, nil + default: + return mergeMostRestrictive(repoRule, orgRule), nil + } +} + +// getFirstMatchOrgProtectedBranchRule returns the matching org-level rule for a +// branch expressed as a repo-scoped ProtectedBranch (RepoID set so downstream +// permission checks work), or nil if the repo's owner is not an organization or no +// org rule matches. +func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) { repo, err := repo_model.GetRepositoryByID(ctx, repoID) if err != nil { return nil, err @@ -119,7 +140,7 @@ func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchN return nil, nil } - // Convert org rule to a ProtectedBranch with RepoID set so callers work correctly + // Convert org rule to a ProtectedBranch with RepoID set so callers work correctly. pb := orgRule.ToProtectedBranch() pb.RepoID = repoID return pb, nil diff --git a/models/git/protected_branch_merge.go b/models/git/protected_branch_merge.go new file mode 100644 index 0000000000..d7a0465799 --- /dev/null +++ b/models/git/protected_branch_merge.go @@ -0,0 +1,178 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package git + +import "strings" + +// mergeMostRestrictive combines a repo-level and an org-level protected-branch rule +// that both match the same branch into a single effective rule, always applying the +// STRICTER constraint from each side (fail-closed). This makes an org-level rule a +// mandatory floor that a repo rule can only tighten, never weaken. See issue #727. +// +// Combination directions: +// - "Can*" / allow booleans -> AND (an action is allowed only if both allow it) +// - "Enable*/Block*/Require*" -> OR (a gate is on if either side turns it on) +// - RequiredApprovals -> max +// - required-set lists -> union (status contexts, protected files) +// - allow-set lists -> intersection (whitelists, unprotected files) +// +// Identity (ID, RepoID, RuleName, Priority) is taken from the repo rule so that +// downstream permission checks (which LoadRepo via RepoID) keep working. +func mergeMostRestrictive(repoRule, orgRule *ProtectedBranch) *ProtectedBranch { + eff := *repoRule + + // Direct push. + eff.CanPush = repoRule.CanPush && orgRule.CanPush + eff.EnableWhitelist, eff.WhitelistUserIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistUserIDs, orgRule.EnableWhitelist, orgRule.WhitelistUserIDs) + _, eff.WhitelistTeamIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistTeamIDs, orgRule.EnableWhitelist, orgRule.WhitelistTeamIDs) + eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys && orgRule.WhitelistDeployKeys + eff.WhitelistActionsUser = repoRule.WhitelistActionsUser && orgRule.WhitelistActionsUser + + // Force push. + eff.CanForcePush = repoRule.CanForcePush && orgRule.CanForcePush + eff.EnableForcePushAllowlist, eff.ForcePushAllowlistUserIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistUserIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistUserIDs) + _, eff.ForcePushAllowlistTeamIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistTeamIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistTeamIDs) + eff.ForcePushAllowlistDeployKeys = repoRule.ForcePushAllowlistDeployKeys && orgRule.ForcePushAllowlistDeployKeys + eff.ForcePushAllowlistActionsUser = repoRule.ForcePushAllowlistActionsUser && orgRule.ForcePushAllowlistActionsUser + + // Delete. + eff.CanDelete = repoRule.CanDelete && orgRule.CanDelete + eff.EnableDeleteAllowlist, eff.DeleteAllowlistUserIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistUserIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistUserIDs) + _, eff.DeleteAllowlistTeamIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistTeamIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistTeamIDs) + eff.DeleteAllowlistDeployKeys = repoRule.DeleteAllowlistDeployKeys && orgRule.DeleteAllowlistDeployKeys + eff.DeleteAllowlistActionsUser = repoRule.DeleteAllowlistActionsUser && orgRule.DeleteAllowlistActionsUser + + // Merge whitelist. + eff.EnableMergeWhitelist, eff.MergeWhitelistUserIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistUserIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistUserIDs) + _, eff.MergeWhitelistTeamIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistTeamIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistTeamIDs) + eff.MergeWhitelistActionsUser = repoRule.MergeWhitelistActionsUser && orgRule.MergeWhitelistActionsUser + + // Status checks. + eff.EnableStatusCheck = repoRule.EnableStatusCheck || orgRule.EnableStatusCheck + eff.StatusCheckContexts = unionStrings(repoRule.StatusCheckContexts, orgRule.StatusCheckContexts) + + // Approvals and reviews. + eff.RequiredApprovals = maxInt64(repoRule.RequiredApprovals, orgRule.RequiredApprovals) + eff.EnableApprovalsWhitelist, eff.ApprovalsWhitelistUserIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistUserIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistUserIDs) + _, eff.ApprovalsWhitelistTeamIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistTeamIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistTeamIDs) + eff.BlockOnRejectedReviews = repoRule.BlockOnRejectedReviews || orgRule.BlockOnRejectedReviews + eff.BlockOnOfficialReviewRequests = repoRule.BlockOnOfficialReviewRequests || orgRule.BlockOnOfficialReviewRequests + eff.BlockOnOutdatedBranch = repoRule.BlockOnOutdatedBranch || orgRule.BlockOnOutdatedBranch + eff.DismissStaleApprovals = repoRule.DismissStaleApprovals || orgRule.DismissStaleApprovals + eff.IgnoreStaleApprovals = repoRule.IgnoreStaleApprovals || orgRule.IgnoreStaleApprovals + + // Commits, files, admin override. + eff.RequireSignedCommits = repoRule.RequireSignedCommits || orgRule.RequireSignedCommits + eff.ProtectedFilePatterns = unionPatterns(repoRule.ProtectedFilePatterns, orgRule.ProtectedFilePatterns) + eff.UnprotectedFilePatterns = intersectPatterns(repoRule.UnprotectedFilePatterns, orgRule.UnprotectedFilePatterns) + eff.BlockAdminMergeOverride = repoRule.BlockAdminMergeOverride || orgRule.BlockAdminMergeOverride + + return &eff +} + +// mergeAllowlist combines two allow-lists under most-restrictive semantics. An +// allow-list only narrows access when its Enable flag is set; a disabled list means +// "everyone", so it imposes no constraint. Therefore: if both are enabled the result +// is the intersection (a principal must be allowed by both); if only one is enabled +// its list is used as-is; if neither is enabled the list is irrelevant. +func mergeAllowlist(aEnabled bool, aIDs []int64, bEnabled bool, bIDs []int64) (bool, []int64) { + switch { + case aEnabled && bEnabled: + return true, intersectInt64(aIDs, bIDs) + case aEnabled: + return true, aIDs + case bEnabled: + return true, bIDs + default: + return false, nil + } +} + +func intersectInt64(a, b []int64) []int64 { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[int64]struct{}, len(a)) + for _, x := range a { + set[x] = struct{}{} + } + var out []int64 + for _, x := range b { + if _, ok := set[x]; ok { + out = append(out, x) + } + } + return out +} + +func unionStrings(a, b []string) []string { + if len(a) == 0 { + return b + } + if len(b) == 0 { + return a + } + seen := make(map[string]struct{}, len(a)+len(b)) + out := make([]string, 0, len(a)+len(b)) + for _, s := range a { + if _, ok := seen[s]; !ok { + seen[s] = struct{}{} + out = append(out, s) + } + } + for _, s := range b { + if _, ok := seen[s]; !ok { + seen[s] = struct{}{} + out = append(out, s) + } + } + return out +} + +// unionPatterns unions two ';'-separated file-pattern lists (more patterns protected +// = more restrictive). +func unionPatterns(a, b string) string { + return strings.Join(unionStrings(splitPatterns(a), splitPatterns(b)), ";") +} + +// intersectPatterns intersects two ';'-separated file-pattern lists. Unprotected +// patterns are carve-outs that REDUCE protection, so the restrictive combination +// keeps only the exemptions present in both. +func intersectPatterns(a, b string) string { + as, bs := splitPatterns(a), splitPatterns(b) + set := make(map[string]struct{}, len(as)) + for _, s := range as { + set[s] = struct{}{} + } + seen := make(map[string]struct{}, len(bs)) + var out []string + for _, s := range bs { + if _, ok := set[s]; !ok { + continue + } + if _, dup := seen[s]; dup { + continue + } + seen[s] = struct{}{} + out = append(out, s) + } + return strings.Join(out, ";") +} + +func splitPatterns(s string) []string { + var out []string + for _, p := range strings.Split(s, ";") { + if p = strings.TrimSpace(p); p != "" { + out = append(out, p) + } + } + return out +} + +func maxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} -- 2.52.0 From 86bd8a2cad081ef62bbee7d7aaf366a281239df0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 20:25:24 -0500 Subject: [PATCH 25/26] feat(org): show inherited org branch-protection rules in repo settings (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org "floor" is enforced implicitly at the choke point, so a repo admin couldn't see which org-level rules apply to their repo. Surface them in the repo's Branch Protection settings page (read-only), the way GitHub shows organization rulesets in a repository. - ProtectedBranchRules handler: when the owner is an org, load FindOrgProtectedBranchRules and expose them as OrgProtectedBranches. - branches.tmpl: new read-only "Organization Branch Protection" section listing each org rule with an "Organization" badge, a lock/read-only marker, and compact indicators (required approvals, signed commits, status checks). No edit/delete controls — these are managed at the org level. - en-US locale strings. Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here. Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT --- options/locale/locale_en-US.json | 7 +++++ routers/web/repo/setting/protected_branch.go | 12 +++++++++ templates/repo/settings/branches.tmpl | 27 ++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index ea52615095..f1bf71288f 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2411,6 +2411,13 @@ "repo.settings.protected_branch": "Branch Protection", "repo.settings.protected_branch.save_rule": "Save Rule", "repo.settings.protected_branch.delete_rule": "Delete Rule", + "repo.settings.org_protected_branch": "Organization Branch Protection", + "repo.settings.org_protected_branch_desc": "These rules are defined by the organization and are enforced on top of this repository's own rules — the stricter of the two applies. They cannot be edited here.", + "repo.settings.org_protected_branch.inherited": "Organization", + "repo.settings.org_protected_branch.read_only": "Read-only", + "repo.settings.org_protected_branch.approvals": "%d approvals required", + "repo.settings.org_protected_branch.signed": "Signed commits", + "repo.settings.org_protected_branch.status_check": "Status checks", "repo.settings.protected_branch_can_push": "Allow push?", "repo.settings.protected_branch_can_push_yes": "You can push", "repo.settings.protected_branch_can_push_no": "You cannot push", diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index c8881838ab..06da1aa15e 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -46,6 +46,18 @@ func ProtectedBranchRules(ctx *context.Context) { } ctx.Data["ProtectedBranches"] = rules + // Surface the organization-level rules that also apply to this repo (read-only), + // so admins can see the org "floor" that is layered on top of the repo's own + // rules at enforcement time. See issue #727. + if ctx.Repo.Owner.IsOrganization() { + orgRules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Repo.Owner.ID) + if err != nil { + ctx.ServerError("FindOrgProtectedBranchRules", err) + return + } + ctx.Data["OrgProtectedBranches"] = orgRules + } + repo.PrepareBranchList(ctx) if ctx.Written() { return diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index 6ec80c057f..7bfc6c3dcc 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -62,6 +62,33 @@ {{end}}
+ + {{if .OrgProtectedBranches}} +

+ {{ctx.Locale.Tr "repo.settings.org_protected_branch"}} +

+
+

{{ctx.Locale.Tr "repo.settings.org_protected_branch_desc"}}

+
+ {{range .OrgProtectedBranches}} +
+
+
+
{{.RuleName}}
+ {{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}} + {{if gt .RequiredApprovals 0}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals" .RequiredApprovals}}{{end}} + {{if .RequireSignedCommits}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}{{end}} + {{if .EnableStatusCheck}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}{{end}} +
+
+
+ {{svg "octicon-lock" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}} +
+
+ {{end}} +
+
+ {{end}} {{end}} -- 2.52.0 From 4b68853f08c21812667fd8418504586507917475 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 21:16:24 -0500 Subject: [PATCH 26/26] feat(org): add branch-deletion protection + expandable inherited-rule view (#727) Two related additions: 1. Branch deletion as an org-level ability. OrgProtectedBranch gained CanDelete / EnableDeleteAllowlist / DeleteAllowlistTeamIDs (migration 362), ToProtectedBranch maps them, and the API (create/edit/response DTOs + handlers) exposes enable_delete / enable_delete_allowlist / delete_allowlist_teams. The layering merge already combined delete fields, so org delete-protection now enforces once ToProtectedBranch populates them. 2. The repo Branch Protection view now renders each inherited org rule as an expandable detail (direct push, force-push, branch deletion, merge, required approvals, status checks, protected files) with team names resolved, instead of three headline badges. Still read-only. Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here. Verified by hand: struct-field gofmt alignment, template block nesting balances, every .Rule field exists on OrgProtectedBranch, and all locale keys referenced in the template are defined. Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT --- CHANGELOG.md | 2 + models/git/org_protected_branch.go | 6 ++ models/migrations/migrations.go | 1 + models/migrations/v1_27/v363.go | 18 +++++ modules/structs/org_branch.go | 9 +++ options/locale/locale_en-US.json | 19 ++++- routers/api/v1/org/branch_protection.go | 23 ++++++ routers/web/repo/setting/protected_branch.go | 64 +++++++++++++++- templates/repo/settings/branches.tmpl | 79 +++++++++++++++++--- 9 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 models/migrations/v1_27/v363.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a1d05706..0be02342ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Added +- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727) +- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727) - Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552) - Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460) - Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507) diff --git a/models/git/org_protected_branch.go b/models/git/org_protected_branch.go index 99c09e6e03..881edab6d1 100644 --- a/models/git/org_protected_branch.go +++ b/models/git/org_protected_branch.go @@ -33,6 +33,9 @@ type OrgProtectedBranch struct { CanForcePush bool `xorm:"NOT NULL DEFAULT false"` EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"` ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + CanDelete bool `xorm:"NOT NULL DEFAULT false"` + EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"` + DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` StatusCheckContexts []string `xorm:"JSON TEXT"` RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` @@ -96,6 +99,9 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch { CanForcePush: o.CanForcePush, EnableForcePushAllowlist: o.EnableForcePushAllowlist, ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs, + CanDelete: o.CanDelete, + EnableDeleteAllowlist: o.EnableDeleteAllowlist, + DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs, EnableStatusCheck: o.EnableStatusCheck, StatusCheckContexts: o.StatusCheckContexts, RequiredApprovals: o.RequiredApprovals, diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 557a1ede75..2476df640d 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -439,6 +439,7 @@ func prepareMigrationTasks() []*migration { newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest), newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch), newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable), + newMigration(362, "Add delete allowlist to org protected branch", v1_27.AddDeleteAllowlistToOrgProtectedBranch), } return preparedMigrations } diff --git a/models/migrations/v1_27/v363.go b/models/migrations/v1_27/v363.go new file mode 100644 index 0000000000..8e2e3faa22 --- /dev/null +++ b/models/migrations/v1_27/v363.go @@ -0,0 +1,18 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package v1_27 + +import "xorm.io/xorm" + +// AddDeleteAllowlistToOrgProtectedBranch adds branch-deletion protection columns to +// org-level branch protection rules, mirroring the per-repo delete allowlist so an +// org rule can also protect branches from deletion. See issue #727. +func AddDeleteAllowlistToOrgProtectedBranch(x *xorm.Engine) error { + type OrgProtectedBranch struct { + CanDelete bool `xorm:"NOT NULL DEFAULT false"` + EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"` + DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + } + return x.Sync(new(OrgProtectedBranch)) +} diff --git a/modules/structs/org_branch.go b/modules/structs/org_branch.go index 7333a4192d..0b66b47756 100644 --- a/modules/structs/org_branch.go +++ b/modules/structs/org_branch.go @@ -17,6 +17,9 @@ type OrgBranchProtection struct { EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` + EnableDelete bool `json:"enable_delete"` + EnableDeleteAllowlist bool `json:"enable_delete_allowlist"` + DeleteAllowlistTeams []string `json:"delete_allowlist_teams"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` EnableStatusCheck bool `json:"enable_status_check"` @@ -49,6 +52,9 @@ type CreateOrgBranchProtectionOption struct { EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` + EnableDelete bool `json:"enable_delete"` + EnableDeleteAllowlist bool `json:"enable_delete_allowlist"` + DeleteAllowlistTeams []string `json:"delete_allowlist_teams"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` EnableStatusCheck bool `json:"enable_status_check"` @@ -76,6 +82,9 @@ type EditOrgBranchProtectionOption struct { EnableForcePush *bool `json:"enable_force_push"` EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` + EnableDelete *bool `json:"enable_delete"` + EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"` + DeleteAllowlistTeams []string `json:"delete_allowlist_teams"` EnableMergeWhitelist *bool `json:"enable_merge_whitelist"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` EnableStatusCheck *bool `json:"enable_status_check"` diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index f1bf71288f..4b56cde6a6 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2415,9 +2415,24 @@ "repo.settings.org_protected_branch_desc": "These rules are defined by the organization and are enforced on top of this repository's own rules — the stricter of the two applies. They cannot be edited here.", "repo.settings.org_protected_branch.inherited": "Organization", "repo.settings.org_protected_branch.read_only": "Read-only", - "repo.settings.org_protected_branch.approvals": "%d approvals required", + "repo.settings.org_protected_branch.approvals": "Required approvals", "repo.settings.org_protected_branch.signed": "Signed commits", - "repo.settings.org_protected_branch.status_check": "Status checks", + "repo.settings.org_protected_branch.status_check": "Required status checks", + "repo.settings.org_protected_branch.direct_push": "Direct push", + "repo.settings.org_protected_branch.force_push": "Force push", + "repo.settings.org_protected_branch.deletion": "Branch deletion", + "repo.settings.org_protected_branch.merge": "Merge restricted to", + "repo.settings.org_protected_branch.protected_files": "Protected files", + "repo.settings.org_protected_branch.also": "Also enforces", + "repo.settings.org_protected_branch.blocked": "Blocked", + "repo.settings.org_protected_branch.allowed": "Allowed", + "repo.settings.org_protected_branch.restricted": "Restricted to specific teams", + "repo.settings.org_protected_branch.write_access": "Anyone with write access", + "repo.settings.org_protected_branch.teams": "Teams: %s", + "repo.settings.org_protected_branch.any": "Any configured checks", + "repo.settings.org_protected_branch.block_outdated": "Block on outdated branch", + "repo.settings.org_protected_branch.block_rejected": "Block on rejected reviews", + "repo.settings.org_protected_branch.block_admin": "Block admin merge override", "repo.settings.protected_branch_can_push": "Allow push?", "repo.settings.protected_branch_can_push_yes": "You can push", "repo.settings.protected_branch_can_push_no": "You cannot push", diff --git a/routers/api/v1/org/branch_protection.go b/routers/api/v1/org/branch_protection.go index ff188f278d..010904087b 100644 --- a/routers/api/v1/org/branch_protection.go +++ b/routers/api/v1/org/branch_protection.go @@ -47,6 +47,9 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec EnableForcePush: rule.CanForcePush, EnableForcePushAllowlist: rule.EnableForcePushAllowlist, ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs), + EnableDelete: rule.CanDelete, + EnableDeleteAllowlist: rule.EnableDeleteAllowlist, + DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs), EnableMergeWhitelist: rule.EnableMergeWhitelist, MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs), EnableStatusCheck: rule.EnableStatusCheck, @@ -211,6 +214,10 @@ func CreateOrgBranchProtection(ctx *context.APIContext) { if !ok { return } + deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams) + if !ok { + return + } rule := &git_model.OrgProtectedBranch{ OrgID: orgID, @@ -222,6 +229,9 @@ func CreateOrgBranchProtection(ctx *context.APIContext) { CanForcePush: form.EnablePush && form.EnableForcePush, EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist, ForcePushAllowlistTeamIDs: forcePushTeams, + CanDelete: form.EnableDelete, + EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist, + DeleteAllowlistTeamIDs: deleteTeams, EnableMergeWhitelist: form.EnableMergeWhitelist, MergeWhitelistTeamIDs: mergeTeams, EnableStatusCheck: form.EnableStatusCheck, @@ -323,6 +333,19 @@ func EditOrgBranchProtection(ctx *context.APIContext) { } rule.ForcePushAllowlistTeamIDs = ids } + if form.EnableDelete != nil { + rule.CanDelete = *form.EnableDelete + } + if form.EnableDeleteAllowlist != nil { + rule.EnableDeleteAllowlist = *form.EnableDeleteAllowlist + } + if form.DeleteAllowlistTeams != nil { + ids, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams) + if !ok { + return + } + rule.DeleteAllowlistTeamIDs = ids + } if form.EnableMergeWhitelist != nil { rule.EnableMergeWhitelist = *form.EnableMergeWhitelist } diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 06da1aa15e..1b670038ba 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -34,6 +34,64 @@ const ( tplProtectedBranch templates.TplName = "repo/settings/protected_branch" ) +// orgBranchProtectionView is a read-only presentation of an org-level branch +// protection rule for the repo settings page, with team IDs resolved to names. +type orgBranchProtectionView struct { + Rule *git_model.OrgProtectedBranch + PushTeams string + ForcePushTeams string + DeleteTeams string + MergeTeams string + ApprovalTeams string + StatusContexts string +} + +// prepareOrgProtectedBranches loads the owning organization's branch protection +// rules and exposes them (with team IDs resolved to names) as read-only view models +// under the "OrgProtectedBranches" template key. +func prepareOrgProtectedBranches(ctx *context.Context) error { + orgRules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Repo.Owner.ID) + if err != nil { + return err + } + if len(orgRules) == 0 { + return nil + } + + teams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID) + if err != nil { + return err + } + teamNames := make(map[int64]string, len(teams)) + for _, t := range teams { + teamNames[t.ID] = t.Name + } + join := func(ids []int64) string { + names := make([]string, 0, len(ids)) + for _, id := range ids { + if n, ok := teamNames[id]; ok { + names = append(names, n) + } + } + return strings.Join(names, ", ") + } + + views := make([]*orgBranchProtectionView, len(orgRules)) + for i, r := range orgRules { + views[i] = &orgBranchProtectionView{ + Rule: r, + PushTeams: join(r.WhitelistTeamIDs), + ForcePushTeams: join(r.ForcePushAllowlistTeamIDs), + DeleteTeams: join(r.DeleteAllowlistTeamIDs), + MergeTeams: join(r.MergeWhitelistTeamIDs), + ApprovalTeams: join(r.ApprovalsWhitelistTeamIDs), + StatusContexts: strings.Join(r.StatusCheckContexts, ", "), + } + } + ctx.Data["OrgProtectedBranches"] = views + return nil +} + // ProtectedBranchRules render the page to protect the repository func ProtectedBranchRules(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.branches") @@ -50,12 +108,10 @@ func ProtectedBranchRules(ctx *context.Context) { // so admins can see the org "floor" that is layered on top of the repo's own // rules at enforcement time. See issue #727. if ctx.Repo.Owner.IsOrganization() { - orgRules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Repo.Owner.ID) - if err != nil { - ctx.ServerError("FindOrgProtectedBranchRules", err) + if err := prepareOrgProtectedBranches(ctx); err != nil { + ctx.ServerError("prepareOrgProtectedBranches", err) return } - ctx.Data["OrgProtectedBranches"] = orgRules } repo.PrepareBranchList(ctx) diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index 7bfc6c3dcc..40947c86b9 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -71,18 +71,73 @@

{{ctx.Locale.Tr "repo.settings.org_protected_branch_desc"}}

{{range .OrgProtectedBranches}} -
-
-
-
{{.RuleName}}
- {{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}} - {{if gt .RequiredApprovals 0}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals" .RequiredApprovals}}{{end}} - {{if .RequireSignedCommits}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}{{end}} - {{if .EnableStatusCheck}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}{{end}} -
-
-
- {{svg "octicon-lock" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}} +
+
+
+ +
{{.Rule.RuleName}}
+ {{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}} + {{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}} +
+ + + + + + + + + + + + + + + {{if gt .Rule.RequiredApprovals 0}} + + + + + {{end}} + {{if .Rule.EnableMergeWhitelist}} + + + + + {{end}} + {{if .Rule.EnableStatusCheck}} + + + + + {{end}} + {{if .Rule.RequireSignedCommits}} + + + + + {{end}} + {{if .Rule.ProtectedFilePatterns}} + + + + + {{end}} + {{if or .Rule.BlockOnOutdatedBranch .Rule.BlockOnRejectedReviews .Rule.BlockAdminMergeOverride}} + + + + + {{end}} + +
{{ctx.Locale.Tr "repo.settings.org_protected_branch.direct_push"}}{{if not .Rule.CanPush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableWhitelist}}{{if .PushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .PushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.write_access"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.force_push"}}{{if not .Rule.CanForcePush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableForcePushAllowlist}}{{if .ForcePushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .ForcePushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.deletion"}}{{if not .Rule.CanDelete}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableDeleteAllowlist}}{{if .DeleteTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .DeleteTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals"}}{{.Rule.RequiredApprovals}}{{if .ApprovalTeams}} ({{.ApprovalTeams}}){{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.merge"}}{{if .MergeTeams}}{{.MergeTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}{{if .StatusContexts}}{{.StatusContexts}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.any"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}{{svg "octicon-check" 14}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.protected_files"}}{{.Rule.ProtectedFilePatterns}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.also"}} +
+ {{if .Rule.BlockOnOutdatedBranch}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_outdated"}}{{end}} + {{if .Rule.BlockOnRejectedReviews}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_rejected"}}{{end}} + {{if .Rule.BlockAdminMergeOverride}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_admin"}}{{end}} +
+
+
{{end}} -- 2.52.0