Feature: Add per-runner “Disable/Pause” (#36776)

This PR adds per-runner disable/enable support for Gitea Actions so a
registered runner can be paused from picking up new jobs without
unregistering.

Disabled runners stay registered and online but are excluded from new
task assignment; running tasks are allowed to finish. Re-enabling
restores pickup, and runner list/get responses now expose disabled
state.

Also added an endpoint for testing
http://localhost:3000/devtest/runner-edit/enable

<img width="1509" height="701" alt="Bildschirmfoto 2026-02-27 um 22 13
24"
src="https://github.com/user-attachments/assets/5328eda9-e59c-46b6-b398-f436e50ee3da"
/>


Fixes: https://github.com/go-gitea/gitea/issues/36767
This commit is contained in:
Nicolas
2026-03-16 18:24:36 +01:00
committed by GitHub
parent 6372cd7c7d
commit b3b2d111da
27 changed files with 860 additions and 24 deletions

View File

@@ -27,6 +27,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJobWithNeeds(t *testing.T) {
@@ -349,6 +350,122 @@ jobs:
})
}
func TestRunnerDisableEnable(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("BasicDisableEnable", func(t *testing.T) {
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-disable-enable", "mock-runner", "runner-disable-enable")
task1 := testData.runner.fetchTask(t)
require.NotNil(t, task1)
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
testData.runner.fetchNoTask(t, 2*time.Second)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
task2 := testData.runner.fetchTask(t, 5*time.Second)
require.NotNil(t, task2)
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
})
t.Run("TasksVersionPath", func(t *testing.T) {
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-version-path", "mock-runner-version-path", "runner-version-path")
var firstVersion int64
var task1 *runnerv1.Task
ddl := time.Now().Add(5 * time.Second)
for time.Now().Before(ddl) {
task1, firstVersion = testData.runner.fetchTaskOnce(t, 0)
if task1 != nil {
break
}
time.Sleep(200 * time.Millisecond)
}
require.NotNil(t, task1, "expected to receive first task")
require.NotZero(t, firstVersion, "response TasksVersion should be set")
// Trigger a second run so there is a pending job after we re-enable the runner
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
time.Sleep(500 * time.Millisecond) // allow workflow run to be created
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
// Fetch with the version we had before disable. Server has bumped version on disable,
// so we enter PickTask with a re-loaded runner (disabled) and get no task.
taskAfterDisable, _ := testData.runner.fetchTaskOnce(t, firstVersion)
assert.Nil(t, taskAfterDisable, "disabled runner must not receive a task when sending previous TasksVersion")
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
task2 := testData.runner.fetchTask(t, 5*time.Second)
require.NotNil(t, task2, "after re-enable runner should receive tasks again")
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
})
})
}
type runnerDisableEnableTestData struct {
repo *api.Repository
runner *mockRunner
runnerID int64
}
func prepareRunnerDisableEnableTest(t *testing.T, user *user_model.User, authToken, repoName, runnerName, workflowName string) *runnerDisableEnableTestData {
t.Helper()
apiRepo := createActionsTestRepo(t, authToken, repoName, false)
runner := newMockRunner()
runner.registerAsRepoRunner(t, user.Name, apiRepo.Name, runnerName, []string{"ubuntu-latest"}, false)
wfTreePath := fmt.Sprintf(".gitea/workflows/%s.yml", workflowName)
wfContent := fmt.Sprintf(`name: %s
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo %s
`, workflowName, workflowName)
opts := getWorkflowCreateFileOptions(user, apiRepo.DefaultBranch, "create workflow", wfContent)
createWorkflowFile(t, authToken, user.Name, apiRepo.Name, wfTreePath, opts)
return &runnerDisableEnableTestData{
repo: apiRepo,
runner: runner,
runnerID: getRepoRunnerID(t, authToken, user.Name, apiRepo.Name),
}
}
func triggerRunnerDisableEnableRun(t *testing.T, user *user_model.User, authToken string, repo *api.Repository, treePath string) {
t.Helper()
opts := getWorkflowCreateFileOptions(user, repo.DefaultBranch, "second push", "second run")
createWorkflowFile(t, authToken, user.Name, repo.Name, treePath, opts)
}
func getRepoRunnerID(t *testing.T, authToken, ownerName, repoName string) int64 {
t.Helper()
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners", ownerName, repoName)).AddTokenAuth(authToken)
resp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, resp, &runnerList)
require.Len(t, runnerList.Entries, 1)
return runnerList.Entries[0].ID
}
func TestActionsGiteaContext(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})

