docs(api): OpenAPI spec + README/CHANGELOG for org-governance (#727, #738) #739

Open
jmiller wants to merge 2 commits from feat/org-governance-openapi into dev
11 changed files with 5608 additions and 14 deletions
+4
View File
@@ -63,6 +63,10 @@
- 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)
+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
+57
View File
@@ -24,6 +24,23 @@ func toAPIOrgEmailDomainPolicy(policy *git_model.OrgEmailDomainPolicy, orgID int
// 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 {
@@ -35,6 +52,31 @@ func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
// 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
@@ -59,6 +101,21 @@ func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
// 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
+57
View File
@@ -32,6 +32,23 @@ func toAPIOrgPushPolicy(policy *git_model.OrgPushPolicy, orgID int64) *api.OrgPu
// 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 {
@@ -43,6 +60,31 @@ func GetOrgPushPolicy(ctx *context.APIContext) {
// 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
@@ -80,6 +122,21 @@ func EditOrgPushPolicy(ctx *context.APIContext) {
// 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
+57
View File
@@ -42,6 +42,23 @@ func toAPIOrgRepoDefaults(d *git_model.OrgRepoDefaults, orgID int64) *api.OrgRep
// 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 {
@@ -53,6 +70,31 @@ func GetOrgRepoDefaults(ctx *context.APIContext) {
// 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
@@ -109,6 +151,21 @@ func EditOrgRepoDefaults(ctx *context.APIContext) {
// 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
+119
View File
@@ -41,6 +41,23 @@ func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtected
// 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)
@@ -55,6 +72,29 @@ func ListOrgTagProtections(ctx *context.APIContext) {
// 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)
@@ -69,6 +109,33 @@ func GetOrgTagProtection(ctx *context.APIContext) {
// 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
@@ -101,6 +168,37 @@ func CreateOrgTagProtection(ctx *context.APIContext) {
// 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
@@ -134,6 +232,27 @@ func EditOrgTagProtection(ctx *context.APIContext) {
// 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 {
+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"`
}
+2556 -2
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff