Compare commits

..

9 Commits

7 changed files with 7 additions and 264 deletions
+2 -104
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
required: true
default: 'latest'
environment:
@@ -30,7 +30,6 @@ env:
DEPLOY_HOST: git.mokoconsulting.tech
DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
deploy:
@@ -48,30 +47,15 @@ jobs:
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
echo "instance_url=https://git.mokoconsulting.tech" >> $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
fi
- name: Enable maintenance mode
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
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)"
- name: Build and deploy via SSH
- name: Build, push, and deploy via SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TAG: ${{ steps.config.outputs.tag }}
@@ -140,92 +124,6 @@ jobs:
exit 1
"
- name: Update updates.xml
if: success()
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
DEPLOY_ENV: ${{ github.event.inputs.environment }}
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 moko version from tag (e.g. v1.26.1-moko.05.01.01 -> 05.01.01)
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
if [ -z "$MOKO_VER" ]; then
echo "Could not extract moko version from tag: $TAG"
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 version, infourl, and docker tag
content = re.sub(
r"(<tags><tag>stable</tag></tags>[\s\S]*?<version>)[^<]*(</version>)",
rf"\g<1>{moko_ver}\2", content)
content = re.sub(
r"(<tags><tag>stable</tag></tags>[\s\S]*?<infourl[^>]*>)[^<]*(</infourl>)",
rf"\g<1>{release_url}\2", content)
content = re.sub(
r"(<tags><tag>stable</tag></tags>[\s\S]*?<downloadurl[^>]*>)[^<]*(</downloadurl>)",
rf"\g<1>{docker_img}\2", content)
# Also update VERSION comment at top
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
+1 -1
View File
@@ -32,7 +32,7 @@ var (
UpdateChecker = struct {
Enabled bool
Endpoint string
Channel string // stable, rc, beta, alpha, development
Channel string // stable, dev, security
}{
Enabled: true,
Endpoint: "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/updates.xml",
+4 -15
View File
@@ -26,14 +26,9 @@ type UpdateInfo struct {
CheckedAt time.Time
}
// NotifyFunc is called when a new update is detected for the first time.
// Set this from the cron/mailer layer to send admin email notifications.
var NotifyFunc func(info *UpdateInfo)
var (
cachedInfo *UpdateInfo
lastNotifiedVer string
mu sync.RWMutex
cachedInfo *UpdateInfo
mu sync.RWMutex
)
// xmlUpdates mirrors the updates.xml structure (Joomla-style).
@@ -139,22 +134,16 @@ func CheckForUpdate() error {
}
// Update is available if the latest version string is not a prefix of the current version.
// e.g., current "1.26.1+305-gabcdef" does not start with "04.00.00"
// This handles both moko semver and git-describe suffixed versions.
info.UpdateAvailable = latestVersion != "" && !strings.Contains(currentVersion, latestVersion)
mu.Lock()
cachedInfo = info
// Notify only once per new version (avoid spamming on every cron tick)
shouldNotify := info.UpdateAvailable && latestVersion != lastNotifiedVer
if shouldNotify {
lastNotifiedVer = latestVersion
}
mu.Unlock()
if info.UpdateAvailable {
log.Info("MokoGitea update available: %s [%s] (current: %s)", latestVersion, channel, currentVersion)
if shouldNotify && NotifyFunc != nil {
NotifyFunc(info)
}
} else {
log.Debug("MokoGitea is up to date: %s [%s]", currentVersion, channel)
}
-31
View File
@@ -144,14 +144,6 @@ func Dashboard(ctx *context.Context) {
ctx.Data["ReleaseURL"] = info.ReleaseURL
ctx.Data["UpdateChannel"] = info.Channel
ctx.Data["DockerImage"] = info.DockerImage
ctx.Data["CurrentUpdateChannel"] = setting.UpdateChecker.Channel
ctx.Data["UpdateChannels"] = []map[string]string{
{"value": "stable", "label": "Stable", "desc": "Production-ready releases"},
{"value": "rc", "label": "Release Candidate", "desc": "Pre-release builds from merged PRs"},
{"value": "beta", "label": "Beta", "desc": "Feature-complete, under testing"},
{"value": "alpha", "label": "Alpha", "desc": "Early access, may have rough edges"},
{"value": "development", "label": "Development", "desc": "Latest dev branch, bleeding edge"},
}
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
@@ -174,29 +166,6 @@ func DashboardPost(ctx *context.Context) {
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
// Handle update channel change
if channel := ctx.FormString("update_channel"); channel != "" {
validChannels := []string{"stable", "rc", "beta", "alpha", "development"}
isValid := false
for _, v := range validChannels {
if channel == v {
isValid = true
break
}
}
if isValid {
setting.UpdateChecker.Channel = channel
go func() {
if err := updatechecker.CheckForUpdate(); err != nil {
log.Error("CheckForUpdate after channel change: %v", err)
}
}()
ctx.Flash.Success("Update channel changed to: " + channel)
}
ctx.Redirect(setting.AppSubURL + "/-/admin")
return
}
// Run operation.
if form.Op != "" {
switch form.Op {
-6
View File
@@ -15,7 +15,6 @@ import (
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/updatechecker"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mailer"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/migrations"
mirror_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mirror"
packages_cleanup_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/packages/cleanup"
@@ -191,11 +190,6 @@ func initBasicTasks() {
}
func registerUpdateChecker() {
// Wire up email notification for admin when updates are detected
updatechecker.NotifyFunc = func(info *updatechecker.UpdateInfo) {
mailer.SendUpdateNotification(info.LatestVersion, info.Channel, info.ReleaseURL, info.DockerImage)
}
RegisterTaskFatal("update_checker", &BaseConfig{
Enabled: true,
RunAtStart: true,
-93
View File
@@ -1,93 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package mailer
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
sender_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mailer/sender"
)
// SendUpdateNotification emails the admin and sends ntfy push when a new MokoGitea version is available.
func SendUpdateNotification(version, channel, releaseURL, dockerImage string) {
subject := fmt.Sprintf("[MokoGitea] Update available: %s (%s)", version, channel)
body := fmt.Sprintf(`MokoGitea Update Available
A new version is available on the %s channel.
Version: %s
Channel: %s
Current: %s`, channel, version, channel, setting.AppVer)
if releaseURL != "" {
body += fmt.Sprintf("\nRelease: %s", releaseURL)
}
if dockerImage != "" {
body += fmt.Sprintf("\nDocker: docker pull %s", dockerImage)
}
body += fmt.Sprintf("\n\nUpdate the channel in Site Administration > Dashboard.\n\n— %s", setting.AppName)
// Send email to admin
if setting.MailService != nil {
admin, err := user_model.GetAdminUser(context.Background())
if err != nil {
log.Error("SendUpdateNotification: GetAdminUser: %v", err)
} else {
msg := sender_service.NewMessage(admin.EmailTo(), subject, body)
msg.Info = "Update notification"
SendAsync(msg)
log.Info("Update email sent to %s for version %s [%s]", admin.Email, version, channel)
}
}
// Send ntfy push notification
if setting.Ntfy.Enabled && setting.Ntfy.ServerURL != "" {
sendNtfyNotification(subject, body, releaseURL)
}
}
func sendNtfyNotification(title, body, clickURL string) {
url := fmt.Sprintf("%s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
req, err := http.NewRequest("POST", url, bytes.NewBufferString(body))
if err != nil {
log.Error("ntfy: create request: %v", err)
return
}
req.Header.Set("Title", title)
req.Header.Set("Priority", "high")
req.Header.Set("Tags", "arrow_up,mokogitea")
if clickURL != "" {
req.Header.Set("Click", clickURL)
}
if setting.Ntfy.Token != "" {
req.Header.Set("Authorization", "Bearer "+setting.Ntfy.Token)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error("ntfy: send notification: %v", err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= 300 {
log.Error("ntfy: unexpected status %d", resp.StatusCode)
return
}
log.Info("ntfy: update notification sent to %s/%s", setting.Ntfy.ServerURL, setting.Ntfy.DefaultTopic)
}
-14
View File
@@ -8,20 +8,6 @@
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
</div>
{{end}}
<div class="ui segment">
<h4 class="ui header">{{svg "octicon-broadcast" 16}} Update Channel</h4>
<form method="post" action="{{AppSubUrl}}/-/admin" class="tw-flex tw-items-end tw-gap-4">
{{.CsrfTokenHtml}}
<div class="field tw-flex-1">
<select name="update_channel" class="ui dropdown">
{{range .UpdateChannels}}
<option value="{{.value}}" {{if eq $.CurrentUpdateChannel .value}}selected{{end}}>{{.label}}{{.desc}}</option>
{{end}}
</select>
</div>
<button type="submit" class="ui primary button">{{svg "octicon-sync" 14}} Apply</button>
</form>
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
</h4>