Compare commits
18 Commits
dev
...
release-candidate
| Author | SHA1 | Date | |
|---|---|---|---|
| da66b19e77 | |||
| a506e39955 | |||
| aeb36e4312 | |||
| 2135f4c37c | |||
| d7bc3c3879 | |||
| e7e2c5f7a2 | |||
| 45a0338fc3 | |||
| ce5f3570fb | |||
| 5acf10f766 | |||
| e7fd70e0f2 | |||
| 34b1ef6638 | |||
| ce05f9f3c6 | |||
| 6f9d7ca03a | |||
| 1c7d43df38 | |||
| b2b31f6c7b | |||
| 5ba1d0b2e5 | |||
| b762c94a25 | |||
| 6070f7dbd4 |
@@ -4,7 +4,7 @@
|
|||||||
<name>MokoGitea</name>
|
<name>MokoGitea</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||||
<version>06.13.00</version>
|
<version>06.15.00</version>
|
||||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Version tag (e.g. v1.26.1+MOKO06.12.00)'
|
description: 'Version tag'
|
||||||
required: true
|
required: true
|
||||||
default: 'latest'
|
default: 'latest'
|
||||||
environment:
|
environment:
|
||||||
@@ -47,8 +47,6 @@ jobs:
|
|||||||
- name: Determine settings
|
- name: Determine settings
|
||||||
id: config
|
id: config
|
||||||
run: |
|
run: |
|
||||||
# On push to main, auto-deploy to production with git-derived version.
|
|
||||||
# On workflow_dispatch, use the provided inputs.
|
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
|
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
|
||||||
ENV="production"
|
ENV="production"
|
||||||
@@ -56,217 +54,102 @@ jobs:
|
|||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
ENV="${{ github.event.inputs.environment }}"
|
ENV="${{ github.event.inputs.environment }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$ENV" = "production" ]; then
|
if [ "$ENV" = "production" ]; then
|
||||||
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
|
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
|
||||||
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
||||||
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
||||||
echo "branch=main" >> $GITHUB_OUTPUT
|
echo "branch=main" >> $GITHUB_OUTPUT
|
||||||
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "instance_url=https://code.mokoconsulting.tech" >> $GITHUB_OUTPUT
|
|
||||||
else
|
else
|
||||||
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||||
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
||||||
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
||||||
echo "branch=dev" >> $GITHUB_OUTPUT
|
echo "branch=dev" >> $GITHUB_OUTPUT
|
||||||
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
|
||||||
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Enable maintenance mode
|
- name: Write deploy key
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "Enabling maintenance mode on ${INSTANCE_URL}..."
|
mkdir -p ~/.ssh
|
||||||
curl -sf -X POST \
|
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
chmod 600 ~/.ssh/deploy_key
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
"${INSTANCE_URL}/-/admin/config" \
|
|
||||||
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":true}' \
|
|
||||||
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
|
|
||||||
|
|
||||||
- name: Build and deploy via SSH
|
- name: Build and deploy via SSH
|
||||||
env:
|
env:
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
REGISTRY_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
TAG: ${{ steps.config.outputs.tag }}
|
||||||
BRANCH: ${{ steps.config.outputs.branch }}
|
BRANCH: ${{ steps.config.outputs.branch }}
|
||||||
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
||||||
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
||||||
CONTAINER: ${{ steps.config.outputs.container }}
|
CONTAINER: ${{ steps.config.outputs.container }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
|
|
||||||
SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}"
|
SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}"
|
||||||
|
|
||||||
$SSH_CMD "echo 'SSH connected'"
|
$SSH_CMD "echo 'SSH connected'"
|
||||||
|
|
||||||
# Pre-deploy cleanup: free disk and memory for the build
|
|
||||||
$SSH_CMD "
|
$SSH_CMD "
|
||||||
echo 'Cleaning Docker build cache and unused images...'
|
echo 'Cleaning Docker build cache and unused images...'
|
||||||
docker builder prune -af 2>/dev/null || true
|
docker builder prune -af 2>/dev/null || true
|
||||||
docker image prune -af 2>/dev/null || true
|
docker image prune -af 2>/dev/null || true
|
||||||
echo 'Clearing swap...'
|
|
||||||
sudo swapoff -a && sudo swapon -a 2>/dev/null || true
|
sudo swapoff -a && sudo swapon -a 2>/dev/null || true
|
||||||
echo 'Cleanup complete'
|
|
||||||
free -m | head -3
|
free -m | head -3
|
||||||
"
|
"
|
||||||
|
|
||||||
# Pull latest source
|
|
||||||
$SSH_CMD "
|
$SSH_CMD "
|
||||||
set -e
|
set -e
|
||||||
if [ ! -d ${SOURCE_DIR}/.git ]; then
|
if [ ! -d $SOURCE_DIR/.git ]; then
|
||||||
git clone -b ${BRANCH} https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${SOURCE_DIR}
|
git clone -b $BRANCH https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git $SOURCE_DIR
|
||||||
fi
|
fi
|
||||||
cd ${SOURCE_DIR}
|
cd $SOURCE_DIR
|
||||||
git fetch origin ${BRANCH}
|
git fetch origin $BRANCH
|
||||||
git reset --hard origin/${BRANCH}
|
git reset --hard origin/$BRANCH
|
||||||
"
|
"
|
||||||
|
|
||||||
# Build Docker image
|
|
||||||
$SSH_CMD "
|
$SSH_CMD "
|
||||||
set -e
|
set -e
|
||||||
cd ${SOURCE_DIR}
|
cd $SOURCE_DIR
|
||||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG} \
|
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
"
|
"
|
||||||
|
|
||||||
# Push to container registry
|
|
||||||
$SSH_CMD "
|
$SSH_CMD "
|
||||||
set -e
|
set -e
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG}
|
echo '$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||||
"
|
"
|
||||||
|
|
||||||
# Update compose and restart
|
|
||||||
$SSH_CMD "
|
$SSH_CMD "
|
||||||
set -e
|
set -e
|
||||||
cd ${COMPOSE_DIR}
|
cd $COMPOSE_DIR
|
||||||
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:${TAG}|' docker-compose.yml
|
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
|
||||||
docker compose up -d ${CONTAINER}
|
docker compose up -d $CONTAINER
|
||||||
"
|
"
|
||||||
|
|
||||||
# Health check
|
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
||||||
|
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
|
||||||
$SSH_CMD "
|
$SSH_CMD "
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
sleep 15
|
sleep 15
|
||||||
if docker inspect --format='{{.State.Health.Status}}' ${CONTAINER} 2>/dev/null | grep -q healthy; then
|
if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then
|
||||||
echo 'Container healthy!'
|
echo 'Container healthy!'
|
||||||
docker inspect --format='Image: {{.Config.Image}}' ${CONTAINER}
|
docker inspect --format='$IMAGE_FMT' $CONTAINER
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo \"Waiting... (attempt \$i/8)\"
|
echo 'Waiting... (attempt '\$i'/8)'
|
||||||
done
|
done
|
||||||
echo 'Health check failed'
|
echo 'Health check failed'
|
||||||
docker logs ${CONTAINER} --tail 20
|
docker logs $CONTAINER --tail 20
|
||||||
exit 1
|
exit 1
|
||||||
"
|
"
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: success()
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
|
||||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
|
||||||
DEPLOY_ENV: ${{ github.event.inputs.environment || 'production' }}
|
|
||||||
run: |
|
|
||||||
# Only update updates.xml for production stable releases
|
|
||||||
if [ "$DEPLOY_ENV" != "production" ]; then
|
|
||||||
echo "Skipping updates.xml — dev deployments don't update stable channel"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract project version by stripping the version prefix from the tag.
|
|
||||||
# Reads prefix from manifest API (e.g. "v1.26.1+MOKO"), falls back to legacy pattern.
|
|
||||||
API_BASE="https://${REGISTRY}/api/v1/repos/MokoConsulting/MokoGitea"
|
|
||||||
PREFIX=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API_BASE}/manifest" | python3 -c "import json,sys; print(json.load(sys.stdin).get('version_prefix',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$PREFIX" ]; then
|
|
||||||
MOKO_VER="${TAG#$PREFIX}"
|
|
||||||
else
|
|
||||||
# Legacy fallback: strip everything up to and including "-moko."
|
|
||||||
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$MOKO_VER" ]; then
|
|
||||||
echo "Could not extract version from tag: $TAG (prefix: ${PREFIX:-none})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
RELEASE_URL="https://${REGISTRY}/MokoConsulting/MokoGitea/releases/tag/${TAG}"
|
|
||||||
DOCKER_IMG="${REGISTRY}/${IMAGE}:${TAG}"
|
|
||||||
|
|
||||||
python3 << PYEOF
|
|
||||||
import json, os, re, base64, urllib.request
|
|
||||||
|
|
||||||
token = os.environ["GITEA_TOKEN"]
|
|
||||||
registry = os.environ["REGISTRY"]
|
|
||||||
tag = os.environ["TAG"]
|
|
||||||
moko_ver = os.environ["MOKO_VER"]
|
|
||||||
release_url = os.environ["RELEASE_URL"]
|
|
||||||
docker_img = os.environ["DOCKER_IMG"]
|
|
||||||
api = f"https://{registry}/api/v1/repos/MokoConsulting/MokoGitea"
|
|
||||||
|
|
||||||
# Fetch current updates.xml
|
|
||||||
req = urllib.request.Request(f"{api}/contents/updates.xml?ref=main",
|
|
||||||
headers={"Authorization": f"token {token}"})
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
data = json.loads(resp.read())
|
|
||||||
sha = data["sha"]
|
|
||||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
|
||||||
|
|
||||||
# Update stable channel — match the <update> block containing <tag>stable</tag>
|
|
||||||
def replace_channel(xml, channel, ver, url, docker):
|
|
||||||
pattern = rf"(<update>\s*<name>MokoGitea</name>[\s\S]*?<tags><tag>{channel}</tag></tags>[\s\S]*?</update>)"
|
|
||||||
def replacer(m):
|
|
||||||
block = m.group(1)
|
|
||||||
block = re.sub(r"<version>[^<]*</version>", f"<version>{ver}</version>", block)
|
|
||||||
block = re.sub(r"(<infourl[^>]*>)[^<]*(</infourl>)", rf"\1{url}\2", block)
|
|
||||||
block = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\1{docker}\2", block)
|
|
||||||
return block
|
|
||||||
return re.sub(pattern, replacer, xml)
|
|
||||||
|
|
||||||
content = replace_channel(content, "stable", moko_ver, release_url, docker_img)
|
|
||||||
content = re.sub(r"VERSION: [^\n]*", f"VERSION: {moko_ver}", content)
|
|
||||||
|
|
||||||
# Push updated file
|
|
||||||
encoded = base64.b64encode(content.encode()).decode()
|
|
||||||
payload = json.dumps({
|
|
||||||
"message": f"chore(ci): update updates.xml to {moko_ver}",
|
|
||||||
"content": encoded,
|
|
||||||
"sha": sha,
|
|
||||||
"branch": "main",
|
|
||||||
}).encode()
|
|
||||||
req = urllib.request.Request(f"{api}/contents/updates.xml",
|
|
||||||
data=payload, method="PUT",
|
|
||||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
print(f"updates.xml updated to {moko_ver}")
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
- name: Disable maintenance mode
|
|
||||||
if: always()
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
|
||||||
run: |
|
|
||||||
echo "Disabling maintenance mode on ${INSTANCE_URL}..."
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
"${INSTANCE_URL}/-/admin/config" \
|
|
||||||
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":false}' \
|
|
||||||
|| echo "WARNING: Could not disable maintenance mode"
|
|
||||||
|
|
||||||
- name: Verify
|
- name: Verify
|
||||||
run: |
|
run: |
|
||||||
sleep 5
|
sleep 5
|
||||||
curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " — API healthy"
|
curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy"
|
||||||
|
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Automation
|
# INGROUP: mokoplatform.Automation
|
||||||
# VERSION: 06.13.00
|
# VERSION: 06.15.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
+5
-73
@@ -1,9 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [06.15.00] --- 2026-06-09
|
||||||
|
|
||||||
|
|
||||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||||
|
|
||||||
## [Unreleased]
|
## [06.14.00] --- 2026-06-09
|
||||||
|
|
||||||
* FEATURES
|
* FEATURES
|
||||||
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
|
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
|
||||||
@@ -155,75 +159,3 @@ All notable changes to MokoGitea are documented here. Versions follow the format
|
|||||||
* fix(build): restore build/ directory after accidental deletion
|
* fix(build): restore build/ directory after accidental deletion
|
||||||
* fix(licenses): master key banner removed, master keys sort first in table
|
* fix(licenses): master key banner removed, master keys sort first in table
|
||||||
* fix(issues): issue sidebar loads org-level fields instead of legacy repo-level fields
|
* fix(issues): issue sidebar loads org-level fields instead of legacy repo-level fields
|
||||||
|
|
||||||
## [v1.26.1-moko.05] - 2026-05-31
|
|
||||||
|
|
||||||
* BREAKING CHANGES
|
|
||||||
* Deprecated Issue.Ref branch selector UI (#307)
|
|
||||||
* Removed branch/tag selector from issue sidebar and new issue form
|
|
||||||
* DB column and commit-close logic preserved for backward compatibility
|
|
||||||
* FEATURES
|
|
||||||
* feat(ui): generic combo-multiselect component (#361)
|
|
||||||
* Reusable dropdown with search, checkable items, and selected-items display
|
|
||||||
* Template: `shared/combolist.tmpl`
|
|
||||||
* feat(updates): extension metadata settings for update feed generation
|
|
||||||
* feat(licenses): platform enforcement, key deletion, expired key cleanup
|
|
||||||
* feat(actions): rebrand actions bot user to mokogitea-actions (#233, #234)
|
|
||||||
* Backward-compatible: recognizes github-actions[bot], gitea-actions[bot]
|
|
||||||
* feat(actions): actions bot user in branch protection whitelist (#233, #234)
|
|
||||||
* WhitelistActionsUser, MergeWhitelistActionsUser, ForcePushAllowlistActionsUser
|
|
||||||
* TECH DEBT
|
|
||||||
* chore: full namespace migration to code.mokoconsulting.tech (#336, #337, #344)
|
|
||||||
* fix(blame): set HasSourceRenderedToggle for renderable files (#344)
|
|
||||||
* fix(settings): translate team permission strings via data-locale (#344)
|
|
||||||
* fix(dropzone): use relative path for non-image attachment markdown links (#344)
|
|
||||||
* fix(templates): add required validation to issue dropdown fields (#350)
|
|
||||||
* refactor(go): replace ValuesRepository with maps.Values (Go 1.21+) (#357)
|
|
||||||
* refactor(go): remove CanEnableEditor wrapper (#357)
|
|
||||||
* fix(ts): parseIssueHref uses URL pathname and trims appSubUrl (#360)
|
|
||||||
* fix(actions): enforce MaxJobNumPerRun (256) limit (#360)
|
|
||||||
* fix(css): use calc(infinity * 1px) for --border-radius-full (#361)
|
|
||||||
* fix(css): remove legacy .center class, replace with tw-text-center (#361)
|
|
||||||
* fix(routes): remove dead legacy /cherry-pick/{sha} route
|
|
||||||
* fix(feed): use full ref name instead of ShortName for file feed revision
|
|
||||||
* BUGFIXES
|
|
||||||
* fix(build): use slices.Collect for maps.Values (Go 1.23+ compat)
|
|
||||||
* fix(licenses): remove duplicate DeleteLicenseKey declaration
|
|
||||||
* fix(licenses): only show licenses tab when licensing is enabled
|
|
||||||
* fix(licenses): show feed URLs based on repo update platform setting
|
|
||||||
* fix(updates): correct dlid prefix and align XML with Joomla standard
|
|
||||||
* INFRASTRUCTURE
|
|
||||||
* fix(ci): auto-deploy to production on merge to main (#235)
|
|
||||||
|
|
||||||
## [v1.26.1-moko.04] - 2026-05-24
|
|
||||||
|
|
||||||
* SECURITY
|
|
||||||
* Backport 12 upstream v1.26.2 security fixes:
|
|
||||||
* golang.org/x/net v0.55.0 security update (#140)
|
|
||||||
* Token scope enforcement on raw/media/attachment downloads (#141)
|
|
||||||
* OAuth PKCE hardening and refresh token replay protection (#142)
|
|
||||||
* Wiki git write and LFS token access enforcement (#143)
|
|
||||||
* Public-only token filtering in API queries (#144)
|
|
||||||
* Artifact signature payload hardening (#146)
|
|
||||||
* AWS credentials encryption (#161)
|
|
||||||
* Mermaid v11.15.0 security update (#162)
|
|
||||||
* Composer package permission check (#164)
|
|
||||||
* BUGFIXES
|
|
||||||
* fix(actions): nil pointer dereference in concurrency during PR creation (#136)
|
|
||||||
* fix(ui): actions runs list broken row layout (#138)
|
|
||||||
* fix: scheduled action panic with null event payload
|
|
||||||
* fix: treat email addresses case-insensitively
|
|
||||||
* fix: .mod lexer panic — removed invalid AMPL mapping
|
|
||||||
* FEATURES
|
|
||||||
* Joomla-style updates.xml with channel selection
|
|
||||||
* Update checker with configurable CHANNEL setting
|
|
||||||
* Admin dashboard update banner with docker pull command
|
|
||||||
* Upstream bug sync workflow — daily automated issue creation
|
|
||||||
* PR RC release workflow — auto-build RC on PR to main
|
|
||||||
* INFRASTRUCTURE
|
|
||||||
* New 3-part versioning: v{upstream}-moko.{major}.{minor}.{patch}
|
|
||||||
* Branding updates: error pages, home page, settings link
|
|
||||||
* Deploy workflow updated for new version format
|
|
||||||
* PROCESS
|
|
||||||
* Created `type: bug` and `upstream` labels for automated issue tracking
|
|
||||||
* Closed 24 upstream bug/security issues after backporting
|
|
||||||
|
|||||||
Submodule mcp-mokogitea-api deleted from c9eb6cfc89
+11
-9
@@ -157,7 +157,8 @@ func Wiki(ctx *context.Context) {
|
|||||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit.
|
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
|
||||||
|
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .profile.wiki.git).
|
||||||
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
|
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
|
||||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
|
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -167,19 +168,20 @@ func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*rep
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbRepo.IsEmpty {
|
// Open the wiki git repo (.wiki.git sidecar), not the main repo.
|
||||||
|
wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo.WikiStorageRepo())
|
||||||
|
if err != nil {
|
||||||
|
// Wiki repo doesn't exist yet — not an error, just no wiki.
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo)
|
branch := dbRepo.DefaultWikiBranch
|
||||||
if err != nil {
|
if branch == "" {
|
||||||
log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err)
|
branch = "main"
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
commit, err := wikiGitRepo.GetBranchCommit(branch)
|
||||||
commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err)
|
log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ type PrepareOwnerHeaderResult struct {
|
|||||||
const (
|
const (
|
||||||
RepoNameProfilePrivate = ".profile-private"
|
RepoNameProfilePrivate = ".profile-private"
|
||||||
RepoNameProfile = ".profile"
|
RepoNameProfile = ".profile"
|
||||||
RepoNameWikiPublic = "wiki"
|
RepoNameWikiPublic = ".profile"
|
||||||
RepoNameWikiPrivate = "wiki-private"
|
RepoNameWikiPrivate = ".profile-private"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
||||||
@@ -209,11 +209,13 @@ func loadHeaderCount(ctx *context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty.
|
// OrgWikiRepoExists checks whether a profile repo's wiki exists and has content.
|
||||||
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
|
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
|
||||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
|
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
|
||||||
if err != nil || dbRepo.IsEmpty {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
// Check if the wiki sidecar repo exists by trying to get its default branch.
|
||||||
|
_, err = gitrepo.GetDefaultBranch(ctx, dbRepo.WikiStorageRepo())
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||||
@@ -163,7 +162,7 @@ func NormalizeChannel(ch string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extensionMetadata holds resolved metadata for feed generation.
|
// extensionMetadata holds resolved metadata for feed generation.
|
||||||
// Fields are resolved with priority: custom field → config table → default.
|
// Fields are resolved with priority: manifest → config table (gating only) → default.
|
||||||
type extensionMetadata struct {
|
type extensionMetadata struct {
|
||||||
Element string
|
Element string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
@@ -176,8 +175,9 @@ type extensionMetadata struct {
|
|||||||
KeyPrefix string
|
KeyPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveExtensionMetadata loads extension metadata with cascading fallback:
|
// resolveExtensionMetadata loads extension metadata from the repo manifest API.
|
||||||
// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults.
|
// The manifest is the single source of truth for extension identity fields.
|
||||||
|
// The config table is only used for licensing/gating fields not in the manifest.
|
||||||
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata {
|
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata {
|
||||||
m := extensionMetadata{
|
m := extensionMetadata{
|
||||||
Element: strings.ToLower(repo.Name),
|
Element: strings.ToLower(repo.Name),
|
||||||
@@ -186,91 +186,49 @@ func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository,
|
|||||||
TargetVersion: "(5|6)\\..*",
|
TargetVersion: "(5|6)\\..*",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply config table values.
|
// Manifest is the source of truth for extension metadata.
|
||||||
|
manifest, err := repo_model.GetRepoManifest(ctx, repo.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("resolveExtensionMetadata: GetRepoManifest for repo %d: %v", repo.ID, err)
|
||||||
|
}
|
||||||
|
if manifest != nil {
|
||||||
|
if manifest.ElementName != "" {
|
||||||
|
m.Element = manifest.ElementName
|
||||||
|
}
|
||||||
|
if manifest.PackageType != "" {
|
||||||
|
m.ExtType = manifest.PackageType
|
||||||
|
}
|
||||||
|
if manifest.DisplayName != "" {
|
||||||
|
m.DisplayName = manifest.DisplayName
|
||||||
|
}
|
||||||
|
if manifest.TargetVersion != "" {
|
||||||
|
m.TargetVersion = manifest.TargetVersion
|
||||||
|
}
|
||||||
|
if manifest.PHPMinimum != "" {
|
||||||
|
m.PHPMinimum = manifest.PHPMinimum
|
||||||
|
}
|
||||||
|
if manifest.Description != "" {
|
||||||
|
m.Description = manifest.Description
|
||||||
|
}
|
||||||
|
if manifest.InfoURL != "" {
|
||||||
|
m.SupportURL = manifest.InfoURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config table: only licensing/gating fields (not in manifest).
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
if cfg.ExtensionName != "" {
|
|
||||||
m.Element = cfg.ExtensionName
|
|
||||||
}
|
|
||||||
if cfg.DisplayName != "" {
|
|
||||||
m.DisplayName = cfg.DisplayName
|
|
||||||
}
|
|
||||||
if cfg.ExtensionType != "" {
|
|
||||||
m.ExtType = cfg.ExtensionType
|
|
||||||
}
|
|
||||||
if cfg.TargetVersion != "" {
|
|
||||||
m.TargetVersion = cfg.TargetVersion
|
|
||||||
}
|
|
||||||
if cfg.PHPMinimum != "" {
|
|
||||||
m.PHPMinimum = cfg.PHPMinimum
|
|
||||||
}
|
|
||||||
if cfg.Description != "" {
|
|
||||||
m.Description = cfg.Description
|
|
||||||
}
|
|
||||||
if cfg.SupportURL != "" {
|
|
||||||
m.SupportURL = cfg.SupportURL
|
|
||||||
}
|
|
||||||
if cfg.DownloadGating != "" {
|
if cfg.DownloadGating != "" {
|
||||||
m.DownloadGating = cfg.DownloadGating
|
m.DownloadGating = cfg.DownloadGating
|
||||||
}
|
}
|
||||||
if cfg.KeyPrefix != "" {
|
if cfg.KeyPrefix != "" {
|
||||||
m.KeyPrefix = cfg.KeyPrefix
|
m.KeyPrefix = cfg.KeyPrefix
|
||||||
}
|
}
|
||||||
}
|
// SupportURL from config as fallback if manifest.InfoURL is empty
|
||||||
|
if m.SupportURL == "" && cfg.SupportURL != "" {
|
||||||
// Override with custom field values (highest priority).
|
m.SupportURL = cfg.SupportURL
|
||||||
fields, err := issues_model.GetCustomFieldsByOwner(ctx, repo.OwnerID, issues_model.CustomFieldScopeRepo)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("resolveExtensionMetadata: GetCustomFieldsByOwner for repo %d: %v", repo.ID, err)
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
if len(fields) == 0 {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
values, err := issues_model.GetCustomFieldValuesMap(ctx, repo.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("resolveExtensionMetadata: GetCustomFieldValuesMap for repo %d: %v", repo.ID, err)
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
if len(values) == 0 {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build name → value map from field definitions + values.
|
|
||||||
named := make(map[string]string, len(fields))
|
|
||||||
for _, f := range fields {
|
|
||||||
if v, ok := values[f.ID]; ok && v != "" {
|
|
||||||
named[f.Name] = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := named["Extension Name"]; v != "" {
|
|
||||||
m.Element = v
|
|
||||||
}
|
|
||||||
if v := named["Display Name"]; v != "" {
|
|
||||||
m.DisplayName = v
|
|
||||||
}
|
|
||||||
if v := named["Extension Type"]; v != "" {
|
|
||||||
m.ExtType = v
|
|
||||||
}
|
|
||||||
if v := named["Target Version"]; v != "" {
|
|
||||||
m.TargetVersion = v
|
|
||||||
}
|
|
||||||
if v := named["PHP Minimum"]; v != "" {
|
|
||||||
m.PHPMinimum = v
|
|
||||||
}
|
|
||||||
if v := named["Support URL"]; v != "" {
|
|
||||||
m.SupportURL = v
|
|
||||||
}
|
|
||||||
if v := named["Description"]; v != "" {
|
|
||||||
m.Description = v
|
|
||||||
}
|
|
||||||
if v := named["Download Gating"]; v != "" {
|
|
||||||
m.DownloadGating = v
|
|
||||||
}
|
|
||||||
if v := named["Key Prefix"]; v != "" {
|
|
||||||
m.KeyPrefix = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,13 +380,13 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
|||||||
infoURL = meta.SupportURL
|
infoURL = meta.SupportURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Joomla <client> element: packages use client_id=0 in #__extensions,
|
// Joomla <client> element: admin-side extensions use "administrator",
|
||||||
// so we must output <client>0</client> for Joomla to match the update
|
// site-side extensions use "site". Packages, components, libraries,
|
||||||
// to the installed extension. Other types default to "site" (client_id=0)
|
// and files are admin-side by default.
|
||||||
// or "administrator" (client_id=1).
|
|
||||||
client := "site"
|
client := "site"
|
||||||
if extType == "package" {
|
switch extType {
|
||||||
client = "0"
|
case "package", "component", "library", "file":
|
||||||
|
client = "administrator"
|
||||||
}
|
}
|
||||||
|
|
||||||
u := xmlUpdate{
|
u := xmlUpdate{
|
||||||
|
|||||||
@@ -68,11 +68,11 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
|
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
|
||||||
<label>Internal wiki (uses <code>wiki</code> / <code>wiki-private</code> repos)</label>
|
<label>Internal wiki (uses <code>.profile</code> / <code>.profile-private</code> repo wikis)</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
|
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
|
||||||
<p class="help">Create repos named <code>wiki</code> (public) and/or <code>wiki-private</code> (members-only) under this organization.</p>
|
<p class="help">Enable the wiki on <code>.profile</code> (public) and/or <code>.profile-private</code> (members-only) repos.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
This organization doesn't have a wiki yet.
|
This organization doesn't have a wiki yet.
|
||||||
</div>
|
</div>
|
||||||
<p class="tw-text-center">
|
<p class="tw-text-center">
|
||||||
Create a repository named <code>wiki</code> (public) or <code>wiki-private</code> (members-only)
|
Enable the wiki on the <code>.profile</code> (public) or <code>.profile-private</code> (members-only)
|
||||||
with markdown files to get started.
|
repository to get started.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
Reference in New Issue
Block a user