mirror of
https://github.com/go-gitea/gitea
synced 2026-07-02 14:43:23 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7bc6b89c1 | ||
|
|
fabc5e6fcb | ||
|
|
a016d678db | ||
|
|
b6e409badd | ||
|
|
2734504cfc | ||
|
|
eee7967b81 | ||
|
|
1df8f91691 | ||
|
|
4654e7eccd |
+268
@@ -4,6 +4,274 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.27.0-rc0](https://github.com/go-gitea/gitea/releases/tag/v1.27.0-rc0) - 2026-06-28
|
||||
|
||||
* BREAKING
|
||||
* Feat(actions)!: improve support for reusable workflows (#37478)
|
||||
* Use Content-Security-Policy: script nonce (#37232)
|
||||
|
||||
* SECURITY
|
||||
* Fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 [security] (#37786)
|
||||
* Fix(oauth): restrict introspection to the token's client (#38042)
|
||||
* Fix(api): don't expose private org membership via public_members (#38145)
|
||||
* Fix(actions): deny fork-PR cross-repo access via collaborative owner (#38214)
|
||||
* Fix(migrations): prevent path traversal in repository restore (#38215)
|
||||
|
||||
* FEATURES
|
||||
* Feat(actions): add workflow status badge modal (#38196)
|
||||
* Feat(actions): support owner-level and global scoped workflows (#38154)
|
||||
* Feat(api): support ref suffixes in compare (#38148)
|
||||
* Feat(actions): implement `jobs.<job_id>.continue-on-error` (#38100)
|
||||
* Feat(actions): show run status on browser tab favicon (#38071)
|
||||
* Feat(api): add token introspection and self-deletion endpoint (#37995)
|
||||
* Feat(api): add q parameter to list branches API for server-side filtering (#37982)
|
||||
* Feat(repo): split repository creation limit into user and org scopes (#37872)
|
||||
* Feat(actions): bulk delete, disable and enable runners in admin UI (#37869)
|
||||
* Feat(actions): List workflows that were executed once but got removed from the default branch (#37835)
|
||||
* Feat(org): add team visibility so org members can discover teams (#37680)
|
||||
* Feat: add raw diff/patch endpoint for repository comparisons (#37632)
|
||||
* Feat: Add avatar stacks (#37594)
|
||||
* Feat(actions): add job summaries (GITHUB_STEP_SUMMARY) (#37500)
|
||||
* Feat(web): Add Jupyter Notebook (.ipynb) Rendering Support (#37433)
|
||||
* Support for Custom URI Schemes in OAuth2 Redirect URIs (#37356)
|
||||
* Feat(orgs): Add search bar for organization members tab page (#37347)
|
||||
* Feat(api): Add assignees APIs (#37330)
|
||||
* Feat(api): Add GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs (#37196)
|
||||
* Serve OpenAPI 3.0 spec at /openapi.v1.json (#37038)
|
||||
* Add project column picker to issue and pull request sidebar (#37037)
|
||||
* Allow multiple projects per issue and pull requests (#36784)
|
||||
* Feat(ui): add "follow rename" to file commit history list (#34994)
|
||||
* Feat(ssh): auto generate additional ssh keys (#33974)
|
||||
|
||||
* ENHANCEMENTS
|
||||
* Enhance: allow builtin default git config options to be overridden (#38172)
|
||||
* Enhance: allow MathML core elements (#38034)
|
||||
* Enhance(markup): improve issue title rendering (#37908)
|
||||
* Enhance(actions): set descriptive browser tab title on run view (#37870)
|
||||
* Enhance: Migrate remaining gopkg.in/yaml.v3 usages to go.yaml.in/yaml/v4 (#37866)
|
||||
* Enhance(actions): show workflow name from YAML instead of filename (#37833)
|
||||
* Feat(actions): add before/after to PR synchronize event payload (#37827)
|
||||
* Enhance(actions): add branch filters to run list (#37826)
|
||||
* Enhance(actions): Make Summary UI more beautiful with more infos (#37824)
|
||||
* Feat: add copy button to action step header, improve other copy buttons (#37744)
|
||||
* Fix(icon): use repo-forked icon to display forks count (#37731)
|
||||
* Feat(api): add sort and order query parameters to job list endpoints (#37672)
|
||||
* Feat(api): add last_sync to repository API (#37566)
|
||||
* Enhance: Adjust Workflow Graph styling (#37497)
|
||||
* Improve code editor text selection and clean up lint enablement (#37474)
|
||||
* Add mirror auth updates to repo edit API and settings (#37468)
|
||||
* Replace `olivere/elastic` with REST API client, add OpenSearch support (#37411)
|
||||
* Feat: Add default PR branch update style setting (#37410)
|
||||
* Fix inconsistent disabled styling on logged-out repo header buttons (#37406)
|
||||
* Allow fast-forward-only merge when signed commits are required (#37335)
|
||||
* Enhance styling in actions page (#37323)
|
||||
* Fix: improve actions status icons and texts (#37206)
|
||||
* Make Markdown fenced code block work with more syntaxes (#37154)
|
||||
* Fix: Sort action run jobs by JobID and Name with matrix examples (#37046)
|
||||
* Add API endpoint to reply to pull request review comments (#36683)
|
||||
|
||||
* PERFORMANCE
|
||||
* Perf(web): sort the action_run query by a repo-scoped index when possible (#38155)
|
||||
* Perf: Various performance regression fixes (#38078)
|
||||
* Perf: extend action `c_u` index to include `created_unix` for faster dashboard feeds (#38076)
|
||||
* Batch-load related data in actions run, job, and task API endpoints (#37032)
|
||||
|
||||
* BUGFIXES
|
||||
* Fix: update npm dependencies, fix misc issues (#38257)
|
||||
* Fix(api): respect since/until when counting commits for X-Total-Count (#38204)
|
||||
* Fix: codemirror regressions (#38248)
|
||||
* Fix(api): support HEAD requests on all API GET endpoints (#38245)
|
||||
* Fix(actions): Cleanup workflow status badge code (#38241)
|
||||
* Fix(web): Correctly align the "disabled" label on larger workflow names (#38240)
|
||||
* Fix(actions): don't swallow HTML entities into linkified URLs (#38239)
|
||||
* Fix(packages): accept npm "repository" and "bin" in string form (#38236)
|
||||
* Fix(actions): fix 500 error when canceling a canceling task (#38223)
|
||||
* Fix(deps): update module golang.org/x/image to v0.43.0 [security] (#38219)
|
||||
* Fix(mssql): convert legacy DATETIME columns to DATETIME2 (#38216)
|
||||
* Fix(api): deny private org member enumeration via /members (#38213)
|
||||
* Fix(actions): ensure all waiting jobs get runners in large workflows (#38200)
|
||||
* Fix(deps): update go dependencies (#38194)
|
||||
* Fix(deps): update npm dependencies (#38193)
|
||||
* Fix(cli): default must-change-password to false for bot users (#38175)
|
||||
* Fix(actions): show run index in run view and fix summary graph height (#38165)
|
||||
* Fix: csp (#38162)
|
||||
* Fix(deps): update npm dependencies (#38123)
|
||||
* Fix(mssql): expand legacy issue and comment long-text columns (#38120)
|
||||
* Fix(packages): validate debian distribution and component names (#38116)
|
||||
* Fix(packages): validate module version in goproxy ParsePackage (#38104)
|
||||
* Fix(deps): update dependency esbuild to v0.28.1 [security] (#38097)
|
||||
* Fix: git push hook post receive (#38089)
|
||||
* Fix(ui): prevent commit status popup overflowing its row (#38081)
|
||||
* Fix: validate gem name in rubygems parseMetadataFile (#38061)
|
||||
* Fix: commit display name (#38057)
|
||||
* Fix: csp regressions (#38047)
|
||||
* Fix: api error message (#38031)
|
||||
* Fix(deps): update npm dependencies (#38029)
|
||||
* Fix: pgsql lint (#38022)
|
||||
* Fix(indexer): fix assignee filters in issue search (#38021)
|
||||
* Fix: various dropdown problems (#38020)
|
||||
* Fix: refactor git error handling and make archive streaming handle non-existing commit id (#38007)
|
||||
* Fix: raise git required version to 2.13 (#37996)
|
||||
* Fix: remove "no-transfrom" from the cache-control header (#37985)
|
||||
* Fix(deps): update module github.com/google/go-github/v87 to v88 (#37971)
|
||||
* Fix: use committer time where ever possible as default (#37969)
|
||||
* Fix(deps): update npm dependencies, remove nolyfill (#37968)
|
||||
* Fix(deps): update go dependencies (#37967)
|
||||
* Fix(pull): preserve squash message trailers and additional commit messages (#37954)
|
||||
* Fix(deps): update module golang.org/x/image to v0.41.0 [security] (#37904)
|
||||
* Fix: support ##[command] log prefix in action run UI (#37882)
|
||||
* Fix(deps): update module github.com/google/go-github/v86 to v87 (#37845)
|
||||
* Fix(deps): update npm dependencies (#37844)
|
||||
* Fix(deps): update go dependencies (#37841)
|
||||
* Fix(frontend): resolve Vite assets by manifest source path (#37836)
|
||||
* Fix(locales): Replace hardcoded strings (#37788)
|
||||
* Fix(packages): render markdown links relative to linked repo (#37676)
|
||||
* Fix: persist mirror repository metadata (#37519)
|
||||
* Fix cmd tests by mocking builtin paths (#37369)
|
||||
* Add `form-fetch-action` to some forms, fix "fetch action" resp bug (#37305)
|
||||
* Feat: execute post run cleanup when workflow is cancelled (#37275)
|
||||
* Fix `relative-time` error and improve global error handler (#37241)
|
||||
* Refactor flash message and remove SanitizeHTML template func (#37179)
|
||||
|
||||
* TESTING
|
||||
* Test: speed up two tests (#37905)
|
||||
* Test: Fix random failure test (#37887)
|
||||
* Test: fix flaky `issue-comment` close test (#37880)
|
||||
* Test: enable WAL for sqlite integration tests (#37861)
|
||||
* Test: fix flaky `TestResourceIndex` and reduce its runtime (#37847)
|
||||
* Test: run `TestAPIRepoMigrate` offline via a local clone source (#37817)
|
||||
* Ci: shard tests and reduce redundant work (#37618)
|
||||
* Test(e2e): run playwright via container (#37300)
|
||||
* Remove external service dependencies in migration tests (#36866)
|
||||
|
||||
* BUILD
|
||||
* Fix(actions): authenticate snapcraft before nightly remote build (#38252)
|
||||
* Ci: cap Elasticsearch heap in db-tests (#37816)
|
||||
* Build(snap): publish nightly version to snapcraft via actions (#37814)
|
||||
* Ci: split pgsql shards into plain jobs, dedupe setup actions (#37802)
|
||||
* Ci: narrow files-changed frontend filter (#37749)
|
||||
* Ci: add `zizmor` to `lint-actions` (#37720)
|
||||
* Chore: clean up "contrib" dir (#37690)
|
||||
* Fix: snap build (main branch) (#37685)
|
||||
* Ci: Also lint json5 files (#37659)
|
||||
* Feat(editor): broaden language detection in web code editor (#37619)
|
||||
* Build: update pnpm to v11 (#37591)
|
||||
* Refactor(deps): migrate from `nektos/act` fork to `gitea/runner` (#37557)
|
||||
* Refactor: lint bare `fill`/`stroke` colors, add vars for git graph color series (#37543)
|
||||
* Update go js py dependencies (#37525)
|
||||
* Ci: lint PR titles with commitlint (#37498)
|
||||
* Chore: upgrade Go version in devcontainer image to 1.26 (#37374)
|
||||
* Update GitHub Actions to latest major versions (#37313)
|
||||
* Update go js dependencies (#37312)
|
||||
* Fail vite build on rolldown warnings via NODE_ENV=test (#37270)
|
||||
* Remove htmx (#37224)
|
||||
* Replace custom Go formatter with `golangci-lint fmt` (#37194)
|
||||
* Refactor htmx and fetch-action related code (#37186)
|
||||
* Integrate renovate bot for all dependency updates (#37050)
|
||||
* Build(sign): move to sigstore (#38250)
|
||||
|
||||
* DOCS
|
||||
* Docs: update changelog for 1.26.3 & 1.26.4 (#38178)
|
||||
* Docs: fix duplicated word in foreachref doc comment (#38161)
|
||||
* Docs: Clarify criteria for becoming a merger (#38113)
|
||||
* Docs: Publish TOC Election Result 2026 (#38111)
|
||||
* Docs: mark openapi3 as autogenerated in attributes (#37963)
|
||||
* Docs: add development setup guide (#37960)
|
||||
|
||||
* MISC
|
||||
* Revert(sign): restore gpg (#38251)
|
||||
* Refactor: replace legacy `delete-button` with `link-action` (#38143)
|
||||
* Refactor(actions): read runner capabilities from proto field (#38068)
|
||||
* Refactor(api): clarify APIError message usage and fix legacy lint error (#38012)
|
||||
* Refactor: Use db.Get[] instead of db.GetEngine(ctx).Get(bean) to avoid zero value fetching wrong database record (#37977)
|
||||
* Fix(deps): update go dependencies (#37851)
|
||||
* Ci: Fix sync PR labels from the conventional-commit title (#37784) (#37825)
|
||||
* Ci: tweak `files-changed`, add `free-disk-space` (#37819)
|
||||
* Fix(deps): update module golang.org/x/crypto to v0.52.0 [security] (#37806)
|
||||
* Test(e2e): add comment, release, star, PR and fork tests (#37800)
|
||||
* Chore: simplify issue and pull request templates (#37799)
|
||||
* Chore: Update giteabot to fix failure when backport (#37789)
|
||||
* Fix(api): handle partial failures in push mirror synchronization gracefully (#37782)
|
||||
* Fix(deps): update module gitlab.com/gitlab-org/api/client-go/v2 to v2.26.0 (#37771)
|
||||
* Ci: split giteabot workflow (#37770)
|
||||
* Fix(deps): update npm dependencies (#37768)
|
||||
* Refactor(waitgroup): replace Add/Done goroutines with WaitGroup.Go (#37764)
|
||||
* Fix(deps): update module google.golang.org/grpc to v1.81.1 (#37762)
|
||||
* Ci: fix cache-related issues (#37761)
|
||||
* Chore: fix tests (#37760)
|
||||
* Fix(deps): update module github.com/google/go-github/v85 to v86 (#37754)
|
||||
* Fix(deps): update npm dependencies (#37753)
|
||||
* Fix(deps): update go dependencies (#37752)
|
||||
* Chore(deps): update action dependencies (#37751)
|
||||
* Fix(markup): wrap indented code blocks for the code-copy button (#37748)
|
||||
* Chore(db): introduce db.Session and db.EngineMigration interfaces (#37746)
|
||||
* Feat(web): also display PR counts in repo list (#37739)
|
||||
* Refactor(glob): use strings.Builder for regexp compilation (#37730)
|
||||
* Chore(doctor): remove four obsolete doctor check implementations (#37728)
|
||||
* Refactor(org): simplify owner-team org repo creation logic (#37727)
|
||||
* Refactor: move `workflowpattern` into `modules/actions` (#37717)
|
||||
* Chore: clean up tests (#37715)
|
||||
* Style: misc UI fixes (#37691)
|
||||
* Ci: add shellcheck linter (#37682)
|
||||
* Fix: catch and fix more lint problems (#37674)
|
||||
* Fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test (#37662)
|
||||
* Fix(deps): update npm dependencies (#37647)
|
||||
* Ci(renovate): update Go import paths on major bumps (#37641)
|
||||
* Fix(deps): update go dependencies (major) (#37639)
|
||||
* Chore(deps): update action dependencies (major) (#37638)
|
||||
* Fix(deps): update module code.gitea.io/sdk/gitea to v0.25.0 (#37637)
|
||||
* Fix(deps): update npm dependencies (#37636)
|
||||
* Refactor(log): replace log.Critical with log.Error (#37624)
|
||||
* Build(deps): bump fast-uri from 3.1.0 to 3.1.2 (#37616)
|
||||
* Feat(oauth): Support AWS Cognito OAuth2 provider (#37607)
|
||||
* Chore(deps): update action dependencies (#37603)
|
||||
* Ci: allow `chore` type in PR title lint (#37575)
|
||||
* Refactor: only reset a database table when the table's data was changed (#37573)
|
||||
* Ci: increase renovate frequency and fix RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS (#37565)
|
||||
* Refactor: use modernc sqlite driver as default (#37562)
|
||||
* Docs: fix 4 typos in CHANGELOG.md (#37549)
|
||||
* Fix(deps): update go dependencies (#37541)
|
||||
* Chore(deps): update action dependencies (#37540)
|
||||
* Refactor pull request view (6) (#37522)
|
||||
* Fix: redirect early CLI console logger to stderr (#37507)
|
||||
* Refactor "flex-list" to "flex-divided-list" (#37505)
|
||||
* Refactor compare diff/pull page (1) (#37481)
|
||||
* Refactor pull request view (4) (#37451)
|
||||
* Update 1.26.1 changelog in main (#37442)
|
||||
* Refactor: use named `Permission` field in `Repository` struct instead of anonymous embedding (#37441)
|
||||
* Refactor: serve site manifest via `/assets/site-manifest.json` endpoint (#37405)
|
||||
* Remove IsValidExternalURL/IsAPIURL and use IsValidURL at call sites (#37364)
|
||||
* Update `Block a user` form (#37359)
|
||||
* Move review request functions to a standalone file (#37358)
|
||||
* Feat(security): set X-Content-Type-Options: nosniff by default (#37354)
|
||||
* Enable strict TypeScript, add `errorMessage` helper (#37292)
|
||||
* Refactor frontend `tw-justify-between` layouts to `flex-left-right` (#37291)
|
||||
* Update Nix flake (#37284)
|
||||
* Fix Repository transferring page (#37277)
|
||||
* Remove `SubmitEvent` polyfill (#37276)
|
||||
* Remove dead code identified by `deadcode` tool (#37271)
|
||||
* Upgrade go-git to v5.18.0 (#37268)
|
||||
* Don't add useless labels which will bother changelog generation (#37267)
|
||||
* Move heatmap to first-party code (#37262)
|
||||
* Tests/integration: simplify code (#37249)
|
||||
* Add pagination and search box to org teams list (#37245)
|
||||
* Remove error returns from crypto random helpers and callers (#37240)
|
||||
* Add `ExternalIDClaim` option for OAuth2 OIDC auth source (#37229)
|
||||
* Refactor: simplify ParseCatFileTreeLine and catBatchParseTreeEntries (#37210)
|
||||
* Refactor "htmx" to "fetch action" (#37208)
|
||||
* Update go js py dependencies (#37204)
|
||||
* Add comment for the design of "user activity time" (#37195)
|
||||
* Remove outdated RunUser logic (#37180)
|
||||
* Models/fixtures: add "DO NOT add more test data" comment to all yml fixture files (#37150)
|
||||
* Update javascript dependencies (#37142)
|
||||
* Update go dependencies (#37141)
|
||||
* Frontport changelog of v1.26.0-rc0 (#37138)
|
||||
* Introduce `ActionRunAttempt` to represent each execution of a run (#37119)
|
||||
* Workflow Artifact Info Hover (#37100)
|
||||
* Extend issue context popup beyond markdown content (#36908)
|
||||
* Add bulk repository deletion for organizations (#36763)
|
||||
* Feat: Add bypass allowlist for branch protection (#36514)
|
||||
|
||||
## [1.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21
|
||||
|
||||
* SECURITY
|
||||
|
||||
@@ -135,8 +135,8 @@ type StatusInfo struct {
|
||||
|
||||
// GetStatusInfoList returns a slice of StatusInfo
|
||||
func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
|
||||
// same as those in aggregateJobStatus
|
||||
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning, StatusCancelling}
|
||||
// same as those in aggregateJobStatus (StatusUnknown excluded; it's the "shouldn't happen" fallback)
|
||||
allStatus := []Status{StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped, StatusWaiting, StatusRunning, StatusBlocked, StatusCancelling}
|
||||
statusInfoList := make([]StatusInfo, 0, len(allStatus))
|
||||
for _, s := range allStatus {
|
||||
statusInfoList = append(statusInfoList, StatusInfo{
|
||||
|
||||
@@ -73,8 +73,11 @@ func TestGetStatusInfoList(t *testing.T) {
|
||||
assert.Equal(t, []StatusInfo{
|
||||
{Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"},
|
||||
{Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"},
|
||||
{Status: int(StatusCancelled), StatusName: StatusCancelled.String(), DisplayedStatus: "actions.status.cancelled"},
|
||||
{Status: int(StatusSkipped), StatusName: StatusSkipped.String(), DisplayedStatus: "actions.status.skipped"},
|
||||
{Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"},
|
||||
{Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"},
|
||||
{Status: int(StatusBlocked), StatusName: StatusBlocked.String(), DisplayedStatus: "actions.status.blocked"},
|
||||
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
|
||||
}, statusInfoList)
|
||||
}
|
||||
|
||||
@@ -55,29 +55,34 @@ func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, er
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event, returning those whose `on:` matches.
|
||||
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event.
|
||||
// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered).
|
||||
func MatchScopedWorkflows(
|
||||
parsed []*ParsedScopedWorkflow,
|
||||
consumerGitRepo *git.Repository,
|
||||
consumerCommit *git.Commit,
|
||||
triggedEvent webhook_module.HookEventType,
|
||||
payload api.Payloader,
|
||||
) []*DetectedWorkflow {
|
||||
workflows := make([]*DetectedWorkflow, 0, len(parsed))
|
||||
) (matched, filtered []*DetectedWorkflow) {
|
||||
for _, p := range parsed {
|
||||
for _, evt := range p.Events {
|
||||
if evt.IsSchedule() {
|
||||
// schedule is a non-target for scoped workflows
|
||||
continue
|
||||
}
|
||||
if detectMatched(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
|
||||
workflows = append(workflows, &DetectedWorkflow{
|
||||
EntryName: p.EntryName,
|
||||
TriggerEvent: evt,
|
||||
Content: p.Content,
|
||||
})
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: p.EntryName,
|
||||
TriggerEvent: evt,
|
||||
Content: p.Content,
|
||||
}
|
||||
switch detectWorkflowMatch(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
|
||||
case detectMatched:
|
||||
matched = append(matched, dwf)
|
||||
case detectFilteredOut:
|
||||
filtered = append(filtered, dwf)
|
||||
case detectNotApplicable:
|
||||
}
|
||||
}
|
||||
}
|
||||
return workflows
|
||||
return matched, filtered
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ type DetectedWorkflow struct {
|
||||
Content []byte
|
||||
}
|
||||
|
||||
type detectResult int
|
||||
|
||||
const (
|
||||
detectMatched detectResult = iota // event matched; run normally
|
||||
detectNotApplicable // event/type doesn't apply; create nothing
|
||||
detectFilteredOut // matched but excluded by a branch/paths filter; emits a skipped commit status
|
||||
)
|
||||
|
||||
func init() {
|
||||
model.OnDecodeNodeError = func(node yaml.Node, out any, err error) {
|
||||
// Log the error instead of panic or fatal.
|
||||
@@ -172,18 +180,16 @@ func DetectWorkflows(
|
||||
triggedEvent webhook_module.HookEventType,
|
||||
payload api.Payloader,
|
||||
detectSchedule bool,
|
||||
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
||||
) (workflows, schedules, filtered []*DetectedWorkflow, err error) {
|
||||
_, entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
workflows := make([]*DetectedWorkflow, 0, len(entries))
|
||||
schedules := make([]*DetectedWorkflow, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
content, err := GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// one workflow may have multiple events
|
||||
@@ -203,18 +209,24 @@ func DetectWorkflows(
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
} else {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
workflows = append(workflows, dwf)
|
||||
switch detectWorkflowMatch(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
case detectMatched:
|
||||
workflows = append(workflows, dwf)
|
||||
case detectFilteredOut:
|
||||
filtered = append(filtered, dwf)
|
||||
case detectNotApplicable:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return workflows, schedules, nil
|
||||
return workflows, schedules, filtered, nil
|
||||
}
|
||||
|
||||
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
|
||||
@@ -252,9 +264,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D
|
||||
return wfs, nil
|
||||
}
|
||||
|
||||
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
||||
func detectWorkflowMatch(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) detectResult {
|
||||
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
||||
return false
|
||||
return detectNotApplicable
|
||||
}
|
||||
|
||||
switch triggedEvent {
|
||||
@@ -268,7 +280,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
||||
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
|
||||
}
|
||||
// no special filter parameters for these events, just return true if name matched
|
||||
return true
|
||||
return detectMatched
|
||||
|
||||
case // push
|
||||
webhook_module.HookEventPush:
|
||||
@@ -279,14 +291,20 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
||||
webhook_module.HookEventIssueAssign,
|
||||
webhook_module.HookEventIssueLabel,
|
||||
webhook_module.HookEventIssueMilestone:
|
||||
return matchIssuesEvent(payload.(*api.IssuePayload), evt)
|
||||
if matchIssuesEvent(payload.(*api.IssuePayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
case // issue_comment
|
||||
webhook_module.HookEventIssueComment,
|
||||
// `pull_request_comment` is same as `issue_comment`
|
||||
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
|
||||
webhook_module.HookEventPullRequestComment:
|
||||
return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt)
|
||||
if matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
case // pull_request
|
||||
webhook_module.HookEventPullRequest,
|
||||
@@ -300,34 +318,49 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
||||
case // pull_request_review
|
||||
webhook_module.HookEventPullRequestReviewApproved,
|
||||
webhook_module.HookEventPullRequestReviewRejected:
|
||||
return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt)
|
||||
if matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
case // pull_request_review_comment
|
||||
webhook_module.HookEventPullRequestReviewComment:
|
||||
return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt)
|
||||
if matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
case // release
|
||||
webhook_module.HookEventRelease:
|
||||
return matchReleaseEvent(payload.(*api.ReleasePayload), evt)
|
||||
if matchReleaseEvent(payload.(*api.ReleasePayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
case // registry_package
|
||||
webhook_module.HookEventPackage:
|
||||
return matchPackageEvent(payload.(*api.PackagePayload), evt)
|
||||
if matchPackageEvent(payload.(*api.PackagePayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
case // workflow_run
|
||||
webhook_module.HookEventWorkflowRun:
|
||||
return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt)
|
||||
if matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectNotApplicable
|
||||
|
||||
default:
|
||||
log.Warn("unsupported event %q", triggedEvent)
|
||||
return false
|
||||
return detectNotApplicable
|
||||
}
|
||||
}
|
||||
|
||||
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool {
|
||||
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) detectResult {
|
||||
// with no special filter parameters
|
||||
if len(evt.Acts()) == 0 {
|
||||
return true
|
||||
return detectMatched
|
||||
}
|
||||
|
||||
matchTimes := 0
|
||||
@@ -393,14 +426,14 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||
} else {
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
return detectNotApplicable
|
||||
}
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
case "paths-ignore":
|
||||
if refName.IsTag() {
|
||||
@@ -410,14 +443,14 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||
} else {
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
return detectNotApplicable
|
||||
}
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
default:
|
||||
log.Warn("push event unsupported condition %q", cond)
|
||||
@@ -427,7 +460,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if hasBranchFilter && hasTagFilter {
|
||||
matchTimes++
|
||||
}
|
||||
return matchTimes == len(evt.Acts())
|
||||
if matchTimes == len(evt.Acts()) {
|
||||
return detectMatched
|
||||
}
|
||||
return detectFilteredOut
|
||||
}
|
||||
|
||||
func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
|
||||
@@ -478,7 +514,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
|
||||
return matchTimes == len(evt.Acts())
|
||||
}
|
||||
|
||||
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
|
||||
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) detectResult {
|
||||
acts := evt.Acts()
|
||||
activityTypeMatched := false
|
||||
matchTimes := 0
|
||||
@@ -525,7 +561,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha)
|
||||
if err != nil {
|
||||
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
|
||||
return false
|
||||
return detectNotApplicable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,33 +593,39 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
return detectNotApplicable
|
||||
}
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
case "paths-ignore":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
return detectNotApplicable
|
||||
}
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
default:
|
||||
log.Warn("pull request event unsupported condition %q", cond)
|
||||
}
|
||||
}
|
||||
return activityTypeMatched && matchTimes == len(evt.Acts())
|
||||
if !activityTypeMatched {
|
||||
return detectNotApplicable
|
||||
}
|
||||
if matchTimes != len(evt.Acts()) {
|
||||
return detectFilteredOut
|
||||
}
|
||||
return detectMatched
|
||||
}
|
||||
|
||||
func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {
|
||||
|
||||
@@ -101,49 +101,49 @@ func TestDetectMatched(t *testing.T) {
|
||||
triggedEvent webhook_module.HookEventType
|
||||
payload api.Payloader
|
||||
yamlOn string
|
||||
expected bool
|
||||
expected detectResult
|
||||
}{
|
||||
{
|
||||
desc: "HookEventCreate(create) matches GithubEventCreate(create)",
|
||||
triggedEvent: webhook_module.HookEventCreate,
|
||||
payload: nil,
|
||||
yamlOn: "on: create",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
|
||||
triggedEvent: webhook_module.HookEventIssues,
|
||||
payload: &api.IssuePayload{Action: api.HookIssueOpened},
|
||||
yamlOn: "on: issues",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
|
||||
triggedEvent: webhook_module.HookEventIssues,
|
||||
payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
|
||||
yamlOn: "on: issues",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
|
||||
triggedEvent: webhook_module.HookEventPullRequestSync,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
|
||||
yamlOn: "on: pull_request",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
|
||||
triggedEvent: webhook_module.HookEventPullRequest,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
||||
yamlOn: "on: pull_request",
|
||||
expected: false,
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
|
||||
triggedEvent: webhook_module.HookEventPullRequest,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
|
||||
yamlOn: "on: pull_request",
|
||||
expected: false,
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
|
||||
@@ -155,56 +155,56 @@ func TestDetectMatched(t *testing.T) {
|
||||
},
|
||||
},
|
||||
yamlOn: "on:\n pull_request:\n branches: [main]",
|
||||
expected: false,
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
|
||||
triggedEvent: webhook_module.HookEventPullRequest,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
|
||||
yamlOn: "on:\n pull_request:\n types: [labeled]",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
|
||||
triggedEvent: webhook_module.HookEventPullRequestReviewComment,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
|
||||
yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
|
||||
triggedEvent: webhook_module.HookEventPullRequestReviewRejected,
|
||||
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
|
||||
yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
|
||||
expected: false,
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
|
||||
triggedEvent: webhook_module.HookEventRelease,
|
||||
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
|
||||
yamlOn: "on:\n release:\n types: [published]",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
|
||||
triggedEvent: webhook_module.HookEventPackage,
|
||||
payload: &api.PackagePayload{Action: api.HookPackageCreated},
|
||||
yamlOn: "on:\n registry_package:\n types: [updated]",
|
||||
expected: false,
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
|
||||
triggedEvent: webhook_module.HookEventWiki,
|
||||
payload: nil,
|
||||
yamlOn: "on: gollum",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
|
||||
triggedEvent: webhook_module.HookEventSchedule,
|
||||
payload: nil,
|
||||
yamlOn: "on: schedule",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "push to tag matches workflow with paths condition (should skip paths check)",
|
||||
@@ -222,7 +222,19 @@ func TestDetectMatched(t *testing.T) {
|
||||
},
|
||||
commit: nil,
|
||||
yamlOn: "on:\n push:\n paths:\n - src/**",
|
||||
expected: true,
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "push branch filter excludes -> filtered out",
|
||||
triggedEvent: webhook_module.HookEventPush,
|
||||
payload: &api.PushPayload{
|
||||
Ref: "refs/heads/feature/x",
|
||||
Before: "0000000",
|
||||
Commits: []*api.PayloadCommit{{ID: "abc", Added: []string{"a.go"}, Message: "x"}},
|
||||
},
|
||||
commit: nil,
|
||||
yamlOn: "on:\n push:\n branches: [main]",
|
||||
expected: detectFilteredOut,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -231,7 +243,7 @@ func TestDetectMatched(t *testing.T) {
|
||||
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, evts, 1)
|
||||
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
|
||||
assert.Equal(t, tc.expected, detectWorkflowMatch(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,6 @@ func TestCommitsCount(t *testing.T) {
|
||||
assert.Equal(t, int64(3), commitsCount)
|
||||
}
|
||||
|
||||
func TestCommitsCountWithoutBase(t *testing.T) {
|
||||
bareRepo1 := &mockRepository{path: "repo1_bare"}
|
||||
|
||||
commitsCount, err := CommitsCount(t.Context(), bareRepo1,
|
||||
CommitsCountOptions{
|
||||
Not: "master",
|
||||
Revision: []string{"branch1"},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), commitsCount)
|
||||
}
|
||||
|
||||
func TestCommitsCountWithSinceUntil(t *testing.T) {
|
||||
bareRepo1 := &mockRepository{path: "repo1_bare"}
|
||||
revision := []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"}
|
||||
@@ -65,6 +52,19 @@ func TestCommitsCountWithSinceUntil(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitsCountWithoutBase(t *testing.T) {
|
||||
bareRepo1 := &mockRepository{path: "repo1_bare"}
|
||||
|
||||
commitsCount, err := CommitsCount(t.Context(), bareRepo1,
|
||||
CommitsCountOptions{
|
||||
Not: "master",
|
||||
Revision: []string{"branch1"},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), commitsCount)
|
||||
}
|
||||
|
||||
func TestGetLatestCommitTime(t *testing.T) {
|
||||
bareRepo1 := &mockRepository{path: "repo1_bare"}
|
||||
lct, err := GetLatestCommitTime(t.Context(), bareRepo1)
|
||||
|
||||
@@ -258,8 +258,27 @@ func GetAllCommits(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if commitsCountTotal == 0 {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
// when date filters are active, a zero count may just mean no
|
||||
// commits in the requested range — not that the path is invalid
|
||||
if since == "" && until == "" {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
// verify the path actually exists in the revision history
|
||||
totalWithoutDate, err := gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
|
||||
gitrepo.CommitsCountOptions{
|
||||
Not: not,
|
||||
Revision: []string{sha},
|
||||
RelPath: []string{path},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if totalWithoutDate == 0 {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
|
||||
@@ -147,6 +147,9 @@ func MustInitSessioner() func(next http.Handler) http.Handler {
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
Domain: setting.SessionConfig.Domain,
|
||||
|
||||
// in the future, if websocket is used, the websocket handler should manage its own session sync (release)
|
||||
IgnoreReleaseForWebSocket: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("common.Sessioner failed: %v", err)
|
||||
|
||||
@@ -31,9 +31,7 @@ import (
|
||||
type preReceiveContext struct {
|
||||
*gitea_context.PrivateContext
|
||||
|
||||
// loadedPusher indicates that where the following information are loaded
|
||||
loadedPusher bool
|
||||
user *user_model.User // it's the org user if a DeployKey is used
|
||||
user *user_model.User // the "pusher", it's the org user if a DeployKey is used
|
||||
userPerm access_model.Permission
|
||||
deployKeyAccessMode perm_model.AccessMode
|
||||
|
||||
@@ -53,10 +51,7 @@ type preReceiveContext struct {
|
||||
|
||||
func (ctx *preReceiveContext) canWriteCodeUnit() bool {
|
||||
if ctx.canWriteCodeUnitCached == nil {
|
||||
var canWrite bool
|
||||
if ctx.loadPusherAndPermission() {
|
||||
canWrite = ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
|
||||
}
|
||||
canWrite := ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
|
||||
ctx.canWriteCodeUnitCached = &canWrite
|
||||
}
|
||||
return *ctx.canWriteCodeUnitCached
|
||||
@@ -91,9 +86,6 @@ func (ctx *preReceiveContext) assertCanWriteRef(refFullName git.RefName) bool {
|
||||
// CanCreatePullRequest returns true if pusher can create pull requests
|
||||
func (ctx *preReceiveContext) CanCreatePullRequest() bool {
|
||||
if !ctx.checkedCanCreatePullRequest {
|
||||
if !ctx.loadPusherAndPermission() {
|
||||
return false
|
||||
}
|
||||
ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
|
||||
ctx.checkedCanCreatePullRequest = true
|
||||
}
|
||||
@@ -124,6 +116,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
if !ourCtx.loadPusherAndPermission() {
|
||||
return // if error occurs, loadPusherAndPermission had written the error response
|
||||
}
|
||||
|
||||
// Iterate across the provided old commit IDs
|
||||
for i := range opts.OldCommitIDs {
|
||||
oldCommitID := opts.OldCommitIDs[i]
|
||||
@@ -281,18 +277,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
|
||||
}
|
||||
} else {
|
||||
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if isForcePush {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user)
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.user)
|
||||
} else {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,12 +342,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
return
|
||||
}
|
||||
|
||||
// although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below
|
||||
if !ctx.loadPusherAndPermission() {
|
||||
// if error occurs, loadPusherAndPermission had written the error response
|
||||
return
|
||||
}
|
||||
|
||||
// Now check if the user is allowed to merge PRs for this repository
|
||||
// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
|
||||
allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user)
|
||||
@@ -499,10 +481,6 @@ func generateGitEnv(opts *private.HookOptions) (env []string) {
|
||||
|
||||
// loadPusherAndPermission returns false if an error occurs, and it writes the error response
|
||||
func (ctx *preReceiveContext) loadPusherAndPermission() bool {
|
||||
if ctx.loadedPusher {
|
||||
return true
|
||||
}
|
||||
|
||||
if ctx.opts.UserID == user_model.ActionsUserID {
|
||||
taskID := ctx.opts.ActionsTaskID
|
||||
ctx.user = user_model.NewActionsUserWithTaskID(taskID)
|
||||
@@ -555,7 +533,5 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
|
||||
}
|
||||
ctx.deployKeyAccessMode = deployKey.Mode
|
||||
}
|
||||
|
||||
ctx.loadedPusher = true
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ func TestPreReceiveCanWriteCodePerBranch(t *testing.T) {
|
||||
mockCtx, _ := contexttest.MockPrivateContext(t, "/")
|
||||
ctx := &preReceiveContext{
|
||||
PrivateContext: mockCtx,
|
||||
loadedPusher: true,
|
||||
user: maintainer,
|
||||
userPerm: headPerm,
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
@@ -357,7 +357,7 @@ func SignInPost(ctx *context.Context) {
|
||||
// User will need to use WebAuthn, save data
|
||||
updates["totpEnrolled"] = u.ID
|
||||
}
|
||||
if err := updateSession(ctx, nil, updates); err != nil {
|
||||
if err := regenerateSession(ctx, nil, updates); err != nil {
|
||||
ctx.ServerError("UserSignIn: Unable to update session", err)
|
||||
return
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, []string{
|
||||
if err := regenerateSession(ctx, []string{
|
||||
// Delete the openid, 2fa and link_account data
|
||||
"openid_verified_uri",
|
||||
"openid_signin_remember",
|
||||
@@ -884,7 +884,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
||||
|
||||
log.Trace("User activated: %s", user.Name)
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
"uid": user.ID,
|
||||
"uname": user.Name,
|
||||
}); err != nil {
|
||||
@@ -936,7 +936,7 @@ func ActivateEmail(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
||||
}
|
||||
|
||||
func updateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
|
||||
func regenerateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
|
||||
if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
|
||||
return fmt.Errorf("regenerate session: %w", err)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,12 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := Oauth2SetLinkAccountData(ctx, *linkAccountData); err != nil {
|
||||
ctx.ServerError("Oauth2SetLinkAccountData", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
||||
"twofaUid": u.ID,
|
||||
"twofaRemember": remember,
|
||||
|
||||
@@ -285,9 +285,7 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
|
||||
}
|
||||
|
||||
func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error {
|
||||
return updateSession(ctx, nil, map[string]any{
|
||||
"linkAccountData": linkAccountData,
|
||||
})
|
||||
return ctx.Session.Set("linkAccountData", linkAccountData)
|
||||
}
|
||||
|
||||
func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.User) {
|
||||
@@ -409,7 +407,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
@@ -434,7 +432,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
}
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
||||
"twofaUid": u.ID,
|
||||
"twofaRemember": false,
|
||||
|
||||
@@ -213,7 +213,7 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
if u != nil {
|
||||
nickname = u.LowerName
|
||||
}
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
"openid_verified_uri": id,
|
||||
"openid_determined_email": email,
|
||||
"openid_determined_username": nickname,
|
||||
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
actions_module "gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/commitstatus"
|
||||
"gitea.dev/modules/log"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
commitstatus_service "gitea.dev/services/repository/commitstatus"
|
||||
@@ -147,21 +149,78 @@ func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event,
|
||||
// scopedPrefix is computed once per run by the caller. The settings page derives the same string to preview expected checks.
|
||||
ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, job.Name, event)
|
||||
}
|
||||
targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID)
|
||||
return createWorkflowCommitStatus(ctx, repo, commitID, ctxName, run.WorkflowID, toCommitStatus(job.Status), targetURL, toCommitStatusDescription(job))
|
||||
}
|
||||
|
||||
// CreateSkippedCommitStatusForFilteredWorkflow posts a skipped commit status for each job of a
|
||||
// workflow that matched the triggering event but was excluded by a branch/paths filter.
|
||||
// This lets a required status check tied to that context be satisfied without the workflow running.
|
||||
// No ActionRun is created, so the status has no target URL (there is no run/job to link to).
|
||||
// A non-empty scopedPrefix prefixes each context with its source repo, matching scoped runs.
|
||||
func CreateSkippedCommitStatusForFilteredWorkflow(ctx context.Context, repo *repo_model.Repository, event webhook_module.HookEventType, triggerEvent, workflowID string, content []byte, payload api.Payloader, scopedPrefix string) error {
|
||||
// Derive the status event name and target commit from the payload.
|
||||
// TODO: this mirrors getCommitStatusEventNameAndCommitID, which derives the same from a persisted run. Should merge the logic if possible.
|
||||
var statusEvent, commitID string
|
||||
switch event {
|
||||
case webhook_module.HookEventPush:
|
||||
if p, ok := payload.(*api.PushPayload); ok && p.HeadCommit != nil {
|
||||
statusEvent, commitID = "push", p.HeadCommit.ID
|
||||
}
|
||||
case webhook_module.HookEventPullRequest,
|
||||
webhook_module.HookEventPullRequestSync,
|
||||
webhook_module.HookEventPullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestReviewRequest,
|
||||
webhook_module.HookEventPullRequestMilestone:
|
||||
if p, ok := payload.(*api.PullRequestPayload); ok && p.PullRequest != nil && p.PullRequest.Head != nil {
|
||||
statusEvent, commitID = "pull_request", p.PullRequest.Head.Sha
|
||||
if triggerEvent == actions_module.GithubEventPullRequestTarget {
|
||||
statusEvent = "pull_request_target"
|
||||
}
|
||||
}
|
||||
}
|
||||
if statusEvent == "" || commitID == "" {
|
||||
return nil // unsupported event or missing commit id, nothing to post
|
||||
}
|
||||
|
||||
workflows, err := jobparser.Parse(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jobparser.Parse: %w", err)
|
||||
}
|
||||
|
||||
displayName := actions_module.WorkflowDisplayName(workflowID, content)
|
||||
for _, sw := range workflows {
|
||||
_, job := sw.Job()
|
||||
if job == nil {
|
||||
continue
|
||||
}
|
||||
jobName := util.EllipsisDisplayString(job.Name, 255) // run creation truncates job names the same way
|
||||
ctxName := actions_module.WorkflowStatusContextName(displayName, jobName, statusEvent)
|
||||
if scopedPrefix != "" {
|
||||
ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, jobName, statusEvent)
|
||||
}
|
||||
// "Skipped" mirrors toCommitStatusDescription for StatusSkipped.
|
||||
if err := createWorkflowCommitStatus(ctx, repo, commitID, ctxName, workflowID, commitstatus.CommitStatusSkipped, "", "Skipped"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createWorkflowCommitStatus posts the commit status for one workflow-job context.
|
||||
func createWorkflowCommitStatus(ctx context.Context, repo *repo_model.Repository, commitID, ctxName, workflowID string, state commitstatus.CommitStatusState, targetURL, description string) error {
|
||||
// Mix the workflow file path into the hash so two workflow files that
|
||||
// share the same `name:` and job name produce distinct commit statuses
|
||||
// even though they render identically — matching GitHub's behavior
|
||||
// (issue #35699).
|
||||
ctxHash := git_model.HashCommitStatusContext(ctxName + "\x00" + run.WorkflowID)
|
||||
ctxHash := git_model.HashCommitStatusContext(ctxName + "\x00" + workflowID)
|
||||
// Pre-fix rows were hashed from Context alone. If a pre-existing row with
|
||||
// the legacy hash is still the "latest" for this SHA, reuse that hash so
|
||||
// the new row supersedes it; otherwise the old pending status would stay
|
||||
// stuck forever (it lives in its own dedupe group). Only relevant for
|
||||
// in-flight workflows at upgrade time.
|
||||
legacyHash := git_model.HashCommitStatusContext(ctxName)
|
||||
state := toCommitStatus(job.Status)
|
||||
targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID)
|
||||
description := toCommitStatusDescription(job)
|
||||
|
||||
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
|
||||
if err != nil {
|
||||
|
||||
@@ -183,8 +183,9 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||
}
|
||||
|
||||
var detectedWorkflows []*actions_module.DetectedWorkflow
|
||||
var filteredWorkflows []*actions_module.DetectedWorkflow
|
||||
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
|
||||
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
|
||||
workflows, schedules, filtered, err := actions_module.DetectWorkflows(gitRepo, commit,
|
||||
input.Event,
|
||||
input.Payload,
|
||||
shouldDetectSchedules,
|
||||
@@ -212,6 +213,17 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, wf := range filtered {
|
||||
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
||||
log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName)
|
||||
continue
|
||||
}
|
||||
|
||||
if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget {
|
||||
filteredWorkflows = append(filteredWorkflows, wf)
|
||||
}
|
||||
}
|
||||
|
||||
if input.PullRequest != nil {
|
||||
// detect pull_request_target workflows
|
||||
baseRef := git.BranchPrefix + input.PullRequest.BaseBranch
|
||||
@@ -219,7 +231,7 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
||||
}
|
||||
baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
|
||||
baseWorkflows, _, baseFiltered, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DetectWorkflows: %w", err)
|
||||
}
|
||||
@@ -227,11 +239,24 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||
log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RelativePath(), baseCommit.ID)
|
||||
} else {
|
||||
for _, wf := range baseWorkflows {
|
||||
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
||||
log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName)
|
||||
continue
|
||||
}
|
||||
if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
|
||||
detectedWorkflows = append(detectedWorkflows, wf)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, wf := range baseFiltered {
|
||||
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
||||
log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName)
|
||||
continue
|
||||
}
|
||||
if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget {
|
||||
filteredWorkflows = append(filteredWorkflows, wf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldDetectSchedules {
|
||||
@@ -244,6 +269,8 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||
return err
|
||||
}
|
||||
|
||||
handleFilteredWorkflows(ctx, input, filteredWorkflows)
|
||||
|
||||
return detectAndHandleScopedWorkflows(ctx, input, ref, gitRepo, commit)
|
||||
}
|
||||
|
||||
@@ -369,6 +396,16 @@ func buildApproveAndInsertRun(
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleFilteredWorkflows posts a skipped commit status for each workflow that matched the event but was excluded by a branch/paths filter.
|
||||
func handleFilteredWorkflows(ctx context.Context, input *notifyInput, filteredWorkflows []*actions_module.DetectedWorkflow) {
|
||||
for _, dwf := range filteredWorkflows {
|
||||
if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, ""); err != nil {
|
||||
log.Error("repo %s: skipped commit status for workflow %s: %v", input.Repo.RelativePath(), dwf.EntryName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput {
|
||||
return newNotifyInput(issue.Repo, issue.Poster, event)
|
||||
}
|
||||
@@ -640,7 +677,7 @@ func detectAndHandleScopedWorkflows(
|
||||
continue
|
||||
}
|
||||
|
||||
sourceCommitSHA, detected, err := detectScopedWorkflowsForSource(ctx, input, consumerGitRepo, consumerCommit, sourceRepo)
|
||||
sourceCommitSHA, detected, filtered, err := detectScopedWorkflowsForSource(ctx, input, consumerGitRepo, consumerCommit, sourceRepo)
|
||||
if err != nil {
|
||||
log.Error("scoped workflows: source %d for consumer %s: %v", sourceRepoID, input.Repo.RelativePath(), err)
|
||||
continue
|
||||
@@ -658,23 +695,40 @@ func detectAndHandleScopedWorkflows(
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// A filtered-out scoped workflow posts a skipped commit status.
|
||||
if len(filtered) > 0 {
|
||||
scopedPrefix := actions_model.ScopedStatusContextPrefix(ctx, sourceRepo.ID)
|
||||
for _, dwf := range filtered {
|
||||
if actions_model.ScopedWorkflowOptedOut(actionsConfig, sources, sourceRepo.ID, dwf.EntryName) {
|
||||
continue
|
||||
}
|
||||
if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, scopedPrefix); err != nil {
|
||||
log.Error("scoped workflows: skipped commit status for source %s workflow %s: %v", sourceRepo.RelativePath(), dwf.EntryName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectScopedWorkflowsForSource detects the scoped workflows from the source repo at its default branch
|
||||
// detectScopedWorkflowsForSource detects the scoped workflows from the source repo at its default branch.
|
||||
// detected are the workflows to run; filtered matched the event but were excluded by a branch/paths
|
||||
// filter and post a skipped commit status.
|
||||
func detectScopedWorkflowsForSource(
|
||||
ctx context.Context,
|
||||
input *notifyInput,
|
||||
consumerGitRepo *git.Repository,
|
||||
consumerCommit *git.Commit,
|
||||
sourceRepo *repo_model.Repository,
|
||||
) (sourceCommitSHA string, detected []*actions_module.DetectedWorkflow, err error) {
|
||||
) (sourceCommitSHA string, detected, filtered []*actions_module.DetectedWorkflow, err error) {
|
||||
// scoped workflow content is always taken from the source repo's default branch; the parse is cached per (source, default-branch SHA) and reused across consuming repos/events
|
||||
sourceCommitSHA, parsed, err := LoadParsedScopedWorkflows(ctx, sourceRepo)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
return sourceCommitSHA, actions_module.MatchScopedWorkflows(parsed, consumerGitRepo, consumerCommit, input.Event, input.Payload), nil
|
||||
detected, filtered = actions_module.MatchScopedWorkflows(parsed, consumerGitRepo, consumerCommit, input.Event, input.Payload)
|
||||
return sourceCommitSHA, detected, filtered, nil
|
||||
}
|
||||
|
||||
@@ -42,6 +42,38 @@ type ArchiveRequest struct {
|
||||
archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id"
|
||||
}
|
||||
|
||||
type archiveQueueItem struct {
|
||||
RepoID int64 `json:"RepoID"`
|
||||
Type repo_model.ArchiveType `json:"Type"`
|
||||
CommitID string `json:"CommitID"`
|
||||
Paths []string `json:"Paths,omitempty"`
|
||||
ArchiveRefShortName string `json:"ArchiveRefShortName,omitempty"`
|
||||
}
|
||||
|
||||
func (aReq *ArchiveRequest) toQueueItem() *archiveQueueItem {
|
||||
return &archiveQueueItem{
|
||||
RepoID: aReq.Repo.ID,
|
||||
Type: aReq.Type,
|
||||
CommitID: aReq.CommitID,
|
||||
Paths: aReq.Paths,
|
||||
ArchiveRefShortName: aReq.archiveRefShortName,
|
||||
}
|
||||
}
|
||||
|
||||
func (item *archiveQueueItem) toArchiveRequest(ctx context.Context) (*ArchiveRequest, error) {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, item.RepoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ArchiveRequest{
|
||||
Repo: repo,
|
||||
Type: item.Type,
|
||||
CommitID: item.CommitID,
|
||||
Paths: item.Paths,
|
||||
archiveRefShortName: item.ArchiveRefShortName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewRequest creates an archival request, based on the URI. The
|
||||
// resulting ArchiveRequest is suitable for being passed to Await()
|
||||
// if it's determined that the request still needs to be satisfied.
|
||||
@@ -227,13 +259,18 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
|
||||
return archiver, nil
|
||||
}
|
||||
|
||||
var archiverQueue *queue.WorkerPoolQueue[*ArchiveRequest]
|
||||
var archiverQueue *queue.WorkerPoolQueue[*archiveQueueItem]
|
||||
|
||||
// Init initializes archiver
|
||||
func Init(ctx context.Context) error {
|
||||
handler := func(items ...*ArchiveRequest) []*ArchiveRequest {
|
||||
for _, archiveReq := range items {
|
||||
log.Trace("ArchiverData Process: %#v", archiveReq)
|
||||
handler := func(items ...*archiveQueueItem) []*archiveQueueItem {
|
||||
for _, item := range items {
|
||||
log.Trace("ArchiverData Process: %#v", item)
|
||||
archiveReq, err := item.toArchiveRequest(ctx)
|
||||
if err != nil {
|
||||
log.Error("Archive repo %d: %v", item.RepoID, err)
|
||||
continue
|
||||
}
|
||||
if archiver, err := doArchive(ctx, archiveReq); err != nil {
|
||||
log.Error("Archive %v failed: %v", archiveReq, err)
|
||||
} else {
|
||||
@@ -254,14 +291,15 @@ func Init(ctx context.Context) error {
|
||||
|
||||
// StartArchive push the archive request to the queue
|
||||
func StartArchive(request *ArchiveRequest) error {
|
||||
has, err := archiverQueue.Has(request)
|
||||
item := request.toQueueItem()
|
||||
has, err := archiverQueue.Has(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if has {
|
||||
return nil
|
||||
}
|
||||
return archiverQueue.Push(request)
|
||||
return archiverQueue.Push(item)
|
||||
}
|
||||
|
||||
func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error {
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
@@ -21,6 +23,22 @@ func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
func TestArchiveQueueItemJSON(t *testing.T) {
|
||||
orig := &archiveQueueItem{
|
||||
RepoID: 7,
|
||||
Type: repo_model.ArchiveZip,
|
||||
CommitID: "abc123",
|
||||
Paths: []string{"agents"},
|
||||
ArchiveRefShortName: "main",
|
||||
}
|
||||
bs, err := json.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded archiveQueueItem
|
||||
require.NoError(t, json.Unmarshal(bs, &decoded))
|
||||
assert.Equal(t, *orig, decoded)
|
||||
}
|
||||
|
||||
func TestArchive_Basic(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -344,6 +344,59 @@ jobs:
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Filtered required scoped check passes as skipped and allows merge", func(t *testing.T) {
|
||||
// A required scoped workflow excluded by a paths filter posts a skipped (success) commit status,
|
||||
// so the required check is satisfied and the PR can merge.
|
||||
|
||||
const scopedFilteredPRWorkflow = `name: Scoped Filtered PR
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- src/**
|
||||
jobs:
|
||||
scoped-filtered-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo scoped-filtered
|
||||
`
|
||||
source := createTestRepo(t, "sw-filtered-source", false)
|
||||
createRepoWorkflowFile(t, user2, user2Token, source, ".gitea/scoped_workflows/pr.yaml", scopedFilteredPRWorkflow)
|
||||
registerUserScopedSource(t, source, "pr.yaml") // required
|
||||
|
||||
consumer := createTestRepo(t, "sw-filtered-consumer", false)
|
||||
// Protect the default branch (its own status check stays off, so only the required scoped check gates the merge).
|
||||
user2Session.MakeRequest(t, NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", consumer.OwnerName, consumer.Name), map[string]string{
|
||||
"rule_name": consumer.DefaultBranch,
|
||||
"enable_push": "true",
|
||||
"block_admin_merge_override": "true", // otherwise the repo owner bypasses the status check
|
||||
}), http.StatusSeeOther)
|
||||
|
||||
// Open a PR that changes a file NOT matching the workflow's `paths: [src/**]`, so it is filtered out.
|
||||
prFile := &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: consumer.DefaultBranch, NewBranchName: "filtered-pr", Message: "pr change",
|
||||
Author: api.Identity{Name: user2.Name, Email: user2.Email},
|
||||
Committer: api.Identity{Name: user2.Name, Email: user2.Email},
|
||||
Dates: api.CommitDateOptions{Author: time.Now(), Committer: time.Now()},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("pr change")),
|
||||
}
|
||||
createWorkflowFile(t, user2Token, consumer.OwnerName, consumer.Name, "docs.txt", prFile)
|
||||
apiCtx := NewAPITestContext(t, user2.Name, consumer.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
pr, err := doAPICreatePullRequest(apiCtx, consumer.OwnerName, consumer.Name, consumer.DefaultBranch, "filtered-pr")(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Filtered: no scoped run is created, but a skipped commit status is posted on the PR head.
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: consumer.ID, IsScopedRun: true}), "filtered scoped workflow creates no run")
|
||||
assertSkippedCommitStatusExists(t, consumer.ID, pr.Head.Sha, "pull_request")
|
||||
|
||||
// The skipped (success) status satisfies the required scoped check (prefixed with the source repo), so the merge is allowed.
|
||||
assert.NoError(t, queue.GetManager().FlushAll(t.Context(), 5*time.Second))
|
||||
mergeReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", consumer.OwnerName, consumer.Name, pr.Index),
|
||||
&forms.MergePullRequestForm{Do: string(repo_model.MergeStyleMerge), MergeMessageField: "merge"}).AddTokenAuth(user2Token)
|
||||
user2Session.MakeRequest(t, mergeReq, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Settings page required patterns", func(t *testing.T) {
|
||||
source := createTestRepo(t, "sw-settings-source", false)
|
||||
createRepoWorkflowFile(t, user2, user2Token, source, ".gitea/scoped_workflows/push.yaml", scopedPushWorkflow)
|
||||
|
||||
@@ -215,8 +215,9 @@ jobs:
|
||||
err = pull_service.NewPullRequest(t.Context(), prOpts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// the new pull request cannot trigger actions, so there is still only 1 record
|
||||
// the new pull request is filtered by paths, so no run is created; a skipped commit status is posted instead
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
|
||||
assertSkippedCommitStatusExists(t, baseRepo.ID, addFileToForkedResp.Commit.SHA, "pull_request_target")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -338,6 +339,9 @@ jobs:
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, addFileToBranchResp)
|
||||
// the push to test-skip-ci is filtered by branches, so no run is created; a skipped commit status is posted instead
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
||||
assertSkippedCommitStatusExists(t, repo.ID, addFileToBranchResp.Commit.SHA, "push")
|
||||
|
||||
resp := testPullCreate(t, session, "user2", "skip-ci", true, "master", "test-skip-ci", "[skip ci] test-skip-ci")
|
||||
|
||||
@@ -345,7 +349,7 @@ jobs:
|
||||
url := test.RedirectURL(resp)
|
||||
assert.Regexp(t, "^/user2/skip-ci/pulls/[0-9]*$", url)
|
||||
|
||||
// the pr title contains a configured skip-ci string, so there is still only 1 record
|
||||
// the pr title contains a configured skip-ci string, so no run and no skipped status are created
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
||||
})
|
||||
}
|
||||
@@ -1879,3 +1883,16 @@ jobs:
|
||||
runner.fetchNoTask(t)
|
||||
})
|
||||
}
|
||||
|
||||
// assertSkippedCommitStatusExists asserts that a filtered-out workflow posted a skipped commit status on sha
|
||||
func assertSkippedCommitStatusExists(t *testing.T, repoID int64, sha, eventSuffix string) {
|
||||
t.Helper()
|
||||
statuses, err := git_model.GetLatestCommitStatus(t.Context(), repoID, sha, db.ListOptionsAll)
|
||||
require.NoError(t, err)
|
||||
for _, s := range statuses {
|
||||
if s.State == commitstatus.CommitStatusSkipped && strings.Contains(s.Context, "("+eventSuffix+")") {
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.Failf(t, "missing skipped commit status", "no skipped commit status with event %q on %s (found %d statuses)", eventSuffix, sha, len(statuses))
|
||||
}
|
||||
|
||||
@@ -241,3 +241,27 @@ func TestGetFileHistoryNotOnMaster(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "1", resp.Header().Get("X-Total"))
|
||||
}
|
||||
|
||||
func TestGetFileHistoryEmptyDateRange(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
// Login as User2.
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
// readme.md exists in repo16 but no commits fall before 1970, so the date
|
||||
// filter yields an empty range: this must return 200 with an empty list,
|
||||
// not 404 (regression: a valid path with an empty date range was a 404).
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?path=readme.md&sha=good-sign&until=1970-01-01T00:00:00Z", user.Name).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
apiData := DecodeJSON(t, resp, []api.Commit{})
|
||||
assert.Empty(t, apiData)
|
||||
assert.Equal(t, "0", resp.Header().Get("X-Total"))
|
||||
|
||||
// a path that does not exist must still return 404 even with a date filter
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?path=does-not-exist.md&sha=good-sign&until=1970-01-01T00:00:00Z", user.Name).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/builder"
|
||||
@@ -489,3 +490,73 @@ func TestOAuth2GroupClaimsManualLinking(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOAuth2AutoLinkWithTwoFactor verifies that automatic account linking completes
|
||||
// after the user passes local 2FA when an OIDC identity matches an existing account.
|
||||
func TestOAuth2AutoLinkWithTwoFactor(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
||||
defer test.MockVariableValue(&setting.OAuth2Client.AccountLinking, setting.OAuth2AccountLinkingAuto)()
|
||||
defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameEmail)()
|
||||
|
||||
const (
|
||||
sourceName = "test-oauth-auto-link-2fa"
|
||||
sub = "oidc-auto-link-2fa-sub"
|
||||
email = "oidc-auto-link-2fa@example.com"
|
||||
userName = "oidc-auto-link-2fa"
|
||||
)
|
||||
|
||||
srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: sub, Email: email, Name: "OIDC Auto Link 2FA"})
|
||||
addOAuth2Source(t, sourceName, oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
ClientID: "test-client-id",
|
||||
ClientSecret: "test-client-secret",
|
||||
OpenIDConnectAutoDiscoveryURL: srv.URL + "/.well-known/openid-configuration",
|
||||
})
|
||||
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), sourceName)
|
||||
require.NoError(t, err)
|
||||
|
||||
localUser := &user_model.User{Name: userName, Email: email}
|
||||
require.NoError(t, user_model.CreateUser(t.Context(), localUser, &user_model.Meta{}))
|
||||
|
||||
otpKey, err := totp.Generate(totp.GenerateOpts{
|
||||
SecretSize: 40,
|
||||
Issuer: "gitea-test",
|
||||
AccountName: localUser.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tfa := &auth_model.TwoFactor{UID: localUser.ID}
|
||||
require.NoError(t, tfa.SetSecret(otpKey.Secret()))
|
||||
require.NoError(t, auth_model.NewTwoFactor(t.Context(), tfa))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.ExternalLoginUser{ExternalID: sub, LoginSourceID: authSource.ID}, unittest.OrderBy("external_id ASC"))
|
||||
|
||||
session := emptyTestSession(t)
|
||||
resp := session.MakeRequest(t, NewRequest(t, "GET", "/user/oauth2/"+sourceName), http.StatusTemporaryRedirect)
|
||||
|
||||
location := resp.Header().Get("Location")
|
||||
u, err := url.Parse(location)
|
||||
require.NoError(t, err)
|
||||
state := u.Query().Get("state")
|
||||
require.NotEmpty(t, state)
|
||||
|
||||
callbackURL := fmt.Sprintf("/user/oauth2/%s/callback?code=test-code&state=%s", sourceName, url.QueryEscape(state))
|
||||
resp = session.MakeRequest(t, NewRequest(t, "GET", callbackURL), http.StatusSeeOther)
|
||||
assert.Contains(t, resp.Header().Get("Location"), "/user/two_factor")
|
||||
|
||||
session.MakeRequest(t, NewRequest(t, "GET", "/user/two_factor"), http.StatusOK)
|
||||
|
||||
passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/two_factor", map[string]string{
|
||||
"passcode": passcode,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
externalLink := unittest.AssertExistsAndLoadBean(t, &user_model.ExternalLoginUser{ExternalID: sub, LoginSourceID: authSource.ID}, unittest.OrderBy("external_id ASC"))
|
||||
assert.Equal(t, localUser.ID, externalLink.UserID)
|
||||
|
||||
session.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue';
|
||||
import {SvgIcon} from '../svg.ts';
|
||||
import ActionStatusIcon from './ActionStatusIcon.vue';
|
||||
import {addDelegatedEventListener, createElementFromAttrs, toggleElem} from '../utils/dom.ts';
|
||||
import {addDelegatedEventListener, createElementFromAttrs} from '../utils/dom.ts';
|
||||
import {formatDatetime, formatDatetimeISO} from '../utils/time.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {copyToClipboardWithFeedback} from '../modules/clipboard.ts';
|
||||
@@ -247,9 +247,6 @@ function createLogLine(stepIndex: number, startTime: number, line: LogLine, cmd:
|
||||
`${seconds}s`, // for "Show seconds"
|
||||
);
|
||||
|
||||
toggleElem(logTimeStamp, timeVisible.value['log-time-stamp']);
|
||||
toggleElem(logTimeSeconds, timeVisible.value['log-time-seconds']);
|
||||
|
||||
const lineClass = cmd?.name ? `job-log-line log-line-${cmd.name}` : 'job-log-line';
|
||||
return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: lineClass},
|
||||
lineNum, logTimeStamp, logMsg, logTimeSeconds,
|
||||
@@ -391,9 +388,6 @@ function elStepsContainer(): HTMLElement {
|
||||
|
||||
function toggleTimeDisplay(type: 'seconds' | 'stamp') {
|
||||
timeVisible.value[`log-time-${type}`] = !timeVisible.value[`log-time-${type}`];
|
||||
for (const el of elStepsContainer().querySelectorAll(`.log-time-${type}`)) {
|
||||
toggleElem(el, timeVisible.value[`log-time-${type}`]);
|
||||
}
|
||||
saveLocaleStorageOptions();
|
||||
}
|
||||
|
||||
@@ -473,7 +467,15 @@ async function hashChangeListener() {
|
||||
</div>
|
||||
</div>
|
||||
<!-- always create the node because we have our own event listeners on it, don't use "v-if" -->
|
||||
<div class="job-step-container" ref="stepsContainer" v-show="!isCallerJob && currentJob.steps.length">
|
||||
<div
|
||||
class="job-step-container"
|
||||
ref="stepsContainer"
|
||||
v-show="!isCallerJob && currentJob.steps.length"
|
||||
:class="{
|
||||
'log-line-show-timestamps': timeVisible['log-time-stamp'],
|
||||
'log-line-show-seconds': timeVisible['log-time-seconds']
|
||||
}"
|
||||
>
|
||||
<div class="job-step-section" v-for="(jobStep, stepIdx) in currentJob.steps" :key="stepIdx">
|
||||
<div
|
||||
class="job-step-summary"
|
||||
@@ -681,8 +683,22 @@ async function hashChangeListener() {
|
||||
scroll-margin-top: 95px;
|
||||
}
|
||||
|
||||
.job-log-line .log-time-stamp,
|
||||
.job-log-line .log-time-seconds {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-line-show-timestamps .job-log-line .log-time-stamp {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.log-line-show-seconds .job-log-line .log-time-seconds {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */
|
||||
.job-log-line .line-num, .log-time-seconds {
|
||||
.job-log-line .line-num,
|
||||
.job-log-line .log-time-seconds {
|
||||
width: 48px;
|
||||
color: var(--color-text-light-3);
|
||||
text-align: right;
|
||||
@@ -699,16 +715,16 @@ async function hashChangeListener() {
|
||||
}
|
||||
|
||||
.job-log-line .log-time,
|
||||
.log-time-stamp {
|
||||
.job-log-line .log-time-stamp {
|
||||
color: var(--color-text-light-3);
|
||||
margin-left: 10px;
|
||||
margin-left: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.job-step-logs .job-log-line .log-msg {
|
||||
flex: 1;
|
||||
white-space: break-spaces;
|
||||
margin-left: 10px;
|
||||
margin-left: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -775,30 +791,28 @@ async function hashChangeListener() {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.job-log-group .job-log-list .job-log-line .log-msg {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.job-log-group-summary {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: list-item;
|
||||
list-style: disclosure-closed inside;
|
||||
padding-left: 58px; /* line-num gutter (48px) + log-msg margin (10px), so the marker sits in the content column */
|
||||
list-style: none; /* hide the standard disclosure marker (Chrome, Edge, Firefox) */
|
||||
}
|
||||
|
||||
.job-log-group[open] > .job-log-group-summary {
|
||||
list-style-type: disclosure-open;
|
||||
.job-log-group-summary::-webkit-details-marker { /* hide the disclosure marker on Safari */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.job-log-group-summary > .job-log-line {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1; /* sit behind the disclosure marker */
|
||||
overflow: hidden;
|
||||
.log-line-group .log-msg::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: -2.5px;
|
||||
margin-right: 8px;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 6px solid var(--color-text-light-3);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.job-log-group-summary > .job-log-line .log-msg {
|
||||
margin-left: 21px;
|
||||
.job-log-group[open] .log-line-group .log-msg::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user