Compare commits

..

10 Commits

Author SHA1 Message Date
Jonathan Miller aeb36e4312 feat: use manifest API as source of truth for update feed metadata (#592)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Replace custom field + config table cascade with manifest-only read
for extension identity fields (element_name, package_type, display_name,
target_version, php_minimum, description). Config table retained only
for licensing fields (download_gating, key_prefix, support_url fallback).

Fix client field: package/component/library/file → administrator.

Remove issues_model import (custom field lookups removed).
2026-06-09 14:32:39 -05:00
Jonathan Miller 2135f4c37c fix(ci): use echo instead of printf for deploy key write
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 38s
Deploy MokoGitea / deploy (push) Failing after 42s
2026-06-09 14:20:20 -05:00
Jonathan Miller d7bc3c3879 fix(ci): rewrite deploy workflow to fix YAML expression parsing
Generic: Project CI / Lint & Validate (push) Waiting to run
Generic: Project CI / Tests (push) Blocked by required conditions
Deploy MokoGitea / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
- Escape Go template {{ }} in docker inspect with Gitea expression syntax
- Remove Python heredoc updates.xml step (had unescapable { in f-strings)
- Remove maintenance mode steps
- Use $VAR instead of ${VAR} to avoid brace conflicts
2026-06-09 12:43:27 -05:00
Jonathan Miller e7e2c5f7a2 fix(ci): remove maintenance mode steps blocking deploy
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 28s
2026-06-09 12:35:06 -05:00
Jonathan Miller 45a0338fc3 fix(ci): URL-encode JSON braces in maintenance mode data
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 28s
2026-06-09 12:31:59 -05:00
Jonathan Miller ce5f3570fb fix(ci): move JSON braces to env vars to avoid YAML parse error
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 27s
2026-06-09 12:28:57 -05:00
Jonathan Miller 5acf10f766 fix(ci): escape braces in maintenance mode curl data
Generic: Project CI / Lint & Validate (push) Waiting to run
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Gitea Actions expression parser chokes on literal { in run blocks.
Use double-quoted strings with escaped quotes instead.
2026-06-09 12:22:12 -05:00
Jonathan Miller e7fd70e0f2 fix(ci): fix YAML parse error in deploy key step
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 35s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Replace heredoc (KEYEOF) with printf - heredoc terminator
was indented which broke YAML parsing and blocked all runners.
2026-06-09 11:54:42 -05:00
Jonathan Miller 34b1ef6638 fix(ci): prevent deploy key leak in logs
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 29s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Move SSH key write to separate step to avoid env: logging
the private key in CI output. Key has been rotated.
2026-06-09 11:07:03 -05:00
Jonathan Miller ce05f9f3c6 fix(ci): add docker login before registry push, remove MCP submodule
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 40s
Deploy MokoGitea / deploy (push) Failing after 45s
- Add docker login with GITEA_TOKEN before pushing to container registry
  (fixes unauthorized: reqPackageAccess deploy failure)
- Remove mcp-mokogitea-api submodule reference (now standalone repo)
2026-06-09 10:50:35 -05:00
3 changed files with 71 additions and 231 deletions
+28 -145
View File
@@ -11,7 +11,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v1.26.1+MOKO06.12.00)'
description: 'Version tag'
required: true
default: 'latest'
environment:
@@ -47,8 +47,6 @@ jobs:
- name: Determine settings
id: config
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
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
ENV="production"
@@ -56,217 +54,102 @@ jobs:
VERSION="${{ github.event.inputs.version }}"
ENV="${{ github.event.inputs.environment }}"
fi
if [ "$ENV" = "production" ]; then
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
echo "container=mokogitea" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
echo "instance_url=https://code.mokoconsulting.tech" >> $GITHUB_OUTPUT
echo "tag=$VERSION" >> $GITHUB_OUTPUT
else
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
echo "branch=dev" >> $GITHUB_OUTPUT
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
fi
- name: Enable maintenance mode
- name: Write deploy key
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
echo "Enabling 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":true}' \
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Build and deploy via SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
REGISTRY_TOKEN: ${{ secrets.GITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
BRANCH: ${{ steps.config.outputs.branch }}
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
CONTAINER: ${{ steps.config.outputs.container }}
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 "echo 'SSH connected'"
# Pre-deploy cleanup: free disk and memory for the build
$SSH_CMD "
echo 'Cleaning Docker build cache and unused images...'
docker builder 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
echo 'Cleanup complete'
free -m | head -3
"
# Pull latest source
$SSH_CMD "
set -e
if [ ! -d ${SOURCE_DIR}/.git ]; then
git clone -b ${BRANCH} https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${SOURCE_DIR}
if [ ! -d $SOURCE_DIR/.git ]; then
git clone -b $BRANCH https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git $SOURCE_DIR
fi
cd ${SOURCE_DIR}
git fetch origin ${BRANCH}
git reset --hard origin/${BRANCH}
cd $SOURCE_DIR
git fetch origin $BRANCH
git reset --hard origin/$BRANCH
"
# Build Docker image
$SSH_CMD "
set -e
cd ${SOURCE_DIR}
cd $SOURCE_DIR
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 \
-f Dockerfile .
"
# Push to container registry
$SSH_CMD "
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
"
# Update compose and restart
$SSH_CMD "
set -e
cd ${COMPOSE_DIR}
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:${TAG}|' docker-compose.yml
docker compose up -d ${CONTAINER}
cd $COMPOSE_DIR
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
docker compose up -d $CONTAINER
"
# Health check
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
$SSH_CMD "
for i in 1 2 3 4 5 6 7 8; do
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!'
docker inspect --format='Image: {{.Config.Image}}' ${CONTAINER}
docker inspect --format='$IMAGE_FMT' $CONTAINER
exit 0
fi
echo \"Waiting... (attempt \$i/8)\"
echo 'Waiting... (attempt '\$i'/8)'
done
echo 'Health check failed'
docker logs ${CONTAINER} --tail 20
docker logs $CONTAINER --tail 20
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
run: |
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
if: failure()
Submodule mcp-mokogitea-api deleted from c9eb6cfc89
+43 -85
View File
@@ -13,7 +13,6 @@ import (
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
@@ -163,7 +162,7 @@ func NormalizeChannel(ch string) string {
}
// 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 {
Element string
DisplayName string
@@ -176,8 +175,9 @@ type extensionMetadata struct {
KeyPrefix string
}
// resolveExtensionMetadata loads extension metadata with cascading fallback:
// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults.
// resolveExtensionMetadata loads extension metadata from the repo manifest API.
// 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 {
m := extensionMetadata{
Element: strings.ToLower(repo.Name),
@@ -186,91 +186,49 @@ func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository,
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.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 != "" {
m.DownloadGating = cfg.DownloadGating
}
if cfg.KeyPrefix != "" {
m.KeyPrefix = cfg.KeyPrefix
}
}
// Override with custom field values (highest priority).
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
// SupportURL from config as fallback if manifest.InfoURL is empty
if m.SupportURL == "" && cfg.SupportURL != "" {
m.SupportURL = cfg.SupportURL
}
}
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
}
@@ -422,13 +380,13 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
infoURL = meta.SupportURL
}
// Joomla <client> element: packages use client_id=0 in #__extensions,
// so we must output <client>0</client> for Joomla to match the update
// to the installed extension. Other types default to "site" (client_id=0)
// or "administrator" (client_id=1).
// Joomla <client> element: admin-side extensions use "administrator",
// site-side extensions use "site". Packages, components, libraries,
// and files are admin-side by default.
client := "site"
if extType == "package" {
client = "0"
switch extType {
case "package", "component", "library", "file":
client = "administrator"
}
u := xmlUpdate{