diff --git a/Makefile b/Makefile index b86361d73e..3c7582dd57 100644 --- a/Makefile +++ b/Makefile @@ -713,7 +713,7 @@ generate-go: $(TAGS_PREREQ) .PHONY: security-check security-check: - GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... + GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) diff --git a/flake.lock b/flake.lock index 4cbc85b87a..a608aa3b89 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1760038930, - "narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=", + "lastModified": 1771369470, + "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3", + "rev": "0182a361324364ae3f436a63005877674cf45efb", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index f784ac2581..b7a3af6a3f 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.4 github.com/go-git/go-billy/v5 v5.7.0 - github.com/go-git/go-git/v5 v5.16.4 + github.com/go-git/go-git/v5 v5.16.5 github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-redsync/redsync/v4 v4.15.0 github.com/go-sql-driver/mysql v1.9.3 diff --git a/go.sum b/go.sum index b10e259c91..1a6decc18b 100644 --- a/go.sum +++ b/go.sum @@ -332,8 +332,8 @@ github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9n github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= diff --git a/models/actions/run.go b/models/actions/run.go index be332d6857..99e6267071 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -168,7 +168,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { } func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { - if run.Event.IsPullRequest() { + if run.Event.IsPullRequest() || run.Event.IsPullRequestReview() { var payload api.PullRequestPayload if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { return nil, err diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 89c6a4bfe5..18a4086710 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -98,6 +98,20 @@ func (h HookEventType) IsPullRequest() bool { return h.Event() == "pull_request" } +// IsPullRequestReview returns true for pull request review events +// (approved, rejected, comment). These events use the same PullRequestPayload +// as regular pull_request events. +func (h HookEventType) IsPullRequestReview() bool { + switch h { + case HookEventPullRequestReviewApproved, + HookEventPullRequestReviewRejected, + HookEventPullRequestReviewComment: + return true + default: + return false + } +} + // HookType is the type of a webhook type HookType = string diff --git a/package.json b/package.json index 9c5116d6b5..a7792c5fee 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "swagger-ui-dist": "5.31.1", "tailwindcss": "3.4.17", "throttle-debounce": "5.0.2", - "tinycolor2": "1.6.0", + "colord": "2.9.3", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", @@ -82,7 +82,6 @@ "@types/sortablejs": "1.15.9", "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", - "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.4", "@typescript-eslint/parser": "8.56.0", "@vitejs/plugin-vue": "6.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 383e0c8c2e..e67da15ad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: clippie: specifier: 4.1.10 version: 4.1.10 + colord: + specifier: 2.9.3 + version: 2.9.3 compare-versions: specifier: 6.1.1 version: 6.1.1 @@ -164,9 +167,6 @@ importers: throttle-debounce: specifier: 5.0.2 version: 5.0.2 - tinycolor2: - specifier: 1.6.0 - version: 1.6.0 tippy.js: specifier: 6.3.7 version: 6.3.7 @@ -249,9 +249,6 @@ importers: '@types/throttle-debounce': specifier: 5.0.2 version: 5.0.2 - '@types/tinycolor2': - specifier: 1.4.6 - version: 1.4.6 '@types/toastify-js': specifier: 1.12.4 version: 1.12.4 @@ -1297,9 +1294,6 @@ packages: '@types/throttle-debounce@5.0.2': resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==} - '@types/tinycolor2@1.4.6': - resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} - '@types/toastify-js@1.12.4': resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==} @@ -4056,9 +4050,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -5241,8 +5232,6 @@ snapshots: '@types/throttle-debounce@5.0.2': {} - '@types/tinycolor2@1.4.6': {} - '@types/toastify-js@1.12.4': {} '@types/trusted-types@2.0.7': @@ -8215,8 +8204,6 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 7271f58091..884b98e966 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -115,6 +115,21 @@ func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, c return "", "", errors.New("head of pull request is missing in event payload") } commitID = payload.PullRequest.Head.Sha + case // pull_request_review events share the same PullRequestPayload as pull_request + webhook_module.HookEventPullRequestReviewApproved, + webhook_module.HookEventPullRequestReviewRejected, + webhook_module.HookEventPullRequestReviewComment: + event = run.TriggerEvent + payload, err := run.GetPullRequestEventPayload() + if err != nil { + return "", "", fmt.Errorf("GetPullRequestEventPayload: %w", err) + } + if payload.PullRequest == nil { + return "", "", errors.New("pull request is missing in event payload") + } else if payload.PullRequest.Head == nil { + return "", "", errors.New("head of pull request is missing in event payload") + } + commitID = payload.PullRequest.Head.Sha case webhook_module.HookEventRelease: event = string(run.Event) commitID = run.CommitSHA diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index b0eabdd432..7fff796af6 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -691,6 +691,144 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL, assert.NoError(t, err) } +func TestPullRequestReviewCommitStatusEvent(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // reviewer + + // create a repo + repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{ + Name: "repo-pull-request-review", + Description: "test pull-request-review commit status", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // add user4 as collaborator so they can review + ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository) + t.Run("AddUser4AsCollaboratorWithWriteAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeWrite)) + + // add workflow file that triggers on pull_request_review + addWorkflow, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/pr-review.yml", + ContentReader: strings.NewReader(`name: test +on: + pull_request_review: + types: [submitted] +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo helloworld +`), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflow) + + // create a branch and a PR + testBranch := "test-review-branch" + err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch) + assert.NoError(t, err) + + // add a file on the test branch so the PR has changes + addFileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "test.txt", + ContentReader: strings.NewReader("test content"), + }, + }, + Message: "add test file", + OldBranch: testBranch, + NewBranch: testBranch, + Author: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + GitUserName: user2.Name, + GitUserEmail: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addFileResp) + sha := addFileResp.Commit.SHA + + pullIssue := &issues_model.Issue{ + RepoID: repo.ID, + Title: "A test PR for review", + PosterID: user2.ID, + Poster: user2, + IsPull: true, + } + pullRequest := &issues_model.PullRequest{ + HeadRepoID: repo.ID, + BaseRepoID: repo.ID, + HeadBranch: testBranch, + BaseBranch: "main", + HeadRepo: repo, + BaseRepo: repo, + Type: issues_model.PullRequestGitea, + } + prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest} + err = pull_service.NewPullRequest(t.Context(), prOpts) + assert.NoError(t, err) + + // submit an approval review as user4 + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + assert.NoError(t, err) + defer gitRepo.Close() + + _, _, err = pull_service.SubmitReview(t.Context(), user4, gitRepo, pullIssue, issues_model.ReviewTypeApprove, "lgtm", sha, nil) + assert.NoError(t, err) + + // verify that a commit status was created for the review event + assert.Eventually(t, func() bool { + latestCommitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, sha, db.ListOptionsAll) + assert.NoError(t, err) + if len(latestCommitStatuses) == 0 { + return false + } + if latestCommitStatuses[0].State == commitstatus.CommitStatusPending { + insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) + return true + } + return false + }, 1*time.Second, 100*time.Millisecond) + }) +} + func TestWorkflowDispatchPublicApi(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 47f378c47a..b2aa9ea1c5 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -1,4 +1,4 @@ -import tinycolor from 'tinycolor2'; +import {colord} from 'colord'; import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; import {onInputDebounce} from '../utils/dom.ts'; import type MonacoNamespace from 'monaco-editor'; @@ -94,7 +94,7 @@ function updateTheme(monaco: Monaco): void { // https://github.com/microsoft/monaco-editor/issues/2427 // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format const styles = window.getComputedStyle(document.documentElement); - const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6'); + const getColor = (name: string) => colord(styles.getPropertyValue(name).trim()).alpha(1).toHex(); monaco.editor.defineTheme('gitea', { base: isDarkTheme() ? 'vs-dark' : 'vs', diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts index 57c909b8a0..096356983a 100644 --- a/web_src/js/utils/color.ts +++ b/web_src/js/utils/color.ts @@ -1,22 +1,21 @@ -import tinycolor from 'tinycolor2'; -import type {ColorInput} from 'tinycolor2'; +import {colord} from 'colord'; +import type {AnyColor} from 'colord'; /** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */ // Keep this in sync with modules/util/color.go -function getRelativeLuminance(color: ColorInput): number { - const {r, g, b} = tinycolor(color).toRgb(); +function getRelativeLuminance(color: AnyColor): number { + const {r, g, b} = colord(color).toRgb(); return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -function useLightText(backgroundColor: ColorInput): boolean { +function useLightText(backgroundColor: AnyColor): boolean { return getRelativeLuminance(backgroundColor) < 0.453; } -/** Given a background color, returns a black or white foreground color that the highest - * contrast ratio. */ +/** Given a background color, returns a black or white foreground color with the highest contrast ratio. */ // In the future, the APCA contrast function, or CSS `contrast-color` will be better. // https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 -export function contrastColor(backgroundColor: ColorInput): string { +export function contrastColor(backgroundColor: AnyColor): string { return useLightText(backgroundColor) ? '#fff' : '#000'; }