Compare commits

...

55 Commits

Author SHA1 Message Date
jmiller 10760463f8 Merge pull request 'chore(sync): cascade main -> dev' (#757) from main into dev
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Has started running
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-07-06 07:38:07 +00:00
jmiller bc3bb0f778 Merge pull request 'Release: org-wide governance series (#727) — dev → main' (#733) from dev into main
Sync Workflows to Repos / sync (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 6s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
PR RC Release / Build RC Release (pull_request) Successful in 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 17s
Generic: Project CI / Lint & Validate (pull_request) Successful in 44s
Generic: Project CI / Tests (pull_request) Successful in 58s
Universal: PR Check / Secret Scan (pull_request) Successful in 2m54s
Deploy MokoGitea / deploy (push) Failing after 5m51s
2026-07-06 07:37:35 +00:00
jmiller d955bac72b chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-06 03:49:59 +00:00
jmiller afc470f513 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-07-06 03:49:52 +00:00
jmiller 6505840839 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-06 03:49:44 +00:00
jmiller 7d98098a87 Merge pull request 'fix(ci): make Tests an independent job (work around Gitea needs-chain scheduler stall)' (#756) from fix/ci-generic-scheduler into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 49s
Generic: Project CI / Tests (pull_request) Successful in 51s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 1m29s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m37s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Universal: PR Check / Require Docs Update (pull_request) Successful in 1m7s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m14s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 12m25s
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 1m18s
2026-07-06 03:42:40 +00:00
jmiller 28b9d94658 fix(ci): make Tests an independent job to work around Gitea needs-chain scheduler stall
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 58s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Generic: Project CI / Tests (pull_request) Successful in 33s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 57s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The Gitea Actions scheduler on the current build does not offer the dependent
2nd job of a needs-chain to runners: the Tests job (needs: lint) reaches
'waiting' but is never claimed even with idle, label-matching runners, then is
reaped by ABANDONED_JOB_TIMEOUT. Verified via dispatch on idle runners (run
34933: lint claimed+success, Tests waiting/runner_id 0, never scheduled).

Drop 'needs: lint' + the always() gate; guard template repos directly so Tests
runs as an independent, schedulable job.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 22:39:40 -05:00
jmiller 8d8ecf176a Merge pull request 'fix(tests): repair tests/integration compile errors (upstream API/import drift)' (#755) from fix/integration-tests-compile-wt into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m31s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Require Docs Update (pull_request) Successful in 4m17s
PR RC Release / Build RC Release (pull_request) Successful in 4m19s
Universal: PR Check / Secret Scan (pull_request) Successful in 4m19s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 5m55s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-06 03:14:18 +00:00
jmiller 7203628004 fix(tests): repair tests/integration compile errors from upstream API/import drift
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 40s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m27s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m22s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Two compile errors blocked the whole tests/integration package (prereq for the
layered test pipeline, Template-Go#4, and the clean go-compile owed by #733):

- api_packages_composer_test.go: composer.PackageMetadataResponse{} (value) used
  where *PackageMetadataResponse (pointer) is required -> take the address.
- oauth_avatar_test.go: 10 stale upstream 'code.gitea.io/gitea/' imports (never
  updated after the fork module rename) -> 'code.mokoconsulting.tech/MokoConsulting/MokoGitea/'.

go vet ./tests/integration/... now exits 0 (compiles clean).

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 22:11:44 -05:00
jmiller 36a824be1d Merge pull request 'chore(manifest): remove deprecated .mokogitea/manifest.xml parser; mokoplatform→mokocli' (#754) from feature/752-chore-deploy-rebuild-redeploy-for-moko-p into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 4s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 20s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 44s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m51s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Require Docs Update (pull_request) Successful in 4m48s
PR RC Release / Build RC Release (pull_request) Successful in 4m51s
Universal: PR Check / Secret Scan (pull_request) Successful in 4m46s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 23m32s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-06 02:38:10 +00:00
jmiller fcf33d35df chore(api): remove /manifest backward-compat alias route (use /metadata)
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 14s
Universal: Auto Version Bump / Version Bump (push) Successful in 25s
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m21s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The /manifest GET+PUT routes were backward-compat aliases for the canonical
/metadata routes. Completing the manifest->metadata migration (#752): drop the
alias so /metadata is the single path. Handlers (GetRepoMetadata/UpdateRepoMetadata)
are unchanged. The MCP already calls /metadata (mcp-mokogitea-api v1.4.2,
gitea_metadata_get/update); the stale gitea_manifest_* tools only exist in older
deployed MCP builds and drop out on redeploy.

Refs #752

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 21:07:05 -05:00
jmiller 358606e235 chore(manifest): remove deprecated .mokogitea/manifest.xml parser; mokoplatform->mokocli
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 44s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m38s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The .mokogitea/manifest.xml file is deprecated - the first-class RepoMetadata DB
fields (set via the /manifest API) are the authoritative repository metadata (see
issue #752). Remove the legacy XML auto-sync path rather than teaching it the
renamed mokocli root element:

- delete services/repository/manifest_sync.go (manifestXML parser)
- drop the SyncMetadataFromCommit call on default-branch push (push.go)
- rebrand remaining mokoplatform->mokocli refs (repo_manifest.go, v347.go comment,
  locale manifest_desc)

The RepoMetadata model, /manifest API, settings UI, and update-feed generation
(first-class fields) are unchanged. gofmt-normalized repo_manifest.go struct tags.

Refs #752

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 21:00:28 -05:00
jmiller c74173bb86 ci(deploy-rc): add workflow_dispatch for isolated manual tests
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 21s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m57s
PR RC Release / Build RC Release (pull_request) Successful in 5m58s
Universal: PR Check / Require Docs Update (pull_request) Successful in 6m4s
Universal: PR Check / Secret Scan (pull_request) Successful in 6m4s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 7m28s
Deploy (RC) / Build & Deploy to RC (push) Successful in 10m18s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Lets deploy-rc be run on demand (from a ref carrying current source) to verify
the RC pipeline end-to-end without a full rc promotion + pre-release.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 19:15:55 -05:00
jmiller b2447da3dd Merge pull request 'feat(ci): deploy-rc.yml — auto-deploy rc branch to the new RC environment' (#751) from feat/deploy-rc-workflow into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 10s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m53s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 5m27s
Universal: PR Check / Require Docs Update (pull_request) Successful in 5m29s
Universal: PR Check / Secret Scan (pull_request) Successful in 5m30s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 10m14s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-06 00:03:55 +00:00
jmiller 7be712571c refactor(ci): tier-scope deploy-rc variables (RC_SSH_*, RC_REGISTRY*, RC_IMAGE)
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 47s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m36s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Shared DEPLOY_HOST/PORT/USER assumed all tiers live on one host. Scope every
deploy variable to the rc tier (mirrors the org's DEV_SSH_* convention) so a repo
inheriting this template can host rc/dev/prod on separate machines independently.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 19:01:38 -05:00
jmiller 6634b049ab fix(ci): deploy-rc reuses existing DEPLOY_SSH_KEY + MOKOGITEA_TOKEN secrets
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 10s
Generic: Project CI / Lint & Validate (pull_request) Successful in 52s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m47s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The registry token is the org-wide MOKOGITEA_TOKEN secret (already configured,
used by deploy-dev/deploy-mokogitea), not a new DEPLOY_REGISTRY_TOKEN. Reuse the
existing secret names so no new secret values are needed; only the non-sensitive
tier variables get set per-repo.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 18:54:27 -05:00
jmiller 5c62cefcd3 refactor(ci): deploy-rc.yml -> root workflows, fully parameterized via repo vars/secrets
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 33s
Universal: PR Check / Secret Scan (pull_request) Successful in 53s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
- Moved from custom/ to the root .mokogitea/workflows/ (synced/template-owned area).
- All deployment config now comes from repo Actions variables (DEPLOY_HOST/PORT/USER,
  DEPLOY_REGISTRY/IMAGE, RC_CONTAINER/COMPOSE_PROJECT/COMPOSE_DIR/SOURCE_DIR/TAG_ENV/
  HEALTH_URL) and secrets (DEPLOY_SSH_KEY, DEPLOY_REGISTRY_TOKEN) — nothing hardcoded,
  so it works across Go server repos. Clone URL derived from github.server_url/repository.
- To become Template-Go-owned (Template-Go#3).

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 18:37:33 -05:00
jmiller f2ca569906 feat(ci): deploy-rc.yml — auto-deploy the rc branch to the RC environment
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 43s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m6s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Adds a custom deploy workflow that builds and deploys the release candidate
to rc.git.mokoconsulting.tech (mokogitea-rc container, port 3110, DB
mokogitea_rc) on push to the rc branch — the branch promote-rc creates when
a PR to main is opened. Mirrors the fixed deploy-dev.yml: env-var image tag
(MOKOGITEA_RC_TAG) driving the shared compose file (no sed), rm -f before
force-recreate, quoted-heredoc remote expansion, health check + external
verify. Gives a real release-candidate environment to validate before prod.

Intended to move to Template-Go as the canonical synced source once validated
(kept in custom/ for now so it isn't overwritten by workflow sync).

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 18:31:51 -05:00
jmiller b9301a8f31 Merge pull request 'feat: org branch protection per-user (username/email) allowlists + actions-bot toggle (#727)' (#750) from feature/org-branch-protection-user-allowlists into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 53s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 14s
PR RC Release / Build RC Release (pull_request) Successful in 4m28s
Universal: PR Check / Require Docs Update (pull_request) Successful in 4m57s
Universal: PR Check / Secret Scan (pull_request) Successful in 4m55s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 2m51s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 15m28s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-07-05 22:54:30 +00:00
jmiller cac06c2ac7 feat: org branch protection per-user allowlists + Actions-bot toggle (#727)
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Successful in 14s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m3s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m42s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Extend org-level branch protection to support per-user allowlists (resolved
from username OR email) and an "allow Actions bot" toggle, alongside the
existing team allowlists, for all five categories (push, merge, force-push,
delete, approvals).

- models/git/org_protected_branch.go: add WhitelistUserIDs, MergeWhitelistUserIDs,
  ForcePushAllowlistUserIDs, DeleteAllowlistUserIDs, ApprovalsWhitelistUserIDs
  ([]int64) plus WhitelistActionsUser, MergeWhitelistActionsUser,
  ForcePushAllowlistActionsUser, DeleteAllowlistActionsUser (bool); copy all 9
  into ProtectedBranch in ToProtectedBranch().
- models/migrations/v1_27/v368.go: migration 367 adds the 9 columns.
- modules/structs/org_branch.go: add *Usernames []string and *ActionsUser bool
  to Create/Edit options and the response, matching repo-level json names.
- routers/api/v1/org/branch_protection.go: resolveUserIDs (username then email,
  dedupe, 422 on unknown); wire into Create + Edit and toAPIOrgBranchProtection.
- models/git/protected_branch_merge.go: add mergeAllowFlag and merge the four
  Actions-user flags most-restrictively (org can now express them); deploy-key
  flags stay repo-only pass-through.
- models/git/protected_branch_merge_test.go: mergeAllowFlag truth table, user-ID
  intersection, deploy-key pass-through.
- regenerate swagger v1 + openapi3 specs.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 17:48:47 -05:00
jmiller 6c0c2c3f1f Merge pull request 'chore(sync): cascade main -> dev' (#748) from main into dev
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m8s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 1m38s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Successful in 17s
Generic: Repo Health / Access control (pull_request) Successful in 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 1m56s
Universal: PR Check / Require Docs Update (pull_request) Successful in 3m36s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m39s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 14m4s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 22:03:31 +00:00
jmiller 4db6f03efd Merge fix/rebrand-wiki-docs: normalize in-repo wiki branding
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 13s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 16s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Universal: PR Check / Secret Scan (pull_request) Successful in 3m59s
Deploy MokoGitea / deploy (push) Failing after 14m2s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-05 22:01:50 +00:00
jmiller 3efbab985b Merge pull request 'fix(locale): duplicate en-US key crashes boot under jsonv2 (#696/#728)' (#743) from fix/locale-dup-key-jsonv2 into dev
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Project CI / Lint & Validate (pull_request) Successful in 52s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 19s
PR RC Release / Build RC Release (pull_request) Successful in 4m20s
Universal: PR Check / Secret Scan (pull_request) Successful in 4m21s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 10m14s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-07-05 20:32:07 +00:00
jmiller 98b1ed2f7b fix(locale): duplicate en-US key crashes server boot under jsonv2
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 41s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 47s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m39s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The branch-protection delete feature (#696/#728) added a second
"repo.settings.event_delete" entry ("Branch Deletion") to locale_en-US.json,
reusing the existing webhook-event key (value "Delete"). The old JSON decoder
silently kept the last value; Go 1.26's jsonv2 decoder rejects duplicate
object keys, so InitLocales fails ("duplicate object member name
repo.settings.event_delete") and the server crash-loops at startup. Like the
code-scanner regexp panic, this only surfaces on a fresh boot, which is why it
shipped unnoticed.

Give the branch-protection section header its own key
"repo.settings.protect_branch_deletion" and point protected_branch.tmpl at it,
so the webhook "Delete" label and the branch-protection "Branch Deletion"
header both render correctly and the JSON has no duplicate. Verified: no
duplicate keys remain in any options/locale/*.json.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 15:21:31 -05:00
jmiller ccfc9a604b Merge pull request 'fix(security): code scanner RE2 lookahead panics server at startup (#552)' (#742) from fix/code-scanner-re2-panic into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m4s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m31s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 4m7s
Universal: PR Check / Secret Scan (pull_request) Successful in 4m5s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 7m9s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 20:07:45 +00:00
jmiller 2f119fbd95 fix(security): code scanner panics at startup on RE2-incompatible regexp
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 16s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 44s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m29s
Universal: PR Check / Secret Scan (pull_request) Successful in 3m49s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The "deserialize-yaml-py" rule in services/security/code_scanner.go used a
negative lookahead `(?!\s*#)` in regexp.MustCompile. Go's regexp engine is
RE2, which has no lookahead/lookbehind, so MustCompile panics during the
package init() — crash-looping the entire server at startup. `go build` and
`go vet` do not execute init(), and CI never boots the binary, so this
shipped to main via #552 undetected; the running instances survived only
because they predate that image.

Replace the pattern with an RE2-safe equivalent `(?i)yaml\.load\s*\(`, which
matches the rule's stated intent (flag yaml.load() without SafeLoader,
CWE-502). Add a regression test that forces the package init and asserts
every DefaultCodeRules pattern compiled, so a future RE2-incompatible
pattern fails in CI here instead of on a live deploy.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 15:06:57 -05:00
jmiller 7f229ba01c Merge pull request 'fix: org-governance release review findings + dev deploy targeting (#727, #733)' (#741) from fix/org-governance-review into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 40s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m24s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
PR RC Release / Build RC Release (pull_request) Successful in 1m32s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m43s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 6m22s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 19:51:06 +00:00
jmiller e98fca780e fix: address org-governance release review (#727, #733) + dev deploy targeting
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 50s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Universal: PR Check / Secret Scan (pull_request) Successful in 59s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Code-review findings on the org-governance release:

- Fail closed on org-rule lookup error: getFirstMatchProtectedBranchRule
  swallowed FindOrgBranchRuleForBranch errors (returned nil,nil), silently
  dropping the org floor and falling back to the repo rule on a transient DB
  error. Propagate the error so the org rule stays enforced.

- Stop the org rule locking out deploy-key and Actions-bot pushes:
  OrgProtectedBranch is team-only, so mergeMostRestrictive was ANDing the
  repo's WhitelistDeployKeys / WhitelistActionsUser (and the force-push,
  delete and merge counterparts) against the org side's always-false zero
  value, blocking every deploy-key and Actions push in any org with a
  matching branch rule. Carry those org-unmanaged fields through from the
  repo rule unchanged.

- Org push-policy max-file-size now inspects only the pushed delta
  (diff-tree + cat-file --batch-check) instead of the full tip tree via
  ls-tree, so a pre-existing oversized file can no longer permanently block
  unrelated pushes. New branches (no base commit) still scan the full tree.

Dev deploy targeting:

- deploy-dev.yml drove the dev container image via `sed` on the SHARED
  compose file, but the pattern matched the *prod* service line
  (container_name: mokogitea) — leaving the dev service pinned to a stale
  image (so every "green" deploy recreated old code) while corrupting the
  prod image pin. Drive the dev service image from ${MOKOGITEA_DEV_TAG}
  instead; the env-var only affects the dev service.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 14:46:01 -05:00
jmiller 7a4dc5e809 Merge pull request 'docs(api): OpenAPI spec + README/CHANGELOG for org-governance (#727, #738)' (#739) from feat/org-governance-openapi into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 59s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 1m8s
PR RC Release / Build RC Release (pull_request) Successful in 2m46s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 4m25s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 15:19:08 +00:00
jmiller 8c63b00953 Merge pull request 'fix(ci): recreate dev container to avoid name conflict on deploy' (#740) from fix/deploy-dev-container-conflict into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 14s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m15s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 1m20s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m19s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Successful in 7m29s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 15:18:53 +00:00
jmiller 6b81922c47 fix(ci): recreate dev container to avoid name conflict on deploy
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m0s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 34s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m11s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
After the tag fix (#737) the dev deploy builds and pushes the image
fine but fails at `docker compose up -d` with:
  Conflict. The container name "/mokogitea-dev" is already in use

The dev service uses a fixed container_name, and the symlinked
/opt/gitea-dev path makes compose's derived project name unstable, so
an existing container is not recognized as the project's and `up`
tries to create rather than recreate. Remove any lingering
fixed-name container first, pin the compose project name, and force a
fresh recreate so migrations run against the new image.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 10:17:08 -05:00
jmiller 93365cdd95 docs(api): swagger annotations + response models for org-governance endpoints (#727, #738)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Generic: Project CI / Lint & Validate (pull_request) Successful in 41s
Universal: PR Check / Secret Scan (pull_request) Successful in 59s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Annotate the four previously undocumented org-governance API handlers
(tag_protection, push_policy, repo_defaults, email_domain) with
swagger:operation blocks, and register the swagger:response models the
branch_protection operations already referenced. Register the org
option DTOs in the parameterBodies hack so their definitions are
emitted.

Also fix pre-existing spec-generation blockers surfaced once the spec
became regenerable: a stray comment glued to the repoUpdateManifest
swagger block (broke YAML parsing), missing owner/repo path params on
the manifest operations, a Manifest response registration, and missing
definitions for EditAccessTokenOption, the IssueBulk* options, and the
Issue{Priority,Status,Type}Def types. Regenerated v1_json.tmpl and
v1_openapi3_json.tmpl; spec now validates cleanly against Swagger 2.0.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 01:58:08 -05:00
jmiller 81ea2fcb05 docs: README org-governance feature + CHANGELOG CI fixes (#727)
Add an Org Governance entry to the README key-features list (org-wide
branch/tag protection, push policy, repo defaults, email-domain
allowlist) and record the recent build/CI fixes (#734, #735, #736,
#737) under CHANGELOG [Unreleased].

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 01:28:36 -05:00
jmiller 2713c49aec Merge pull request 'fix(ci): pass TAG/REGISTRY_TOKEN into remote shell in dev deploy' (#737) from fix/deploy-dev-var-expansion into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m13s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2m48s
Universal: PR Check / Secret Scan (pull_request) Successful in 2m49s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 4m55s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 06:15:01 +00:00
jmiller 3917bf6a29 fix(ci): pass TAG/REGISTRY_TOKEN into remote shell in dev deploy
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Generic: Project CI / Lint & Validate (pull_request) Successful in 37s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m7s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m12s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The dev deploy step used an unquoted SSH heredoc and referenced
runner-side values as \$TAG / \$REGISTRY_TOKEN, deferring their
expansion to the remote shell where those names are unset. The
Docker build tag collapsed to "mokogitea:" and every dev deploy
failed with `invalid tag ... invalid reference format` before any
migration or server boot could run.

Inject TAG and REGISTRY_TOKEN as an env prefix on the ssh command
(`TAG='...' REGISTRY_TOKEN='...' bash -s`) and switch to a quoted
heredoc so every $var expands in exactly one place: the remote host.
Also fixes HEALTH_FMT (was defined on the runner but referenced
remotely) and adds an explicit empty-TAG guard so a future
regression fails loudly instead of building an untagged image.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 01:08:32 -05:00
jmiller 89ed32e961 Merge pull request 'fix: repair unit-test compile + vet failures (partial integration cleanup)' (#736) from fix/vet-test-suite-blockers into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m7s
PR RC Release / Build RC Release (pull_request) Successful in 1m20s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m20s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 2m33s
Universal: PR Check / Secret Scan (pull_request) Successful in 2m31s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-05 05:58:38 +00:00
jmiller 948e7bcd21 fix: partial repair of tests/integration compile errors (license test)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 55s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Generic: Project CI / Lint & Validate (pull_request) Successful in 41s
Universal: PR Check / Secret Scan (pull_request) Successful in 57s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
api_license_keys_test.go used the outdated NewRequestWithBody signature
(passing []byte where io.Reader is now required) — wrapped the string bodies in
strings.NewReader. Note: tests/integration remains broadly pre-existing-broken
across multiple other fork-added files (api_packages_composer type mismatch,
etc.); those are a separate dedicated cleanup, not part of #727.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 00:27:30 -05:00
jmiller 5d797431f0 fix: repair pre-existing test-suite compile/vet failures
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m15s
`go vet ./...` (finally runnable with a local Go toolchain) surfaced three
pre-existing failures that prevented the whole test tree from compiling — which
is very likely why the "Project CI / Tests" job never went green. None relate to
#727; all pre-existing on main.

- modules/util/util_test.go: CryptoRandomInt/String/Bytes now return (value,
  error); the tests used single-value assignment. Updated to capture + assert
  the error (and dropped a now-redundant `var err error`).
- tests/integration/auth_oauth2_test.go: `newFakeOIDCServer` was declared twice
  with different signatures (redeclaration = build failure). Renamed the
  config-struct variant to `newFakeOIDCServerWithConfig` and updated its caller;
  the (sub, oid) variant keeps the original name for its caller.
- routers/web/repo/issue_comment.go: removed a redundant `&& statusIDStr != ""`
  duplicate condition (vet: redundant and).

Verified: `go vet ./modules/util` clean; full `go vet ./...` re-run.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 00:23:04 -05:00
jmiller 63f773aa56 Merge pull request 'fix: repair build (renamed org-visibility helper) + gofmt' (#735) from fix/compile-hasorgvisible-and-gofmt into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 18s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m28s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m28s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 1m40s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 2m44s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 05:07:58 +00:00
jmiller 125eefc650 fix: repair build (renamed org-visibility helper) + gofmt
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Successful in 19s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m10s
Universal: PR Check / Secret Scan (pull_request) Successful in 2m43s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Two pre-existing issues surfaced when the org-governance series was compiled
locally with a real Go toolchain (go1.26.3) for the first time:

- routers/api/v1/api.go:519 called organization.HasOrgOrUserVisible, which no
  longer exists — it was renamed to IsOwnerVisibleToDoer (models/organization/
  org.go:548, identical signature). This one missed call site meant the whole
  routers/api/v1 package (and therefore the server binary) failed `go build`.
  With the rename, `go build ./...` is clean.
- gofmt: api.go (a mis-indented commented-out /projects route block) and
  release.go (import sort: repo before updateserver) were gofmt-dirty. Fixed
  with gofmt -w on the two files this change already touches.

Not part of #727, but blocks building/releasing the fork; found while validating
the dev -> main promotion (#733).

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 00:06:48 -05:00
jmiller d07cfd412b Merge pull request 'chore: remove stray package-lock.json accidentally committed to dev' (#734) from chore/remove-stray-package-lock into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m2s
Universal: PR Check / Validate PR (pull_request) Successful in 17s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 1m26s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m17s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 3m13s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 3m17s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 04:40:58 +00:00
jmiller bd821e2d44 chore: remove stray package-lock.json accidentally committed to dev
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 40s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m14s
Universal: PR Check / Secret Scan (pull_request) Successful in 2m54s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
package-lock.json (13.9k lines of generated npm lockfile) was swept into the
org push-policy commit (3aac1b456c, #730) by a `git add -A` during a gofmt-fix
restack. It is not tracked on main and is not part of the org-governance work.
Removing it so the dev -> main promotion (#733) doesn't introduce it.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 23:40:16 -05:00
jmiller aeed197ea5 Merge pull request 'feat(org): org-level email domain policy for members (#727)' (#732) from feat/org-email-domain into dev
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m53s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Generic: Project CI / Lint & Validate (pull_request) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m11s
Universal: Build & Release / Promote to RC (pull_request) Failing after 21s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 1m16s
PR RC Release / Build RC Release (pull_request) Successful in 1m23s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
2026-07-05 04:34:14 +00:00
jmiller 45fc346d52 Merge pull request 'feat(org): org-level repository defaults applied on repo create/transfer (#727)' (#731) from feat/org-repo-defaults into dev
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 2m7s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 59s
2026-07-05 04:33:25 +00:00
jmiller 02071a23d6 Merge pull request 'feat(org): org-level push policy enforced in the pre-receive hook (#727)' (#730) from feat/org-push-policy into dev
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m39s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m3s
2026-07-05 04:33:09 +00:00
jmiller 3a5c6a37cf Merge pull request 'feat(org): org-level tag protection, layered with per-repo protected tags (#727)' (#729) from feat/org-tag-protection into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m28s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m15s
2026-07-05 04:32:53 +00:00
jmiller 37fb3703c7 Merge pull request 'fix(org): layer org-level branch protection with repo rules — most-restrictive wins (#727)' (#728) from fix/727-materialize-org-branch-protection into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m16s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m36s
2026-07-05 04:32:24 +00:00
jmiller 6a3db171c1 feat(org): org-level email domain policy for members (#727)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Generic: Project CI / Lint & Validate (pull_request) Successful in 25s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 4s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Restricts which email domains an organization's members may have. When a policy
is configured, a user can only be added to the org (via any team) if their
primary email matches one of the allowed domain globs.

Enforced at the single membership choke point services/org.AddTeamMember, which
every add path (API, web, group-sync) funnels through — so one check covers them
all. On violation it returns a typed ErrEmailDomainNotAllowed; the API team-add
handler maps it to 422.

- models/git/org_email_domain.go: OrgEmailDomainPolicy model + EmailAllowed
  (domain glob match) + OrgEmailDomainAllowed + typed error + CRUD. Migration 366.
- API: GET/PATCH/DELETE /orgs/{org}/email_domain_policy.
- Enforcement in services/org/team.go; 422 mapping in routers/api/v1/org/team.go.

An empty policy imposes no restriction. This is the one bounded piece of the
"access/security" tier; org 2FA-required and IP allowlists were deliberately NOT
built here — they are cross-cutting enforcement (auth gating / request
middleware) that needs a compiler + tests, not a blind stacked PR.

Stacked on #731/#730/#729/#728 for migration ordering (this = 366). Swagger
omitted.

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.
Hand-verified: gofmt (tabs, no blank-in-block), imports (git_model added to the
api team handler, gci order), typed-error detection, migration contiguous (366).

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 23:23:11 -05:00
jmiller d3134b1c53 feat(org): org-level repository defaults applied on repo create/transfer (#727)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m5s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Adds a single per-org repository-defaults config, applied to a repo when it is
created in or transferred into the org via a notifier (services/org):

- ForcePrivate — force new/transferred repos private (Repository.IsPrivate).
- PR defaults (when ApplyPRDefaults) — allowed merge styles, default merge
  style, and auto-delete-branch-after-merge, written to the repo's pull-requests
  unit config via repo_service.UpdateRepositoryUnits.

Best-effort: the notifier logs and swallows errors, so a defaults bug can never
break repository creation or transfer.

- models/git/org_repo_defaults.go: OrgRepoDefaults model + CRUD + migration 365.
- API: GET/PATCH/DELETE /orgs/{org}/repo_defaults.
- services/org/notifier.go: CreateRepository/TransferRepository -> apply defaults;
  registered from routers/init.go (org_service.Init()).

Stacked on #730/#729/#728 for migration ordering (this = 365). Swagger omitted.

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.
Hand-verified: gofmt (tabs, no blank-in-block, struct/DTO alignment), imports
used, no Init() collision in services/org, migration contiguous (365), notifier
signatures match the Notifier interface.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 23:22:38 -05:00
jmiller 3aac1b456c feat(org): org-level push policy enforced in the pre-receive hook (#727)
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
PR RC Release / Build RC Release (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 23s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m13s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Adds a single per-org push policy that cascades to every repo of the org and is
enforced in the pre-receive hook:

- Branch/tag name conventions (glob) — a pushed ref name must match. Fail-closed.
- Mandatory secret-scanning block-on-push — org can force secret blocking that a
  repo cannot disable (overrides the per-repo scanner config in the orchestrator).
- Max pushed-file size — rejects a tip tree containing a blob over the limit.
- Blocked file-path patterns — rejects pushes changing matching paths (reuses
  pull_service.CheckFileProtection).

The two content checks (blocked paths, max size) FAIL OPEN on any error so a
policy/parsing bug can never wedge all pushes; naming is fail-closed.

- models/git/org_push_policy.go: OrgPushPolicy model + CRUD + matchers +
  GetOrgPushPolicyForRepo. Migration 364.
- API: GET/PATCH/DELETE /orgs/{org}/push_policy (routers/api/v1/org/push_policy.go,
  DTOs in modules/structs/org_push_policy.go, wired in api.go).
- Enforcement: routers/private/hook_pre_receive.go (branch: naming + blocked paths
  + max size; tag: naming) and services/security/orchestrator.go (secret mandate).

Deferred: a repo-facing read-only view of the org push policy (it is an org-wide
config, not per-repo overlay rules; readable via the API for now).

Stacked on #729/#728 for migration ordering (this = 364). Swagger annotations
omitted (can't regenerate without the toolchain).

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.
Hand-verified: gofmt (tabs, no blank-in-block), escape sequences in the ls-tree
parser, imports used, migration contiguous (364), fail-open on content checks.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 23:20:39 -05:00
jmiller b31336d1fe feat(org): org-level tag protection, layered with per-repo protected tags (#727)
Universal: Build & Release / Promote to RC (pull_request) Failing after 18s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
PR RC Release / Build RC Release (pull_request) Successful in 2m21s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m17s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Adds org-level tag protection as a parallel to org-level branch protection.
An org tag rule is {NamePattern, AllowlistTeamIDs}; it cascades to every repo
in the org and layers on top of the repo's own protected tags — a tag is
controllable (push/delete) only if allowed at BOTH levels (fail-closed).

- models/git/org_protected_tag.go: OrgProtectedTag model + CRUD +
  ToProtectedTag() (reuses the ProtectedTag matcher/allowlist logic) +
  IsUserAllowedToControlTagInRepo() which ANDs the repo decision with the org
  decision. Migration 363.
- API: /orgs/{org}/tag_protections CRUD (routers/api/v1/org/tag_protection.go,
  DTOs in modules/structs/org_tag.go, wired in api.go).
- Enforcement: the git push/delete hook (hook_pre_receive.go) and the two
  release paths (release.go create/delete) now call the layered check, so no
  per-site tag logic changes beyond swapping the helper.
- View: the repo Tag settings page lists inherited org tag rules read-only.

Stacked on #728 (branch-protection PR) for migration ordering — merge #728
first. Swagger annotations omitted (can't regenerate the swagger JSON without
the toolchain); routes still register.

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.
Hand-verified: gofmt (tabs, no blank-in-block, struct alignment), template
nesting balances, all .Rule fields exist on OrgProtectedTag, all locale keys
defined, JSON valid, migration contiguous (363).

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 21:37:18 -05:00
jmiller 4b68853f08 feat(org): add branch-deletion protection + expandable inherited-rule view (#727)
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m3s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 1m8s
Generic: Project CI / Tests (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Universal: PR Check / Secret Scan (pull_request) Successful in 3m47s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Two related additions:

1. Branch deletion as an org-level ability. OrgProtectedBranch gained
   CanDelete / EnableDeleteAllowlist / DeleteAllowlistTeamIDs (migration 362),
   ToProtectedBranch maps them, and the API (create/edit/response DTOs +
   handlers) exposes enable_delete / enable_delete_allowlist /
   delete_allowlist_teams. The layering merge already combined delete fields, so
   org delete-protection now enforces once ToProtectedBranch populates them.

2. The repo Branch Protection view now renders each inherited org rule as an
   expandable detail (direct push, force-push, branch deletion, merge, required
   approvals, status checks, protected files) with team names resolved, instead
   of three headline badges. Still read-only.

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.
Verified by hand: struct-field gofmt alignment, template block nesting balances,
every .Rule field exists on OrgProtectedBranch, and all locale keys referenced
in the template are defined.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 21:16:24 -05:00
jmiller 86bd8a2cad feat(org): show inherited org branch-protection rules in repo settings (#727)
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 42s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m18s
PR RC Release / Build RC Release (pull_request) Successful in 1m17s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 1m32s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
The org "floor" is enforced implicitly at the choke point, so a repo admin
couldn't see which org-level rules apply to their repo. Surface them in the
repo's Branch Protection settings page (read-only), the way GitHub shows
organization rulesets in a repository.

- ProtectedBranchRules handler: when the owner is an org, load
  FindOrgProtectedBranchRules and expose them as OrgProtectedBranches.
- branches.tmpl: new read-only "Organization Branch Protection" section listing
  each org rule with an "Organization" badge, a lock/read-only marker, and
  compact indicators (required approvals, signed commits, status checks). No
  edit/delete controls — these are managed at the org level.
- en-US locale strings.

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 20:25:24 -05:00
jmiller 24b3516c1d fix(org): layer org-level branch protection with repo rules, most-restrictive wins (#727)
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Project CI / Lint & Validate (pull_request) Successful in 38s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m8s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 3m15s
Universal: PR Check / Secret Scan (pull_request) Successful in 3m5s
Generic: Project CI / Tests (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Org-level branch protection was already consulted at the single enforcement
choke point `GetFirstMatchProtectedBranchRule`, but only as a FALLBACK: if any
repo-level rule matched the branch, the org rule was ignored entirely. That let
a repo define a looser rule for a pattern and effectively opt out of the org's
protection.

Make the choke point LAYER the two rules instead: when both an org rule and a
repo rule match a branch, return their most-restrictive (fail-closed)
combination, so the org rule is a mandatory floor a repo can only tighten.

- models/git/protected_branch_merge.go: mergeMostRestrictive + helpers. Allow
  flags AND'd; gate/require/block flags OR'd; RequiredApprovals max'd; required
  sets (status contexts, protected files) unioned; allow sets (whitelists,
  unprotected files) intersected. A disabled allowlist means "everyone", so it
  only constrains when enabled.
- models/git/protected_branch_list.go: GetFirstMatchProtectedBranchRule now
  fetches both the repo rule and the org rule and merges when both match;
  returns whichever exists when only one matches. Org lookup factored into
  getFirstMatchOrgProtectedBranchRule.

Supersedes the materialization approach previously proposed for this issue —
the org fallback already existed, so only this one function needed to change.

Fail-closed by design: any merge edge errs toward MORE protection (over-restrict)
rather than less, so it cannot open a hole.

Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here —
relying on CI to validate build, formatting, and tests.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-04 19:42:08 -05:00
jmiller 343cba690e Update .mokogitea/ISSUE_TEMPLATE/feature_request.md
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 54s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m15s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
2026-07-01 05:18:57 +00:00
64 changed files with 8499 additions and 255 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '[FEATURE] '
title: '(feat) '
labels: 'enhancement'
assignees: ''
+1 -1
View File
@@ -382,7 +382,7 @@ jobs:
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
+6 -5
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-generic.yml
@@ -131,10 +131,11 @@ jobs:
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
# Run only when lint succeeded; always() forces evaluation so a skipped
# lint (e.g. template repos) skips this job cleanly instead of hanging.
if: ${{ always() && needs.lint.result == 'success' }}
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
# directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
+32 -14
View File
@@ -52,51 +52,69 @@ jobs:
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
run: |
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
# Inject runner-side values (TAG, REGISTRY_TOKEN) into the remote shell's
# environment via a command prefix, then use a *quoted* heredoc so every
# $var below expands in exactly one place: the remote dev host. This avoids
# the local-vs-remote expansion trap that previously left TAG empty.
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
set -e
echo 'SSH connected to dev environment'
if [ -z "$TAG" ]; then
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
exit 1
fi
HEALTH_FMT='{{.State.Health.Status}}'
echo 'Cleaning Docker build cache...'
docker builder prune -af 2>/dev/null || true
docker image prune -af 2>/dev/null || true
echo 'Pulling source...'
SOURCE_DIR=/opt/gitea-dev/source
if [ ! -d \$SOURCE_DIR/.git ]; then
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR
if [ ! -d "$SOURCE_DIR/.git" ]; then
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git "$SOURCE_DIR"
fi
cd \$SOURCE_DIR
cd "$SOURCE_DIR"
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
git fetch origin dev
git reset --hard origin/dev
echo 'Building Docker image...'
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
docker build --no-cache --build-arg GOFLAGS='-p 1' \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \
--tag "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" \
-f Dockerfile .
echo 'Pushing to registry...'
echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
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"
echo 'Restarting dev container...'
cd /opt/gitea-dev
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
docker compose up -d mokogitea-dev
# The dev service in the SHARED compose file reads its image tag from
# ${MOKOGITEA_DEV_TAG}. Drive it from the freshly built tag instead of
# rewriting the file with sed: the old sed pattern matched the *prod*
# service line (container_name: mokogitea) and left the dev service pinned
# to a stale image, so every dev deploy recreated old code while silently
# corrupting the prod image pin. The env-var only affects the dev service.
# Remove any lingering fixed-name container first so the recreate can't hit
# a name conflict, pin the project name for determinism, then force-recreate.
docker rm -f mokogitea-dev 2>/dev/null || true
MOKOGITEA_DEV_TAG="$TAG" docker compose -p gitea-dev up -d --force-recreate mokogitea-dev
echo 'Health check...'
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format='\$HEALTH_FMT' mokogitea-dev 2>/dev/null | grep -q healthy; then
if docker inspect --format="$HEALTH_FMT" mokogitea-dev 2>/dev/null | grep -q healthy; then
echo 'Dev container healthy!'
exit 0
fi
echo "Waiting... (attempt \$i/8)"
echo "Waiting... (attempt $i/8)"
done
echo 'Health check failed'
docker logs mokogitea-dev --tail 20
+137
View File
@@ -0,0 +1,137 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Build and deploy to the RC environment on push to the rc branch.
# Portable across Go server repos: ALL deployment config comes from repo
# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded.
# The rc branch is created by promote-rc when a PR to main opens.
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
#
# Required repo VARIABLES (all tier-scoped so each environment is independent —
# a repo may host its rc/dev/prod tiers on different machines):
# RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME - SSH deploy target for the rc tier
# RC_REGISTRY, RC_REGISTRY_USER, RC_IMAGE - container registry + login user + image
# RC_CONTAINER - compose service/container to recreate
# RC_COMPOSE_PROJECT - docker compose -p project name
# RC_COMPOSE_DIR - dir containing docker-compose.yml on host
# RC_SOURCE_DIR - build source checkout on host
# RC_TAG_ENV - compose env-var name that pins the image tag
# RC_HEALTH_URL - external URL to verify after deploy
# Required SECRETS (already configured org-wide; reused, not re-set):
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
# MOKOGITEA_TOKEN - registry/API token (org secret)
name: Deploy (RC)
on:
push:
branches:
- rc
# Manual trigger for isolated end-to-end tests without a full RC promotion.
# Runs on the ref it is dispatched from; that ref must carry current source
# (>= the RC database migration version) or the rebuilt image will refuse the
# newer DB. Dispatch from `rc` once `rc` is current.
workflow_dispatch:
concurrency:
group: deploy-rc
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
deploy-rc:
name: "Build & Deploy to RC"
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine version
id: config
run: |
VERSION=$(git describe --tags --always 2>/dev/null || echo "rc-$(git rev-parse --short HEAD)")
echo "tag=${VERSION}-rc" >> $GITHUB_OUTPUT
echo "Version: ${VERSION}-rc"
- name: Write deploy key
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Build and deploy to RC via SSH
env:
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
run: |
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
# via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the
# remote. Repo variables (vars.*) are substituted inline by Actions before ssh.
ssh -i ~/.ssh/deploy_key -p ${{ vars.RC_SSH_PORT }} \
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }} \
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
set -e
echo 'SSH connected to RC environment'
if [ -z "$TAG" ]; then
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
exit 1
fi
HEALTH_FMT='{{.State.Health.Status}}'
echo 'Cleaning Docker build cache...'
docker builder prune -af 2>/dev/null || true
docker image prune -af 2>/dev/null || true
echo 'Pulling source...'
SOURCE_DIR='${{ vars.RC_SOURCE_DIR }}'
if [ ! -d "$SOURCE_DIR/.git" ]; then
git clone -b rc ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR"
fi
cd "$SOURCE_DIR"
git remote set-url origin ${{ github.server_url }}/${{ github.repository }}.git 2>/dev/null || true
git fetch origin rc
git reset --hard origin/rc
echo "Building image: ${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG"
docker build --no-cache --build-arg GOFLAGS='-p 1' \
--tag "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" \
-f Dockerfile .
echo 'Pushing to registry...'
echo "$REGISTRY_TOKEN" | docker login ${{ vars.RC_REGISTRY }} -u ${{ vars.RC_REGISTRY_USER }} --password-stdin
docker push "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG"
echo 'Restarting RC container...'
cd '${{ vars.RC_COMPOSE_DIR }}'
# Drive the rc service image tag via its compose env-var (no sed on the shared
# file); remove any lingering fixed-name container first, then force-recreate.
docker rm -f '${{ vars.RC_CONTAINER }}' 2>/dev/null || true
${{ vars.RC_TAG_ENV }}="$TAG" docker compose -p '${{ vars.RC_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.RC_CONTAINER }}'
echo 'Health check...'
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format="$HEALTH_FMT" '${{ vars.RC_CONTAINER }}' 2>/dev/null | grep -q healthy; then
echo 'RC container healthy!'
exit 0
fi
echo "Waiting... (attempt $i/8)"
done
echo 'Health check failed'
docker logs '${{ vars.RC_CONTAINER }}' --tail 20
exit 1
DEPLOY_EOF
- name: Verify RC instance
run: |
sleep 5
curl -sf "${{ vars.RC_HEALTH_URL }}" && echo " RC API healthy"
+1 -1
View File
@@ -210,7 +210,7 @@ jobs:
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
+11
View File
@@ -3,6 +3,12 @@
## [Unreleased]
### Added
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
- Org-level push policy: one policy per org, enforced in the pre-receive hook across all its repositories — branch/tag name conventions (glob), a mandatory secret-scanning block-on-push that repos cannot disable, a max pushed-file size, and blocked file-path patterns. API at `/orgs/{org}/push_policy`. Naming is fail-closed; the content checks (blocked paths, max size) fail open on error so a policy bug can never block every push (#727)
- Org-level repository defaults: an org can force new/transferred repositories private and set default pull-request settings (allowed merge styles, default merge style, auto-delete branch after merge), applied via a notifier when a repo is created in or transferred into the org (best-effort — never blocks repo creation). API at `/orgs/{org}/repo_defaults` (#727)
- Org-level email domain policy: restrict which email domains an organization's members may have — a user can only be added to the org (via any team) if their primary email matches one of the allowed domain globs. Enforced at the single membership-add choke point (`AddTeamMember`); API at `/orgs/{org}/email_domain_policy` (#727)
- Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552)
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
@@ -57,6 +63,11 @@
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
### Fixed
- 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)
- Removed a stray `package-lock.json` (13.9k lines) that a `git add -A` had accidentally swept into the org-push-policy branch (#734)
- Org-level branch protection now **layers** with per-repo rules instead of being ignored whenever a repo rule exists. When both an org rule and a repo rule match a branch, the effective rule is the most-restrictive (fail-closed) combination — the org rule is a mandatory floor a repo cannot weaken: allow flags AND'd, gate/require/block flags OR'd, required approvals max'd, status checks and protected-file patterns unioned, whitelists intersected. Previously a repo rule shadowed the org rule entirely at the enforcement choke point (`GetFirstMatchProtectedBranchRule`), letting a repo opt out of org protection (#727)
- Org Teams page: list now renders — the handler wrote `ctx.Data["OrgListTeams"]` but the template reads `.Teams`, so the page showed header/nav but no teams (#720)
- Issue type: now editable after creation for users with issue write permission — the sidebar gated editing on a `FieldEditFlags` data key that was never populated (always read-only); now uses `HasIssuesOrPullsWritePermission` like the priority field (#721)
- 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
+2 -1
View File
@@ -1,6 +1,6 @@
# MokoGitea
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org metadata, CI standardization, and project board API.
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org-level governance, org metadata, CI standardization, and project board API.
![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square)
@@ -17,6 +17,7 @@ Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cas
- **Default Org Teams** -- auto-create Developers, Reviewers, and CI/CD teams on org creation
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
- **Branch Protection** -- delete allowlist for protected branches (per-user/team/deploy-key)
- **Org Governance** -- organization-wide rules that layer onto every repository: branch protection as a most-restrictive floor a repo cannot weaken, tag protection (team allowlist), push policy (branch/tag naming, mandatory secret-block, max file size, blocked paths), repository defaults (force-private, PR merge settings), and member email-domain allowlists
- **Project Board API** -- REST endpoints for project columns and cards
- **CI Infrastructure** -- reusable workflows, centralized ci-issue-reporter, standardized MOKOGITEA_TOKEN naming
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgEmailDomainPolicy restricts which email domains an organization's members may
// have. When configured, a user can only be added to the org if their primary email
// matches one of the allowed domain globs. At most one row per org. See issue #727.
type OrgEmailDomainPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
AllowedDomains string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgEmailDomainPolicy))
}
// ErrEmailDomainNotAllowed is returned when a user's email domain is not permitted
// by the organization's email domain policy.
type ErrEmailDomainNotAllowed struct {
Email string
OrgID int64
}
func (e ErrEmailDomainNotAllowed) Error() string {
return fmt.Sprintf("email %q is not in an allowed domain for organization %d", e.Email, e.OrgID)
}
// IsErrEmailDomainNotAllowed reports whether err is an ErrEmailDomainNotAllowed.
func IsErrEmailDomainNotAllowed(err error) bool {
_, ok := err.(ErrEmailDomainNotAllowed)
return ok
}
func (p *OrgEmailDomainPolicy) domainGlobs() []glob.Glob {
var out []glob.Glob
for _, d := range strings.Split(p.AllowedDomains, ";") {
d = strings.TrimSpace(strings.ToLower(d))
if d == "" {
continue
}
if g, err := glob.Compile(d); err == nil {
out = append(out, g)
} else {
log.Warn("Invalid org email domain glob %q: %v", d, err)
}
}
return out
}
// EmailAllowed reports whether email's domain satisfies the policy. An empty policy
// (no configured domains) allows any email.
func (p *OrgEmailDomainPolicy) EmailAllowed(email string) bool {
globs := p.domainGlobs()
if len(globs) == 0 {
return true
}
at := strings.LastIndexByte(email, '@')
if at < 0 {
return false
}
domain := strings.ToLower(email[at+1:])
for _, g := range globs {
if g.Match(domain) {
return true
}
}
return false
}
// GetOrgEmailDomainPolicy returns the org's email domain policy, or nil if none.
func GetOrgEmailDomainPolicy(ctx context.Context, orgID int64) (*OrgEmailDomainPolicy, error) {
policy, exist, err := db.Get[OrgEmailDomainPolicy](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return policy, nil
}
// OrgEmailDomainAllowed reports whether email is permitted for the org. It returns
// true when the org has no policy configured.
func OrgEmailDomainAllowed(ctx context.Context, orgID int64, email string) (bool, error) {
policy, err := GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
return false, err
}
if policy == nil {
return true, nil
}
return policy.EmailAllowed(email), nil
}
// UpsertOrgEmailDomainPolicy creates or updates the single policy row for an org.
func UpsertOrgEmailDomainPolicy(ctx context.Context, policy *OrgEmailDomainPolicy) error {
existing, err := GetOrgEmailDomainPolicy(ctx, policy.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
return fmt.Errorf("Insert OrgEmailDomainPolicy: %v", err)
}
return nil
}
policy.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
return fmt.Errorf("Update OrgEmailDomainPolicy: %v", err)
}
return nil
}
// DeleteOrgEmailDomainPolicy removes an org's email domain policy.
func DeleteOrgEmailDomainPolicy(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgEmailDomainPolicy))
return err
}
+27 -2
View File
@@ -28,16 +28,28 @@ type OrgProtectedBranch struct {
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"`
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"`
@@ -81,8 +93,9 @@ func (o *OrgProtectedBranch) Match(branchName string) bool {
}
// ToProtectedBranch converts an org-level rule to a ProtectedBranch for use
// in the standard enforcement path. Fields that are user-scoped (WhitelistUserIDs etc.)
// are left empty because org rules only reference teams.
// in the standard enforcement path. Both team-scoped and user-scoped allowlists
// (plus the Actions-bot toggles) are carried across; deploy-key allowances remain
// repo-only because an org rule cannot express them.
func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
return &ProtectedBranch{
ID: o.ID,
@@ -91,16 +104,28 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
CanPush: o.CanPush,
EnableWhitelist: o.EnableWhitelist,
WhitelistTeamIDs: o.WhitelistTeamIDs,
WhitelistUserIDs: o.WhitelistUserIDs,
WhitelistActionsUser: o.WhitelistActionsUser,
EnableMergeWhitelist: o.EnableMergeWhitelist,
MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs,
MergeWhitelistUserIDs: o.MergeWhitelistUserIDs,
MergeWhitelistActionsUser: o.MergeWhitelistActionsUser,
CanForcePush: o.CanForcePush,
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
ForcePushAllowlistUserIDs: o.ForcePushAllowlistUserIDs,
ForcePushAllowlistActionsUser: o.ForcePushAllowlistActionsUser,
CanDelete: o.CanDelete,
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
DeleteAllowlistUserIDs: o.DeleteAllowlistUserIDs,
DeleteAllowlistActionsUser: o.DeleteAllowlistActionsUser,
EnableStatusCheck: o.EnableStatusCheck,
StatusCheckContexts: o.StatusCheckContexts,
RequiredApprovals: o.RequiredApprovals,
EnableApprovalsWhitelist: o.EnableApprovalsWhitelist,
ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs,
ApprovalsWhitelistUserIDs: o.ApprovalsWhitelistUserIDs,
BlockOnRejectedReviews: o.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: o.BlockOnOutdatedBranch,
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgProtectedTag represents an org-level tag protection rule. It cascades to all
// repositories in the organization and layers on top of each repo's own protected
// tags (a tag is controllable only if allowed at both levels). Org rules reference
// teams only (like OrgProtectedBranch). See issue #727.
type OrgProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
NamePattern string `xorm:"UNIQUE(s)"`
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgProtectedTag))
}
// ToProtectedTag converts an org-level tag rule into a repo-scoped ProtectedTag so
// the standard name-matching and allowlist logic can be reused. Org rules are
// team-only, so the user allowlist is left empty.
func (o *OrgProtectedTag) ToProtectedTag() *ProtectedTag {
return &ProtectedTag{
NamePattern: o.NamePattern,
AllowlistTeamIDs: o.AllowlistTeamIDs,
}
}
// GetOrgProtectedTagByID retrieves a single org tag rule by org ID and rule ID.
func GetOrgProtectedTagByID(ctx context.Context, orgID, id int64) (*OrgProtectedTag, error) {
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "id": id})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return rule, nil
}
// GetOrgProtectedTagByNamePattern retrieves a single org tag rule by its pattern.
func GetOrgProtectedTagByNamePattern(ctx context.Context, orgID int64, pattern string) (*OrgProtectedTag, error) {
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "name_pattern": pattern})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return rule, nil
}
// FindOrgProtectedTags loads all org-level tag protection rules for an organization.
func FindOrgProtectedTags(ctx context.Context, orgID int64) ([]*OrgProtectedTag, error) {
var rules []*OrgProtectedTag
err := db.GetEngine(ctx).Where("org_id = ?", orgID).Asc("created_unix").Find(&rules)
return rules, err
}
// CreateOrgProtectedTag creates a new org-level tag protection rule.
func CreateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
if _, err := db.GetEngine(ctx).Insert(rule); err != nil {
return fmt.Errorf("Insert OrgProtectedTag: %v", err)
}
return nil
}
// UpdateOrgProtectedTag updates an existing org-level tag protection rule.
func UpdateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
if _, err := db.GetEngine(ctx).ID(rule.ID).AllCols().Update(rule); err != nil {
return fmt.Errorf("Update OrgProtectedTag: %v", err)
}
return nil
}
// DeleteOrgProtectedTag deletes an org-level tag protection rule.
func DeleteOrgProtectedTag(ctx context.Context, orgID, id int64) error {
affected, err := db.GetEngine(ctx).Where("org_id = ? AND id = ?", orgID, id).Delete(new(OrgProtectedTag))
if err != nil {
return err
}
if affected == 0 {
return fmt.Errorf("org tag protection rule ID(%d) not found", id)
}
return nil
}
// IsUserAllowedToControlTagInRepo layers org-level tag rules on top of a repo's own
// protected tags: the user must be allowed by BOTH levels (most-restrictive). The
// repo decision is evaluated first (using the already-loaded repoTags); if it
// allows and the owner is an organization, the org-level rules must also allow.
func IsUserAllowedToControlTagInRepo(ctx context.Context, repoTags []*ProtectedTag, repo *repo_model.Repository, tagName string, userID int64) (bool, error) {
allowed, err := IsUserAllowedToControlTag(ctx, repoTags, tagName, userID)
if err != nil || !allowed {
return allowed, err
}
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return false, err
}
if !owner.IsOrganization() {
return true, nil
}
orgRules, err := FindOrgProtectedTags(ctx, owner.ID)
if err != nil {
return false, err
}
if len(orgRules) == 0 {
return true, nil
}
orgTags := make([]*ProtectedTag, len(orgRules))
for i, r := range orgRules {
orgTags[i] = r.ToProtectedTag()
}
return IsUserAllowedToControlTag(ctx, orgTags, tagName, userID)
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgPushPolicy is a single org-wide policy enforced in the pre-receive hook on
// every repository of the organization. Unlike the branch/tag rulesets there is at
// most one policy per org. Empty pattern / zero fields mean "no constraint". See #727.
type OrgPushPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
BranchNamePattern string `xorm:"TEXT"`
TagNamePattern string `xorm:"TEXT"`
RequireSecretBlock bool `xorm:"NOT NULL DEFAULT false"`
MaxFileSize int64 `xorm:"NOT NULL DEFAULT 0"`
BlockedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgPushPolicy))
}
// nameMatchesPattern reports whether name satisfies a glob pattern. An empty pattern
// imposes no constraint; an invalid pattern fails open (no constraint) so a
// misconfigured policy never blocks all pushes.
func nameMatchesPattern(pattern, name string) bool {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return true
}
g, err := glob.Compile(pattern, '/')
if err != nil {
log.Warn("Invalid org push policy name pattern %q: %v", pattern, err)
return true
}
return g.Match(name)
}
// BranchNameAllowed reports whether a branch name satisfies the naming policy.
func (p *OrgPushPolicy) BranchNameAllowed(name string) bool {
return nameMatchesPattern(p.BranchNamePattern, name)
}
// TagNameAllowed reports whether a tag name satisfies the naming policy.
func (p *OrgPushPolicy) TagNameAllowed(name string) bool {
return nameMatchesPattern(p.TagNamePattern, name)
}
// BlockedFileGlobs parses the ';'-separated blocked file pattern list.
func (p *OrgPushPolicy) BlockedFileGlobs() []glob.Glob {
var out []glob.Glob
for _, expr := range strings.Split(p.BlockedFilePatterns, ";") {
expr = strings.TrimSpace(strings.ToLower(expr))
if expr == "" {
continue
}
if g, err := glob.Compile(expr, '.', '/'); err == nil {
out = append(out, g)
} else {
log.Warn("Invalid org push policy blocked file pattern %q: %v", expr, err)
}
}
return out
}
// GetOrgPushPolicy returns the org's push policy, or nil if none is configured.
func GetOrgPushPolicy(ctx context.Context, orgID int64) (*OrgPushPolicy, error) {
policy, exist, err := db.Get[OrgPushPolicy](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return policy, nil
}
// GetOrgPushPolicyForRepo returns the push policy of the repo's owning organization,
// or nil if the owner is not an organization or has no policy.
func GetOrgPushPolicyForRepo(ctx context.Context, repo *repo_model.Repository) (*OrgPushPolicy, error) {
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil //nolint:nilnil
}
return GetOrgPushPolicy(ctx, owner.ID)
}
// UpsertOrgPushPolicy creates or updates the single push policy for an org.
func UpsertOrgPushPolicy(ctx context.Context, policy *OrgPushPolicy) error {
existing, err := GetOrgPushPolicy(ctx, policy.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
return fmt.Errorf("Insert OrgPushPolicy: %v", err)
}
return nil
}
policy.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
return fmt.Errorf("Update OrgPushPolicy: %v", err)
}
return nil
}
// DeleteOrgPushPolicy removes an org's push policy.
func DeleteOrgPushPolicy(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgPushPolicy))
return err
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgRepoDefaults holds an organization's default repository settings, applied to a
// repository when it is created in or transferred into the org (via a notifier).
// There is at most one row per org. See issue #727.
type OrgRepoDefaults struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
ForcePrivate bool `xorm:"NOT NULL DEFAULT false"`
ApplyPRDefaults bool `xorm:"NOT NULL DEFAULT false"`
AllowMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowRebase bool `xorm:"NOT NULL DEFAULT true"`
AllowRebaseMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowSquash bool `xorm:"NOT NULL DEFAULT true"`
AllowFastForwardOnly bool `xorm:"NOT NULL DEFAULT true"`
DefaultMergeStyle string `xorm:"TEXT"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgRepoDefaults))
}
// GetOrgRepoDefaults returns the org's repo defaults, or nil if none are configured.
func GetOrgRepoDefaults(ctx context.Context, orgID int64) (*OrgRepoDefaults, error) {
defaults, exist, err := db.Get[OrgRepoDefaults](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return defaults, nil
}
// GetOrgRepoDefaultsForRepo returns the repo-defaults of the repo's owning org, or
// nil if the owner is not an organization or has none configured.
func GetOrgRepoDefaultsForRepo(ctx context.Context, repo *repo_model.Repository) (*OrgRepoDefaults, error) {
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil //nolint:nilnil
}
return GetOrgRepoDefaults(ctx, owner.ID)
}
// UpsertOrgRepoDefaults creates or updates the single repo-defaults row for an org.
func UpsertOrgRepoDefaults(ctx context.Context, defaults *OrgRepoDefaults) error {
existing, err := GetOrgRepoDefaults(ctx, defaults.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(defaults); err != nil {
return fmt.Errorf("Insert OrgRepoDefaults: %v", err)
}
return nil
}
defaults.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(defaults); err != nil {
return fmt.Errorf("Update OrgRepoDefaults: %v", err)
}
return nil
}
// DeleteOrgRepoDefaults removes an org's repo defaults.
func DeleteOrgRepoDefaults(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgRepoDefaults))
return err
}
+31 -8
View File
@@ -85,19 +85,40 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
return results, nil
}
// GetFirstMatchProtectedBranchRule returns the first matched rule.
// It checks repo-level rules first; if none match, it falls back to org-level rules
// (if the repo belongs to an organization).
// GetFirstMatchProtectedBranchRule returns the effective protected-branch rule for a
// branch. It combines the matching repo-level rule with the matching org-level rule
// (when the repo belongs to an organization): if both match they are layered with
// mergeMostRestrictive so the org rule acts as a floor the repo cannot weaken; if
// only one matches that one is returned; if neither matches, nil.
func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
rules, err := FindRepoProtectedBranchRules(ctx, repoID)
if err != nil {
return nil, err
}
if matched := rules.GetFirstMatched(branchName); matched != nil {
return matched, nil
repoRule := rules.GetFirstMatched(branchName)
orgRule, err := getFirstMatchOrgProtectedBranchRule(ctx, repoID, branchName)
if err != nil {
return nil, err
}
// Fall back to org-level rules
switch {
case repoRule == nil && orgRule == nil:
return nil, nil
case orgRule == nil:
return repoRule, nil
case repoRule == nil:
return orgRule, nil
default:
return mergeMostRestrictive(repoRule, orgRule), nil
}
}
// getFirstMatchOrgProtectedBranchRule returns the matching org-level rule for a
// branch expressed as a repo-scoped ProtectedBranch (RepoID set so downstream
// permission checks work), or nil if the repo's owner is not an organization or no
// org rule matches.
func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, err
@@ -112,14 +133,16 @@ func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchN
orgRule, err := FindOrgBranchRuleForBranch(ctx, owner.ID, branchName)
if err != nil {
// Fail closed: propagate the error so callers keep the org floor in force
// rather than silently falling back to the repo rule on a transient error.
log.Error("FindOrgBranchRuleForBranch: %v", err)
return nil, nil
return nil, err
}
if orgRule == nil {
return nil, nil
}
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly.
pb := orgRule.ToProtectedBranch()
pb.RepoID = repoID
return pb, nil
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import "strings"
// mergeMostRestrictive combines a repo-level and an org-level protected-branch rule
// that both match the same branch into a single effective rule, always applying the
// STRICTER constraint from each side (fail-closed). This makes an org-level rule a
// mandatory floor that a repo rule can only tighten, never weaken. See issue #727.
//
// Combination directions:
// - "Can*" / allow booleans -> AND (an action is allowed only if both allow it)
// - "Enable*/Block*/Require*" -> OR (a gate is on if either side turns it on)
// - RequiredApprovals -> max
// - required-set lists -> union (status contexts, protected files)
// - allow-set lists -> intersection (whitelists, unprotected files)
//
// Identity (ID, RepoID, RuleName, Priority) is taken from the repo rule so that
// downstream permission checks (which LoadRepo via RepoID) keep working.
func mergeMostRestrictive(repoRule, orgRule *ProtectedBranch) *ProtectedBranch {
eff := *repoRule
// Direct push.
eff.CanPush = repoRule.CanPush && orgRule.CanPush
eff.EnableWhitelist, eff.WhitelistUserIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistUserIDs, orgRule.EnableWhitelist, orgRule.WhitelistUserIDs)
_, eff.WhitelistTeamIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistTeamIDs, orgRule.EnableWhitelist, orgRule.WhitelistTeamIDs)
// Deploy-key allowances are still not expressible in an OrgProtectedBranch, so the
// org rule imposes no constraint on them; carry the repo value through unchanged.
// ANDing against the org side's always-false zero value would silently lock out
// every deploy-key push in any org that has a matching branch rule (see #727 review).
// The Actions-bot toggle IS now expressible org-side, so merge it most-restrictively.
eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys
eff.WhitelistActionsUser = mergeAllowFlag(repoRule.EnableWhitelist, repoRule.WhitelistActionsUser, orgRule.EnableWhitelist, orgRule.WhitelistActionsUser)
// Force push.
eff.CanForcePush = repoRule.CanForcePush && orgRule.CanForcePush
eff.EnableForcePushAllowlist, eff.ForcePushAllowlistUserIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistUserIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistUserIDs)
_, eff.ForcePushAllowlistTeamIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistTeamIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistTeamIDs)
eff.ForcePushAllowlistDeployKeys = repoRule.ForcePushAllowlistDeployKeys
eff.ForcePushAllowlistActionsUser = mergeAllowFlag(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistActionsUser, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistActionsUser)
// Delete.
eff.CanDelete = repoRule.CanDelete && orgRule.CanDelete
eff.EnableDeleteAllowlist, eff.DeleteAllowlistUserIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistUserIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistUserIDs)
_, eff.DeleteAllowlistTeamIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistTeamIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistTeamIDs)
eff.DeleteAllowlistDeployKeys = repoRule.DeleteAllowlistDeployKeys
eff.DeleteAllowlistActionsUser = mergeAllowFlag(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistActionsUser, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistActionsUser)
// Merge whitelist.
eff.EnableMergeWhitelist, eff.MergeWhitelistUserIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistUserIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistUserIDs)
_, eff.MergeWhitelistTeamIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistTeamIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistTeamIDs)
eff.MergeWhitelistActionsUser = mergeAllowFlag(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistActionsUser, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistActionsUser)
// Status checks.
eff.EnableStatusCheck = repoRule.EnableStatusCheck || orgRule.EnableStatusCheck
eff.StatusCheckContexts = unionStrings(repoRule.StatusCheckContexts, orgRule.StatusCheckContexts)
// Approvals and reviews.
eff.RequiredApprovals = maxInt64(repoRule.RequiredApprovals, orgRule.RequiredApprovals)
eff.EnableApprovalsWhitelist, eff.ApprovalsWhitelistUserIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistUserIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistUserIDs)
_, eff.ApprovalsWhitelistTeamIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistTeamIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistTeamIDs)
eff.BlockOnRejectedReviews = repoRule.BlockOnRejectedReviews || orgRule.BlockOnRejectedReviews
eff.BlockOnOfficialReviewRequests = repoRule.BlockOnOfficialReviewRequests || orgRule.BlockOnOfficialReviewRequests
eff.BlockOnOutdatedBranch = repoRule.BlockOnOutdatedBranch || orgRule.BlockOnOutdatedBranch
eff.DismissStaleApprovals = repoRule.DismissStaleApprovals || orgRule.DismissStaleApprovals
eff.IgnoreStaleApprovals = repoRule.IgnoreStaleApprovals || orgRule.IgnoreStaleApprovals
// Commits, files, admin override.
eff.RequireSignedCommits = repoRule.RequireSignedCommits || orgRule.RequireSignedCommits
eff.ProtectedFilePatterns = unionPatterns(repoRule.ProtectedFilePatterns, orgRule.ProtectedFilePatterns)
eff.UnprotectedFilePatterns = intersectPatterns(repoRule.UnprotectedFilePatterns, orgRule.UnprotectedFilePatterns)
eff.BlockAdminMergeOverride = repoRule.BlockAdminMergeOverride || orgRule.BlockAdminMergeOverride
return &eff
}
// mergeAllowlist combines two allow-lists under most-restrictive semantics. An
// allow-list only narrows access when its Enable flag is set; a disabled list means
// "everyone", so it imposes no constraint. Therefore: if both are enabled the result
// is the intersection (a principal must be allowed by both); if only one is enabled
// its list is used as-is; if neither is enabled the list is irrelevant.
func mergeAllowlist(aEnabled bool, aIDs []int64, bEnabled bool, bIDs []int64) (bool, []int64) {
switch {
case aEnabled && bEnabled:
return true, intersectInt64(aIDs, bIDs)
case aEnabled:
return true, aIDs
case bEnabled:
return true, bIDs
default:
return false, nil
}
}
// mergeAllowFlag combines two "allow the Actions bot" toggles under most-restrictive
// semantics, mirroring mergeAllowlist. A toggle only grants access when its Enable flag
// is set; a disabled allowlist means "everyone", so it imposes no constraint. Therefore:
// if both allowlists are enabled the bot is allowed only when BOTH sides allow it; if
// only one is enabled that side's flag governs; if neither is enabled the flag is
// irrelevant (fall back to the repo/a value).
func mergeAllowFlag(aEnabled, aFlag, bEnabled, bFlag bool) bool {
switch {
case aEnabled && bEnabled:
return aFlag && bFlag
case aEnabled:
return aFlag
case bEnabled:
return bFlag
default:
return aFlag
}
}
func intersectInt64(a, b []int64) []int64 {
if len(a) == 0 || len(b) == 0 {
return nil
}
set := make(map[int64]struct{}, len(a))
for _, x := range a {
set[x] = struct{}{}
}
var out []int64
for _, x := range b {
if _, ok := set[x]; ok {
out = append(out, x)
}
}
return out
}
func unionStrings(a, b []string) []string {
if len(a) == 0 {
return b
}
if len(b) == 0 {
return a
}
seen := make(map[string]struct{}, len(a)+len(b))
out := make([]string, 0, len(a)+len(b))
for _, s := range a {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
}
}
for _, s := range b {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
}
}
return out
}
// unionPatterns unions two ';'-separated file-pattern lists (more patterns protected
// = more restrictive).
func unionPatterns(a, b string) string {
return strings.Join(unionStrings(splitPatterns(a), splitPatterns(b)), ";")
}
// intersectPatterns intersects two ';'-separated file-pattern lists. Unprotected
// patterns are carve-outs that REDUCE protection, so the restrictive combination
// keeps only the exemptions present in both.
func intersectPatterns(a, b string) string {
as, bs := splitPatterns(a), splitPatterns(b)
set := make(map[string]struct{}, len(as))
for _, s := range as {
set[s] = struct{}{}
}
seen := make(map[string]struct{}, len(bs))
var out []string
for _, s := range bs {
if _, ok := set[s]; !ok {
continue
}
if _, dup := seen[s]; dup {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
return strings.Join(out, ";")
}
func splitPatterns(s string) []string {
var out []string
for _, p := range strings.Split(s, ";") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMergeAllowFlag(t *testing.T) {
cases := []struct {
name string
aEnabled bool
aFlag bool
bEnabled bool
bFlag bool
expected bool
}{
// Both allowlists enabled: bot allowed only when BOTH allow it.
{"both enabled, both allow", true, true, true, true, true},
{"both enabled, a denies", true, false, true, true, false},
{"both enabled, b denies", true, true, true, false, false},
{"both enabled, both deny", true, false, true, false, false},
// Only one side enabled: that side governs.
{"only a enabled, a allows", true, true, false, false, true},
{"only a enabled, a denies", true, false, false, true, false},
{"only b enabled, b allows", false, false, true, true, true},
{"only b enabled, b denies", false, true, true, false, false},
// Neither enabled: flag irrelevant, falls back to a's flag.
{"neither enabled, a true", false, true, false, false, true},
{"neither enabled, a false", false, false, false, true, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.expected, mergeAllowFlag(c.aEnabled, c.aFlag, c.bEnabled, c.bFlag))
})
}
}
func TestMergeMostRestrictiveUserIDsIntersect(t *testing.T) {
repoRule := &ProtectedBranch{
EnableWhitelist: true,
WhitelistUserIDs: []int64{1, 2, 3},
// Deploy keys are repo-only and must always pass through unchanged.
WhitelistDeployKeys: true,
ForcePushAllowlistDeployKeys: true,
DeleteAllowlistDeployKeys: true,
}
orgRule := &ProtectedBranch{
EnableWhitelist: true,
WhitelistUserIDs: []int64{2, 3, 4},
// Org rule cannot express deploy keys; these zero values must NOT override repo.
}
eff := mergeMostRestrictive(repoRule, orgRule)
// User IDs are intersected when both allowlists are enabled.
assert.True(t, eff.EnableWhitelist)
assert.ElementsMatch(t, []int64{2, 3}, eff.WhitelistUserIDs)
// Deploy-key allowances still pass through from the repo rule.
assert.True(t, eff.WhitelistDeployKeys)
assert.True(t, eff.ForcePushAllowlistDeployKeys)
assert.True(t, eff.DeleteAllowlistDeployKeys)
}
func TestMergeMostRestrictiveActionsUserFlags(t *testing.T) {
// Both sides enable their allowlist and both allow the bot -> allowed.
repoRule := &ProtectedBranch{
EnableWhitelist: true,
WhitelistActionsUser: true,
EnableForcePushAllowlist: true,
// force-push: org denies -> effective deny.
ForcePushAllowlistActionsUser: true,
EnableDeleteAllowlist: true,
DeleteAllowlistActionsUser: true,
// merge: repo allowlist disabled, org enabled+denies -> org governs (deny).
EnableMergeWhitelist: false,
MergeWhitelistActionsUser: true,
}
orgRule := &ProtectedBranch{
EnableWhitelist: true,
WhitelistActionsUser: true,
EnableForcePushAllowlist: true,
ForcePushAllowlistActionsUser: false,
EnableDeleteAllowlist: true,
DeleteAllowlistActionsUser: true,
EnableMergeWhitelist: true,
MergeWhitelistActionsUser: false,
}
eff := mergeMostRestrictive(repoRule, orgRule)
assert.True(t, eff.WhitelistActionsUser, "push: both allow")
assert.False(t, eff.ForcePushAllowlistActionsUser, "force-push: org denies")
assert.True(t, eff.DeleteAllowlistActionsUser, "delete: both allow")
assert.False(t, eff.MergeWhitelistActionsUser, "merge: org enabled and denies")
}
+6
View File
@@ -439,6 +439,12 @@ func prepareMigrationTasks() []*migration {
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch),
newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable),
newMigration(362, "Add delete allowlist to org protected branch", v1_27.AddDeleteAllowlistToOrgProtectedBranch),
newMigration(363, "Add org protected tag table", v1_27.AddOrgProtectedTagTable),
newMigration(364, "Add org push policy table", v1_27.AddOrgPushPolicyTable),
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),
}
return preparedMigrations
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
// AddRepoManifestTable creates the repo_manifest table for storing
// mokoplatform manifest settings per repository.
// mokocli manifest settings per repository.
func AddRepoManifestTable(x *xorm.Engine) error {
type RepoManifest struct {
ID int64 `xorm:"pk autoincr"`
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddDeleteAllowlistToOrgProtectedBranch adds branch-deletion protection columns to
// org-level branch protection rules, mirroring the per-repo delete allowlist so an
// org rule can also protect branches from deletion. See issue #727.
func AddDeleteAllowlistToOrgProtectedBranch(x *xorm.Engine) error {
type OrgProtectedBranch struct {
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
}
return x.Sync(new(OrgProtectedBranch))
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgProtectedTagTable creates the org-level tag protection table. Org tag rules
// cascade to all repositories in the organization and layer on top of each repo's
// own protected tags. See issue #727.
func AddOrgProtectedTagTable(x *xorm.Engine) error {
type OrgProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
NamePattern string `xorm:"UNIQUE(s)"`
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgProtectedTag))
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgPushPolicyTable creates the org-level push policy table (one row per org),
// enforced in the pre-receive hook across all repositories of the org. See #727.
func AddOrgPushPolicyTable(x *xorm.Engine) error {
type OrgPushPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
BranchNamePattern string `xorm:"TEXT"`
TagNamePattern string `xorm:"TEXT"`
RequireSecretBlock bool `xorm:"NOT NULL DEFAULT false"`
MaxFileSize int64 `xorm:"NOT NULL DEFAULT 0"`
BlockedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgPushPolicy))
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgRepoDefaultsTable creates the org repository-defaults table (one row per
// org), applied to repositories created in or transferred into the org. See #727.
func AddOrgRepoDefaultsTable(x *xorm.Engine) error {
type OrgRepoDefaults struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
ForcePrivate bool `xorm:"NOT NULL DEFAULT false"`
ApplyPRDefaults bool `xorm:"NOT NULL DEFAULT false"`
AllowMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowRebase bool `xorm:"NOT NULL DEFAULT true"`
AllowRebaseMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowSquash bool `xorm:"NOT NULL DEFAULT true"`
AllowFastForwardOnly bool `xorm:"NOT NULL DEFAULT true"`
DefaultMergeStyle string `xorm:"TEXT"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgRepoDefaults))
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgEmailDomainPolicyTable creates the org email-domain policy table (one row
// per org) restricting the email domains of members added to the org. See #727.
func AddOrgEmailDomainPolicyTable(x *xorm.Engine) error {
type OrgEmailDomainPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
AllowedDomains string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgEmailDomainPolicy))
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddUserAllowlistsToOrgProtectedBranch adds per-user allowlist columns (username or
// email resolved to user IDs) plus the "allow Actions bot" toggles to org-level branch
// protection rules for the push, merge, force-push and delete categories, mirroring the
// per-repo user allowlists so an org rule can also grant access to individual users and
// the Actions bot. See issue #727.
func AddUserAllowlistsToOrgProtectedBranch(x *xorm.Engine) error {
type OrgProtectedBranch struct {
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
}
return x.Sync(new(OrgProtectedBranch))
}
+21 -21
View File
@@ -15,50 +15,50 @@ func init() {
db.RegisterModel(new(RepoMetadata))
}
// RepoMetadata stores mokoplatform metadata settings for a repository.
// These fields correspond to the .mokogitea/manifest.xml schema and are
// exposed via API for use by Actions workflows and the mokoplatform CLI.
// RepoMetadata stores mokocli metadata settings for a repository.
// These first-class fields are the authoritative repository metadata,
// exposed via API for use by Actions workflows and mokocli.
type RepoMetadata struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
// identity section
Name string `xorm:"TEXT 'name'"` // project name
Org string `xorm:"TEXT 'org'"` // organization name
Description string `xorm:"TEXT 'description'"` // project description
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
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
// governance section
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokocli standards version
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
// versioning
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
// distribution metadata (used by update server feed generation)
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"
// build section
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
// deploy section
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
+36
View File
@@ -14,16 +14,28 @@ type OrgBranchProtection struct {
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
@@ -46,16 +58,28 @@ type CreateOrgBranchProtectionOption struct {
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
@@ -73,16 +97,28 @@ type EditOrgBranchProtectionOption struct {
EnablePush *bool `json:"enable_push"`
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"`
EnableForcePush *bool `json:"enable_force_push"`
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"`
EnableDelete *bool `json:"enable_delete"`
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
DeleteAllowlistActionsUser *bool `json:"delete_allowlist_actions_user"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"`
EnableStatusCheck *bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals *int64 `json:"required_approvals"`
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"`
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// OrgEmailDomainPolicy represents an organization's email domain policy
type OrgEmailDomainPolicy struct {
OrgID int64 `json:"org_id"`
AllowedDomains string `json:"allowed_domains"`
}
// EditOrgEmailDomainPolicyOption options for editing an org's email domain policy
type EditOrgEmailDomainPolicyOption struct {
AllowedDomains *string `json:"allowed_domains"`
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgPushPolicy represents an organization's push policy (one per org)
type OrgPushPolicy struct {
OrgID int64 `json:"org_id"`
BranchNamePattern string `json:"branch_name_pattern"`
TagNamePattern string `json:"tag_name_pattern"`
RequireSecretBlock bool `json:"require_secret_block"`
MaxFileSize int64 `json:"max_file_size"`
BlockedFilePatterns string `json:"blocked_file_patterns"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// EditOrgPushPolicyOption options for editing an organization's push policy. Only
// fields that are set will be changed.
type EditOrgPushPolicyOption struct {
BranchNamePattern *string `json:"branch_name_pattern"`
TagNamePattern *string `json:"tag_name_pattern"`
RequireSecretBlock *bool `json:"require_secret_block"`
MaxFileSize *int64 `json:"max_file_size"`
BlockedFilePatterns *string `json:"blocked_file_patterns"`
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// OrgRepoDefaults represents an organization's default repository settings
type OrgRepoDefaults struct {
OrgID int64 `json:"org_id"`
ForcePrivate bool `json:"force_private"`
ApplyPRDefaults bool `json:"apply_pr_defaults"`
AllowMerge bool `json:"allow_merge"`
AllowRebase bool `json:"allow_rebase"`
AllowRebaseMerge bool `json:"allow_rebase_merge"`
AllowSquash bool `json:"allow_squash"`
AllowFastForwardOnly bool `json:"allow_fast_forward_only"`
DefaultMergeStyle string `json:"default_merge_style"`
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge"`
}
// EditOrgRepoDefaultsOption options for editing an org's repo defaults. Only fields
// that are set will be changed.
type EditOrgRepoDefaultsOption struct {
ForcePrivate *bool `json:"force_private"`
ApplyPRDefaults *bool `json:"apply_pr_defaults"`
AllowMerge *bool `json:"allow_merge"`
AllowRebase *bool `json:"allow_rebase"`
AllowRebaseMerge *bool `json:"allow_rebase_merge"`
AllowSquash *bool `json:"allow_squash"`
AllowFastForwardOnly *bool `json:"allow_fast_forward_only"`
DefaultMergeStyle *string `json:"default_merge_style"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge"`
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgTagProtection represents an org-level tag protection rule
type OrgTagProtection struct {
ID int64 `json:"id"`
OrgID int64 `json:"org_id"`
NamePattern string `json:"name_pattern"`
WhitelistTeams []string `json:"whitelist_teams"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateOrgTagProtectionOption options for creating an org-level tag protection
type CreateOrgTagProtectionOption struct {
NamePattern string `json:"name_pattern" binding:"Required"`
WhitelistTeams []string `json:"whitelist_teams"`
}
// EditOrgTagProtectionOption options for editing an org-level tag protection
type EditOrgTagProtectionOption struct {
NamePattern *string `json:"name_pattern"`
WhitelistTeams []string `json:"whitelist_teams"`
}
+18 -10
View File
@@ -86,31 +86,35 @@ func Test_NormalizeEOL(t *testing.T) {
}
func Test_RandomInt(t *testing.T) {
randInt := CryptoRandomInt(255)
randInt, err := CryptoRandomInt(255)
assert.NoError(t, err)
assert.GreaterOrEqual(t, randInt, int64(0))
assert.LessOrEqual(t, randInt, int64(255))
}
func Test_RandomString(t *testing.T) {
str1 := CryptoRandomString(32)
var err error
str1, err := CryptoRandomString(32)
assert.NoError(t, err)
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
assert.NoError(t, err)
assert.True(t, matches)
str2 := CryptoRandomString(32)
str2, err := CryptoRandomString(32)
assert.NoError(t, err)
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
assert.NoError(t, err)
assert.True(t, matches)
assert.NotEqual(t, str1, str2)
str3 := CryptoRandomString(256)
str3, err := CryptoRandomString(256)
assert.NoError(t, err)
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
assert.NoError(t, err)
assert.True(t, matches)
str4 := CryptoRandomString(256)
str4, err := CryptoRandomString(256)
assert.NoError(t, err)
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
assert.NoError(t, err)
assert.True(t, matches)
@@ -119,15 +123,19 @@ func Test_RandomString(t *testing.T) {
}
func Test_RandomBytes(t *testing.T) {
bytes1 := CryptoRandomBytes(32)
bytes1, err := CryptoRandomBytes(32)
assert.NoError(t, err)
bytes2 := CryptoRandomBytes(32)
bytes2, err := CryptoRandomBytes(32)
assert.NoError(t, err)
assert.NotEqual(t, bytes1, bytes2)
bytes3 := CryptoRandomBytes(256)
bytes3, err := CryptoRandomBytes(256)
assert.NoError(t, err)
bytes4 := CryptoRandomBytes(256)
bytes4, err := CryptoRandomBytes(256)
assert.NoError(t, err)
assert.NotEqual(t, bytes3, bytes4)
}
+27 -2
View File
@@ -2411,6 +2411,31 @@
"repo.settings.protected_branch": "Branch Protection",
"repo.settings.protected_branch.save_rule": "Save Rule",
"repo.settings.protected_branch.delete_rule": "Delete Rule",
"repo.settings.org_protected_branch": "Organization Branch Protection",
"repo.settings.org_protected_branch_desc": "These rules are defined by the organization and are enforced on top of this repository's own rules — the stricter of the two applies. They cannot be edited here.",
"repo.settings.org_protected_branch.inherited": "Organization",
"repo.settings.org_protected_branch.read_only": "Read-only",
"repo.settings.org_protected_branch.approvals": "Required approvals",
"repo.settings.org_protected_branch.signed": "Signed commits",
"repo.settings.org_protected_branch.status_check": "Required status checks",
"repo.settings.org_protected_branch.direct_push": "Direct push",
"repo.settings.org_protected_branch.force_push": "Force push",
"repo.settings.org_protected_branch.deletion": "Branch deletion",
"repo.settings.org_protected_branch.merge": "Merge restricted to",
"repo.settings.org_protected_branch.protected_files": "Protected files",
"repo.settings.org_protected_branch.also": "Also enforces",
"repo.settings.org_protected_branch.blocked": "Blocked",
"repo.settings.org_protected_branch.allowed": "Allowed",
"repo.settings.org_protected_branch.restricted": "Restricted to specific teams",
"repo.settings.org_protected_branch.write_access": "Anyone with write access",
"repo.settings.org_protected_branch.teams": "Teams: %s",
"repo.settings.org_protected_branch.any": "Any configured checks",
"repo.settings.org_protected_branch.block_outdated": "Block on outdated branch",
"repo.settings.org_protected_branch.block_rejected": "Block on rejected reviews",
"repo.settings.org_protected_branch.block_admin": "Block admin merge override",
"repo.settings.org_protected_tag": "Organization Tag Protection",
"repo.settings.org_protected_tag_desc": "These tag protection rules are defined by the organization and are enforced on top of this repository's own rules. They cannot be edited here.",
"repo.settings.org_protected_tag.read_only": "Read-only",
"repo.settings.protected_branch_can_push": "Allow push?",
"repo.settings.protected_branch_can_push_yes": "You can push",
"repo.settings.protected_branch_can_push_no": "You cannot push",
@@ -2439,7 +2464,7 @@
"repo.settings.protect_force_push_allowlist_teams": "Allowlisted teams for force pushing:",
"repo.settings.protect_force_push_allowlist_deploy_keys": "Allowlist deploy keys with push access to force push.",
"repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.",
"repo.settings.event_delete": "Branch Deletion",
"repo.settings.protect_branch_deletion": "Branch Deletion",
"repo.settings.protect_disable_delete": "Disable Deletion",
"repo.settings.protect_disable_delete_desc": "This branch cannot be deleted.",
"repo.settings.protect_enable_delete_all": "Enable Deletion",
@@ -2747,7 +2772,7 @@
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
"repo.settings.custom_fields": "Custom Fields",
"repo.settings.manifest": "Manifest",
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the MokoPlatform manifest. These are accessible via API for Actions workflows and the MokoPlatform CLI.",
"repo.settings.manifest_desc": "Project identity, governance, and build settings for the repository. These are accessible via API for Actions workflows and MokoCLI.",
"repo.settings.manifest_identity": "Identity",
"repo.settings.manifest_name": "Project Name",
"repo.settings.manifest_element_name": "Element Name",
+52 -26
View File
@@ -516,7 +516,7 @@ func reqOrgVisible() func(ctx *context.APIContext) {
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
return
}
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return
}
@@ -1543,8 +1543,6 @@ func Routes() *web.Router {
}, reqAnyRepoReader())
m.Get("/metadata", repo.GetRepoMetadata)
m.Put("/metadata", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
m.Get("/manifest", repo.GetRepoMetadata) // backward compat
m.Put("/manifest", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
// MokoGitea badge engine
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
@@ -1698,29 +1696,29 @@ func Routes() *web.Router {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
// m.Group("/projects", func() {
// m.Combo("").Get(repo.ListProjects).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
// m.Group("/{id}", func() {
// m.Combo("").Get(repo.GetProject).
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
// m.Group("/columns", func() {
// m.Combo("").Get(repo.ListProjectColumns).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
// m.Group("/{columnId}", func() {
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
// })
// })
// m.Group("/issues/{issueId}", func() {
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
// })
// })
// })
// m.Group("/projects", func() {
// m.Combo("").Get(repo.ListProjects).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
// m.Group("/{id}", func() {
// m.Combo("").Get(repo.GetProject).
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
// m.Group("/columns", func() {
// m.Combo("").Get(repo.ListProjectColumns).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
// m.Group("/{columnId}", func() {
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
// })
// })
// m.Group("/issues/{issueId}", func() {
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
// })
// })
// })
// Repo custom fields (repo-scoped key-value metadata)
m.Group("/custom-fields", func() {
m.Get("", repo.GetRepoCustomFields)
@@ -1823,6 +1821,34 @@ func Routes() *web.Router {
})
}, reqToken(), reqOrgOwnership())
m.Group("/tag_protections", func() {
m.Combo("").Get(org.ListOrgTagProtections).
Post(bind(api.CreateOrgTagProtectionOption{}), org.CreateOrgTagProtection)
m.Group("/{id}", func() {
m.Get("", org.GetOrgTagProtection)
m.Patch("", bind(api.EditOrgTagProtectionOption{}), org.EditOrgTagProtection)
m.Delete("", org.DeleteOrgTagProtection)
})
}, reqToken(), reqOrgOwnership())
m.Group("/push_policy", func() {
m.Combo("").Get(org.GetOrgPushPolicy).
Patch(bind(api.EditOrgPushPolicyOption{}), org.EditOrgPushPolicy).
Delete(org.DeleteOrgPushPolicy)
}, reqToken(), reqOrgOwnership())
m.Group("/repo_defaults", func() {
m.Combo("").Get(org.GetOrgRepoDefaults).
Patch(bind(api.EditOrgRepoDefaultsOption{}), org.EditOrgRepoDefaults).
Delete(org.DeleteOrgRepoDefaults)
}, reqToken(), reqOrgOwnership())
m.Group("/email_domain_policy", func() {
m.Combo("").Get(org.GetOrgEmailDomainPolicy).
Patch(bind(api.EditOrgEmailDomainPolicyOption{}), org.EditOrgEmailDomainPolicy).
Delete(org.DeleteOrgEmailDomainPolicy)
}, reqToken(), reqOrgOwnership())
m.Group("/blocks", func() {
m.Get("", org.ListBlocks)
m.Group("/{username}", func() {
+156
View File
@@ -8,6 +8,7 @@ import (
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
@@ -36,6 +37,18 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
return names
}
resolveUserNames := func(ids []int64) []string {
names := make([]string, 0, len(ids))
for _, id := range ids {
u, err := user_model.GetUserByID(ctx, id)
if err != nil {
continue
}
names = append(names, u.Name)
}
return names
}
return &api.OrgBranchProtection{
ID: rule.ID,
OrgID: rule.OrgID,
@@ -44,16 +57,28 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
EnablePush: rule.CanPush,
EnablePushWhitelist: rule.EnableWhitelist,
PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs),
PushWhitelistUsernames: resolveUserNames(rule.WhitelistUserIDs),
PushWhitelistActionsUser: rule.WhitelistActionsUser,
EnableForcePush: rule.CanForcePush,
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
ForcePushAllowlistUsernames: resolveUserNames(rule.ForcePushAllowlistUserIDs),
ForcePushAllowlistActionsUser: rule.ForcePushAllowlistActionsUser,
EnableDelete: rule.CanDelete,
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
DeleteAllowlistUsernames: resolveUserNames(rule.DeleteAllowlistUserIDs),
DeleteAllowlistActionsUser: rule.DeleteAllowlistActionsUser,
EnableMergeWhitelist: rule.EnableMergeWhitelist,
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
MergeWhitelistUsernames: resolveUserNames(rule.MergeWhitelistUserIDs),
MergeWhitelistActionsUser: rule.MergeWhitelistActionsUser,
EnableStatusCheck: rule.EnableStatusCheck,
StatusCheckContexts: rule.StatusCheckContexts,
RequiredApprovals: rule.RequiredApprovals,
EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist,
ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs),
ApprovalsWhitelistUsernames: resolveUserNames(rule.ApprovalsWhitelistUserIDs),
BlockOnRejectedReviews: rule.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch,
@@ -85,6 +110,41 @@ func resolveTeamIDs(ctx *context.APIContext, orgID int64, names []string) ([]int
return ids, true
}
// resolveUserIDs converts a list of user identifiers (each a username or an email
// address) to deduped user IDs, returning 422 on the first unknown identifier. Each
// entry is looked up by username first, then falls back to an activated email address.
func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) {
if len(names) == 0 {
return nil, true
}
seen := make(map[int64]struct{}, len(names))
ids := make([]int64, 0, len(names))
for _, name := range names {
u, err := user_model.GetUserByName(ctx, name)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.APIErrorInternal(err)
return nil, false
}
u, err = user_model.GetUserByEmail(ctx, name)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "unknown user: "+name)
} else {
ctx.APIErrorInternal(err)
}
return nil, false
}
}
if _, dup := seen[u.ID]; dup {
continue
}
seen[u.ID] = struct{}{}
ids = append(ids, u.ID)
}
return ids, true
}
// ListOrgBranchProtections list org-level branch protection rules
func ListOrgBranchProtections(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections
@@ -211,6 +271,30 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
if !ok {
return
}
deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
if !ok {
return
}
pushUsers, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
if !ok {
return
}
forcePushUsers, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames)
if !ok {
return
}
mergeUsers, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames)
if !ok {
return
}
approvalsUsers, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
if !ok {
return
}
deleteUsers, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames)
if !ok {
return
}
rule := &git_model.OrgProtectedBranch{
OrgID: orgID,
@@ -219,16 +303,28 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
WhitelistTeamIDs: pushTeams,
WhitelistUserIDs: pushUsers,
WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser,
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: forcePushTeams,
ForcePushAllowlistUserIDs: forcePushUsers,
ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
CanDelete: form.EnableDelete,
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: deleteTeams,
DeleteAllowlistUserIDs: deleteUsers,
DeleteAllowlistActionsUser: form.EnableDelete && form.EnableDeleteAllowlist && form.DeleteAllowlistActionsUser,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistTeamIDs: mergeTeams,
MergeWhitelistUserIDs: mergeUsers,
MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
RequiredApprovals: form.RequiredApprovals,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
ApprovalsWhitelistTeamIDs: approvalsTeams,
ApprovalsWhitelistUserIDs: approvalsUsers,
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
@@ -310,6 +406,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.WhitelistTeamIDs = ids
}
if form.PushWhitelistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
if !ok {
return
}
rule.WhitelistUserIDs = ids
}
if form.PushWhitelistActionsUser != nil {
rule.WhitelistActionsUser = *form.PushWhitelistActionsUser
}
if form.EnableForcePush != nil {
rule.CanForcePush = *form.EnableForcePush
}
@@ -323,6 +429,39 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.ForcePushAllowlistTeamIDs = ids
}
if form.ForcePushAllowlistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames)
if !ok {
return
}
rule.ForcePushAllowlistUserIDs = ids
}
if form.ForcePushAllowlistActionsUser != nil {
rule.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser
}
if form.EnableDelete != nil {
rule.CanDelete = *form.EnableDelete
}
if form.EnableDeleteAllowlist != nil {
rule.EnableDeleteAllowlist = *form.EnableDeleteAllowlist
}
if form.DeleteAllowlistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
if !ok {
return
}
rule.DeleteAllowlistTeamIDs = ids
}
if form.DeleteAllowlistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames)
if !ok {
return
}
rule.DeleteAllowlistUserIDs = ids
}
if form.DeleteAllowlistActionsUser != nil {
rule.DeleteAllowlistActionsUser = *form.DeleteAllowlistActionsUser
}
if form.EnableMergeWhitelist != nil {
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
}
@@ -333,6 +472,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.MergeWhitelistTeamIDs = ids
}
if form.MergeWhitelistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames)
if !ok {
return
}
rule.MergeWhitelistUserIDs = ids
}
if form.MergeWhitelistActionsUser != nil {
rule.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser
}
if form.EnableStatusCheck != nil {
rule.EnableStatusCheck = *form.EnableStatusCheck
}
@@ -352,6 +501,13 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.ApprovalsWhitelistTeamIDs = ids
}
if form.ApprovalsWhitelistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
if !ok {
return
}
rule.ApprovalsWhitelistUserIDs = ids
}
if form.BlockOnRejectedReviews != nil {
rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
func toAPIOrgEmailDomainPolicy(policy *git_model.OrgEmailDomainPolicy, orgID int64) *api.OrgEmailDomainPolicy {
if policy == nil {
return &api.OrgEmailDomainPolicy{OrgID: orgID}
}
return &api.OrgEmailDomainPolicy{
OrgID: policy.OrgID,
AllowedDomains: policy.AllowedDomains,
}
}
// GetOrgEmailDomainPolicy get the organization's email domain policy
func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/email_domain_policy organization orgGetEmailDomainPolicy
// ---
// summary: Get the organization's email domain policy
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgEmailDomainPolicy"
// "404":
// "$ref": "#/responses/notFound"
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgEmailDomainPolicy(policy, orgID))
}
// EditOrgEmailDomainPolicy create or update the organization's email domain policy
func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/email_domain_policy organization orgEditEmailDomainPolicy
// ---
// summary: Create or update the organization's email domain policy. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgEmailDomainPolicyOption"
// responses:
// "200":
// "$ref": "#/responses/OrgEmailDomainPolicy"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditOrgEmailDomainPolicyOption)
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if policy == nil {
policy = &git_model.OrgEmailDomainPolicy{OrgID: orgID}
}
if form.AllowedDomains != nil {
policy.AllowedDomains = *form.AllowedDomains
}
if err := git_model.UpsertOrgEmailDomainPolicy(ctx, policy); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgEmailDomainPolicy(policy, orgID))
}
// DeleteOrgEmailDomainPolicy remove the organization's email domain policy
func DeleteOrgEmailDomainPolicy(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/email_domain_policy organization orgDeleteEmailDomainPolicy
// ---
// summary: Remove the organization's email domain policy
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := git_model.DeleteOrgEmailDomainPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgPushPolicy converts the model to its API representation. A nil policy is
// rendered as an all-empty policy so clients always get a consistent shape.
func toAPIOrgPushPolicy(policy *git_model.OrgPushPolicy, orgID int64) *api.OrgPushPolicy {
if policy == nil {
return &api.OrgPushPolicy{OrgID: orgID}
}
return &api.OrgPushPolicy{
OrgID: policy.OrgID,
BranchNamePattern: policy.BranchNamePattern,
TagNamePattern: policy.TagNamePattern,
RequireSecretBlock: policy.RequireSecretBlock,
MaxFileSize: policy.MaxFileSize,
BlockedFilePatterns: policy.BlockedFilePatterns,
Created: policy.CreatedUnix.AsTime(),
Updated: policy.UpdatedUnix.AsTime(),
}
}
// GetOrgPushPolicy get the organization's push policy
func GetOrgPushPolicy(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/push_policy organization orgGetPushPolicy
// ---
// summary: Get the organization's push policy
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgPushPolicy"
// "404":
// "$ref": "#/responses/notFound"
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgPushPolicy(policy, orgID))
}
// EditOrgPushPolicy create or update the organization's push policy
func EditOrgPushPolicy(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/push_policy organization orgEditPushPolicy
// ---
// summary: Create or update the organization's push policy. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgPushPolicyOption"
// responses:
// "200":
// "$ref": "#/responses/OrgPushPolicy"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditOrgPushPolicyOption)
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if policy == nil {
policy = &git_model.OrgPushPolicy{OrgID: orgID}
}
if form.BranchNamePattern != nil {
policy.BranchNamePattern = *form.BranchNamePattern
}
if form.TagNamePattern != nil {
policy.TagNamePattern = *form.TagNamePattern
}
if form.RequireSecretBlock != nil {
policy.RequireSecretBlock = *form.RequireSecretBlock
}
if form.MaxFileSize != nil {
policy.MaxFileSize = *form.MaxFileSize
}
if form.BlockedFilePatterns != nil {
policy.BlockedFilePatterns = *form.BlockedFilePatterns
}
if err := git_model.UpsertOrgPushPolicy(ctx, policy); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgPushPolicy(policy, orgID))
}
// DeleteOrgPushPolicy remove the organization's push policy
func DeleteOrgPushPolicy(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/push_policy organization orgDeletePushPolicy
// ---
// summary: Remove the organization's push policy
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := git_model.DeleteOrgPushPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgRepoDefaults converts the model to its API representation. A nil value is
// rendered as the effective defaults (all merge styles allowed) so clients always
// get a consistent shape.
func toAPIOrgRepoDefaults(d *git_model.OrgRepoDefaults, orgID int64) *api.OrgRepoDefaults {
if d == nil {
return &api.OrgRepoDefaults{
OrgID: orgID,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
}
}
return &api.OrgRepoDefaults{
OrgID: d.OrgID,
ForcePrivate: d.ForcePrivate,
ApplyPRDefaults: d.ApplyPRDefaults,
AllowMerge: d.AllowMerge,
AllowRebase: d.AllowRebase,
AllowRebaseMerge: d.AllowRebaseMerge,
AllowSquash: d.AllowSquash,
AllowFastForwardOnly: d.AllowFastForwardOnly,
DefaultMergeStyle: d.DefaultMergeStyle,
DeleteBranchAfterMerge: d.DeleteBranchAfterMerge,
}
}
// GetOrgRepoDefaults get the organization's default repository settings
func GetOrgRepoDefaults(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/repo_defaults organization orgGetRepoDefaults
// ---
// summary: Get the organization's default repository settings
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgRepoDefaults"
// "404":
// "$ref": "#/responses/notFound"
orgID := ctx.Org.Organization.ID
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgRepoDefaults(defaults, orgID))
}
// EditOrgRepoDefaults create or update the organization's default repository settings
func EditOrgRepoDefaults(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/repo_defaults organization orgEditRepoDefaults
// ---
// summary: Create or update the organization's default repository settings. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgRepoDefaultsOption"
// responses:
// "200":
// "$ref": "#/responses/OrgRepoDefaults"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditOrgRepoDefaultsOption)
orgID := ctx.Org.Organization.ID
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if defaults == nil {
defaults = &git_model.OrgRepoDefaults{
OrgID: orgID,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
}
}
if form.ForcePrivate != nil {
defaults.ForcePrivate = *form.ForcePrivate
}
if form.ApplyPRDefaults != nil {
defaults.ApplyPRDefaults = *form.ApplyPRDefaults
}
if form.AllowMerge != nil {
defaults.AllowMerge = *form.AllowMerge
}
if form.AllowRebase != nil {
defaults.AllowRebase = *form.AllowRebase
}
if form.AllowRebaseMerge != nil {
defaults.AllowRebaseMerge = *form.AllowRebaseMerge
}
if form.AllowSquash != nil {
defaults.AllowSquash = *form.AllowSquash
}
if form.AllowFastForwardOnly != nil {
defaults.AllowFastForwardOnly = *form.AllowFastForwardOnly
}
if form.DefaultMergeStyle != nil {
defaults.DefaultMergeStyle = *form.DefaultMergeStyle
}
if form.DeleteBranchAfterMerge != nil {
defaults.DeleteBranchAfterMerge = *form.DeleteBranchAfterMerge
}
if err := git_model.UpsertOrgRepoDefaults(ctx, defaults); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgRepoDefaults(defaults, orgID))
}
// DeleteOrgRepoDefaults remove the organization's default repository settings
func DeleteOrgRepoDefaults(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/repo_defaults organization orgDeleteRepoDefaults
// ---
// summary: Remove the organization's default repository settings
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := git_model.DeleteOrgRepoDefaults(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+271
View File
@@ -0,0 +1,271 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgTagProtection converts an org tag rule to its API representation.
func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtectedTag) *api.OrgTagProtection {
teams, err := organization.FindOrgTeams(ctx, rule.OrgID)
if err != nil {
teams = nil
}
teamNamesByID := make(map[int64]string, len(teams))
for _, t := range teams {
teamNamesByID[t.ID] = t.Name
}
names := make([]string, 0, len(rule.AllowlistTeamIDs))
for _, id := range rule.AllowlistTeamIDs {
if name, ok := teamNamesByID[id]; ok {
names = append(names, name)
}
}
return &api.OrgTagProtection{
ID: rule.ID,
OrgID: rule.OrgID,
NamePattern: rule.NamePattern,
WhitelistTeams: names,
Created: rule.CreatedUnix.AsTime(),
Updated: rule.UpdatedUnix.AsTime(),
}
}
// ListOrgTagProtections list org-level tag protection rules
func ListOrgTagProtections(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/tag_protections organization orgListTagProtections
// ---
// summary: List an organization's tag protection rules
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgTagProtectionList"
// "404":
// "$ref": "#/responses/notFound"
rules, err := git_model.FindOrgProtectedTags(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiRules := make([]*api.OrgTagProtection, len(rules))
for i, rule := range rules {
apiRules[i] = toAPIOrgTagProtection(ctx, rule)
}
ctx.JSON(http.StatusOK, apiRules)
}
// GetOrgTagProtection get a specific org-level tag protection rule
func GetOrgTagProtection(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/tag_protections/{id} organization orgGetTagProtection
// ---
// summary: Get a specific org-level tag protection rule
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protection rule
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgTagProtection"
// "404":
// "$ref": "#/responses/notFound"
rule, err := git_model.GetOrgProtectedTagByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
}
// CreateOrgTagProtection create an org-level tag protection rule
func CreateOrgTagProtection(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/tag_protections organization orgCreateTagProtection
// ---
// summary: Create an org-level tag protection rule
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrgTagProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/OrgTagProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgTagProtectionOption)
orgID := ctx.Org.Organization.ID
existing, err := git_model.GetOrgProtectedTagByNamePattern(ctx, orgID, form.NamePattern)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if existing != nil {
ctx.APIError(http.StatusForbidden, "org tag protection rule already exists for this pattern")
return
}
teams, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
if !ok {
return
}
rule := &git_model.OrgProtectedTag{
OrgID: orgID,
NamePattern: form.NamePattern,
AllowlistTeamIDs: teams,
}
if err := git_model.CreateOrgProtectedTag(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toAPIOrgTagProtection(ctx, rule))
}
// EditOrgTagProtection edit an org-level tag protection rule
func EditOrgTagProtection(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/tag_protections/{id} organization orgEditTagProtection
// ---
// summary: Edit an org-level tag protection rule. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protection rule
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgTagProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/OrgTagProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditOrgTagProtectionOption)
orgID := ctx.Org.Organization.ID
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if form.NamePattern != nil {
rule.NamePattern = *form.NamePattern
}
if form.WhitelistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
if !ok {
return
}
rule.AllowlistTeamIDs = ids
}
if err := git_model.UpdateOrgProtectedTag(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
}
// DeleteOrgTagProtection delete an org-level tag protection rule
func DeleteOrgTagProtection(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/tag_protections/{id} organization orgDeleteTagProtection
// ---
// summary: Delete an org-level tag protection rule
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protection rule
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
orgID := ctx.Org.Organization.ID
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if err := git_model.DeleteOrgProtectedTag(ctx, orgID, rule.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+3
View File
@@ -9,6 +9,7 @@ import (
"net/http"
activities_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/activities"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
@@ -491,6 +492,8 @@ func AddTeamMember(ctx *context.APIContext) {
if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else if git_model.IsErrEmailDomainNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
+39 -9
View File
@@ -30,7 +30,7 @@ type apiMetadata struct {
TargetVersion string `json:"target_version"`
PHPMinimum string `json:"php_minimum"`
Language string `json:"language"`
ExtensionType string `json:"extension_type"`
ExtensionType string `json:"extension_type"`
EntryPoint string `json:"entry_point"`
// deploy
@@ -44,6 +44,13 @@ type apiMetadata struct {
HealthURL string `json:"health_url,omitempty"`
}
// Manifest
// swagger:response Manifest
type swaggerResponseManifest struct {
// in:body
Body apiMetadata `json:"body"`
}
// GetRepoMetadata returns the manifest settings for a repository.
func GetRepoMetadata(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
@@ -51,6 +58,17 @@ func GetRepoMetadata(ctx *context.APIContext) {
// summary: Get repo manifest settings
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Manifest"
@@ -71,9 +89,9 @@ func GetRepoMetadata(ctx *context.APIContext) {
return
}
ctx.JSON(http.StatusOK, &apiMetadata{
Name: m.Name,
Org: m.Org,
Description: m.Description,
Name: m.Name,
Org: m.Org,
Description: m.Description,
LicenseSPDX: m.LicenseSPDX,
LicenseName: m.LicenseName,
@@ -89,7 +107,7 @@ func GetRepoMetadata(ctx *context.APIContext) {
TargetVersion: m.TargetVersion,
PHPMinimum: m.PHPMinimum,
Language: m.Language,
ExtensionType: m.ExtensionType,
ExtensionType: m.ExtensionType,
EntryPoint: m.EntryPoint,
DeployHost: m.DeployHost,
DeployPort: m.DeployPort,
@@ -111,9 +129,21 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Manifest"
// Decode into a map to detect which fields were actually sent.
var raw map[string]any
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
@@ -173,9 +203,9 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
}
ctx.JSON(http.StatusOK, &apiMetadata{
Name: m.Name,
Org: m.Org,
Description: m.Description,
Name: m.Name,
Org: m.Org,
Description: m.Description,
LicenseSPDX: m.LicenseSPDX,
LicenseName: m.LicenseName,
@@ -191,7 +221,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
TargetVersion: m.TargetVersion,
PHPMinimum: m.PHPMinimum,
Language: m.Language,
ExtensionType: m.ExtensionType,
ExtensionType: m.ExtensionType,
EntryPoint: m.EntryPoint,
DeployHost: m.DeployHost,
DeployPort: m.DeployPort,
+38
View File
@@ -159,6 +159,44 @@ type swaggerParameterBodies struct {
// in:body
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
// in:body
CreateOrgBranchProtectionOption api.CreateOrgBranchProtectionOption
// in:body
EditOrgBranchProtectionOption api.EditOrgBranchProtectionOption
// in:body
CreateOrgTagProtectionOption api.CreateOrgTagProtectionOption
// in:body
EditOrgTagProtectionOption api.EditOrgTagProtectionOption
// in:body
EditOrgPushPolicyOption api.EditOrgPushPolicyOption
// in:body
EditOrgRepoDefaultsOption api.EditOrgRepoDefaultsOption
// in:body
EditOrgEmailDomainPolicyOption api.EditOrgEmailDomainPolicyOption
// in:body
EditAccessTokenOption api.EditAccessTokenOption
// in:body
IssueBulkAssigneesOption api.IssueBulkAssigneesOption
// in:body
IssueBulkLabelsOption api.IssueBulkLabelsOption
// in:body
IssueBulkMilestoneOption api.IssueBulkMilestoneOption
// in:body
IssueBulkStateOption api.IssueBulkStateOption
// in:body
IssuePriorityDef api.IssuePriorityDef
// in:body
IssueStatusDef api.IssueStatusDef
// in:body
IssueTypeDef api.IssueTypeDef
// in:body
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
+49
View File
@@ -41,3 +41,52 @@ type swaggerResponseOrganizationPermissions struct {
// in:body
Body api.OrganizationPermissions `json:"body"`
}
// OrgBranchProtection
// swagger:response OrgBranchProtection
type swaggerResponseOrgBranchProtection struct {
// in:body
Body api.OrgBranchProtection `json:"body"`
}
// OrgBranchProtectionList
// swagger:response OrgBranchProtectionList
type swaggerResponseOrgBranchProtectionList struct {
// in:body
Body []*api.OrgBranchProtection `json:"body"`
}
// OrgTagProtection
// swagger:response OrgTagProtection
type swaggerResponseOrgTagProtection struct {
// in:body
Body api.OrgTagProtection `json:"body"`
}
// OrgTagProtectionList
// swagger:response OrgTagProtectionList
type swaggerResponseOrgTagProtectionList struct {
// in:body
Body []*api.OrgTagProtection `json:"body"`
}
// OrgPushPolicy
// swagger:response OrgPushPolicy
type swaggerResponseOrgPushPolicy struct {
// in:body
Body api.OrgPushPolicy `json:"body"`
}
// OrgRepoDefaults
// swagger:response OrgRepoDefaults
type swaggerResponseOrgRepoDefaults struct {
// in:body
Body api.OrgRepoDefaults `json:"body"`
}
// OrgEmailDomainPolicy
// swagger:response OrgEmailDomainPolicy
type swaggerResponseOrgEmailDomainPolicy struct {
// in:body
Body api.OrgEmailDomainPolicy `json:"body"`
}
+2
View File
@@ -48,6 +48,7 @@ import (
repo_migrations "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/migrations"
mirror_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mirror"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/oauth2_provider"
org_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/org"
packages_spec "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/packages/pkgspec"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
release_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/release"
@@ -155,6 +156,7 @@ func InitWebInstalled(ctx context.Context) {
mustInit(pull_service.Init)
mustInit(automerge.Init)
cascade.Init()
org_service.Init()
mustInit(task.Init)
mustInit(repo_migrations.Init)
eventsource.GetManager().Init()
+166 -1
View File
@@ -8,6 +8,8 @@ import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
asymkey_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/asymkey"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
@@ -160,6 +162,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
gitRepo := ctx.Repo.GitRepo
objectFormat := ctx.Repo.GetObjectFormat()
if ctx.checkOrgPushPolicyBranch(oldCommitID, newCommitID, branchName) {
return
}
if newCommitID != objectFormat.EmptyObjectID().String() {
newCommit, err := gitRepo.GetCommit(newCommitID)
if err != nil {
@@ -455,6 +461,10 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
tagName := refFullName.TagName()
if ctx.checkOrgPushPolicyTag(tagName) {
return
}
if !ctx.gotProtectedTags {
var err error
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
@@ -468,7 +478,7 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
ctx.gotProtectedTags = true
}
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, ctx.protectedTags, ctx.Repo.Repository, tagName, ctx.opts.UserID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
@@ -596,3 +606,158 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
ctx.loadedPusher = true
return true
}
// checkOrgPushPolicyBranch enforces the owning organization's push policy on a
// branch push. It writes a 403 response and returns true when the push is rejected.
// Content checks (blocked paths, max file size) fail open on unexpected errors so a
// policy or parsing bug can never block every push in the organization.
func (ctx *preReceiveContext) checkOrgPushPolicyBranch(oldCommitID, newCommitID, branchName string) bool {
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetOrgPushPolicyForRepo for %-v: %v", ctx.Repo.Repository, err)
return false
}
if policy == nil {
return false
}
if !policy.BranchNameAllowed(branchName) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Branch name %q is not allowed by the organization push policy (pattern: %s)", branchName, policy.BranchNamePattern),
})
return true
}
// Deletions have no content to inspect.
if newCommitID == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
return false
}
if globs := policy.BlockedFileGlobs(); len(globs) > 0 {
if _, err := pull_service.CheckFileProtection(ctx.Repo.GitRepo, branchName, oldCommitID, newCommitID, globs, 10, ctx.env); err != nil {
if pull_service.IsErrFilePathProtected(err) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Push rejected by the organization push policy: a changed file matches a blocked path pattern",
})
return true
}
log.Error("org push policy blocked-path check for %-v: %v", ctx.Repo.Repository, err) // fail open
}
}
if policy.MaxFileSize > 0 {
if path, size := ctx.largestBlobOverLimit(oldCommitID, newCommitID, policy.MaxFileSize); path != "" {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Push rejected by the organization push policy: %q is %d bytes, over the %d-byte limit", path, size, policy.MaxFileSize),
})
return true
}
}
return false
}
// checkOrgPushPolicyTag enforces the organization tag naming policy. Returns true
// (with a 403 written) when the tag name is rejected.
func (ctx *preReceiveContext) checkOrgPushPolicyTag(tagName string) bool {
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetOrgPushPolicyForRepo for %-v: %v", ctx.Repo.Repository, err)
return false
}
if policy == nil || policy.TagNameAllowed(tagName) {
return false
}
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Tag name %q is not allowed by the organization push policy (pattern: %s)", tagName, policy.TagNamePattern),
})
return true
}
// largestBlobOverLimit returns the first file (and its size) added or modified by the
// push (oldCommitID..newCommitID) whose blob exceeds limit bytes, or ("", 0) if none —
// or on any error (fail open). Only the pushed delta is inspected so a pre-existing
// oversized file committed before the policy existed cannot permanently block unrelated
// pushes. For a new branch (no old commit) the full new tree is scanned, since every
// blob is effectively introduced by the push.
func (ctx *preReceiveContext) largestBlobOverLimit(oldCommitID, newCommitID string, limit int64) (string, int64) {
emptyID := ctx.Repo.GetObjectFormat().EmptyObjectID().String()
// New branch: no base commit to diff against, so scan the full tip tree. ls-tree
// --long carries the blob size inline, so no extra sizing pass is needed.
if oldCommitID == "" || oldCommitID == emptyID {
output, _, err := gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
gitcmd.NewCommand("ls-tree", "-r", "--long").AddDynamicArguments(newCommitID).WithEnv(ctx.env))
if err != nil {
log.Error("org push policy ls-tree for %-v: %v", ctx.Repo.Repository, err)
return "", 0
}
for _, line := range strings.Split(output, "\n") {
tab := strings.IndexByte(line, '\t')
if tab < 0 {
continue
}
fields := strings.Fields(line[:tab]) // mode, type, hash, size
if len(fields) < 4 || fields[1] != "blob" {
continue
}
if size, perr := strconv.ParseInt(fields[3], 10, 64); perr == nil && size > limit {
return line[tab+1:], size
}
}
return "", 0
}
// Existing branch: inspect only blobs added or modified by this push. diff-tree
// gives the changed paths and their new object ids but not sizes, so collect the
// candidate blob ids and size them in a single cat-file --batch-check pass.
output, _, err := gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
gitcmd.NewCommand("diff-tree", "--no-commit-id", "-r").AddDynamicArguments(oldCommitID, newCommitID).WithEnv(ctx.env))
if err != nil {
log.Error("org push policy diff-tree for %-v: %v", ctx.Repo.Repository, err)
return "", 0
}
shaToPath := make(map[string]string)
var order []string
for _, line := range strings.Split(output, "\n") {
tab := strings.IndexByte(line, '\t')
if tab < 0 {
continue
}
fields := strings.Fields(line[:tab]) // ":oldmode newmode oldsha newsha status"
if len(fields) < 5 {
continue
}
newMode, newSha, status := fields[1], fields[3], fields[4]
// Only regular, executable, or symlink blobs; skip submodule gitlinks (160000).
if newMode != "100644" && newMode != "100755" && newMode != "120000" {
continue
}
if status == "D" || newSha == emptyID {
continue
}
if _, seen := shaToPath[newSha]; !seen {
order = append(order, newSha)
}
shaToPath[newSha] = line[tab+1:]
}
if len(order) == 0 {
return "", 0
}
output, _, err = gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
gitcmd.NewCommand("cat-file", "--batch-check").WithStdinBytes([]byte(strings.Join(order, "\n")+"\n")).WithEnv(ctx.env))
if err != nil {
log.Error("org push policy cat-file for %-v: %v", ctx.Repo.Repository, err)
return "", 0
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line) // "<sha> <type> <size>" or "<sha> missing"
if len(fields) != 3 || fields[1] != "blob" {
continue
}
if size, perr := strconv.ParseInt(fields[2], 10, 64); perr == nil && size > limit {
return shaToPath[fields[0]], size
}
}
return "", 0
}
+1 -1
View File
@@ -185,7 +185,7 @@ func NewComment(ctx *context.Context) {
} // end if: handle close or reopen
// Handle custom status from the status dropdown (replaces close button for issues with org statuses).
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" && statusIDStr != "" {
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" {
if statusIDStr == "reopen" {
// Reopen via dropdown
if issue.IsClosed {
@@ -34,6 +34,64 @@ const (
tplProtectedBranch templates.TplName = "repo/settings/protected_branch"
)
// orgBranchProtectionView is a read-only presentation of an org-level branch
// protection rule for the repo settings page, with team IDs resolved to names.
type orgBranchProtectionView struct {
Rule *git_model.OrgProtectedBranch
PushTeams string
ForcePushTeams string
DeleteTeams string
MergeTeams string
ApprovalTeams string
StatusContexts string
}
// prepareOrgProtectedBranches loads the owning organization's branch protection
// rules and exposes them (with team IDs resolved to names) as read-only view models
// under the "OrgProtectedBranches" template key.
func prepareOrgProtectedBranches(ctx *context.Context) error {
orgRules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Repo.Owner.ID)
if err != nil {
return err
}
if len(orgRules) == 0 {
return nil
}
teams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
if err != nil {
return err
}
teamNames := make(map[int64]string, len(teams))
for _, t := range teams {
teamNames[t.ID] = t.Name
}
join := func(ids []int64) string {
names := make([]string, 0, len(ids))
for _, id := range ids {
if n, ok := teamNames[id]; ok {
names = append(names, n)
}
}
return strings.Join(names, ", ")
}
views := make([]*orgBranchProtectionView, len(orgRules))
for i, r := range orgRules {
views[i] = &orgBranchProtectionView{
Rule: r,
PushTeams: join(r.WhitelistTeamIDs),
ForcePushTeams: join(r.ForcePushAllowlistTeamIDs),
DeleteTeams: join(r.DeleteAllowlistTeamIDs),
MergeTeams: join(r.MergeWhitelistTeamIDs),
ApprovalTeams: join(r.ApprovalsWhitelistTeamIDs),
StatusContexts: strings.Join(r.StatusCheckContexts, ", "),
}
}
ctx.Data["OrgProtectedBranches"] = views
return nil
}
// ProtectedBranchRules render the page to protect the repository
func ProtectedBranchRules(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
@@ -46,6 +104,16 @@ func ProtectedBranchRules(ctx *context.Context) {
}
ctx.Data["ProtectedBranches"] = rules
// Surface the organization-level rules that also apply to this repo (read-only),
// so admins can see the org "floor" that is layered on top of the repo's own
// rules at enforcement time. See issue #727.
if ctx.Repo.Owner.IsOrganization() {
if err := prepareOrgProtectedBranches(ctx); err != nil {
ctx.ServerError("prepareOrgProtectedBranches", err)
return
}
}
repo.PrepareBranchList(ctx)
if ctx.Written() {
return
+37
View File
@@ -138,6 +138,13 @@ func DeleteProtectedTagPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}
// orgProtectedTagView is a read-only presentation of an org-level tag rule for the
// repo settings page, with allowlist team IDs resolved to names.
type orgProtectedTagView struct {
Rule *git_model.OrgProtectedTag
Teams string
}
func setTagsContext(ctx *context.Context) error {
ctx.Data["Title"] = ctx.Tr("repo.settings.tags")
ctx.Data["PageIsSettingsTags"] = true
@@ -163,6 +170,36 @@ func setTagsContext(ctx *context.Context) error {
return err
}
ctx.Data["Teams"] = teams
// Surface the organization's tag protection rules read-only, so admins can see
// the org "floor" layered on top of this repo's own protected tags (#727).
orgRules, err := git_model.FindOrgProtectedTags(ctx, ctx.Repo.Owner.ID)
if err != nil {
ctx.ServerError("FindOrgProtectedTags", err)
return err
}
if len(orgRules) > 0 {
allTeams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
if err != nil {
ctx.ServerError("FindOrgTeams", err)
return err
}
teamNames := make(map[int64]string, len(allTeams))
for _, t := range allTeams {
teamNames[t.ID] = t.Name
}
views := make([]*orgProtectedTagView, len(orgRules))
for i, r := range orgRules {
names := make([]string, 0, len(r.AllowlistTeamIDs))
for _, id := range r.AllowlistTeamIDs {
if n, ok := teamNames[id]; ok {
names = append(names, n)
}
}
views[i] = &orgProtectedTagView{Rule: r, Teams: strings.Join(names, ", ")}
}
ctx.Data["OrgProtectedTags"] = views
}
}
return nil
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"context"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
repo_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/repository"
)
// Init registers the org notifier so organization repository defaults are applied to
// repositories as they are created in or transferred into the org. The notifier
// pattern avoids the services/repository -> services/org import cycle.
func Init() {
notify_service.RegisterNotifier(&repoDefaultsNotifier{})
}
type repoDefaultsNotifier struct {
notify_service.NullNotifier
}
func (n *repoDefaultsNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
applyOrgRepoDefaults(ctx, u, repo)
}
func (n *repoDefaultsNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
applyOrgRepoDefaults(ctx, nil, repo)
}
// applyOrgRepoDefaults applies the owning organization's default repository settings
// to a repo that has just joined the org. Best-effort: errors are logged and never
// propagated, so a defaults bug can never break repository creation or transfer.
func applyOrgRepoDefaults(ctx context.Context, owner *user_model.User, repo *repo_model.Repository) {
if owner == nil {
if err := repo.LoadOwner(ctx); err != nil {
log.Error("org repo defaults: load owner of repo %d: %v", repo.ID, err)
return
}
owner = repo.Owner
}
if owner == nil || !owner.IsOrganization() {
return
}
defaults, err := git_model.GetOrgRepoDefaults(ctx, owner.ID)
if err != nil {
log.Error("org repo defaults: load for org %d: %v", owner.ID, err)
return
}
if defaults == nil {
return
}
if defaults.ForcePrivate && !repo.IsPrivate {
repo.IsPrivate = true
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
log.Error("org repo defaults: force private on repo %d: %v", repo.ID, err)
}
}
if defaults.ApplyPRDefaults {
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
// The repository may not have pull requests enabled; nothing to apply.
return
}
cfg := prUnit.PullRequestsConfig()
cfg.AllowMerge = defaults.AllowMerge
cfg.AllowRebase = defaults.AllowRebase
cfg.AllowRebaseMerge = defaults.AllowRebaseMerge
cfg.AllowSquash = defaults.AllowSquash
cfg.AllowFastForwardOnly = defaults.AllowFastForwardOnly
cfg.DefaultDeleteBranchAfterMerge = defaults.DeleteBranchAfterMerge
if defaults.DefaultMergeStyle != "" {
cfg.DefaultMergeStyle = repo_model.MergeStyle(defaults.DefaultMergeStyle)
}
if err := repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{{
RepoID: repo.ID,
Type: unit.TypePullRequests,
Config: cfg,
}}, nil); err != nil {
log.Error("org repo defaults: update PR unit on repo %d: %v", repo.ID, err)
}
}
}
+7
View File
@@ -220,6 +220,13 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode
return err
}
// Enforce the organization email domain policy for new members.
if allowed, err := git_model.OrgEmailDomainAllowed(ctx, team.OrgID, user.Email); err != nil {
return err
} else if !allowed {
return git_model.ErrEmailDomainNotAllowed{Email: user.Email, OrgID: team.OrgID}
}
if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
return err
}
+3 -3
View File
@@ -11,8 +11,8 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
@@ -92,7 +92,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
// Trim '--' prefix to prevent command line argument vulnerability.
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, rel.Repo, rel.TagName, rel.PublisherID)
if err != nil {
return false, err
}
@@ -439,7 +439,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
if err != nil {
return fmt.Errorf("GetProtectedTags: %w", err)
}
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, repo, rel.TagName, doer.ID)
if err != nil {
return err
}
-115
View File
@@ -1,115 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repository
import (
"context"
"encoding/xml"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
type manifestXML struct {
XMLName xml.Name `xml:"mokoplatform"`
Identity manifestIdentity `xml:"identity"`
Governance manifestGovernance `xml:"governance"`
Distribution manifestDistribution `xml:"distribution"`
Build manifestBuild `xml:"build"`
}
type manifestDistribution struct {
DisplayName string `xml:"display-name"`
Maintainer string `xml:"maintainer"`
MaintainerURL string `xml:"maintainer-url"`
InfoURL string `xml:"info-url"`
TargetVersion string `xml:"target-version"`
PHPMinimum string `xml:"php-minimum"`
}
type manifestIdentity struct {
Name string `xml:"name"`
Org string `xml:"org"`
Description string `xml:"description"`
VersionPrefix string `xml:"version-prefix"`
ElementName string `xml:"element-name"`
License manifestLicense `xml:"license"`
}
type manifestLicense struct {
SPDX string `xml:"spdx,attr"`
Name string `xml:",chardata"`
}
type manifestGovernance struct {
Platform string `xml:"platform"`
StandardsVersion string `xml:"standards-version"`
StandardsSource string `xml:"standards-source"`
}
type manifestBuild struct {
Language string `xml:"language"`
ExtensionType string `xml:"package-type"`
EntryPoint string `xml:"entry-point"`
}
// SyncMetadataFromCommit reads .mokogitea/manifest.xml from the given commit
// and upserts the values into the repo_manifest database table.
// This is called on push to the default branch to keep the database in sync
// with the XML file. If no manifest.xml exists, this is a no-op.
func SyncMetadataFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
if commit == nil {
return
}
entry, err := commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
if err != nil || entry == nil {
return // no manifest.xml — not an error
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Error("SyncMetadata: read blob for %s: %v", repo.FullName(), err)
return
}
defer reader.Close()
var mxml manifestXML
decoder := xml.NewDecoder(reader)
if err := decoder.Decode(&mxml); err != nil {
log.Error("SyncMetadata: parse XML for %s: %v", repo.FullName(), err)
return
}
manifest := &repo_model.RepoMetadata{
RepoID: repo.ID,
Name: mxml.Identity.Name,
Org: mxml.Identity.Org,
Description: mxml.Identity.Description,
VersionPrefix: mxml.Identity.VersionPrefix,
ElementName: mxml.Identity.ElementName,
LicenseSPDX: mxml.Identity.License.SPDX,
LicenseName: mxml.Identity.License.Name,
Platform: mxml.Governance.Platform,
StandardsVersion: mxml.Governance.StandardsVersion,
StandardsSource: mxml.Governance.StandardsSource,
Maintainer: mxml.Distribution.Maintainer,
MaintainerURL: mxml.Distribution.MaintainerURL,
InfoURL: mxml.Distribution.InfoURL,
TargetVersion: mxml.Distribution.TargetVersion,
PHPMinimum: mxml.Distribution.PHPMinimum,
Language: mxml.Build.Language,
ExtensionType: mxml.Build.ExtensionType,
EntryPoint: mxml.Build.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
log.Error("SyncMetadata: save for %s: %v", repo.FullName(), err)
return
}
log.Info("SyncMetadata: synced .mokogitea/manifest.xml for %s", repo.FullName())
}
-2
View File
@@ -194,8 +194,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
log.Error("DelRepoDivergenceFromCache: %v", err)
}
// Auto-sync .mokogitea/manifest.xml to database on default branch push
SyncMetadataFromCommit(ctx, repo, newCommit)
// Run security scanners on default branch push
security_service.ScanOnPush(ctx, repo, newCommit)
} else {
+1 -1
View File
@@ -160,7 +160,7 @@ var DefaultCodeRules = []CodeRule{
{
ID: "deserialize-yaml-py", Title: "Insecure Deserialization: yaml.load() (Python)",
Severity: security_model.SeverityHigh, CWE: "CWE-502",
Pattern: regexp.MustCompile(`yaml\.load\s*\([^)]*(?:Loader\s*=\s*yaml\.(?:Unsafe|Full)Loader|[^)]*\)(?!\s*#))`),
Pattern: regexp.MustCompile(`(?i)yaml\.load\s*\(`),
Description: "yaml.load() without SafeLoader allows arbitrary code execution — use yaml.safe_load()",
Languages: []string{".py"},
},
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import "testing"
// TestDefaultCodeRulesCompile forces the package init, which builds every scanner
// rule's regexp via regexp.MustCompile, and asserts each pattern is present. Go's
// regexp engine is RE2 and rejects Perl-only constructs (lookahead/lookbehind/
// backreferences) by panicking in MustCompile at init — which crash-loops the
// server at startup. This test executes that init so such a pattern fails CI here
// instead of on a live deploy.
func TestDefaultCodeRulesCompile(t *testing.T) {
if len(DefaultCodeRules) == 0 {
t.Fatal("DefaultCodeRules is empty")
}
for _, r := range DefaultCodeRules {
if r.Pattern == nil {
t.Errorf("rule %q has a nil Pattern", r.ID)
}
}
}
+22 -1
View File
@@ -6,6 +6,7 @@ package security
import (
"context"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
@@ -21,7 +22,11 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [
return nil
}
if !cfg.Enabled || !cfg.BlockOnPush || !cfg.SecretScanner {
return nil
// The owning organization may mandate secret blocking regardless of the
// repository's own scanner config (org push policy).
if !orgRequiresSecretBlock(ctx, repoID) {
return nil
}
}
scanner := NewSecretScanner()
@@ -33,6 +38,22 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [
return findings
}
// orgRequiresSecretBlock reports whether the repo's owning organization mandates
// secret blocking on push via its org push policy.
func orgRequiresSecretBlock(ctx context.Context, repoID int64) bool {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
log.Error("orgRequiresSecretBlock: GetRepositoryByID: %v", err)
return false
}
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, repo)
if err != nil {
log.Error("orgRequiresSecretBlock: GetOrgPushPolicyForRepo: %v", err)
return false
}
return policy != nil && policy.RequireSecretBlock
}
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
// Called from services/repository/push.go on default branch pushes.
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
+82
View File
@@ -62,6 +62,88 @@
{{end}}
</div>
</div>
{{if .OrgProtectedBranches}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_protected_branch"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_branch_desc"}}</p>
<div class="flex-divided-list items-with-main">
{{range .OrgProtectedBranches}}
<div class="item">
<div class="item-main tw-w-full">
<details class="tw-w-full">
<summary class="tw-flex tw-items-center tw-gap-2 tw-flex-wrap tw-cursor-pointer">
<div class="ui basic label">{{.Rule.RuleName}}</div>
<span class="ui tiny label">{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}}</span>
<span class="text grey tw-text-sm">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}}</span>
</summary>
<table class="ui very basic compact table tw-mt-2 tw-w-auto tw-ml-4">
<tbody>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.direct_push"}}</td>
<td>{{if not .Rule.CanPush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableWhitelist}}{{if .PushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .PushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.write_access"}}{{end}}</td>
</tr>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.force_push"}}</td>
<td>{{if not .Rule.CanForcePush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableForcePushAllowlist}}{{if .ForcePushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .ForcePushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}</td>
</tr>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.deletion"}}</td>
<td>{{if not .Rule.CanDelete}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableDeleteAllowlist}}{{if .DeleteTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .DeleteTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}</td>
</tr>
{{if gt .Rule.RequiredApprovals 0}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals"}}</td>
<td>{{.Rule.RequiredApprovals}}{{if .ApprovalTeams}} ({{.ApprovalTeams}}){{end}}</td>
</tr>
{{end}}
{{if .Rule.EnableMergeWhitelist}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.merge"}}</td>
<td>{{if .MergeTeams}}{{.MergeTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}</td>
</tr>
{{end}}
{{if .Rule.EnableStatusCheck}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}</td>
<td>{{if .StatusContexts}}{{.StatusContexts}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.any"}}{{end}}</td>
</tr>
{{end}}
{{if .Rule.RequireSignedCommits}}
<tr>
<td class="tw-font-semibold">{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}</td>
<td>{{svg "octicon-check" 14}}</td>
</tr>
{{end}}
{{if .Rule.ProtectedFilePatterns}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.protected_files"}}</td>
<td><code>{{.Rule.ProtectedFilePatterns}}</code></td>
</tr>
{{end}}
{{if or .Rule.BlockOnOutdatedBranch .Rule.BlockOnRejectedReviews .Rule.BlockAdminMergeOverride}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.also"}}</td>
<td>
<div class="tw-flex tw-gap-1 tw-flex-wrap">
{{if .Rule.BlockOnOutdatedBranch}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_outdated"}}</span>{{end}}
{{if .Rule.BlockOnRejectedReviews}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_rejected"}}</span>{{end}}
{{if .Rule.BlockAdminMergeOverride}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_admin"}}</span>{{end}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</details>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
</div>
@@ -172,7 +172,7 @@
</div>
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_delete"}}</h5>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_branch_deletion"}}</h5>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="enable_delete" value="none" class="toggle-target-disabled" data-target="#delete_allowlist_box" {{if not .Rule.CanDelete}}checked{{end}}>
+24
View File
@@ -116,6 +116,30 @@
{{end}}
</tbody>
</table>
{{if .OrgProtectedTags}}
<h5 class="ui top attached header tw-mt-4">
{{svg "octicon-organization" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_tag"}}
</h5>
<div class="ui attached segment">
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_tag_desc"}}</p>
<table class="ui single line table">
<thead>
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.pattern"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed"}}</th>
<th class="tw-text-right">{{ctx.Locale.Tr "repo.settings.org_protected_tag.read_only"}}</th>
</thead>
<tbody>
{{range .OrgProtectedTags}}
<tr>
<td><pre>{{.Rule.NamePattern}}</pre></td>
<td>{{if .Teams}}{{.Teams}}{{else}}{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.noone"}}{{end}}</td>
<td class="tw-text-right"><span class="text grey">{{svg "octicon-lock" 14}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</div>
</div>
+2709 -2
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -10
View File
@@ -6,6 +6,7 @@ package integration
import (
"fmt"
"net/http"
"strings"
"testing"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
@@ -36,7 +37,7 @@ func TestAPILicensePackages(t *testing.T) {
t.Run("CreatePackage", func(t *testing.T) {
body := `{"name":"Test Pro Annual","description":"Annual pro subscription","duration_days":365,"max_sites":5}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -51,7 +52,7 @@ func TestAPILicensePackages(t *testing.T) {
t.Run("CreatePackageNoName", func(t *testing.T) {
body := `{"description":"Missing name"}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
MakeRequest(t, req, http.StatusUnprocessableEntity)
@@ -68,7 +69,7 @@ func TestAPILicenseKeys(t *testing.T) {
// Create a package first.
body := `{"name":"Test Package","duration_days":30}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -80,7 +81,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("CreateKey", func(t *testing.T) {
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"John Doe","licensee_email":"john@example.com"}`, pkg.ID)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -104,7 +105,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("EditKey", func(t *testing.T) {
body := `{"licensee_name":"Jane Doe","domain_restriction":"example.com,test.com"}`
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), []byte(body)).
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusOK)
@@ -124,7 +125,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("ValidateKey", func(t *testing.T) {
body := fmt.Sprintf(`{"key":"%s","domain":"example.com"}`, rawKey)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
SetHeader("Content-Type", "application/json")
// Note: no token — this is a public endpoint.
resp := MakeRequest(t, req, http.StatusOK)
@@ -136,7 +137,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("ValidateInvalidKey", func(t *testing.T) {
body := `{"key":"MOKO-XXXX-XXXX-XXXX-XXXX","domain":"example.com"}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusOK)
var result api.ValidateLicenseKeyResponse
@@ -161,7 +162,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
// Create a package.
body := `{"name":"Purchase Test","duration_days":90}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -170,7 +171,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
t.Run("PurchaseNewKey", func(t *testing.T) {
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"Buyer","licensee_email":"buyer@shop.com","domain":"shop.com","payment_ref":"stripe_pi_test123"}`, pkg.ID)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -183,7 +184,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
t.Run("PurchaseIdempotent", func(t *testing.T) {
// Same payment_ref should return existing key without raw_key.
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"Buyer","payment_ref":"stripe_pi_test123"}`, pkg.ID)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", []byte(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusOK)
@@ -255,7 +255,7 @@ func TestPackageComposer(t *testing.T) {
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
result = &composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
@@ -268,7 +268,7 @@ func TestPackageComposer(t *testing.T) {
AddBasicAuth(otherUser.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
result = &composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
+2 -2
View File
@@ -253,7 +253,7 @@ func TestOAuth2CallbackReactivationGating(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)()
srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
srv := newFakeOIDCServerWithConfig(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
addOAuth2Source(t, "test-oauth-source", oauth2.Source{
Provider: "openidConnect",
ClientID: "test-client-id",
@@ -308,7 +308,7 @@ type FakeOIDCConfig struct {
}
// newFakeOIDCServer starts a httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow
func newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
func newFakeOIDCServerWithConfig(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
t.Helper()
var srv *httptest.Server
+10 -10
View File
@@ -7,16 +7,16 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/tests"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/tests"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"