Compare commits
1 Commits
main
..
feat/vnext
| Author | SHA1 | Date | |
|---|---|---|---|
| 10760463f8 |
@@ -39,10 +39,17 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# NOTE: removed a "Verify dev environment is healthy" gate that curled
|
||||
# git.dev and `exit 1` on failure — gating a PROD deploy on a live DEV
|
||||
# health check is a cross-tier false-negative (a transient dev blip blocked
|
||||
# prod). Pre-prod validation is the dev -> rc -> main pipeline + RC env.
|
||||
- name: Verify dev environment is healthy
|
||||
run: |
|
||||
echo "Checking git.dev.mokoconsulting.tech health..."
|
||||
if curl -sf --max-time 10 https://git.dev.mokoconsulting.tech/api/healthz; then
|
||||
echo " Dev environment is healthy — proceeding with production deploy"
|
||||
else
|
||||
echo "::error::Dev environment is NOT healthy — blocking production deploy"
|
||||
echo "Deploy to dev first (push to dev branch) and verify it passes before merging to main."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout source (for version detection)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -59,19 +66,17 @@ jobs:
|
||||
ENV="${{ github.event.inputs.environment }}"
|
||||
fi
|
||||
if [ "$ENV" = "production" ]; then
|
||||
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||
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 "tag_env=MOKOGITEA_TAG" >> $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 "tag_env=MOKOGITEA_DEV_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Write deploy key
|
||||
@@ -90,7 +95,6 @@ jobs:
|
||||
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
||||
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
||||
CONTAINER: ${{ steps.config.outputs.container }}
|
||||
TAG_ENV: ${{ steps.config.outputs.tag_env }}
|
||||
run: |
|
||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
||||
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
|
||||
@@ -130,12 +134,8 @@ jobs:
|
||||
|
||||
echo 'Restarting container...'
|
||||
cd $COMPOSE_DIR
|
||||
# Drive ONLY the target service's tag via its compose env-var (no sed —
|
||||
# a blanket 'sed s|mokogitea:...|' clobbered the dev + rc service tags too).
|
||||
# Remove any lingering fixed-name container first (avoids the "name already
|
||||
# in use" conflict), then force-recreate under the shared compose project.
|
||||
docker rm -f $CONTAINER 2>/dev/null || true
|
||||
env $TAG_ENV="$TAG" docker compose -p gitea-dev up -d --force-recreate $CONTAINER
|
||||
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
|
||||
docker compose up -d $CONTAINER
|
||||
|
||||
echo 'Health check...'
|
||||
for i in 1 2 3 4 5 6 7 8; do
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
||||
# VERSION: 05.02.01
|
||||
# VERSION: 05.02.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
@@ -162,13 +162,7 @@ jobs:
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
# Push the bump commit, but do NOT fail the release if the target branch
|
||||
# is protected and the release identity is not on the push allowlist.
|
||||
# The build proceeds from the in-tree bumped version regardless; if the
|
||||
# push is rejected, the next run simply re-bumps from the same base.
|
||||
if ! git push origin HEAD 2>&1; then
|
||||
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
|
||||
fi
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
@@ -280,4 +274,4 @@ jobs:
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -63,8 +63,6 @@
|
||||
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
||||
|
||||
### Fixed
|
||||
- Repo metadata `org` is now **derived from the org profile** (the repository owner) instead of being stored/editable, so it can never drift when an organization is renamed. The `org` column is dropped from `repo_manifest` (migration #368) and the field is derived on read via `Repository.DerivedOrgName` (owner display name, falling back to the handle) across the metadata API, the Settings → Metadata page (now read-only), and the Joomla update-server feed. The API `PUT` and MCP `metadata_update` now ignore `org` (read-only, like the already-derived `display_name`) (#771)
|
||||
- Admin Branding: uploading a custom **Nav Icon** now also sets the **app icon** (`logo.png`, the PWA / web-manifest icon and navbar fallback), so a branded instance shows its own installable app icon instead of the shipped default; resetting the Nav Icon reverts both (#773)
|
||||
- Fork server binary now compiles: `routers/api/v1/api.go` called `organization.HasOrgOrUserVisible`, which had been renamed to `IsOwnerVisibleToDoer`; the one missed call site broke `go build` of the entire `routers/api/v1` package (CI's Lint & Validate does not run a full build, so it went unnoticed) (#735)
|
||||
- Dev deploy workflow: the build/deploy step referenced runner-side values as `\$TAG` / `\$REGISTRY_TOKEN` inside an unquoted SSH heredoc, deferring expansion to the remote shell where those names are unset — the Docker tag collapsed to an empty `mokogitea:` and every dev deploy failed with `invalid reference format`. Runner values are now injected via an ssh env-prefix and the heredoc is quoted so each `$var` expands in exactly one place (#737)
|
||||
- Repaired unit-test compile and `go vet` failures: `CryptoRandomInt/String/Bytes` now return two values (updated `modules/util/util_test.go`), removed a redundant `&&` condition in `issue_comment.go`, and cleaned up isolated integration-test compile errors (#736)
|
||||
|
||||
@@ -445,7 +445,6 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
|
||||
newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable),
|
||||
newMigration(367, "Add user allowlists to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
|
||||
newMigration(368, "Drop org from repo manifest (derived from org profile)", v1_27.DropOrgFromRepoManifest),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/migrations/base"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// DropOrgFromRepoManifest removes the org column from repo_manifest. The
|
||||
// organization is now derived from the org profile (repository owner) on read
|
||||
// rather than stored, so it never drifts on rename. See issue #771.
|
||||
func DropOrgFromRepoManifest(x *xorm.Engine) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
return base.DropTableColumns(sess, "repo_manifest", "org")
|
||||
}
|
||||
@@ -486,23 +486,6 @@ func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// DerivedOrgName returns the organization name derived from the org profile
|
||||
// (the repository owner). It prefers the owner's display name (FullName) and
|
||||
// falls back to the owner handle (Name / OwnerName). The organization is always
|
||||
// derived from the profile rather than stored, so it never drifts on rename.
|
||||
func (repo *Repository) DerivedOrgName(ctx context.Context) string {
|
||||
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
|
||||
return repo.OwnerName
|
||||
}
|
||||
if repo.Owner.FullName != "" {
|
||||
return repo.Owner.FullName
|
||||
}
|
||||
if repo.Owner.Name != "" {
|
||||
return repo.Owner.Name
|
||||
}
|
||||
return repo.OwnerName
|
||||
}
|
||||
|
||||
// MustOwner always returns a valid *user_model.User object to avoid
|
||||
// conceptually impossible error handling.
|
||||
// It creates a fake object that contains error details
|
||||
|
||||
@@ -23,10 +23,8 @@ type RepoMetadata struct {
|
||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||
|
||||
// identity section
|
||||
// NOTE: organization is intentionally NOT stored here. It is derived from
|
||||
// the org profile (repository owner) on read via Repository.DerivedOrgName,
|
||||
// so it can never drift from the owner on rename. See issue #771.
|
||||
Name string `xorm:"TEXT 'name'"` // project name
|
||||
Org string `xorm:"TEXT 'org'"` // organization name
|
||||
Description string `xorm:"TEXT 'description'"` // project description
|
||||
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
||||
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// apiMetadata is the JSON representation of a repo manifest.
|
||||
type apiMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Org string `json:"org"` // read-only, derived from the org profile (repo owner)
|
||||
Org string `json:"org"`
|
||||
Description string `json:"description"`
|
||||
LicenseSPDX string `json:"license_spdx"`
|
||||
LicenseName string `json:"license_name"`
|
||||
@@ -83,14 +83,14 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
||||
// Return defaults from repo metadata.
|
||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||
Name: ctx.Repo.Repository.Name,
|
||||
Org: ctx.Repo.Repository.DerivedOrgName(ctx),
|
||||
Org: ctx.Repo.Repository.OwnerName,
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||
Name: m.Name,
|
||||
Org: ctx.Repo.Repository.DerivedOrgName(ctx),
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
@@ -157,6 +157,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
m = &repo_model.RepoMetadata{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.Repo.Repository.Name,
|
||||
Org: ctx.Repo.Repository.OwnerName,
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
}
|
||||
}
|
||||
@@ -170,8 +171,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
setStr("name", &m.Name)
|
||||
// org is intentionally ignored: it is derived from the org profile
|
||||
// (repository owner) on read, not stored. See issue #771.
|
||||
setStr("org", &m.Org)
|
||||
setStr("description", &m.Description)
|
||||
setStr("license_spdx", &m.LicenseSPDX)
|
||||
setStr("license_name", &m.LicenseName)
|
||||
@@ -204,7 +204,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
|
||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||
Name: m.Name,
|
||||
Org: ctx.Repo.Repository.DerivedOrgName(ctx),
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
|
||||
@@ -137,49 +137,27 @@ func BrandingUpload(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// The nav icon doubles as the app icon (logo.png) — used as the PWA / web
|
||||
// manifest icon and the navbar fallback — so branding the nav icon brands
|
||||
// the app icon too. Write the uploaded image to both in a single pass. See #773.
|
||||
targets := []string{filename}
|
||||
if imageType == "nav-icon" {
|
||||
targets = append(targets, "logo.png")
|
||||
destPath := filepath.Join(imgDir, filename)
|
||||
dest, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
ctx.Flash.Error("Failed to save image")
|
||||
log.Error("Create %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
if _, err := io.Copy(dest, file); err != nil {
|
||||
ctx.Flash.Error("Failed to write image")
|
||||
log.Error("Copy to %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Write the uploaded image to each target independently (rewinding the
|
||||
// stream between targets) so a failure on a later target never leaves an
|
||||
// earlier one truncated/empty.
|
||||
for i, name := range targets {
|
||||
if i > 0 {
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
ctx.Flash.Error("Failed to write image")
|
||||
log.Error("Seek branding image %s: %v", name, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
destPath := filepath.Join(imgDir, name)
|
||||
dest, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
ctx.Flash.Error("Failed to save image")
|
||||
log.Error("Create %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(dest, file); err != nil {
|
||||
dest.Close()
|
||||
ctx.Flash.Error("Failed to write image")
|
||||
log.Error("Copy to %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
dest.Close()
|
||||
|
||||
// Remove any SVG override so the uploaded PNG takes effect.
|
||||
svgPath := filepath.Join(imgDir, name[:len(name)-4]+".svg")
|
||||
if fileExists(svgPath) {
|
||||
os.Remove(svgPath)
|
||||
}
|
||||
// Remove SVG override if present
|
||||
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
|
||||
if fileExists(svgPath) {
|
||||
os.Remove(svgPath)
|
||||
}
|
||||
|
||||
ctx.Flash.Success("Branding image updated: " + imageType)
|
||||
@@ -205,36 +183,16 @@ func BrandingReset(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// The nav icon also sets the app icon (logo.png), so resetting it reverts
|
||||
// both back to the built-in default. See #773.
|
||||
targets := []string{filename}
|
||||
if imageType == "nav-icon" {
|
||||
targets = append(targets, "logo.png")
|
||||
}
|
||||
|
||||
removedAny := false
|
||||
failed := false
|
||||
for _, name := range targets {
|
||||
path := filepath.Join(brandingImageDir(), name)
|
||||
if !fileExists(path) {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(brandingImageDir(), filename)
|
||||
if fileExists(path) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
ctx.Flash.Error("Failed to remove custom image")
|
||||
log.Error("Remove %s: %v", path, err)
|
||||
failed = true
|
||||
} else {
|
||||
removedAny = true
|
||||
log.Info("Branding reset to default: %s", name)
|
||||
ctx.Flash.Success("Reset to default: " + imageType)
|
||||
log.Info("Branding reset to default: %s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case failed:
|
||||
// error flash already set
|
||||
case removedAny:
|
||||
ctx.Flash.Success("Reset to default: " + imageType)
|
||||
default:
|
||||
} else {
|
||||
ctx.Flash.Info("Already using default: " + imageType)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,13 +35,11 @@ func Metadata(ctx *context.Context) {
|
||||
manifest = &repo_model.RepoMetadata{
|
||||
RepoID: repoID,
|
||||
Name: ctx.Repo.Repository.Name,
|
||||
Org: ctx.Repo.Repository.OwnerName,
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
}
|
||||
}
|
||||
ctx.Data["Manifest"] = manifest
|
||||
// Organization is derived from the org profile (repo owner), not stored.
|
||||
// It is displayed read-only. See issue #771.
|
||||
ctx.Data["DerivedOrg"] = ctx.Repo.Repository.DerivedOrgName(ctx)
|
||||
|
||||
// Load repo-scoped custom fields.
|
||||
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
|
||||
@@ -104,7 +102,7 @@ func saveMetadata(ctx *context.Context) {
|
||||
manifest := &repo_model.RepoMetadata{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.FormString("name"),
|
||||
// org is not stored: it is derived from the org profile on read. See issue #771.
|
||||
Org: ctx.FormString("org"),
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
LicenseSPDX: spdx,
|
||||
LicenseName: licenseName,
|
||||
|
||||
@@ -269,8 +269,11 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
phpMinimum := meta.PHPMinimum
|
||||
feedDescription := meta.Description
|
||||
|
||||
// Maintainer/organization and URL always come from the org profile.
|
||||
maintainer := repo.DerivedOrgName(ctx)
|
||||
// Maintainer and URL always come from the org profile.
|
||||
maintainer := repo.Owner.FullName
|
||||
if maintainer == "" {
|
||||
maintainer = repo.Owner.Name
|
||||
}
|
||||
maintainerURL := repo.Owner.Website
|
||||
if maintainerURL == "" {
|
||||
maintainerURL = fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Nav Icon</strong> {{if .HasNavIcon}}<span class="ui mini green label">Custom</span>{{else}}<span class="ui mini grey label">Default</span>{{end}}
|
||||
<div class="tw-text-text-light tw-text-sm tw-mt-1">Top-left corner across all pages. Square, 30x30px. Also sets the app icon (PWA / web manifest); use at least 512x512px for a crisp installed icon.</div>
|
||||
<div class="tw-text-text-light tw-text-sm tw-mt-1">Top-left corner across all pages. Square, 30x30px.</div>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Organization</label>
|
||||
<input value="{{.DerivedOrg}}" placeholder="Organization" readonly>
|
||||
<p class="help">Derived from the organization profile (repository owner).</p>
|
||||
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
|
||||
Reference in New Issue
Block a user