diff --git a/.mokogitea/workflows/deploy-mokogitea.yml b/.mokogitea/workflows/deploy-mokogitea.yml index 0754e78ebe..84e4583eee 100644 --- a/.mokogitea/workflows/deploy-mokogitea.yml +++ b/.mokogitea/workflows/deploy-mokogitea.yml @@ -143,7 +143,7 @@ jobs: - name: Update updates.xml if: success() env: - GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} TAG: ${{ steps.config.outputs.tag }} INSTANCE_URL: ${{ steps.config.outputs.instance_url }} DEPLOY_ENV: ${{ github.event.inputs.environment }} diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c2b02a6f91..f084fe1b0a 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Create branch and comment run: | - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_TITLE="${{ github.event.issue.title }}" diff --git a/.mokogitea/workflows/pr-branch-check.yml b/.mokogitea/workflows/pr-branch-check.yml index b8d9742bbb..debdd13b30 100644 --- a/.mokogitea/workflows/pr-branch-check.yml +++ b/.mokogitea/workflows/pr-branch-check.yml @@ -1,90 +1,90 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# Enforces branch merge policy: -# feature/* → dev only -# fix/* → dev only -# hotfix/* → dev or main (emergency) -# dev → main only -# alpha/* → dev only -# beta/* → dev only -# rc/* → main only - -name: Branch Policy Check - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -jobs: - check-target: - name: Verify merge target - runs-on: ubuntu-latest - steps: - - name: Check branch policy - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - alpha/*|beta/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Pre-release branches must target 'dev', not '${BASE}'" - fi - ;; - rc/*) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Release candidate branches must target 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Enforces branch merge policy: +# feature/* → dev only +# fix/* → dev only +# hotfix/* → dev or main (emergency) +# dev → main only +# alpha/* → dev only +# beta/* → dev only +# rc/* → main only + +name: Branch Policy Check + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + check-target: + name: Verify merge target + runs-on: ubuntu-latest + steps: + - name: Check branch policy + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/pr-rc-release.yml b/.mokogitea/workflows/pr-rc-release.yml index 300c4d42be..596eeb39fd 100644 --- a/.mokogitea/workflows/pr-rc-release.yml +++ b/.mokogitea/workflows/pr-rc-release.yml @@ -108,7 +108,7 @@ jobs: - name: Create RC release if: steps.guard.outputs.skip != 'true' env: - GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} RC_TAG: ${{ steps.version.outputs.tag }} RC_VERSION: ${{ steps.version.outputs.version }} PR_TITLE: ${{ github.event.pull_request.title }} @@ -155,7 +155,7 @@ jobs: - name: Commit updates.xml if: steps.guard.outputs.skip != 'true' env: - GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} HEAD_REF: ${{ github.event.pull_request.head.ref }} PR_NUM: ${{ github.event.pull_request.number }} run: | diff --git a/.mokogitea/workflows/upstream-bug-sync.yml b/.mokogitea/workflows/upstream-bug-sync.yml index 8fbf590806..df21bdaa38 100644 --- a/.mokogitea/workflows/upstream-bug-sync.yml +++ b/.mokogitea/workflows/upstream-bug-sync.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Sync upstream bugs env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} + GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} MOKOGITEA_URL: https://git.mokoconsulting.tech MOKOGITEA_REPO: MokoConsulting/MokoGitea diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index b96da67b35..1edb05205e 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -48,6 +48,9 @@ type ProtectedBranch struct { ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` + WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` StatusCheckContexts []string `xorm:"JSON TEXT"` EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` @@ -124,6 +127,11 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use return false } + // Allow the actions bot user if explicitly whitelisted. + if user.IsActions() && protectBranch.WhitelistActionsUser { + return true + } + if !protectBranch.EnableWhitelist { if err := protectBranch.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) @@ -161,6 +169,11 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user return false } + // Allow the actions bot user if explicitly whitelisted. + if user.IsActions() && protectBranch.ForcePushAllowlistActionsUser { + return protectBranch.CanUserPush(ctx, user) + } + if !protectBranch.EnableForcePushAllowlist { return protectBranch.CanUserPush(ctx, user) } @@ -183,6 +196,11 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user // IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool { + // Allow the actions bot user if explicitly whitelisted. + if userID == user_model.ActionsUserID && protectBranch.MergeWhitelistActionsUser { + return true + } + if !protectBranch.EnableMergeWhitelist { // Then we need to fall back on whether the user has write permission return permissionInRepo.CanWrite(unit.TypeCode) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e362332ca5..bb5774a26f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -411,6 +411,7 @@ func prepareMigrationTasks() []*migration { newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel), newMigration(332, "Add org-level branch protection rulesets", v1_27.AddOrgProtectedBranchTable), newMigration(333, "Add require_2fa to user table for org enforcement", v1_27.AddRequire2FAToUser), + newMigration(334, "Add actions user whitelist to protected branches", v1_27.AddActionsUserWhitelistToProtectedBranch), } return preparedMigrations } diff --git a/models/migrations/v1_27/v334.go b/models/migrations/v1_27/v334.go new file mode 100644 index 0000000000..aeb8ed8efa --- /dev/null +++ b/models/migrations/v1_27/v334.go @@ -0,0 +1,17 @@ +// Copyright 2026 The MokoGitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import "xorm.io/xorm" + +// AddActionsUserWhitelistToProtectedBranch adds toggle fields that allow +// the built-in actions bot user to bypass branch protection rules. +func AddActionsUserWhitelistToProtectedBranch(x *xorm.Engine) error { + type ProtectedBranch struct { + WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + } + return x.Sync(new(ProtectedBranch)) +} diff --git a/models/user/avatar.go b/models/user/avatar.go index 291d1467bb..c691357509 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -54,9 +54,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { - // ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user + // ghost user was deleted, actions bot is a system user, 0 means the user should be a virtual user // which comes from git configure information - if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 { + if u.IsGhost() || u.IsActions() || u.ID <= 0 { return avatars.DefaultAvatarLink() } diff --git a/models/user/user.go b/models/user/user.go index f7dcc407e8..435b7af250 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -510,9 +510,9 @@ func (u *User) GitName() string { } // IsMailable checks if a user is eligible to receive emails. -// System users like Ghost and Gitea Actions are excluded. +// System users like Ghost and the actions bot are excluded. func (u *User) IsMailable() bool { - return u.IsActive && !u.IsGiteaActions() && !u.IsGhost() + return u.IsActive && !u.IsActions() && !u.IsGhost() } // IsUserExist checks if given username exist, @@ -627,8 +627,10 @@ var ( "swagger.v1.json", "openapi3.v1.json", - "ghost", // reserved name for deleted users (id: -1) - "gitea-actions", // gitea builtin user (id: -2) + "ghost", // reserved name for deleted users (id: -1) + "mokogitea-actions", // actions bot user (id: -2) + "gitea-actions", // legacy actions bot name + "github-actions", // legacy actions bot name } // These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc. diff --git a/models/user/user_system.go b/models/user/user_system.go index 5671e45487..09703acdab 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -34,8 +34,12 @@ func (u *User) IsGhost() bool { const ( ActionsUserID int64 = -2 - ActionsUserName = "gitea-actions" - ActionsUserEmail = "teabot@gitea.io" + ActionsUserName = "mokogitea-actions" + ActionsUserEmail = "mokogitea-actions[bot]@mokoconsulting.tech" + + // Legacy names recognized as aliases for the actions bot user. + ActionsUserNameLegacyGitea = "gitea-actions" + ActionsUserNameLegacyGitHub = "github-actions" ) // NewActionsUser creates and returns a fake user for running the actions. @@ -45,7 +49,7 @@ func NewActionsUser() *User { Name: ActionsUserName, LowerName: ActionsUserName, IsActive: true, - FullName: "Gitea Actions", + FullName: "MokoGitea Actions", Email: ActionsUserEmail, KeepEmailPrivate: true, LoginName: ActionsUserName, @@ -75,15 +79,30 @@ func GetActionsUserTaskID(u *User) (int64, bool) { return 0, false } -func (u *User) IsGiteaActions() bool { +// IsActions checks whether this user is the built-in actions bot. +func (u *User) IsActions() bool { return u != nil && u.ID == ActionsUserID } +// IsGiteaActions is a deprecated alias for IsActions. +func (u *User) IsGiteaActions() bool { + return u.IsActions() +} + +// isActionsName returns true if the given name (case-insensitive, with +// optional "[bot]" suffix stripped) matches any known actions bot name. +func isActionsName(name string) bool { + clean := strings.TrimSuffix(name, "[bot]") + return strings.EqualFold(clean, ActionsUserName) || + strings.EqualFold(clean, ActionsUserNameLegacyGitea) || + strings.EqualFold(clean, ActionsUserNameLegacyGitHub) +} + func GetSystemUserByName(name string) *User { if strings.EqualFold(name, GhostUserName) { return NewGhostUser() } - if strings.EqualFold(name, ActionsUserName) { + if isActionsName(name) { return NewActionsUser() } return nil diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go index 3ae9c6e366..ae3eec81fd 100644 --- a/models/user/user_system_test.go +++ b/models/user/user_system_test.go @@ -25,13 +25,39 @@ func TestSystemUser(t *testing.T) { uid, u, err = GetPossibleUserByID(t.Context(), -2) require.NoError(t, err) assert.Equal(t, int64(-2), uid) - assert.Equal(t, "gitea-actions", u.Name) - assert.Equal(t, "gitea-actions", u.LowerName) - assert.True(t, u.IsGiteaActions()) + assert.Equal(t, "mokogitea-actions", u.Name) + assert.Equal(t, "mokogitea-actions", u.LowerName) + assert.True(t, u.IsActions()) + assert.True(t, u.IsGiteaActions()) // deprecated alias + // canonical name lookup + u = GetSystemUserByName("mokogitea-actions") + require.NotNil(t, u) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + // legacy name lookups u = GetSystemUserByName("Gitea-actionS") require.NotNil(t, u) - assert.Equal(t, "Gitea Actions", u.FullName) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + u = GetSystemUserByName("github-actions") + require.NotNil(t, u) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + // [bot] suffix lookups + u = GetSystemUserByName("mokogitea-actions[bot]") + require.NotNil(t, u) + assert.Equal(t, "MokoGitea Actions", u.FullName) + + u = GetSystemUserByName("gitea-actions[bot]") + require.NotNil(t, u) + + u = GetSystemUserByName("github-actions[bot]") + require.NotNil(t, u) + + // unknown name returns nil + u = GetSystemUserByName("unknown-bot") + assert.Nil(t, u) uid, u, err = GetPossibleUserByID(t.Context(), 999999) require.NoError(t, err) diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index dee9490998..77b82a11a4 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -42,14 +42,17 @@ type BranchProtection struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"` EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"` + ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` @@ -84,14 +87,17 @@ type CreateBranchProtectionOption struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"` EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"` + ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` @@ -117,14 +123,17 @@ type EditBranchProtectionOption struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"` + PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"` EnableForcePush *bool `json:"enable_force_push"` EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"` ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` ForcePushAllowlistDeployKeys *bool `json:"force_push_allowlist_deploy_keys"` + ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"` EnableMergeWhitelist *bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"` EnableStatusCheck *bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals *int64 `json:"required_approvals"` diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 5c780182f0..cf91027ca8 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2404,15 +2404,18 @@ "repo.settings.protect_whitelist_committers": "Allowlist Restricted Push", "repo.settings.protect_whitelist_committers_desc": "Only allowlisted users or teams will be allowed to push to this branch (but not force push).", "repo.settings.protect_whitelist_deploy_keys": "Allowlist deploy keys with write access to push.", + "repo.settings.protect_whitelist_actions_user": "Allowlist actions bot user to push.", "repo.settings.protect_whitelist_users": "Allowlisted users for pushing:", "repo.settings.protect_whitelist_teams": "Allowlisted teams for pushing:", "repo.settings.protect_force_push_allowlist_users": "Allowlisted users for force pushing:", "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.protect_merge_whitelist_committers": "Enable Merge Allowlist", "repo.settings.protect_merge_whitelist_committers_desc": "Allow only allowlisted users or teams to merge pull requests into this branch.", "repo.settings.protect_merge_whitelist_users": "Allowlisted users for merging:", "repo.settings.protect_merge_whitelist_teams": "Allowlisted teams for merging:", + "repo.settings.protect_merge_whitelist_actions_user": "Allowlist actions bot user to merge.", "repo.settings.protect_check_status_contexts": "Enable Status Check", "repo.settings.protect_status_check_patterns": "Status check patterns:", "repo.settings.protect_status_check_patterns_desc": "Enter patterns to specify which status checks must pass before branches can be merged into a branch that matches this rule. Each line specifies a pattern. Patterns cannot be empty.", diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 4bbc5fc411..5aa5f5813a 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -758,10 +758,13 @@ func CreateBranchProtection(ctx *context.APIContext) { CanPush: form.EnablePush, EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, + WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser, CanForcePush: form.EnablePush && form.EnableForcePush, EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist, ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys, + ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser, EnableMergeWhitelist: form.EnableMergeWhitelist, + MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser, EnableStatusCheck: form.EnableStatusCheck, StatusCheckContexts: form.StatusCheckContexts, EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, @@ -861,17 +864,22 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.CanPush = false protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false } else { protectBranch.CanPush = true if form.EnablePushWhitelist != nil { if !*form.EnablePushWhitelist { protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false } else { protectBranch.EnableWhitelist = true if form.PushWhitelistDeployKeys != nil { protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys } + if form.PushWhitelistActionsUser != nil { + protectBranch.WhitelistActionsUser = *form.PushWhitelistActionsUser + } } } } @@ -882,17 +890,22 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.CanForcePush = false protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false } else { protectBranch.CanForcePush = true if form.EnableForcePushAllowlist != nil { if !*form.EnableForcePushAllowlist { protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false } else { protectBranch.EnableForcePushAllowlist = true if form.ForcePushAllowlistDeployKeys != nil { protectBranch.ForcePushAllowlistDeployKeys = *form.ForcePushAllowlistDeployKeys } + if form.ForcePushAllowlistActionsUser != nil { + protectBranch.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser + } } } } @@ -904,6 +917,12 @@ func EditBranchProtection(ctx *context.APIContext) { if form.EnableMergeWhitelist != nil { protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist + if !*form.EnableMergeWhitelist { + protectBranch.MergeWhitelistActionsUser = false + } + } + if form.MergeWhitelistActionsUser != nil && protectBranch.EnableMergeWhitelist { + protectBranch.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser } if form.EnableStatusCheck != nil { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index c73ecc0814..2e2c3f3b01 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -162,7 +162,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { return nil } - if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() { + if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsActions() { _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID) if err == nil { // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index d983a08c19..14f37a9bad 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -168,10 +168,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanPush = true protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false case "whitelist": protectBranch.CanPush = true protectBranch.EnableWhitelist = true protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys + protectBranch.WhitelistActionsUser = f.WhitelistActionsUser if strings.TrimSpace(f.WhitelistUsers) != "" { whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) } @@ -182,6 +184,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanPush = false protectBranch.EnableWhitelist = false protectBranch.WhitelistDeployKeys = false + protectBranch.WhitelistActionsUser = false } switch f.EnableForcePush { @@ -189,10 +192,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanForcePush = true protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false case "whitelist": protectBranch.CanForcePush = true protectBranch.EnableForcePushAllowlist = true protectBranch.ForcePushAllowlistDeployKeys = f.ForcePushAllowlistDeployKeys + protectBranch.ForcePushAllowlistActionsUser = f.ForcePushAllowlistActionsUser if strings.TrimSpace(f.ForcePushAllowlistUsers) != "" { forcePushAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.ForcePushAllowlistUsers, ",")) } @@ -203,9 +208,11 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.CanForcePush = false protectBranch.EnableForcePushAllowlist = false protectBranch.ForcePushAllowlistDeployKeys = false + protectBranch.ForcePushAllowlistActionsUser = false } protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist + protectBranch.MergeWhitelistActionsUser = f.EnableMergeWhitelist && f.MergeWhitelistActionsUser if f.EnableMergeWhitelist { if strings.TrimSpace(f.MergeWhitelistUsers) != "" { mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index f528ca36d0..1a055ced6e 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -118,7 +118,7 @@ func (input *notifyInput) Notify(ctx context.Context) { func notify(ctx context.Context, input *notifyInput) error { shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch - if input.Doer.IsGiteaActions() { + if input.Doer.IsActions() { // avoiding triggering cyclically, for example: // a comment of an issue will trigger the runner to add a new comment as reply, // and the new comment will trigger the runner again. diff --git a/services/convert/convert.go b/services/convert/convert.go index d543c1a731..2bc181de9a 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -173,14 +173,17 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo PushWhitelistUsernames: pushWhitelistUsernames, PushWhitelistTeams: pushWhitelistTeams, PushWhitelistDeployKeys: bp.WhitelistDeployKeys, + PushWhitelistActionsUser: bp.WhitelistActionsUser, EnableForcePush: bp.CanForcePush, EnableForcePushAllowlist: bp.EnableForcePushAllowlist, ForcePushAllowlistUsernames: forcePushAllowlistUsernames, ForcePushAllowlistTeams: forcePushAllowlistTeams, ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys, + ForcePushAllowlistActionsUser: bp.ForcePushAllowlistActionsUser, EnableMergeWhitelist: bp.EnableMergeWhitelist, MergeWhitelistUsernames: mergeWhitelistUsernames, MergeWhitelistTeams: mergeWhitelistTeams, + MergeWhitelistActionsUser: bp.MergeWhitelistActionsUser, EnableStatusCheck: bp.EnableStatusCheck, StatusCheckContexts: bp.StatusCheckContexts, RequiredApprovals: bp.RequiredApprovals, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 5899af498b..20aff24682 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -172,13 +172,16 @@ type ProtectBranchForm struct { WhitelistUsers string WhitelistTeams string WhitelistDeployKeys bool + WhitelistActionsUser bool EnableForcePush string ForcePushAllowlistUsers string ForcePushAllowlistTeams string ForcePushAllowlistDeployKeys bool + ForcePushAllowlistActionsUser bool EnableMergeWhitelist bool MergeWhitelistUsers string MergeWhitelistTeams string + MergeWhitelistActionsUser bool EnableStatusCheck bool StatusCheckContexts string RequiredApprovals int64 diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index fc26c3b517..01803890c3 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -88,6 +88,12 @@ +
+
+ + +
+
@@ -158,6 +164,12 @@
+
+
+ + +
+
{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}
@@ -303,6 +315,12 @@ {{end}} +
+
+ + +
+
diff --git a/tests/integration/actions_job_token_test.go b/tests/integration/actions_job_token_test.go index 66bfec5bf4..1071eaa426 100644 --- a/tests/integration/actions_job_token_test.go +++ b/tests/integration/actions_job_token_test.go @@ -149,7 +149,7 @@ func TestActionsJobTokenPermissiveAccess(t *testing.T) { require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request")) testURL := *u - testURL.User = url.UserPassword("gitea-actions", task.Token) + testURL.User = url.UserPassword("mokogitea-actions", task.Token) t.Run("ReadGitContent", func(t *testing.T) { testURL.Path = "/user5/repo4.git/HEAD" @@ -443,7 +443,7 @@ jobs: // but it should not have write access req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}). SetHeader("Accept", lfs.MediaType). - AddBasicAuth("gitea-actions", task1Token) + AddBasicAuth("mokogitea-actions", task1Token) MakeRequest(t, req, http.StatusUnauthorized) // set repo1&repo2 max permission to "write" so that the actions token can access code @@ -465,11 +465,11 @@ jobs: // now task1 has write access to repo1, but still only read access to repo2 (different repo) req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}). SetHeader("Accept", lfs.MediaType). - AddBasicAuth("gitea-actions", task1Token) + AddBasicAuth("mokogitea-actions", task1Token) MakeRequest(t, req, http.StatusOK) req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}). SetHeader("Accept", lfs.MediaType). - AddBasicAuth("gitea-actions", task1Token) + AddBasicAuth("mokogitea-actions", task1Token) MakeRequest(t, req, http.StatusUnauthorized) }) } diff --git a/tests/integration/actions_settings_test.go b/tests/integration/actions_settings_test.go index dea36a3831..2c8429bc1e 100644 --- a/tests/integration/actions_settings_test.go +++ b/tests/integration/actions_settings_test.go @@ -57,7 +57,7 @@ jobs: // prepare for clone dstPath := t.TempDir() u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow") - u.User = url.UserPassword("gitea-actions", taskToken) + u.User = url.UserPassword("mokogitea-actions", taskToken) // the git clone will fail doGitCloneFail(u)(t) diff --git a/tests/integration/api_actions_permission_test.go b/tests/integration/api_actions_permission_test.go index 27ced97bd0..37d3ccb13e 100644 --- a/tests/integration/api_actions_permission_test.go +++ b/tests/integration/api_actions_permission_test.go @@ -21,7 +21,7 @@ func testActionUserSignIn(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) u := DecodeJSON(t, resp, &api.User{}) - assert.Equal(t, "gitea-actions", u.UserName) + assert.Equal(t, "mokogitea-actions", u.UserName) } func testActionUserAccessPublicRepo(t *testing.T) {