View File

@@ -60,17 +60,36 @@ func TestActionsRunnerModify(t *testing.T) {
sess.MakeRequest(t, req, expectedStatus)
}
doDisable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=true", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
doEnable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=false", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
doDisable(t, sess, baseURL, id, http.StatusNotFound)
doEnable(t, sess, baseURL, id, http.StatusNotFound)
doDelete(t, sess, baseURL, id, http.StatusNotFound)
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.Empty(t, v.Description)
assert.False(t, v.IsDisabled)
}
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.Equal(t, "ChangedDescription", v.Description)
doDisable(t, sess, baseURL, id, http.StatusOK)
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.True(t, v.IsDisabled)
doEnable(t, sess, baseURL, id, http.StatusOK)
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.False(t, v.IsDisabled)
doDelete(t, sess, baseURL, id, http.StatusOK)
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
}

View File

@@ -19,6 +19,7 @@ import (
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -113,12 +114,8 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
ddl := time.Now().Add(fetchTimeout)
var task *runnerv1.Task
for time.Now().Before(ddl) {
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: 0,
}))
assert.NoError(t, err)
if resp.Msg.Task != nil {
task = resp.Msg.Task
task, _ = r.fetchTaskOnce(t, 0)
if task != nil {
break
}
time.Sleep(200 * time.Millisecond)
@@ -127,6 +124,17 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
return task
}
// fetchTaskOnce performs a single FetchTask request with the given TasksVersion
// and returns the task (if any) and the TasksVersion from the response.
// Used to verify the production path where the runner sends the current version.
func (r *mockRunner) fetchTaskOnce(t *testing.T, tasksVersion int64) (*runnerv1.Task, int64) {
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: tasksVersion,
}))
require.NoError(t, err)
return resp.Msg.Task, resp.Msg.TasksVersion
}
type mockTaskOutcome struct {
result runnerv1.Result
outputs map[string]string

View File

@@ -24,6 +24,12 @@ func TestAPIActionsRunner(t *testing.T) {
t.Run("RepoRunner", testActionsRunnerRepo)
}
func newRunnerUpdateRequest(t *testing.T, url string, disabled bool) *RequestWrapper {
return NewRequestWithJSON(t, http.MethodPatch, url, api.EditActionRunnerOption{
Disabled: &disabled,
})
}
func testActionsRunnerAdmin(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
@@ -45,22 +51,39 @@ func testActionsRunnerAdmin(t *testing.T) {
require.NotEqual(t, -1, idx)
expectedRunner := runnerList.Entries[idx]
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name)
assert.False(t, expectedRunner.Disabled)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
disabledRunner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &disabledRunner)
assert.True(t, disabledRunner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify all returned runners can be requested and deleted
for _, runnerEntry := range runnerList.Entries {
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runnerResp = MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, runnerEntry.Name, runner.Name)
assert.Equal(t, runnerEntry.ID, runner.ID)
assert.Equal(t, runnerEntry.Disabled, runner.Disabled)
assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral)
assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels)
@@ -93,6 +116,7 @@ func testActionsRunnerUser(t *testing.T) {
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name)
assert.Equal(t, int64(34346), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Disabled)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
@@ -107,11 +131,26 @@ func testActionsRunnerUser(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-user", runner.Name)
assert.Equal(t, int64(34346), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -136,6 +175,7 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
@@ -165,6 +205,7 @@ func testActionsRunnerOwner(t *testing.T) {
require.NotNil(t, expectedRunner)
assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
assert.Equal(t, int64(34347), expectedRunner.ID)
assert.False(t, expectedRunner.Disabled)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
@@ -179,11 +220,26 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -202,6 +258,22 @@ func testActionsRunnerOwner(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetRepoScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
@@ -244,6 +316,7 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
@@ -269,6 +342,7 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name)
assert.Equal(t, int64(34348), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Disabled)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
@@ -283,11 +357,26 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -306,6 +395,22 @@ func testActionsRunnerRepo(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetOrganizationScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)