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/.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/.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 0aea9e753f..64ac781fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ - 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 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) 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 diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts index b8468610ca..d633aa9502 100644 --- a/web_src/js/features/admin/config.test.ts +++ b/web_src/js/features/admin/config.test.ts @@ -21,6 +21,17 @@ test('ConfigFormValueMapper', () => { + + + + + + + + + + + `; @@ -44,5 +55,7 @@ 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"}', + 'landingDefault': '{"Mode":"home"}', }); }); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 96c7dcf84b..64cba73213 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -99,6 +99,10 @@ 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. + // 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); @@ -120,6 +124,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 +146,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 +207,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);