Compare commits

...
Author SHA1 Message Date
GiteabotandGitHub 6ae42ca9c4 fix(deps): update module gitlab.com/gitlab-org/api/client-go/v2 to v2.42.0 (#38266)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[gitlab.com/gitlab-org/api/client-go/v2](https://gitlab.com/gitlab-org/api/client-go)
| `v2.40.1` → `v2.42.0` |
![age](https://developer.mend.io/api/mc/badges/age/go/gitlab.com%2fgitlab-org%2fapi%2fclient-go%2fv2/v2.42.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/gitlab.com%2fgitlab-org%2fapi%2fclient-go%2fv2/v2.40.1/v2.42.0?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/37531) for more information.

---

### Release Notes

<details>
<summary>gitlab-org/api/client-go
(gitlab.com/gitlab-org/api/client-go/v2)</summary>

###
[`v2.42.0`](https://gitlab.com/gitlab-org/api/client-go/tags/v2.42.0)

[Compare
Source](https://gitlab.com/gitlab-org/api/client-go/compare/v2.41.0...v2.42.0)

#### 2.42.0

##### 🚀 Features

- Add missing fields to project level jira integration
([!2925](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2925))
by [Heidi Berry](https://gitlab.com/heidi.berry)

###
[2.42.0](https://gitlab.com/gitlab-org/api/client-go/compare/v2.41.0...v2.42.0)
(2026-06-24)

###
[`v2.41.0`](https://gitlab.com/gitlab-org/api/client-go/tags/v2.41.0)

[Compare
Source](https://gitlab.com/gitlab-org/api/client-go/compare/v2.40.1...v2.41.0)

#### 2.41.0

##### 🚀 Features

- Add missing attributes and endpoints to group
([!2905](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2905))
by [Jimmy Spagnola](https://gitlab.com/jspagnola)

##### 🔄 Other Changes

- chore(deps): update docker docker tag to v29.5.3
([!2924](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2924))
by [GitLab Dependency
Bot](https://gitlab.com/gitlab-dependency-update-bot)

###
[2.41.0](https://gitlab.com/gitlab-org/api/client-go/compare/v2.40.1...v2.41.0)
(2026-06-23)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-06-29 18:09:25 +02:00
5e5f5f3116 fix(deps): update go dependencies (#38194)
Update go deps and fix discovered issues

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-29 15:10:47 +00:00
wxiaoguangandGitHub 4ce63a1d57 chore: various UI problems (#38263)
1. fix dirty "list" styles for "githooks" and "webhooks"
2. fix git hook edit page layout
3. fix codemirror editor styles
4. fix incorrect "ui attached header" width
2026-06-29 13:06:25 +00:00
07b18467c0 fix: update npm dependencies, fix misc issues (#38257)
Update all npm dependencies and fix discovered issues.

Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-29 12:59:14 +02:00
GiteabotandGitHub e68ee61879 chore(deps): update action dependencies (#38258)
This PR contains the following updates:

| Package | Type | Update | Change | Pending |
|---|---|---|---|---|
| [actions/setup-go](https://redirect.github.com/actions/setup-go) |
action | minor | `v6.4.0` → `v6.5.0` | |
| [go-gitea/giteabot](https://redirect.github.com/go-gitea/giteabot) |
action | patch | `v1.0.3` → `v1.0.4` | |
| redis | service | digest | `a505f8b` → `c904002` |  |
|
[renovatebot/github-action](https://redirect.github.com/renovatebot/github-action)
| action | patch | `v46.1.15` → `v46.1.16` | `v46.1.17` |

---

### Release Notes

<details>
<summary>actions/setup-go (actions/setup-go)</summary>

###
[`v6.5.0`](https://redirect.github.com/actions/setup-go/releases/tag/v6.5.0)

[Compare
Source](https://redirect.github.com/actions/setup-go/compare/v6.4.0...v6.5.0)

##### What's Changed

##### Dependency update

- Upgrade actions dependencies by
[@&#8203;priyagupta108](https://redirect.github.com/priyagupta108) with
[@&#8203;Copilot](https://redirect.github.com/Copilot) in
[#&#8203;744](https://redirect.github.com/actions/setup-go/pull/744)
- Upgrade [@&#8203;types/node](https://redirect.github.com/types/node)
and typescript-eslint dependencies to resolve npm audit findings by
[@&#8203;HarithaVattikuti](https://redirect.github.com/HarithaVattikuti)
in [#&#8203;755](https://redirect.github.com/actions/setup-go/pull/755)
- Upgrade
[@&#8203;actions/cache](https://redirect.github.com/actions/cache) to
5.1.0, log cache write denied by
[@&#8203;jasongin](https://redirect.github.com/jasongin) in
[#&#8203;758](https://redirect.github.com/actions/setup-go/pull/758)
- Upgrade version to 6.5.0 in package.json and package-lock.json by
[@&#8203;HarithaVattikuti](https://redirect.github.com/HarithaVattikuti)
in [#&#8203;762](https://redirect.github.com/actions/setup-go/pull/762)

##### New Contributors

- [@&#8203;priyagupta108](https://redirect.github.com/priyagupta108)
with [@&#8203;Copilot](https://redirect.github.com/Copilot) made their
first contribution in
[#&#8203;744](https://redirect.github.com/actions/setup-go/pull/744)
- [@&#8203;jasongin](https://redirect.github.com/jasongin) made their
first contribution in
[#&#8203;758](https://redirect.github.com/actions/setup-go/pull/758)

**Full Changelog**:
<https://github.com/actions/setup-go/compare/v6...v6.5.0>

</details>

<details>
<summary>go-gitea/giteabot (go-gitea/giteabot)</summary>

###
[`v1.0.4`](https://redirect.github.com/go-gitea/giteabot/releases/tag/v1.0.4)

[Compare
Source](https://redirect.github.com/go-gitea/giteabot/compare/v1.0.3...v1.0.4)

##### What's Changed

- Keep lgtm status up to date on fork and backport PRs by
[@&#8203;silverwind](https://redirect.github.com/silverwind) in
[#&#8203;9](https://redirect.github.com/go-gitea/giteabot/pull/9)

**Full Changelog**:
<https://github.com/go-gitea/giteabot/compare/v1.0.3...v1.0.4>

</details>

<details>
<summary>renovatebot/github-action (renovatebot/github-action)</summary>

###
[`v46.1.16`](https://redirect.github.com/renovatebot/github-action/releases/tag/v46.1.16)

[Compare
Source](https://redirect.github.com/renovatebot/github-action/compare/v46.1.15...v46.1.16)

##### Documentation

- update references to renovatebot/github-action to v46.1.15
([0013591](https://redirect.github.com/renovatebot/github-action/commit/00135917fdbb8f382071ce3f27c8432ad0f75c2a))

##### Miscellaneous Chores

- **deps:** update dependency
[@&#8203;types/node](https://redirect.github.com/types/node) to v24.13.0
([358d0a4](https://redirect.github.com/renovatebot/github-action/commit/358d0a480c37ecd2b23ab66dcd5170452917642c))
- **deps:** update dependency
[@&#8203;types/node](https://redirect.github.com/types/node) to v24.13.1
([783fe90](https://redirect.github.com/renovatebot/github-action/commit/783fe90b5a88b8a02d5d6c9bb65c01e36b110545))
- **deps:** update dependency
[@&#8203;types/node](https://redirect.github.com/types/node) to v24.13.2
([74b1acf](https://redirect.github.com/renovatebot/github-action/commit/74b1acf27101775dfaf2793c89c214898cd33520))
- **deps:** update dependency
[@&#8203;types/node](https://redirect.github.com/types/node) to v24.13.2
([#&#8203;1049](https://redirect.github.com/renovatebot/github-action/issues/1049))
([23dcba0](https://redirect.github.com/renovatebot/github-action/commit/23dcba0a91738a6e394d2e991c307937406b6587))
- **deps:** update dependency esbuild to v0.28.1 \[security]
([#&#8203;1041](https://redirect.github.com/renovatebot/github-action/issues/1041))
([54012bd](https://redirect.github.com/renovatebot/github-action/commit/54012bd29e2a1a395bbffede3e46a836aebc3e47))
- **deps:** update dependency lint-staged to v17
([#&#8203;1051](https://redirect.github.com/renovatebot/github-action/issues/1051))
([6a9f6dc](https://redirect.github.com/renovatebot/github-action/commit/6a9f6dc5beb03977800c57cd92b5ca15bfc67439))
- **deps:** update dependency npm-run-all2 to v9
([#&#8203;1052](https://redirect.github.com/renovatebot/github-action/issues/1052))
([8757a4e](https://redirect.github.com/renovatebot/github-action/commit/8757a4e574f6d5c0ba3fd6a6706c420c5c96e1c9))
- **deps:** update dependency npm-run-all2 to v9.0.2
([2c2c4e5](https://redirect.github.com/renovatebot/github-action/commit/2c2c4e5c89b4c5bfdd31288b8f30d8acf0a3c4a3))
- **deps:** update linters to v8.60.1
([d40e1b7](https://redirect.github.com/renovatebot/github-action/commit/d40e1b7d86f5ce052321dee875f215d0c0db3443))
- **deps:** update linters to v8.61.0
([#&#8203;1043](https://redirect.github.com/renovatebot/github-action/issues/1043))
([1e06192](https://redirect.github.com/renovatebot/github-action/commit/1e061929c42cf0828b24480be6f542ba6ccf88a3))
- **deps:** update node.js to v24.17.0
([#&#8203;1050](https://redirect.github.com/renovatebot/github-action/issues/1050))
([2cf33bc](https://redirect.github.com/renovatebot/github-action/commit/2cf33bc523576895fa380cf8af2e05336c943486))
- **deps:** update pnpm to v10.34.2
([#&#8203;1048](https://redirect.github.com/renovatebot/github-action/issues/1048))
([63ebb9d](https://redirect.github.com/renovatebot/github-action/commit/63ebb9d84b858604f205265f37e642256bf5f295))
- **deps:** update pnpm to v10.34.3
([#&#8203;1054](https://redirect.github.com/renovatebot/github-action/issues/1054))
([cd3436d](https://redirect.github.com/renovatebot/github-action/commit/cd3436d028bc86910e9cbf348b1c975a58d90135))
- **deps:** update pnpm/action-setup action to v6
([#&#8203;1053](https://redirect.github.com/renovatebot/github-action/issues/1053))
([77e5805](https://redirect.github.com/renovatebot/github-action/commit/77e58054f1f031a8b4f80dcbbd00f65e45643d09))
- **deps:** update prettier packages to v3.8.4
([#&#8203;1045](https://redirect.github.com/renovatebot/github-action/issues/1045))
([d688888](https://redirect.github.com/renovatebot/github-action/commit/d688888385cd5bd04a70a7889c8112b7d960512b))
- **deps:** update semantic-release monorepo to v25.0.4
([#&#8203;1046](https://redirect.github.com/renovatebot/github-action/issues/1046))
([d2dacc8](https://redirect.github.com/renovatebot/github-action/commit/d2dacc89959d7963d77f06ffadf0abcf1265fdc7))
- **deps:** update semantic-release monorepo to v25.0.5
([#&#8203;1047](https://redirect.github.com/renovatebot/github-action/issues/1047))
([d91f80c](https://redirect.github.com/renovatebot/github-action/commit/d91f80c864d00b0ddc134c949cf464395ef1afaa))

##### Build System

- **deps:** lock file maintenance
([26f827f](https://redirect.github.com/renovatebot/github-action/commit/26f827fdc5121b9d4fbe1cf8dcb76d6efe58b78b))

##### Continuous Integration

- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.214.6
([f3fd163](https://redirect.github.com/renovatebot/github-action/commit/f3fd1634318528e2c0bf9402ad11ced1e2583cc4))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.216.1
([8cf15ee](https://redirect.github.com/renovatebot/github-action/commit/8cf15ee083f561ff061f991a7ae7daeef11b0243))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.216.2
([29c9f31](https://redirect.github.com/renovatebot/github-action/commit/29c9f31e4a3ad4f169bbfaf78a3fcbba9569e275))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.216.4
([400f75c](https://redirect.github.com/renovatebot/github-action/commit/400f75cbdb0a8ec5364c9f3f22932c79c91ec56f))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.217.0
([2aea29e](https://redirect.github.com/renovatebot/github-action/commit/2aea29ebc0bebd74676a36c246d9dffc8635aa2a))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.217.1
([268f254](https://redirect.github.com/renovatebot/github-action/commit/268f25430113d2f91e6ab0244171726aff45791d))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.218.0
([ebcc800](https://redirect.github.com/renovatebot/github-action/commit/ebcc800ccdcbca03d3939d07cd5170dcbe3fddf1))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.219.0
([a61593e](https://redirect.github.com/renovatebot/github-action/commit/a61593e15cdcaa203cdf199b01785cbde1b3393b))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.220.0
([#&#8203;1037](https://redirect.github.com/renovatebot/github-action/issues/1037))
([0d198c1](https://redirect.github.com/renovatebot/github-action/commit/0d198c1f3cedd5d962195b498b53d23de67b3c7f))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.222.0
([46f2bd6](https://redirect.github.com/renovatebot/github-action/commit/46f2bd6ed2f60e6e68085b7ccb0d408e498ad646))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.222.1
([90deabf](https://redirect.github.com/renovatebot/github-action/commit/90deabf8530cd43db786bef00bb6a5bbbf66e372))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.224.0
([22d7b5c](https://redirect.github.com/renovatebot/github-action/commit/22d7b5c57aa60ca4659f5f24d368812e789f1141))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.224.1
([39a2ba1](https://redirect.github.com/renovatebot/github-action/commit/39a2ba1236cbe85d17c776505eda386398144ed2))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.225.0
([c2f08ab](https://redirect.github.com/renovatebot/github-action/commit/c2f08ab1a1834784134b79fa3a30b83132ff5c51))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.226.1
([75a5340](https://redirect.github.com/renovatebot/github-action/commit/75a5340ae6e848edb49abbeb7343434c84fd144c))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.227.0
([da1079a](https://redirect.github.com/renovatebot/github-action/commit/da1079ac41ddf8ac9d473f3110b9bc2d20e8c823))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.227.1
([26a0ce7](https://redirect.github.com/renovatebot/github-action/commit/26a0ce7c73154e11c6677da35bd23ddf1c77a704))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.228.0
([9dd450f](https://redirect.github.com/renovatebot/github-action/commit/9dd450fe094737324c6740847d34d21d0a12670e))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.228.1
([066bf0a](https://redirect.github.com/renovatebot/github-action/commit/066bf0aa9449c5bd4e2315df33f2986b10299ee7))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.229.0
([edd7e4f](https://redirect.github.com/renovatebot/github-action/commit/edd7e4f83edd2652f40ed3d13236296a75cb9d9c))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.229.1
([64e44a4](https://redirect.github.com/renovatebot/github-action/commit/64e44a4239e1eade784cf0abd54ffc4e82cb40e1))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.229.2
([dce4d1b](https://redirect.github.com/renovatebot/github-action/commit/dce4d1b6ba43914d91ab2a2a713fd384298a42fe))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.230.0
([30fd043](https://redirect.github.com/renovatebot/github-action/commit/30fd04394d4dfe018df48314de0036334d88d119))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.230.1
([425d313](https://redirect.github.com/renovatebot/github-action/commit/425d313d98c58c11780a712fbee72a7e35d25b92))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.231.0
([ae939aa](https://redirect.github.com/renovatebot/github-action/commit/ae939aab83afa3d641843d1d04300091752e09a8))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.231.1
([cac502d](https://redirect.github.com/renovatebot/github-action/commit/cac502de33fd8931d15a849d8eff67d8b75d253c))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.231.2
([242a56f](https://redirect.github.com/renovatebot/github-action/commit/242a56f27d85117d4b0703ff1e381aaafdc0488e))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.231.3
([3b66329](https://redirect.github.com/renovatebot/github-action/commit/3b6632905280f6bc2656245b3916333c3c224c99))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.232.0
([c0502ab](https://redirect.github.com/renovatebot/github-action/commit/c0502aba634bf921fa2af4eb4a052318991265bd))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.232.1
([d46a7eb](https://redirect.github.com/renovatebot/github-action/commit/d46a7ebfc5129ac009353beed92152c3ddc4ce3b))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.233.1
([b476f30](https://redirect.github.com/renovatebot/github-action/commit/b476f3002f23f603eb89450785a8ce68037ebf20))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.233.2
([bc50ad1](https://redirect.github.com/renovatebot/github-action/commit/bc50ad1e38f4e32bed5288aec6e9c303a5e81117))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.233.3
([908f92d](https://redirect.github.com/renovatebot/github-action/commit/908f92dbc49eaefff1e0c8771aa41a957268baa5))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.233.4
([a48bc32](https://redirect.github.com/renovatebot/github-action/commit/a48bc32b6b570fe1fa55975fda7205bbbb98afa3))
- **deps:** update ghcr.io/renovatebot/renovate docker tag to v43.234.0
([c929092](https://redirect.github.com/renovatebot/github-action/commit/c929092dcc2e71fcddf23dc5c9d2cdf70ed17ed4))
- **deps:** update ghcr.io/zizmorcore/zizmor docker tag to v1.26.1
([#&#8203;1055](https://redirect.github.com/renovatebot/github-action/issues/1055))
([c878bfb](https://redirect.github.com/renovatebot/github-action/commit/c878bfb5430ce52efc451671e7887a2b5d755ffc))
- **deps:** update zizmorcore/zizmor-action action to v0.5.7
([996e7bc](https://redirect.github.com/renovatebot/github-action/commit/996e7bc84761f298cb8bc5c765895b6db953876b))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-06-29 08:30:14 +00:00
0c67849e68 fix(packages): validate debian distribution and component names (#38116)
**Newline injection into the Debian Release and Packages indices**

The `distribution` and `component` come straight from the request path
and are written line by line into the generated `Release` and `Packages`
files (the `Suite`/`Codename`/`Components` lines and the `Filename:
pool/<distribution>/<component>/...` line), but `UploadPackageFile` only
checked they were non-empty. `ctx.PathParam` url-decodes the segment, so
an encoded newline such as `main%0AInjected-Field: x` is accepted,
stored and then re-emitted for that distribution, which lets an
authenticated uploader forge extra fields in the index apt consumes.
Restricted both values to a conservative name pattern in the handler,
since that is the layer that accepts them; this should also keep the
pool paths well formed.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-29 06:50:22 +00:00
GiteabotandGitHub 762c674bc5 chore(deps): update python dependencies (#38256)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [djlint](https://redirect.github.com/djlint/djLint) | `==1.39.2` →
`==1.39.4` |
![age](https://developer.mend.io/api/mc/badges/age/pypi/djlint/1.39.4?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/djlint/1.39.2/1.39.4?slim=true)
|
| [zizmor](https://docs.zizmor.sh)
([source](https://redirect.github.com/zizmorcore/zizmor)) | `==1.25.2` →
`==1.26.1` |
![age](https://developer.mend.io/api/mc/badges/age/pypi/zizmor/1.26.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/zizmor/1.25.2/1.26.1?slim=true)
|

---

### Release Notes

<details>
<summary>djlint/djLint (djlint)</summary>

###
[`v1.39.4`](https://redirect.github.com/djlint/djLint/blob/HEAD/CHANGELOG.md#1394---2026-06-24)

[Compare
Source](https://redirect.github.com/djlint/djLint/compare/v1.39.3...v1.39.4)

##### Fix

- Fix crashes in mypyc-compiled wheels.

###
[`v1.39.3`](https://redirect.github.com/djlint/djLint/blob/HEAD/CHANGELOG.md#1393---2026-06-23)

[Compare
Source](https://redirect.github.com/djlint/djLint/compare/v1.39.2...v1.39.3)

##### Fix

- Use Click instead of tqdm for progress output, send progress to
stderr, respect `--quiet`, and honor `NO_COLOR`. Remove direct
`colorama` and `tqdm` dependencies now that Click handles CLI colors and
progress.
- Avoid false H025 reports after self-closing tags in Django templates.
- Avoid false H025 reports for multiline Go template attributes.
- Keep Django child-template reformatting idempotent when inline control
blocks also appear inside HTML attributes.
- Respect whitespace-control dashes when applying `blank_line_after_tag`
and `blank_line_before_tag`.

</details>

<details>
<summary>zizmorcore/zizmor (zizmor)</summary>

###
[`v1.26.1`](https://redirect.github.com/zizmorcore/zizmor/releases/tag/v1.26.1)

[Compare
Source](https://redirect.github.com/zizmorcore/zizmor/compare/v1.26.0...v1.26.1)

This is a small corrective release for
[1.26.0](https://docs.zizmor.sh/release-notes/#&#8203;1260).

###
[`v1.26.0`](https://redirect.github.com/zizmorcore/zizmor/releases/tag/v1.26.0)

[Compare
Source](https://redirect.github.com/zizmorcore/zizmor/compare/v1.25.2...v1.26.0)

#### New Features
🌈[🔗](https://docs.zizmor.sh/release-notes/#new-features)

- New audit:
[typosquat-uses](https://docs.zizmor.sh/audits/#typosquat-uses) detects
uses: clauses that reference likely typoed actions
([#&#8203;1985](https://redirect.github.com/zizmorcore/zizmor/issues/1985))

Many thanks to [@&#8203;andrew](https://redirect.github.com/andrew) for
proposing and implementing this improvement!

- New audit:
[unsound-ternary](https://docs.zizmor.sh/audits/#unsound-ternary)
detects pseudo-ternary expressions that don't evaluate as expected
([#&#8203;2085](https://redirect.github.com/zizmorcore/zizmor/issues/2085))

Many thanks to [@&#8203;terror](https://redirect.github.com/terror) for
proposing and implementing this improvement!

- New audit:
[adhoc-packages](https://docs.zizmor.sh/audits/#adhoc-packages) detects
run: steps that install packages in an ad-hoc manner
([#&#8203;2061](https://redirect.github.com/zizmorcore/zizmor/issues/2061))

Many thanks to
[@&#8203;connorshea](https://redirect.github.com/connorshea) for
proposing and implementing this improvement!

#### Enhancements
🌱[🔗](https://docs.zizmor.sh/release-notes/#enhancements)

- The [cache-poisoning](https://docs.zizmor.sh/audits/#cache-poisoning)
audit now detects additional cache disablement heuristics
([#&#8203;2053](https://redirect.github.com/zizmorcore/zizmor/issues/2053))

- The
[known-vulnerable-actions](https://docs.zizmor.sh/audits/#known-vulnerable-actions)
audit is now configurable. See [the configuration
documentation](https://docs.zizmor.sh/audits/#known-vulnerable-actions-configuration)
for details
([#&#8203;2084](https://redirect.github.com/zizmorcore/zizmor/issues/2084))

- The
[excessive-permissions](https://docs.zizmor.sh/audits/#excessive-permissions)
audit is now aware of the code-quality permission
([#&#8203;2088](https://redirect.github.com/zizmorcore/zizmor/issues/2088))

- The [unpinned-uses](https://docs.zizmor.sh/audits/#unpinned-uses)
audit's auto-fix now uses the fully qualified version tag (e.g. #
v6.0.2) when fixing a major-version ref (e.g.
[@&#8203;v6](https://redirect.github.com/v6))
([#&#8203;2127](https://redirect.github.com/zizmorcore/zizmor/issues/2127))

#### Performance Improvements
🚄[🔗](https://docs.zizmor.sh/release-notes/#performance-improvements)

- Most online audits are significantly faster, thanks to more precise
retry handling
([#&#8203;2036](https://redirect.github.com/zizmorcore/zizmor/issues/2036))
  Bug Fixes 🐛[🔗](https://docs.zizmor.sh/release-notes/#bug-fixes)

- Fixed a bug where zizmor's LSP would not recognize dependabot.yaml
files in its default configuration
([#&#8203;2026](https://redirect.github.com/zizmorcore/zizmor/issues/2026))

Many thanks to [@&#8203;fionn](https://redirect.github.com/fionn) for
implementing this fix!

- Fixed a bug where
[ref-version-mismatch](https://docs.zizmor.sh/audits/#ref-version-mismatch)
would fail to fully match some version comments
([#&#8203;2040](https://redirect.github.com/zizmorcore/zizmor/issues/2040))

- Fixed a bug where
[dependabot-cooldown](https://docs.zizmor.sh/audits/#dependabot-cooldown)
would fail to honor the user's configured days when performing autofixes
([#&#8203;2055](https://redirect.github.com/zizmorcore/zizmor/issues/2055))

- Steps and jobs gated by statically-false if: conditions (e.g. if:
false, if: ${{ false }}) are now skipped during auditing, since they
cannot execute
([#&#8203;2059](https://redirect.github.com/zizmorcore/zizmor/issues/2059),
[#&#8203;2069](https://redirect.github.com/zizmorcore/zizmor/issues/2069))

- Fixed a bug where
[ref-version-mismatch](https://docs.zizmor.sh/audits/#ref-version-mismatch)
would fail to identify some valid version comments
([#&#8203;2073](https://redirect.github.com/zizmorcore/zizmor/issues/2073))

- Fixed a bug where
[unpinned-images](https://docs.zizmor.sh/audits/#unpinned-images) would
incorrectly flag empty matrix expansions as unpinned container image
references
([#&#8203;2102](https://redirect.github.com/zizmorcore/zizmor/issues/2102))

- Fixed a bug where
[unpinned-images](https://docs.zizmor.sh/audits/#unpinned-images) would
incorrectly flag some matrix expansions as unpinned
([#&#8203;2098](https://redirect.github.com/zizmorcore/zizmor/issues/2098))

- The SARIF (--format=sarif) and GitHub Annotations (--format=github)
output formats now provide more correct/useful paths, particularly when
the user provides a relative path as input to zizmor rather than zizmor
.
([#&#8203;1748](https://redirect.github.com/zizmorcore/zizmor/issues/1748),
[#&#8203;2095](https://redirect.github.com/zizmorcore/zizmor/issues/2095))

#### Changes ⚠️[🔗](https://docs.zizmor.sh/release-notes/#changes)

- The [impostor-commit](https://docs.zizmor.sh/audits/#impostor-commit)
audit no longer suggests auto-fixes, to avoid incorrectly minimizing the
amount of manual remediation work needed
([#&#8203;2054](https://redirect.github.com/zizmorcore/zizmor/issues/2054))

- The JSON and SARIF outputs no longer contain a misleading prefix key
([#&#8203;2095](https://redirect.github.com/zizmorcore/zizmor/issues/2095))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-06-29 08:24:18 +02:00
wxiaoguangandGitHub 8ff71a5e52 fix: flex divided list item shrink (#38255)
don't make the items shrink since they are used as list items.

fix #38220
2026-06-29 06:01:12 +00:00
GiteaBot 8343c47bd1 [skip ci] Updated translations via Crowdin 2026-06-29 01:20:03 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Lunny XiaoCopilot Autofix powered by AI
c6b2394585 fix(actions): authenticate snapcraft before nightly remote build (#38252)
The `release-nightly-snapcraft` workflow’s `build-and-publish` job was
failing because `snapcraft remote-build` fell back to interactive
Launchpad authorization in CI. This change makes authentication explicit
and non-interactive before the remote build step.

- **Workflow change**
  - Add an `Authenticate snapcraft` step before `Remote build`.
- Run `snapcraft login --with` using the existing
`SNAPCRAFT_STORE_CREDENTIALS` secret.
  - Pin that step to `shell: bash` to support process substitution.

- **Why this fixes the failure**
  - Prevents CI from entering browser-based Launchpad auth flow.
  - Ensures `remote-build` runs with preloaded credentials.

```yaml
- name: Authenticate snapcraft
  shell: bash
  env:
    SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
  run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS")
```

---------

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-28 15:36:28 -07:00
TheFox0x7andGitHub 4f41ad7b91 revert(sign): restore gpg (#38251)
partially revert sigstore signing to avoid causing breaking change for v1.27
2026-06-28 20:44:26 +00:00
Augusto XavierandGitHub 4812e35486 fix(api): respect since/until when counting commits for X-Total-Count (#38204)
The repository commits API (`GET /repos/{owner}/{repo}/commits`) accepts
`since` and `until` query parameters and filters the returned page of
commits by commit date. However, the `X-Total-Count` and `X-Total`
response headers reported the *unfiltered* total number of commits, so
the advertised total could be far larger than the number of commits
actually returned for the requested date range. With a range that
matches no commits, the page is correctly empty while the headers still
claim the full repository total.

## Root cause

`gitrepo.CommitsCount` declared `Since` and `Until` options and the API
handler populated them, but the function never appended
`--since`/`--until` to the underlying `git rev-list --count` invocation.
The date filters were silently dropped, so the count always reflected
the entire revision history.

## Fix

Pass the `Since`/`Until` options through to `git rev-list`, mirroring
the existing commit-listing path (`commitsByRangeWithTime`). The
reported total now matches the filtered range used to build the page.

## Testing

Added `TestCommitsCountWithSinceUntil` in
`modules/gitrepo/commit_test.go`, a table-driven unit test against the
`repo1_bare` fixture covering `since`, `until`, and a bounded
`since`+`until` range. It fails on the pre-fix code (every case returns
the full count of 3) and passes after the change. Existing
`CommitsCount` tests remain green.

## Notes

- No new settings, no default changes; this corrects an incorrect header
value and is backward compatible. Clients that depend on `since`/`until`
already filter the returned commits, and the headers now agree with that
filtering.

Fixes #35886.

---

*AI-assistance disclosure:* this change was developed with the
assistance of Claude Code (Claude Opus 4.8). I have reviewed and
understand the change and take responsibility for it.
2026-06-28 19:58:25 +00:00
TheFox0x7andGitHub 98c61942aa build(sign): move to sigstore (#38250)
drops signing with gpg in favor of sigstore based artifact signing
2026-06-28 19:18:12 +00:00
bircniandGitHub cc1df1976b fix: codemirror regressions (#38248) 2026-06-28 20:29:34 +02:00
bircniandGitHub 1c718da16c fix(api): support HEAD requests on all API GET endpoints (#38245)
Fixes #38226

## Summary

Add `chi_middleware.GetHead` as the first `BeforeRouting` middleware on
the API router. This makes every API `GET` endpoint automatically handle
`HEAD` requests, as required by RFC 9110 §9.3.2.

Previously, `HEAD` requests to endpoints like `GET
/repos/{owner}/{repo}/git/commits/{sha}` returned `405 Method Not
Allowed`.

The web router already used this same middleware (see
`routers/web/web.go:261`), so this aligns API behaviour with the web
router.

## Changes

- `routers/api/v1/api.go`: add `chi_middleware.GetHead` middleware to
the API router
- `tests/integration/api_repo_git_commits_test.go`: add
`TestAPIReposGitCommitsHEAD` verifying HEAD returns 200 on a valid ref
and 404 (not 405) on a missing ref
2026-06-28 12:14:39 +00:00
ce8cf22af9 fix(actions): don't swallow HTML entities into linkified URLs (#38239)
In the Actions log viewer, a double-quoted URL renders with a stray
extra `;` after it.

Reported in `gitea/runner#1046`

Remove the buggy AI slop `linkifyURLs` and use new approach to process
URLs in text

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-28 19:37:16 +08:00
5b9251150c fix(actions): address workflow status badge review feedback (#38241)
Follow
https://github.com/go-gitea/gitea/pull/38196#discussion_r3487219492

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-28 10:53:01 +00:00
bircniandGitHub 1d43b736b5 fix(actions): deny fork-PR cross-repo access via collaborative owner (#38214)
### What

`GetActionsUserRepoPermission` (`models/perm/access/repo_permission.go`)
decides whether an Actions task token may access a target repo. Its
cross-repo branches each enforce a fork-PR discriminator — except the
collaborative-owner branch, which was missing the
`!task.IsForkPullRequest` guard that its sibling
`checkSameOwnerCrossRepoAccess` has.

As a result, when a private repo **B** lists owner **A** as a
collaborative owner, an attacker-controlled fork pull-request workflow
whose base repo is owned by A was granted code-read on B — i.e. the
fork's workflow could clone a third private repository it has no rights
to (read-only confidentiality breach).

### Fix

Add the same fork-PR guard the sibling path already enforces:

```go
if taskRepo.IsPrivate && !task.IsForkPullRequest {
    actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
    if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
        return maxPerm, nil
    }
}
```
2026-06-28 10:25:56 +00:00
f46c9a9769 feat(actions): support owner-level and global scoped workflows (#38154)
## Summary

This PR adds **scoped workflows** to Gitea Actions. Workflows defined
centrally in a "source" repository that automatically run on every
repository in scope: an organization's repositories, or (for instance
admins) every repository on the instance. Each scoped run executes in
the consuming repository's own context (its runners, secrets, and
branch) while its content is read from the source repository, so an org
or instance can mandate shared CI across many repositories without
copying workflow files into each one.

An owner or instance admin registers source repositories on a settings
page and can mark individual workflows as **required**. A required
scoped workflow cannot be opted out by a consuming repository and gates
its pull-request merges; an optional one can be disabled per repository.
Scoped workflows live under a dedicated `SCOPED_WORKFLOW_DIRS` (default
`.gitea/scoped_workflows`), kept separate from regular `WORKFLOW_DIRS`.

## Main changes

### Configuration 
New `SCOPED_WORKFLOW_DIRS` setting, validated to not overlap with
`WORKFLOW_DIRS`. Default: `.gitea/scoped_workflows`

### Data model & migration
- New `action_scoped_workflow_source` table mapping a registering owner
(`owner_id`, where `0` = instance-level) to a source repository, with a
per-workflow `WorkflowConfigs` map.
- `ActionRun` gains `WorkflowRepoID` / `WorkflowCommitSHA` (the pinned
content source) and an `IsScopedRun` flag.

###  Detection & run creation
On consumer events, scoped workflows from the effective sources (the
owner's own sources plus instance-level ones) are matched and turned
into runs that execute in the consumer's context, with content pinned to
the source repo's default-branch commit.

`on: workflow_run` and `on: schedule` are currently not supported.

###  Opt-out
A consuming repository can disable an optional scoped workflow (tracked
separately from regular `DisabledWorkflows`); required scoped workflows
can never be disabled, opted out, or bypassed.

###  Commit status 
A scoped run's status context format is `"<source repo full name>:
<workflow display name> / <job> (<event>)"`
(for example: `my-org/scoped-workflows: db-tests / test-sqlite
(pull_request)`),
keeping it distinct from a same-named repo-level workflow and from other
sources.

###  Required status checks
Admins mark workflows required and supply status-check patterns.
`EffectiveRequiredContexts` appends those patterns to the branch
protection's required contexts and they are matched
must-present-and-pass. If the status checks from scoped workflows fail,
the PR cannot be merged.

NOTE: scoped workflows' required status checks patterns can protect any
target branch that has a protection rule, even though the rule's "Status
Check" is disabled. A target branch with no protection rule cannot be
protected.

<details>
  <summary>Screenshots</summary>

<img width="1400" alt="image"
src="https://github.com/user-attachments/assets/a5d1db33-15ec-487e-93be-2bc04b4e6643"
/>

</details>


###  Reusable workflows (`uses:`)
A scoped workflow's local `uses: ./...` resolves against the source
repository. `uses:` directory validation honors the
instance-configurable `WORKFLOW_DIRS` and `SCOPED_WORKFLOW_DIRS`
(previously hardcoded to `.gitea`/`.github/workflows`).

###  Manual dispatch
`workflow_dispatch` is supported for scoped workflows (web and API),
resolving inputs/content from the source repo.

###  Performance
A process-local LRU cache keyed by source repo ID for the per-source
workflow parse, so instance-level and owner-level sources don't open the
source repo and parse workflow files on every event.

### UI
Org / user / admin pages to register and remove sources, search
repositories, and mark workflows required with their status-check
patterns. The repository Actions sidebar groups scoped workflows by
source with owner/instance labels and required/disabled badges.

<details>
  <summary>Screenshots</summary>

Scoped workflows setting page:

<img width="1600" alt="image"
src="https://github.com/user-attachments/assets/9d19f667-97a5-4935-92b2-e53f105e3642"
/>


Consumer repo's Actions runs list:

<img width="1600" alt="image"
src="https://github.com/user-attachments/assets/a77241f9-0aa9-41aa-ba73-12a9a688cb64"
/>

- `Owner`: this is a owner-level scoped workflows source repo
- `Global`: this is a global scoped workflows source repo
- `Required`: this scoped workflow is required, repo admin cannot
disable it

</details>

---

Docs: https://gitea.com/gitea/docs/pulls/447

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-06-28 09:31:35 +00:00
Lunny XiaoandGitHub c9920b7bd0 fix(oauth): restrict introspection to the token's client (#38042)
Bind OAuth token introspection responses to the authenticated client.
Return an inactive response when the token grant belongs to a different
OAuth application to avoid leaking token metadata across clients.

Add integration coverage for cross-client introspection attempts against
both access tokens and refresh tokens.

Assisted-by: GPT-5.4
2026-06-28 08:06:33 +00:00
bircniandGitHub 0319358e5e fix(web): Correctly align the "disabled" label on larger workflow names (#38240) 2026-06-28 07:58:29 +02:00
9540292596 feat(actions): add workflow status badge modal (#38196)
- Add a Create Status Badge button for selected Actions workflows.
- Show badge URL, Markdown, and HTML snippets backed by the existing
workflow badge route.

## Screenshots
<img width="553" height="470" alt="dyn-a5d565ab915b9ffb6c02ac68113494b0"
src="https://github.com/user-attachments/assets/43b4ceb9-bbd1-4024-b058-d85ec8325e88"
/>
<img width="349" height="156" alt="grafik"
src="https://github.com/user-attachments/assets/6eaec62d-ffb0-45c0-b63d-866a41a66005"
/>



Fixes https://github.com/go-gitea/gitea/issues/31462

---------

Signed-off-by: guanzi008 <245205080@qq.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-28 01:36:45 +02:00
maximilizeandGitHub d392fb1438 fix(packages): accept npm "repository" and "bin" in string form (#38236)
## What

npm allows `repository` and `bin` in `package.json` to be either an
object or a plain string (npm docs:
[repository](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#repository),
[bin](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin)).
The npm registry creator modeled `repository` as a struct and `bin` as
`map[string]string`, so publishing a package whose `package.json` uses
the string form failed with:

```
json: cannot unmarshal string into Go struct field PackageMetadataVersion.PackageMetadata.versions.bin of type map[string]string
```

## Fix

`modules/packages/npm/creator.go`: add `UnmarshalJSON` to `Repository`
(string → `URL`) and a `Bin` type with `UnmarshalJSON` (string → a
single command named after the package, per npm semantics), mirroring
the existing `License` / `User` string-or-object handling. The stored
`Metadata` field types are unchanged.

`bundledDependencies` as a boolean (also noted in #38235) is left out of
scope — it is rare and semantically different (`true` = bundle all
deps).

## Test

`TestParsePackage/ValidRepositoryAndBinAsString` parses a package with
string `repository` and `bin`: it fails on `main` with the error above
and passes with this change. The full `modules/packages/npm` suite is
green and `gofmt` is clean.

Fixes #38235

_AI disclosure: prepared with AI assistance; I reviewed and verified it
(reproduction + tests) and can explain and defend the change._
2026-06-27 22:41:46 +02:00
bircniandGitHub 0f5102427e fix(actions): ensure all waiting jobs get runners in large workflows (#38200)
## Summary

Fixes two related bugs that cause jobs in large workflows (50+ parallel
jobs) to never get a runner assigned even though runners are free.

### Bug 1 — Concurrent runner race

When N runners all poll `FetchTask` with a stale `tasksVersion`
simultaneously, they all query the same waiting job list sorted by
`(updated, id)` and all pick **job #1**. Only one wins the `UPDATE WHERE
task_id=0` optimistic lock; the rest return empty-handed but still
receive `latestVersion` in the response. They then consider themselves
"up to date" and skip `PickTask` on every subsequent poll, leaving jobs
#2–50 permanently unassigned.

**Fix:** `CreateTaskForRunner` now iterates through all matching waiting
jobs. When the optimistic lock fails on job #1, it immediately tries job
#2, then #3, etc., each in its own independent transaction so a failed
attempt rolls back cleanly before the next candidate is tried.
`PickTask` no longer wraps this call in an outer `db.WithTx` (which
caused `halfCommitter` entanglement that prevented per-attempt
rollbacks).

### Bug 2 — Idle runner doesn't re-check after finishing a task

`tasks_version` only bumps when a job transitions **to** waiting (new
workflow triggered, blocked→unblocked). After a runner finishes its
current task it polls `FetchTask` with `tasksVersion == latestVersion`,
so the server skips `PickTask` entirely — the remaining 45 waiting jobs
are invisible to the now-idle runner.

**Fix:** Also call `IncreaseTaskVersion` in `UpdateRunJob` when a
(non-reusable-caller) job transitions to a **done** state. Idle runners
then see a version mismatch on their next poll and attempt `PickTask`,
picking up the remaining jobs.
2026-06-27 17:56:12 +00:00
cbe1b703dc refactor: Use db.Get[] instead of db.GetEngine(ctx).Get(bean) to avoid zero value fetching wrong database record (#37977)
This PR replaces a set of struct-based `Get` lookups with explicit
`db.Get` / `db.Exist` conditions in places where zero-value fields can
lead to ambiguous matches or incorrect records being returned.

The main goal is to make read paths deterministic and avoid accidentally
matching the wrong row when only part of a struct is populated.

### What changed

- replace many `db.GetEngine(ctx).Get(bean)` calls with explicit
`builder.Eq` conditions across models such as actions, admin tasks,
issues, pull requests, repositories, users, packages, redirects,
watches, stars, and follows
- use quoted column names where needed for reserved fields like `index`,
`type`, and `name`
- add dedicated user lookup helpers for:
  - primary email
  - OAuth login source / login name
- update sign-in and OAuth-related flows to use explicit individual-user
lookups instead of partially populated `User` structs
- tighten package property and Terraform lock lookups to avoid ambiguous
reads and updates
- keep existing fallback behavior where needed, while removing reliance
on zero-value struct matching

### User-facing impact

These changes primarily affect authentication and account lookup paths:

- email/username sign-in now re-fetches users through explicit keys
- OAuth2 auto-linking now resolves users by name or primary email
explicitly
- OAuth2 login/sync now looks up users by login source, login type, and
login name explicitly
- non-individual accounts are no longer implicitly matched through
partial user lookups in these flows

This should reduce the risk of incorrect account matches and make query
behavior more predictable across the codebase.

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-06-27 10:24:02 -07:00
d5e6f273f0 fix(migrations): prevent path traversal in repository restore (#38215)
## Problem

The repository restorer (`services/migrations/restore.go`) builds
`file://` URLs for release attachments and PR patches by joining
user-supplied paths from `release.yml` and `pull_request.yml` onto the
dump directory:

```go
*asset.DownloadURL = "file://" + filepath.Join(r.baseDir, *asset.DownloadURL)
pr.PatchURL        = "file://" + filepath.Join(r.baseDir, pr.PatchURL)
```

`filepath.Join` cleans the path, so a crafted relative value such as
`../../../../etc/passwd` resolves to an absolute path **outside** the
dump directory. `uri.Open` then reads it via `os.Open` and stores the
content as a release attachment, which is retrievable through the API —
an arbitrary file read (Local File Inclusion) from a dump archive
supplied to `restore-repo`.

## Fix

Add a `localFileURL` helper that resolves the relative path against
`baseDir` and rejects anything that escapes it. Malicious entries are
skipped with a warning so a legitimate restore still completes; in-dump
files keep working unchanged.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-27 14:50:30 +00:00
Keshane GanandGitHub 15ee850ede perf(web): sort the action_run query by a repo-scoped index when possible (#38155)
The `index` column is unique per repo, but the `id` column is scoped to
the whole table

https://github.com/go-gitea/gitea/blob/240d0efa7e3860dfde0eb94932287602cbfae533/models/migrations/v1_27/v331.go#L62-L67.

We have over 60000 action runs in our repo and loading the "Actions" tab
has been very slow, so scoping the sort to the repo helps it load much
faster

## Summary of perf change
Ran tests based on commit 240d0efa7e

| Case | Run | Duration |
|------|-----|----------|
| Before | 1 | 16717.3ms |
| Before | 2 | 9052.5ms |
| Before | 3 | 9347.1ms |
| Before | 4 | 8091.2ms |
| Before | 5 | 8732.1ms |
| **Before** | **Median** | **9052.5ms** |
| After | 1 | 3654.2ms |
| After | 2 | 287.4ms |
| After | 3 | 253.6ms |
| After | 4 | 278.0ms |
| After | 5 | 313.6ms |
| **After** | **Median** | **287.4ms** |

Speedup of 30x on our instance.

## Logs
### Before
```log
2026/06/26 20:33:06 HTTPRequest [W] router: slow      GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:39730, elapsed 3037.6ms @ actions/actions.go:78(actions.List)
2026/06/26 20:33:08 models/actions/run_list.go:156:GetRunWorkflowIDs() [W] [Slow SQL Query] SELECT DISTINCT `workflow_id` FROM `action_run` WHERE repo_id=? ORDER BY `workflow_id` ASC [29] - 5.069413167s
2026/06/26 20:33:12 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:39748, 404 Not Found in 2.1ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 20:33:20 models/db/list.go:208:FindAndCount() [W] [Slow SQL Query] SELECT `id`, `title`, `repo_id`, `owner_id`, `workflow_id`, `index`, `trigger_user_id`, `schedule_id`, `ref`, `commit_sha`, `is_fork_pull_request`, `need_approval`, `approved_by`, `event`, `event_payload`, `trigger_event`, `status`, `version`, `raw_concurrency`, `started`, `stopped`, `previous_duration`, `latest_attempt_id`, `created`, `updated` FROM `action_run` WHERE `action_run`.repo_id=? ORDER BY `action_run`.`id` DESC LIMIT 30 [29] - 11.375193667s
2026/06/26 20:33:20 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:39730, 200 OK in 16717.3ms @ actions/actions.go:78(actions.List)
2026/06/26 20:33:20 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:39730, 404 Not Found in 0.9ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 20:33:20 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:39730, 200 OK in 1.0ms @ web/base.go:25(avatars)
2026/06/26 20:33:20 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:35914, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 20:33:24 HTTPRequest [I] router: polling   GET /user/events for 127.0.0.1:35882, elapsed 3736.2ms @ events/events.go:18(events.Events)
2026/06/26 20:33:29 HTTPRequest [W] router: slow      GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:35896, elapsed 3542.4ms @ actions/actions.go:78(actions.List)
2026/06/26 20:33:35 models/db/list.go:208:FindAndCount() [W] [Slow SQL Query] SELECT `id`, `title`, `repo_id`, `owner_id`, `workflow_id`, `index`, `trigger_user_id`, `schedule_id`, `ref`, `commit_sha`, `is_fork_pull_request`, `need_approval`, `approved_by`, `event`, `event_payload`, `trigger_event`, `status`, `version`, `raw_concurrency`, `started`, `stopped`, `previous_duration`, `latest_attempt_id`, `created`, `updated` FROM `action_run` WHERE `action_run`.repo_id=? ORDER BY `action_run`.`id` DESC LIMIT 30 [29] - 8.581414814s
2026/06/26 20:33:35 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:35896, 200 OK in 9052.5ms @ actions/actions.go:78(actions.List)
2026/06/26 20:33:35 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:35914, 200 OK in 0.2ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 20:33:35 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:35896, 404 Not Found in 0.9ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 20:33:35 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:39748, 200 OK in 1.2ms @ web/base.go:25(avatars)
2026/06/26 20:33:39 HTTPRequest [I] router: polling   GET /user/events for 127.0.0.1:35874, elapsed 3818.6ms @ events/events.go:18(events.Events)
2026/06/26 20:34:05 HTTPRequest [W] router: slow      GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:39730, elapsed 3889.5ms @ actions/actions.go:78(actions.List)
2026/06/26 20:34:11 models/db/list.go:208:FindAndCount() [W] [Slow SQL Query] SELECT `id`, `title`, `repo_id`, `owner_id`, `workflow_id`, `index`, `trigger_user_id`, `schedule_id`, `ref`, `commit_sha`, `is_fork_pull_request`, `need_approval`, `approved_by`, `event`, `event_payload`, `trigger_event`, `status`, `version`, `raw_concurrency`, `started`, `stopped`, `previous_duration`, `latest_attempt_id`, `created`, `updated` FROM `action_run` WHERE `action_run`.repo_id=? ORDER BY `action_run`.`id` DESC LIMIT 30 [29] - 8.861572113s
2026/06/26 20:34:11 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:39730, 200 OK in 9347.1ms @ actions/actions.go:78(actions.List)
2026/06/26 20:34:11 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:35914, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 20:34:11 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:39730, 404 Not Found in 0.6ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 20:34:11 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:39730, 200 OK in 1.3ms @ web/base.go:25(avatars)
2026/06/26 20:34:18 HTTPRequest [W] router: slow      GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:39748, elapsed 3974.2ms @ actions/actions.go:78(actions.List)
2026/06/26 20:34:22 models/db/list.go:208:FindAndCount() [W] [Slow SQL Query] SELECT `id`, `title`, `repo_id`, `owner_id`, `workflow_id`, `index`, `trigger_user_id`, `schedule_id`, `ref`, `commit_sha`, `is_fork_pull_request`, `need_approval`, `approved_by`, `event`, `event_payload`, `trigger_event`, `status`, `version`, `raw_concurrency`, `started`, `stopped`, `previous_duration`, `latest_attempt_id`, `created`, `updated` FROM `action_run` WHERE `action_run`.repo_id=? ORDER BY `action_run`.`id` DESC LIMIT 30 [29] - 7.68828429s
2026/06/26 20:34:22 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:39748, 200 OK in 8091.2ms @ actions/actions.go:78(actions.List)
2026/06/26 20:34:22 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:35914, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 20:34:22 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:39748, 404 Not Found in 0.7ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 20:34:23 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:39748, 200 OK in 0.7ms @ web/base.go:25(avatars)
2026/06/26 20:34:28 HTTPRequest [W] router: slow      GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:34462, elapsed 3193.2ms @ actions/actions.go:78(actions.List)
2026/06/26 20:34:34 models/db/list.go:208:FindAndCount() [W] [Slow SQL Query] SELECT `id`, `title`, `repo_id`, `owner_id`, `workflow_id`, `index`, `trigger_user_id`, `schedule_id`, `ref`, `commit_sha`, `is_fork_pull_request`, `need_approval`, `approved_by`, `event`, `event_payload`, `trigger_event`, `status`, `version`, `raw_concurrency`, `started`, `stopped`, `previous_duration`, `latest_attempt_id`, `created`, `updated` FROM `action_run` WHERE `action_run`.repo_id=? ORDER BY `action_run`.`id` DESC LIMIT 30 [29] - 8.180339918s
2026/06/26 20:34:34 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:34462, 200 OK in 8732.1ms @ actions/actions.go:78(actions.List)
2026/06/26 20:34:34 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:35914, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 20:34:34 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:34462, 404 Not Found in 0.8ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 20:34:38 HTTPRequest [I] router: polling   GET /user/events for 127.0.0.1:58102, elapsed 3887.7ms @ events/events.go:18(events.Events)
```


### After
```log
2026/06/26 21:24:46 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:51940, 200 OK in 3654.2ms @ actions/actions.go:78(actions.List)
2026/06/26 21:24:46 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:51954, 404 Not Found in 0.6ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 21:24:46 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:51954, 200 OK in 18.0ms @ web/base.go:25(avatars)
2026/06/26 21:24:47 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:48712, 200 OK in 3.6ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 21:24:49 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:51960, 200 OK in 287.4ms @ actions/actions.go:78(actions.List)
2026/06/26 21:24:49 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:48712, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 21:24:49 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:51956, 404 Not Found in 0.9ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 21:24:49 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:51960, 200 OK in 0.5ms @ web/base.go:25(avatars)
2026/06/26 21:24:51 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:51956, 200 OK in 253.6ms @ actions/actions.go:78(actions.List)
2026/06/26 21:24:51 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:48712, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 21:24:51 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:51956, 404 Not Found in 0.6ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 21:24:51 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:51960, 200 OK in 1.4ms @ web/base.go:25(avatars)
2026/06/26 21:24:53 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:48738, 200 OK in 278.0ms @ actions/actions.go:78(actions.List)
2026/06/26 21:24:53 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:48712, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 21:24:53 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:48738, 404 Not Found in 0.8ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 21:24:53 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:51960, 200 OK in 0.6ms @ web/base.go:25(avatars)
2026/06/26 21:24:55 HTTPRequest [I] router: completed GET /CareHarmony/SymphonyApp/actions for 127.0.0.1:48738, 200 OK in 313.6ms @ actions/actions.go:78(actions.List)
2026/06/26 21:24:55 HTTPRequest [I] router: completed GET /assets/site-manifest.json for 127.0.0.1:48712, 200 OK in 0.1ms @ misc/misc.go:23(misc.SiteManifest)
2026/06/26 21:24:55 HTTPRequest [I] router: completed GET /.well-known/appspecific/com.chrome.devtools.json for 127.0.0.1:48738, 404 Not Found in 0.8ms @ public/public.go:45(web.registerWebRoutes.(*Router).Group.registerWebRoutes.func19.FileHandlerFunc)
2026/06/26 21:24:55 HTTPRequest [I] router: completed GET /avatars/4aa9c7878cdb541dbdd37da61a586af554baf6c0930283e0281edf3a366b8c36?size=48 for 127.0.0.1:48738, 200 OK in 0.6ms @ web/base.go:25(avatars)
2026/06/26 21:24:58 HTTPRequest [I] router: polling   GET /user/events for 127.0.0.1:48738, elapsed 3035.2ms @ events/events.go:18(events.Events)
```

---------

Signed-off-by: Keshane Gan <kgan@care-harmony.com>
2026-06-27 14:22:40 +00:00
wxiaoguangandGitHub 16c3216dc6 fix: js string split (#38233)
fix #38229
2026-06-27 12:09:01 +00:00
GiteabotandGitHub b565f3e00a fix(deps): update module golang.org/x/image to v0.43.0 [security] (#38219)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) |
[`v0.42.0` →
`v0.43.0`](https://cs.opensource.google/go/x/image/+/refs/tags/v0.42.0...refs/tags/v0.43.0)
|
![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fimage/v0.43.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fimage/v0.42.0/v0.43.0?slim=true)
|

---

### Panic on VP8 alpha channel size mismatch in x/image/webp in
golang.org/x/image
[CVE-2026-46601](https://nvd.nist.gov/vuln/detail/CVE-2026-46601) /
[GO-2026-5061](https://pkg.go.dev/vuln/GO-2026-5061)

<details>
<summary>More information</summary>

#### Details
The webp decoder can panic when processing a VP8 chunk with dimensions
that do not match the canvas size.

#### Severity
Unknown

#### References
- [https://go.dev/cl/787681](https://go.dev/cl/787681)
- [https://go.dev/issue/79869](https://go.dev/issue/79869)

This data is provided by
[OSV](https://osv.dev/vulnerability/GO-2026-5061) and the [Go
Vulnerability Database](https://redirect.github.com/golang/vulndb)
([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
</details>

---

### Lack of limit on tile sizes in x/image/tiff in golang.org/x/image
[CVE-2026-46602](https://nvd.nist.gov/vuln/detail/CVE-2026-46602) /
[GO-2026-5062](https://pkg.go.dev/vuln/GO-2026-5062)

<details>
<summary>More information</summary>

#### Details
The TIFF decoder does not set a limit on the size of tiles in tiled
images, permitting a malicious or corrupt image containing a very large
tile to cause unbounded memory consumption.

#### Severity
Unknown

#### References
- [https://go.dev/cl/788422](https://go.dev/cl/788422)
- [https://go.dev/issue/79905](https://go.dev/issue/79905)

This data is provided by
[OSV](https://osv.dev/vulnerability/GO-2026-5062) and the [Go
Vulnerability Database](https://redirect.github.com/golang/vulndb)
([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
</details>

---

### Panic decoding image with out-of-bounds strip offset in x/image/tiff
in golang.org/x/image
[CVE-2026-46604](https://nvd.nist.gov/vuln/detail/CVE-2026-46604) /
[GO-2026-5066](https://pkg.go.dev/vuln/GO-2026-5066)

<details>
<summary>More information</summary>

#### Details
The TIFF decoder can panic when decoding an invalid image with an
out-of-bounds strip offset.

#### Severity
Unknown

#### References
- [https://go.dev/cl/788421](https://go.dev/cl/788421)
- [https://go.dev/issue/80122](https://go.dev/issue/80122)

This data is provided by
[OSV](https://osv.dev/vulnerability/GO-2026-5066) and the [Go
Vulnerability Database](https://redirect.github.com/golang/vulndb)
([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - ""
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-06-27 13:27:56 +02:00
bircniandGitHub 122ebcf0a8 fix(api): deny private org member enumeration via /members (#38213) 2026-06-26 18:35:13 +00:00
Aidan FaheyandGitHub 1b0992eb2e fix(actions): fix 500 error when canceling a canceling task (#38223) 2026-06-26 18:58:24 +02:00
bircniandGitHub c2f130d352 fix(mssql): convert legacy DATETIME columns to DATETIME2 (#38216)
## Problem

On MSSQL databases created by old Gitea versions, the real datetime
columns `external_login_user.expires_at` and `lfs_lock.created` were
created as `DATETIME`. `DATETIME` parses datetime literals in a
locale-dependent way, so the ISO string `'YYYY-MM-DD HH:MM:SS'` that
xorm sends fails to convert when the session language is not English
(e.g. German defaults to `dmy`):

```
mssql: Bei der Konvertierung eines nvarchar-Datentyps in einen datetime-Datentyp liegt der Wert außerhalb des gültigen Bereichs.
```

This breaks linking an external (OAuth/Keycloak) account to an existing
user, and LFS lock creation, with a 500 error.

## Fix

Current xorm already maps `time.Time` to the locale-independent
`DATETIME2` for new installs, so only legacy databases are affected.
This adds migration `341` that converts these columns to `DATETIME2` on
legacy MSSQL databases (no-op on other databases and on columns already
using `DATETIME2`).

A full audit of persisted `time.Time` columns in `models/` confirmed
these two are the only real datetime columns affected — every other time
value is stored as a unix-timestamp integer.

A regression test (MSSQL-only, mirroring the existing v338 pattern)
downgrades the columns to legacy `DATETIME`, runs the migration, asserts
the type becomes `DATETIME2`, and verifies an ISO datetime insert
succeeds under `SET LANGUAGE German`.

Fixes #38211
2026-06-25 14:38:39 +02:00
2e1be0b114 fix(deps): update npm dependencies (#38203)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[asciinema-player](https://redirect.github.com/asciinema/asciinema-player)
| [`3.15.1` →
`3.16.0`](https://renovatebot.com/diffs/npm/asciinema-player/3.15.1/3.16.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/asciinema-player/3.16.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/asciinema-player/3.15.1/3.16.0?slim=true)
|
|
[eslint-plugin-sonarjs](https://redirect.github.com/SonarSource/SonarJS/blob/master/packages/analysis/src/jsts/rules/README.md)
([source](https://redirect.github.com/SonarSource/SonarJS)) | [`4.0.3` →
`4.1.0`](https://renovatebot.com/diffs/npm/eslint-plugin-sonarjs/4.0.3/4.1.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-sonarjs/4.1.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-sonarjs/4.0.3/4.1.0?slim=true)
|
| [happy-dom](https://redirect.github.com/capricorn86/happy-dom) |
[`20.10.5` →
`20.10.6`](https://renovatebot.com/diffs/npm/happy-dom/20.10.5/20.10.6)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/20.10.6?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/20.10.5/20.10.6?slim=true)
|
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`11.7.0` →
`11.8.0`](https://renovatebot.com/diffs/npm/pnpm/11.7.0/11.8.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/11.8.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/11.7.0/11.8.0?slim=true)
|

---

### Release Notes

<details>
<summary>asciinema/asciinema-player (asciinema-player)</summary>

###
[`v3.16.0`](https://redirect.github.com/asciinema/asciinema-player/releases/tag/v3.16.0):
3.16.0

[Compare
Source](https://redirect.github.com/asciinema/asciinema-player/compare/v3.15.1...v3.16.0)

This is a significant release, with a new keystroke overlay and major
improvements to recording playback.

Notable changes:

- New optional keystroke overlay, toggled with the `k` key
- New `cursorMode` option: `"blinking"`, `"steady"` or `"hidden"`
- TypeScript definitions included in the npm package
- More reliable loading, playback, seeking, stepping and looping
- Recording load failures are now emitted via the `error` event
- Audio loading failures no longer prevent recording playback
- Improved rendering of Powerline and box-drawing symbols
- Improved accessibility of control bar buttons
- Standalone bundle is now compatible with LibreJS

#### Keystroke overlay

The new
[`keystrokeOverlay`](https://docs.asciinema.org/manual/player/options/#keystrokeoverlay)
option displays keys pressed during a recording:

```javascript
AsciinemaPlayer.create("/demo.cast", document.getElementById("demo"), {
  keystrokeOverlay: true
});
```

Recent keystrokes are shown in the lower-right corner. Consecutive text
input is grouped, while repeated special keys use a counter, such as
`Ret × 3`.

The overlay is disabled by default and can be toggled during playback
with the `k` key. It requires a recording containing input events
(`asciinema rec --capture-input ...`).

Demo:


[![asciicast](https://asciinema.org/a/1258082.svg)](https://asciinema.org/a/1258082)

#### Cursor mode

The new
[`cursorMode`](https://docs.asciinema.org/manual/player/options/#cursormode)
option controls cursor visibility:

```javascript
AsciinemaPlayer.create("/demo.cast", document.getElementById("demo"), {
  cursorMode: "steady"
});
```

Supported modes are `"blinking"` (the default), `"steady"` and
`"hidden"`.

#### Playback improvements

The recording playback engine has been significantly reworked. This
fixes several edge cases involving reverse stepping, marker pauses,
looping, seeking, posters and audio playback.

Missing or invalid audio now falls back to terminal-only playback. Fatal
recording load errors are emitted through the new
[`error`](https://docs.asciinema.org/manual/player/api/#error-event)
event.

`getCurrentTime()` and `getDuration()` now return their values directly,
as documented.

#### TypeScript support

The npm package now includes TypeScript definitions for the player API,
options, recording sources, parsers and events.

</details>

<details>
<summary>SonarSource/SonarJS (eslint-plugin-sonarjs)</summary>

###
[`v4.1.0`](https://redirect.github.com/SonarSource/SonarJS/compare/93ac7229b60beb637a7d91644c086f3b854072d9...4ce51a6eec04a87ee57b30792c3103823aad5bdb)

[Compare
Source](https://redirect.github.com/SonarSource/SonarJS/compare/93ac7229b60beb637a7d91644c086f3b854072d9...4ce51a6eec04a87ee57b30792c3103823aad5bdb)

</details>

<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>

###
[`v20.10.6`](https://redirect.github.com/capricorn86/happy-dom/releases/tag/v20.10.6)

[Compare
Source](https://redirect.github.com/capricorn86/happy-dom/compare/v20.10.5...v20.10.6)

##### :construction\_worker\_man: Patch fixes

- Await NodeJS internal ReadableStream promise during teardown - By
**[@&#8203;capricorn86](https://redirect.github.com/capricorn86)** in
task
[#&#8203;2217](https://redirect.github.com/capricorn86/happy-dom/issues/2217)

</details>

<details>
<summary>pnpm/pnpm (pnpm)</summary>

###
[`v11.8.0`](https://redirect.github.com/pnpm/pnpm/releases/tag/v11.8.0):
pnpm 11.8

[Compare
Source](https://redirect.github.com/pnpm/pnpm/compare/v11.7.0...v11.8.0)

#### Minor Changes

- [`c112b61`](https://redirect.github.com/pnpm/pnpm/commit/c112b61):
Added a `--dry-run` option to `pnpm install`. It runs a full dependency
resolution and reports what an install would change, but writes nothing
to disk (no lockfile, no `node_modules`) and always exits with code 0.
This mirrors the preview semantics of `npm install --dry-run`
[#&#8203;7340](https://redirect.github.com/pnpm/pnpm/issues/7340).
- [`179ebc4`](https://redirect.github.com/pnpm/pnpm/commit/179ebc4):
`pnpm run --no-bail` now exits with a non-zero exit code when any of the
executed scripts fail, while still running every matched script to
completion. This makes the exit-code behavior of `--no-bail` consistent
between recursive and non-recursive runs (recursive runs already failed
at the end). Previously, a non-recursive `pnpm run --no-bail` always
exited with code 0, even when a script failed
[#&#8203;8013](https://redirect.github.com/pnpm/pnpm/issues/8013).
- [`0474a9c`](https://redirect.github.com/pnpm/pnpm/commit/0474a9c):
Added support for generating Node.js package maps at
`node_modules/.package-map.json` during isolated and hoisted installs.
Added the `node-experimental-package-map` setting to inject the
generated map into pnpm-managed Node.js script environments, and the
`node-package-map-type` setting to choose between `standard` and `loose`
package maps.
- [`dcededc`](https://redirect.github.com/pnpm/pnpm/commit/dcededc):
`pnpm sbom` now marks components reachable only through
`devDependencies` with CycloneDX `scope: "excluded"` and the
`cdx:npm:package:development` property. The `excluded` scope documents
"component usage for test and other non-runtime purposes", which matches
the semantics of a devDependency; the property is the CycloneDX
npm-taxonomy marker emitted by `@cyclonedx/cyclonedx-npm`, so both
modern (scope) and existing (property) consumers are covered. Components
reachable at runtime (including installed `optionalDependencies`) omit
`scope` and default to `required`.
- [`1495cb0`](https://redirect.github.com/pnpm/pnpm/commit/1495cb0):
Added per-package SBOM generation with `--out` and `--split` flags. Use
`--out out/%s.cdx.json` to write one SBOM per workspace package to
individual files, or `--split` for NDJSON output to stdout. When
`--filter` selects a single package, the SBOM root component now uses
that package's metadata. Workspace inter-dependencies (`workspace:`
protocol) and their transitive dependencies are included. Author,
repository, and license fall back to the root manifest when the package
doesn't define them.
- [`293921a`](https://redirect.github.com/pnpm/pnpm/commit/293921a):
feat(view): support searching project manifest upward when package name
is omitted

When running `pnpm view` without a package name, the command now
searches
upward for the nearest project manifest (`package.json`, `package.yaml`,
or `package.json5`) and uses its `name` field.
  If the manifest exists but lacks a `name` field, an error is thrown.

  This change also replaces the `find-up` dependency with `empathic` for
  improved performance and consistency across workspace tools.

#### Patch Changes

- [`29ab905`](https://redirect.github.com/pnpm/pnpm/commit/29ab905):
Fixed `pnpm update` overriding the version range policy of a named
catalog whose name parses as a version (e.g. `catalog:express4-21`). The
`catalog:` reference carries no pinning of its own, so the prefix from
the catalog entry (such as `~`) is now preserved instead of being
widened to `^`
[#&#8203;10321](https://redirect.github.com/pnpm/pnpm/issues/10321).

- [`bee4bf4`](https://redirect.github.com/pnpm/pnpm/commit/bee4bf4):
Security: validate config dependency names and versions from the env
lockfile (`pnpm-lock.yaml`) before using them to build filesystem paths.
A committed lockfile with a traversal-shaped `configDependencies` name
(such as `../../PWNED`) or version (such as `../../../PWNED`) could
previously cause `pnpm install` to create symlinks or write package
files outside `node_modules/.pnpm-config` and the store. Names must now
be valid npm package names and versions must be exact semver versions;
the same validation is applied to optional subdependencies of config
dependencies, and to the legacy workspace-manifest format before any
lockfile is written. See
[GHSA-qrv3-253h-g69c](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-qrv3-253h-g69c).

- [`96bdd57`](https://redirect.github.com/pnpm/pnpm/commit/96bdd57): Fix
`link:` workspace protocol switching to `file:` after `pnpm rm` is run
from inside a workspace package whose target workspace dependency has
its own dependencies, when `injectWorkspacePackages: true` is set.
Follow-up to
[#&#8203;10575](https://redirect.github.com/pnpm/pnpm/pull/10575), which
fixed the same symptom for workspace packages without dependencies.

- [`302a2f7`](https://redirect.github.com/pnpm/pnpm/commit/302a2f7): No
longer warn about using both `packageManager` and
`devEngines.packageManager` when the two fields pin the same package
manager at the same version with the same integrity hash (e.g. both
`pnpm@11.5.1+sha512.…`). Previously the hash was stripped from the
legacy `packageManager` field but not from `devEngines.packageManager`,
so even identical specifications looked like a mismatch
[#&#8203;12028](https://redirect.github.com/pnpm/pnpm/issues/12028).

The warning still fires on any genuine divergence, and several cases now
state the specific reason instead of a single generic message: a
different package manager, a different version, or contradictory
integrity hashes for the same version.

- [`3f0fb21`](https://redirect.github.com/pnpm/pnpm/commit/3f0fb21):
Fixed the progress line showing leftover characters from external
processes that write to the terminal between progress updates (e.g. an
SSH passphrase prompt would leave a fragment like `added 0sa':`). The
interactive reporter now redraws each frame in place, erasing to the end
of the display before reprinting, so any such remnants are cleared
[#&#8203;12350](https://redirect.github.com/pnpm/pnpm/issues/12350).

- [`564619f`](https://redirect.github.com/pnpm/pnpm/commit/564619f):
Fixed `pnpm approve-builds` reporting "no packages awaiting approval"
when a build-script dependency whose approval was revoked (e.g. after
`git stash` drops the `allowBuilds` from `pnpm-workspace.yaml`) is
re-added. The revoked packages are now correctly recorded in
`.modules.yaml` so `approve-builds` can find them.
[#&#8203;12221](https://redirect.github.com/pnpm/pnpm/issues/12221)

- [`3d1fd20`](https://redirect.github.com/pnpm/pnpm/commit/3d1fd20):
Skip the redundant "target bin directory already contains an exe called
node" warning on Windows when the existing `node.exe` already matches
the target (same hard link or identical content)
[pnpm/pnpm#12203](https://redirect.github.com/pnpm/pnpm/issues/12203).

- [`1b02b47`](https://redirect.github.com/pnpm/pnpm/commit/1b02b47): Fix
macOS Gatekeeper blocking native binaries (`.node`, `.dylib`, `.so`) by
removing the `com.apple.quarantine` extended attribute after importing
them from the store.

When pnpm imports files from its content-addressable store into
`node_modules`, macOS preserves extended attributes, including
`com.apple.quarantine`. If this xattr is present on a store blob (e.g.
it was first written under a Gatekeeper-enabled app such as a Git
client), it propagates to `node_modules`, and Gatekeeper blocks the
native binary from loading even though pnpm already verified the file's
integrity against the lockfile.

After importing a package, pnpm now strips `com.apple.quarantine` from
its native binaries, matching Homebrew's behaviour of dropping
quarantine from verified downloads. The cleanup is macOS-only, runs in a
single batched `xattr` call per package, is restricted to native
binaries (other files are untouched), and is non-fatal (it logs a
warning on unexpected errors).

Fixes
[#&#8203;11056](https://redirect.github.com/pnpm/pnpm/issues/11056)

- [`61969fb`](https://redirect.github.com/pnpm/pnpm/commit/61969fb): Fix
`pnpm install` with `optimisticRepeatInstall` incorrectly reporting
`Already up to date` when `pnpm-lock.yaml` changed but project manifests
did not. This affected workflows such as checking out or restoring only
the lockfile
[#&#8203;12100](https://redirect.github.com/pnpm/pnpm/issues/12100).

Also fixes `checkDepsStatus` to use the correct lockfile path when
`useGitBranchLockfile` is enabled, so the optimistic fast-path and
lockfile modification detection work with `pnpm-lock.<branch>.yaml`
files instead of always stat'ing `pnpm-lock.yaml`. Merge-conflict
detection now reads the resolved lockfile name as well, and with
`mergeGitBranchLockfiles` enabled every `pnpm-lock.*.yaml` is scanned
for modifications and conflicts. The git branch is now resolved by
reading `.git/HEAD` directly (no process spawn) and uses the workspace
directory rather than `process.cwd()`.

- [`5c12968`](https://redirect.github.com/pnpm/pnpm/commit/5c12968): Fix
recursive updates of transitive dependencies when the update command
mixes transitive dependency patterns with direct dependency selectors.
For example, `pnpm up -r "@&#8203;babel/core" uuid` now updates matching
transitive `@babel/core` dependencies even when `uuid` is a direct
dependency selector
[#&#8203;12103](https://redirect.github.com/pnpm/pnpm/issues/12103).

- [`9d79ba1`](https://redirect.github.com/pnpm/pnpm/commit/9d79ba1):
Register the `pnpm update --no-save` flag in the CLI help and option
parser.

- [`0474a9c`](https://redirect.github.com/pnpm/pnpm/commit/0474a9c):
Fixed `pnpm import` for Yarn v2 lockfiles when `js-yaml` v4 is
installed.

- [`9e0c375`](https://redirect.github.com/pnpm/pnpm/commit/9e0c375):
Fixed `pnpm install` repeatedly prompting to remove and reinstall
`node_modules` in a workspace package when `enableGlobalVirtualStore` is
enabled. The post-install build step recorded a per-project
`node_modules/.pnpm` virtual store directory in
`node_modules/.modules.yaml`, overwriting the global `<storeDir>/links`
value the install step had written. The next install then detected a
virtual-store mismatch (`ERR_PNPM_UNEXPECTED_VIRTUAL_STORE`). The build
step now derives the same global virtual store directory as the install
step
[#&#8203;12307](https://redirect.github.com/pnpm/pnpm/issues/12307).

- [`223d060`](https://redirect.github.com/pnpm/pnpm/commit/223d060):
Document the `--cpu`, `--os` and `--libc` flags in the output of `pnpm
install --help`. These flags were already supported but were only
documented on the website
[#&#8203;12359](https://redirect.github.com/pnpm/pnpm/issues/12359).

- [`e85aea2`](https://redirect.github.com/pnpm/pnpm/commit/e85aea2):
Avoid reading `README.md` from disk when publishing if the publish
manifest already provides a `readme` field. The README is now only read
lazily, inside `createExportableManifest`, when it is actually needed.

- [`3188ae7`](https://redirect.github.com/pnpm/pnpm/commit/3188ae7):
Fixed `pnpm peers check` to accept loose peer dependency ranges such as
`>=3.16.0 || >=4.0.0-` when the installed peer version satisfies the
range
[#&#8203;12149](https://redirect.github.com/pnpm/pnpm/issues/12149).

- [`531f2a3`](https://redirect.github.com/pnpm/pnpm/commit/531f2a3):
Fixed `pnpm update` rewriting a `workspace:` dependency that points at a
local path (e.g. `workspace:../packages/foo/dist`) into a normalized
`link:` or version-range specifier. Such specifiers are now preserved
verbatim when the workspace protocol is preserved
[#&#8203;3902](https://redirect.github.com/pnpm/pnpm/issues/3902).

- [`fe66535`](https://redirect.github.com/pnpm/pnpm/commit/fe66535):
Fixed a lockfile non-convergence bug where an incremental install kept a
duplicate transitive dependency that a fresh install would not produce.
When a package is reused from the lockfile, its child edges are taken
verbatim and bypass the preferred-versions walk, so a transitive
dependency could stay pinned to an older version even after a direct
dependency resolved to a higher version that satisfies the same range.
The resolver now refreshes such a stale pin to the higher
direct-dependency version during resolution — so the older version is
never resolved or fetched, and the incremental result converges to the
fresh one.

- [`6d35338`](https://redirect.github.com/pnpm/pnpm/commit/6d35338):
`pnpm install` detects changes inside local file dependencies again. The
optimistic repeat-install fast path only tracks manifest and lockfile
modification times, so edits inside a local dependency's directory (or a
repacked local tarball) were reported as "Already up to date". Projects
with local file dependencies (`file:` and bare local path or tarball
specifiers, declared directly or through `pnpm.overrides`) now always
run a full install, which refetches those dependencies, matching pnpm
v10 behavior
[#&#8203;11795](https://redirect.github.com/pnpm/pnpm/issues/11795).

- [`4ca9247`](https://redirect.github.com/pnpm/pnpm/commit/4ca9247):
Preserve the existing Node.js runtime version prefix when resolving
`node@runtime:<range>` to a concrete version.

- [`30c7590`](https://redirect.github.com/pnpm/pnpm/commit/30c7590):
Create shorter CAFS temporary package directories to leave room for
lifecycle scripts that create IPC socket paths under TMPDIR.

- [`13815ad`](https://redirect.github.com/pnpm/pnpm/commit/13815ad):
Reporter output (warnings, progress) for `pnpm store` and `pnpm config`
subcommands now goes to stderr instead of stdout. This fixes scripts
that capture their stdout (e.g. `PNPM_STORE=$(pnpm store path)`, `pnpm
config list --json | jq`) from getting warnings mixed into the result.

- [`1c05876`](https://redirect.github.com/pnpm/pnpm/commit/1c05876):
Avoid relinking unchanged child dependencies and remove stale child
links during warm installs.

- [`817f99d`](https://redirect.github.com/pnpm/pnpm/commit/817f99d):
Fixed lockfile churn where a package's `transitivePeerDependencies`
could be dropped (and shift between packages) when the package
participates in a dependency cycle. A cycle re-entry resolves against
truncated children, so it must not be cached as "pure"; otherwise
sibling occurrences of the same package short-circuit and lose
transitive peers depending on traversal order
[#&#8203;5108](https://redirect.github.com/pnpm/pnpm/issues/5108).

- [`eba03e0`](https://redirect.github.com/pnpm/pnpm/commit/eba03e0): Fix
`pnpm install` reporting "Already up to date" after a catalog entry in
`pnpm-workspace.yaml` was reverted to a previous version. After an
update modified a catalog, the workspace state cache stored the
pre-update catalog versions, so reverting the entry back to its original
version was not detected as an outdated state
[#&#8203;12418](https://redirect.github.com/pnpm/pnpm/issues/12418).

- [`3b54d79`](https://redirect.github.com/pnpm/pnpm/commit/3b54d79):
`pnpm update` now keeps lockfile `overrides` that resolve through a
catalog in sync with the catalog. Previously, when an override
referenced a catalog (e.g. `overrides: { foo: 'catalog:' }`) and `pnpm
update` bumped that catalog entry, the lockfile's `catalogs` advanced
while the resolved `overrides` kept the old version. The resulting
lockfile was internally inconsistent, so a later `pnpm install
--frozen-lockfile` failed with `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`.

- [`9d0a300`](https://redirect.github.com/pnpm/pnpm/commit/9d0a300):
Fixed `pnpm version --recursive` so it honors the workspace selection.
In recursive mode the version bump now applies to the packages resolved
from the workspace filter (`selectedProjectsGraph`), matching the
behavior of `pnpm publish --recursive`, instead of always bumping every
workspace package
[#&#8203;11348](https://redirect.github.com/pnpm/pnpm/issues/11348).

<!-- sponsors -->

#### Platinum Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer"><img
src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160"
alt="OpenAI" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

#### Gold Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity"
/>
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220"
alt="Discord" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer"><img
src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160"
alt="SerpApi" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220"
alt="CodeRabbit" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190"
alt="Stackblitz" />
          </picture>
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190"
alt="Workleap" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank" rel="noopener noreferrer">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

<!-- sponsors end -->

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-24 21:13:13 +00:00
Eyüp Can AkmanandGitHub ef927f9fa3 feat(api): support ref suffixes in compare (#38148)
Compare API requests with a `^` or `~N` revision suffix (for example
`compare/main...feature^`) were rejected with `400 Unsupported
comparison syntax: ref with suffix`. The fix resolves the suffix to a
commit before comparing, so `base...head^` and `~N` work on either side,
the same as git.

Only `^`/`~N` navigation is resolved. Pull request creation still
requires plain branch refs, and the web compare page keeps rejecting
suffixes since its branch selectors need separate UI work.

Closes #33943
2026-06-24 05:38:02 +00:00
GiteabotandGitHub 59d4825a95 chore(deps): update module golang.org/x/vuln to v1.4.0 (#38201) 2026-06-23 10:25:02 +02:00
GiteaBot 10da460c1b [skip ci] Updated translations via Crowdin 2026-06-23 01:11:28 +00:00
GiteabotandGitHub 2003cf4e87 chore(deps): update actions/checkout action to v7 (#38199) 2026-06-22 22:46:55 +02:00
736ab982c8 enhance: allow builtin default git config options to be overridden (#38172)
This is really a follow-up to
[#38148](https://github.com/go-gitea/gitea/pull/35305) , instead of
having specific mappings of options for git configurations, just honor
any user-provided gitconfig. I include a test which points out the
specific config I have which was previously not honored, but more
generally this means that gitea now only *adds* new gitconfig and never
overwrites any config provided under `[git.config]`.

---------

Signed-off-by: Royce Remer <royceremer@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-22 18:29:06 +00:00
GiteaBot 08a18d36a6 [skip ci] Updated translations via Crowdin 2026-06-22 17:52:08 +00:00
8a6697123f chore(deps): update action dependencies (#38191)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[crowdin/github-action](https://redirect.github.com/crowdin/github-action)
| action | patch | `v2.16.2` → `v2.16.3` |
| [pnpm/action-setup](https://redirect.github.com/pnpm/action-setup) |
action | patch | `v6.0.8` → `v6.0.9` |

---

### Release Notes

<details>
<summary>crowdin/github-action (crowdin/github-action)</summary>

###
[`v2.16.3`](https://redirect.github.com/crowdin/github-action/releases/tag/v2.16.3)

[Compare
Source](https://redirect.github.com/crowdin/github-action/compare/v2.16.2...v2.16.3)

#### What's Changed

- CLI
[4.14.3](https://redirect.github.com/crowdin/crowdin-cli/releases/tag/4.14.3)
by [@&#8203;andrii-bodnar](https://redirect.github.com/andrii-bodnar)

**Full Changelog**:
<https://github.com/crowdin/github-action/compare/v2.16.2...v2.16.3>

</details>

<details>
<summary>pnpm/action-setup (pnpm/action-setup)</summary>

###
[`v6.0.9`](https://redirect.github.com/pnpm/action-setup/releases/tag/v6.0.9)

[Compare
Source](https://redirect.github.com/pnpm/action-setup/compare/v6.0.8...v6.0.9)

##### What's Changed

- fix: update pnpm to v11.7.0 by
[@&#8203;zkochan](https://redirect.github.com/zkochan) in
[#&#8203;267](https://redirect.github.com/pnpm/action-setup/pull/267)

**Full Changelog**:
<https://github.com/pnpm/action-setup/compare/v6...v6.0.9>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: bircni <bircni@icloud.com>
2026-06-22 09:23:46 +00:00
GiteabotandGitHub 2cd4506120 fix(deps): update npm dependencies (#38193)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| @&#8203;codemirror/search | [`6.7.0` →
`6.7.1`](https://renovatebot.com/diffs/npm/@codemirror%2fsearch/6.7.0/6.7.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@codemirror%2fsearch/6.7.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@codemirror%2fsearch/6.7.0/6.7.1?slim=true)
|
| [@playwright/test](https://playwright.dev)
([source](https://redirect.github.com/microsoft/playwright)) | [`1.60.0`
→
`1.61.0`](https://renovatebot.com/diffs/npm/@playwright%2ftest/1.60.0/1.61.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@playwright%2ftest/1.61.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@playwright%2ftest/1.60.0/1.61.0?slim=true)
|
| [happy-dom](https://redirect.github.com/capricorn86/happy-dom) |
[`20.10.2` →
`20.10.5`](https://renovatebot.com/diffs/npm/happy-dom/20.10.2/20.10.5)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/20.10.5?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/20.10.2/20.10.5?slim=true)
|
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`11.5.3` →
`11.7.0`](https://renovatebot.com/diffs/npm/pnpm/11.5.3/11.7.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/11.7.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/11.5.3/11.7.0?slim=true)
|
| [vitest](https://vitest.dev)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest))
| [`4.1.8` →
`4.1.9`](https://renovatebot.com/diffs/npm/vitest/4.1.8/4.1.9) |
![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.9?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.8/4.1.9?slim=true)
|
| [vue](https://vuejs.org/)
([source](https://redirect.github.com/vuejs/core)) | [`3.5.37` →
`3.5.38`](https://renovatebot.com/diffs/npm/vue/3.5.37/3.5.38) |
![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.38?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.37/3.5.38?slim=true)
|
| [vue-tsc](https://redirect.github.com/vuejs/language-tools)
([source](https://redirect.github.com/vuejs/language-tools/tree/HEAD/packages/tsc))
| [`3.3.4` →
`3.3.5`](https://renovatebot.com/diffs/npm/vue-tsc/3.3.4/3.3.5) |
![age](https://developer.mend.io/api/mc/badges/age/npm/vue-tsc/3.3.5?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-tsc/3.3.4/3.3.5?slim=true)
|

---

### Release Notes

<details>
<summary>microsoft/playwright (@&#8203;playwright/test)</summary>

###
[`v1.61.0`](https://redirect.github.com/microsoft/playwright/releases/tag/v1.61.0)

[Compare
Source](https://redirect.github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

#### 🔑 WebAuthn passkeys

New [Credentials](https://playwright.dev/docs/api/class-credentials)
virtual authenticator, available via
[browserContext.credentials](https://playwright.dev/docs/api/class-browsercontext#browser-context-credentials),
lets tests register passkeys and answer `navigator.credentials.create()`
/ `navigator.credentials.get()` ceremonies in the page — no real
hardware key required, works in all browsers:

```js
const context = await browser.newContext();

// Seed a passkey your backend provisioned for a test user.
await context.credentials.create('example.com', {
  id: credentialId,
  userHandle,
  privateKey,
  publicKey,
});
await context.credentials.install();

const page = await context.newPage();
await page.goto('https://example.com/login');
// The page's navigator.credentials.get() is answered with the seeded passkey.
```

You can also let the app register a passkey once in a setup test, read
it back with
[credentials.get()](https://playwright.dev/docs/api/class-credentials#credentials-get),
and seed it into later tests — see
[Credentials](https://playwright.dev/docs/api/class-credentials) for
details.

#### 🗃️ Web Storage

New [WebStorage](https://playwright.dev/docs/api/class-webstorage) API,
available via
[page.localStorage](https://playwright.dev/docs/api/class-page#page-local-storage)
and
[page.sessionStorage](https://playwright.dev/docs/api/class-page#page-session-storage),
reads and writes the page's storage for the current origin:

```js
await page.localStorage.setItem('token', 'abc');
const token = await page.localStorage.getItem('token');
const items = await page.sessionStorage.items();
```

#### New APIs

##### Network

-
[apiResponse.securityDetails()](https://playwright.dev/docs/api/class-apiresponse#api-response-security-details)
and
[apiResponse.serverAddr()](https://playwright.dev/docs/api/class-apiresponse#api-response-server-addr)
mirror the browser-side
[response.securityDetails()](https://playwright.dev/docs/api/class-response#response-security-details)
and
[response.serverAddr()](https://playwright.dev/docs/api/class-response#response-server-addr).

##### Browser and Screencast

- New option `artifactsDir` in
[browserType.connectOverCDP()](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp)
controls where artifacts such as traces and downloads are stored when
attached to an existing browser.
- New option `cursor` in
[screencast.showActions()](https://playwright.dev/docs/api/class-screencast#screencast-show-actions)
controls the cursor decoration rendered for pointer actions.
- The `onFrame` callback in
[screencast.start()](https://playwright.dev/docs/api/class-screencast#screencast-start)
now receives a `timestamp` of when the frame was presented by the
browser.

##### Test runner

- The
[testOptions.video](https://playwright.dev/docs/api/class-testoptions#test-options-video)
option now supports the same set of modes as `trace`: new
`'on-all-retries'`, `'retain-on-first-failure'` and
`'retain-on-failure-and-retries'` values. See the [video modes
table](https://playwright.dev/docs/test-use-options#video-modes) for
which runs are recorded and kept in each mode.
- Supported `expect.soft.poll(...)`.
- New
[fullConfig.argv](https://playwright.dev/docs/api/class-fullconfig#full-config-argv)
— a snapshot of `process.argv` from the runner process, handy for
reading custom arguments passed after the `--` separator.
- New
[fullConfig.failOnFlakyTests](https://playwright.dev/docs/api/class-fullconfig#full-config-fail-on-flaky-tests)
mirrors the config option, so reporters can explain why a flaky run
failed.
-
[testInfo.errors](https://playwright.dev/docs/api/class-testinfo#test-info-errors)
now lists each sub-error of an `AggregateError` as a separate entry.
- New `-G` command line shorthand for `--grep-invert`.

#### 🛠️ Other improvements

- Playwright now supports Ubuntu 26.04.
- HAR and trace recordings now include WebSocket requests.

#### Browser Versions

- Chromium 149.0.7827.55
- Mozilla Firefox 151.0
- WebKit 26.5

This version was also tested against the following stable channels:

- Google Chrome 149
- Microsoft Edge 149

</details>

<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>

###
[`v20.10.5`](https://redirect.github.com/capricorn86/happy-dom/releases/tag/v20.10.5)

[Compare
Source](https://redirect.github.com/capricorn86/happy-dom/compare/v20.10.4...v20.10.5)

##### :construction\_worker\_man: Patch fixes

- Adds cache to query selector parser - By
**[@&#8203;capricorn86](https://redirect.github.com/capricorn86)** in
task
[#&#8203;2142](https://redirect.github.com/capricorn86/happy-dom/issues/2142)
- The selector parser degraded in performance in v20.6.3 to solve more
complex selectors
- Parsing is still a bit slower, but the cache will hopefully mitigate
most of the problem

###
[`v20.10.4`](https://redirect.github.com/capricorn86/happy-dom/releases/tag/v20.10.4)

[Compare
Source](https://redirect.github.com/capricorn86/happy-dom/compare/v20.10.3...v20.10.4)

##### :construction\_worker\_man: Patch fixes

- Coerce null qualifiedName to empty string in createDocument - By
**[@&#8203;Firer](https://redirect.github.com/Firer)** in task
[#&#8203;2206](https://redirect.github.com/capricorn86/happy-dom/issues/2206)

###
[`v20.10.3`](https://redirect.github.com/capricorn86/happy-dom/releases/tag/v20.10.3)

[Compare
Source](https://redirect.github.com/capricorn86/happy-dom/compare/v20.10.2...v20.10.3)

##### :construction\_worker\_man: Patch fixes

- Fix "\~=" attribute selector matching hyphenated substrings in CSS
selectors - By
**[@&#8203;mixelburg](https://redirect.github.com/mixelburg)** in task
[#&#8203;2194](https://redirect.github.com/capricorn86/happy-dom/issues/2194)

</details>

<details>
<summary>pnpm/pnpm (pnpm)</summary>

###
[`v11.7.0`](https://redirect.github.com/pnpm/pnpm/releases/tag/v11.7.0):
pnpm 11.7

[Compare
Source](https://redirect.github.com/pnpm/pnpm/compare/v11.6.0...v11.7.0)

#### Minor Changes

- Added a new setting `frozenStore` (`--frozen-store`) that lets `pnpm
install` run against a package store on a read-only filesystem (e.g. a
Nix store, a read-only bind mount, an OCI layer). When enabled, pnpm
opens the store's SQLite `index.db` through the `immutable=1` URI —
bypassing the WAL/`-shm` sidecar creation that otherwise fails on a
read-only directory — and suppresses every store-write path (the
`index.db` writer and the project-registry write). Pair it with
`--offline --frozen-lockfile` against a fully-populated store. Under the
global virtual store, package directories live inside the store, so if
the store is missing the build output of a package whose lifecycle
scripts are approved (or that has a patch), pnpm fails up front with
`ERR_PNPM_FROZEN_STORE_NEEDS_BUILD` rather than crashing mid-build on a
read-only write — seed the store with those builds first. Incompatible
with `--force` and with a configured pnpr server, since both write into
the store; the side-effects cache is likewise not written under
`frozenStore`. If the store is missing its content directory, the
install fails fast with `ERR_PNPM_FROZEN_STORE_INCOMPLETE` rather than
attempting to initialize it. The read-only `immutable=1` open requires
Node.js >=22.15.0, >=23.11.0, or >=24.0.0; on older runtimes
`--frozen-store` fails with a clear
`ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE` error. Bin-linking also
tolerates a read-only store: under the global virtual store a package's
bin source lives inside the store, so the `chmod` that makes it
executable would be refused — with `EPERM`/`EACCES`, or with `EROFS` on
a genuinely read-only filesystem. That `chmod` is redundant when the
seed already ships its bins executable with a normalized shebang, so it
is now skipped in that case, while a non-executable bin (or one still
carrying a Windows CRLF shebang) on a read-only store still errors.
- When
[`pacquet`](https://redirect.github.com/pnpm/pnpm/tree/main/pacquet)
(the Rust port of pnpm) is declared in `configDependencies`, pnpm now
delegates dependency **resolution** to it too — not just materialization
— provided the installed pacquet is new enough to support full resolving
installs (>= 0.11.7).

Previously pacquet only ran in frozen-install mode: pnpm always resolved
the dependency graph itself (writing `pnpm-lock.yaml`) and handed
pacquet a finished lockfile to fetch / import / link. With pacquet >=
0.11.7, a non-frozen `pnpm install` (default isolated `nodeLinker`,
plain install) is delegated to pacquet end-to-end in a single pass —
pacquet resolves the manifests, writes the lockfile, and materializes
`node_modules`. pnpm detects the capability from the installed pacquet's
version; older pacquet releases keep the resolve-then-materialize split,
and `add` / `update` / `remove` still resolve in pnpm (it has to mutate
the manifests first). This remains an opt-in preview of the Rust install
engine
[#&#8203;11723](https://redirect.github.com/pnpm/pnpm/issues/11723).
- Added a new opt-in `--batch` flag to `pnpm publish --recursive` that
sends all selected packages to the registry in a single `PUT
/-/pnpm/v1/publish` request instead of one request per package. The
target registry has to implement the batch publish endpoint (pnpr does);
registries that don't are reported with a clear
`ERR_PNPM_BATCH_PUBLISH_UNSUPPORTED` error. The batch is processed
all-or-nothing by pnpr: if any package in the batch fails validation,
none of the packages are published.

#### Patch Changes

- Reject path-traversal and reserved dependency aliases (such as
`../../../escape`, `.bin`, `.pnpm`, or `node_modules`) that come from a
lockfile rather than a freshly resolved manifest. A crafted lockfile
alias could otherwise be joined directly under a hoisted `node_modules`
directory, letting package files be written outside the intended install
root or overwrite pnpm-owned layout.

  The fix adds two layers:

- The `nodeLinker: hoisted` graph builder now validates each alias at
the directory sink (`safeJoinModulesDir`), matching the validation pnpm
already performs when resolving aliases from manifests.
- The lockfile verification gate (`verifyLockfileResolutions`) now runs
an always-on, policy-independent check that rejects any importer or
snapshot dependency alias that is not a valid package name, failing the
install early — before any fetch or filesystem work — for every node
linker at once.

- Made shared package child resolution deterministic when the same
package is reached through multiple contexts. pnpm now chooses the
shallowest occurrence, then importer order, then parent path, instead of
letting request timing decide the child context and missing-peer report
[pnpm/pnpm#12358](https://redirect.github.com/pnpm/pnpm/issues/12358).

- Fix garbled summary line after submitting `pnpm update -i` and `pnpm
audit --fix -i`. The interactive checkbox prompt previously printed
every selected choice's full table row (label, current/target versions,
workspace, URL) joined by commas, producing a wall of text after
pressing Enter. The summary now lists only the selected package names
(or vulnerability keys) by setting an explicit `short` per choice; the
in-progress selection UI is unchanged.

- Prevent `pnpm patch-remove` from removing files outside the configured
patches directory.

- Fixed `pnpm publish` ignoring `strictSsl: false` when publishing to
registries with self-signed certificates. The `strictSSL` option is now
forwarded to `libnpmpublish` / `npm-registry-fetch` so that
`strict-ssl=false` in `.npmrc` or `strictSsl: false` in
`pnpm-workspace.yaml` is respected during publish, the same way it is
for `pnpm install`
[pnpm/pnpm#12012](https://redirect.github.com/pnpm/pnpm/issues/12012).

- Fixed `Cannot destructure property 'manifest' of
'manifestsByPath[rootDir]' as it is undefined` regression introduced in
11.6.0 when running `pnpm add <pkg>` outside a workspace on Windows.
`selectProjectByDir` was keying the resulting `ProjectsGraph` by
`opts.dir` instead of `project.rootDir`, so downstream `manifestsByPath`
lookups missed when the two paths normalized differently (typically
drive-letter casing).
[pnpm/pnpm#12379](https://redirect.github.com/pnpm/pnpm/issues/12379)

- Git dependencies that point to a subdirectory of a repository
(`repo#commit&path:/sub/dir`) keep their `path` in the lockfile again.
Since the integrity of git-hosted tarballs started being pinned in the
lockfile, any install that actually downloaded the tarball rebuilt the
lockfile resolution as `{ integrity, tarball, gitHosted }` and dropped
the `path` field, while installs served from the store kept it — so the
field disappeared seemingly at random. Without `path`, later installs
from that lockfile silently unpacked the repository root instead of the
subdirectory
[#&#8203;12304](https://redirect.github.com/pnpm/pnpm/issues/12304).

- Fixed nondeterministic lockfile output that made `pnpm dedupe --check`
fail intermittently in CI. When a locked peer provider was pinned for a
dependency that has no child dependencies of its own, the pinned
provider leaked into the shared parent scope, so siblings resolved after
it could pick up an optional peer they should not see. Which siblings
were affected depended on resolution order, which varies with network
timing.

- Sped up `pnpm install` with a frozen lockfile by running lockfile
verification (the policy revalidation gate added for
`minimumReleaseAge`/`trustPolicy` and the tarball-URL anti-tamper check)
concurrently with fetching and linking instead of blocking the whole
install on it. Dependency lifecycle scripts are still held back until
verification succeeds, so no script runs on an unverified lockfile: if
verification fails the install aborts before any dependency build, and
if linking finishes first the install waits for the verification verdict
before completing.

- User-defined `npm_config_*` environment variables are now preserved
during lifecycle script execution. Previously, all `npm_`-prefixed env
vars were stripped, which caused user-set variables like
`npm_config_platform_arch` to be lost
[pnpm/pnpm#12399](https://redirect.github.com/pnpm/pnpm/issues/12399).

- pnpm can now use different auth tokens for different package scopes,
even when those scopes use the same registry URL.

Previously, auth was selected only by registry URL. If `@org-a` and
`@org-b` both used `https://npm.pkg.github.com/`, they had to share the
same token. This caused problems for registries that issue tokens per
organization or per scope.

Configure a scope-specific token by adding the package scope after the
registry URL in the auth key:

  ```ini
  @&#8203;org-a:registry=https://npm.pkg.github.com/
  @&#8203;org-b:registry=https://npm.pkg.github.com/

  //npm.pkg.github.com/:@&#8203;org-a:_authToken=${ORG_A_TOKEN}
  //npm.pkg.github.com/:@&#8203;org-b:_authToken=${ORG_B_TOKEN}

  //npm.pkg.github.com/:_authToken=${FALLBACK_TOKEN}
  ```

`pnpm login --registry=https://npm.pkg.github.com --scope=@&#8203;org-a`
writes the token to the same scope-specific auth key.

When installing or publishing `@org-a/*`, pnpm uses `ORG_A_TOKEN`. For
`@org-b/*`, pnpm uses `ORG_B_TOKEN`. Packages without a matching scope
continue to use the registry-wide fallback token.

- `pnpm setup` no longer prompts to approve build scripts for
`@pnpm/exe` when installing the standalone executable. pnpm links the
platform-specific binary itself, so the package's install scripts are
skipped during the global self-install
[#&#8203;12377](https://redirect.github.com/pnpm/pnpm/issues/12377).

- Close lockfile reads deterministically before rewriting lockfiles and
keep pacquet's virtual store directory length aligned with pnpm on
Windows.

- A `304 Not Modified` answer from the registry now renews the cached
metadata file's mtime, so the `minimumReleaseAge` freshness shortcut
keeps serving resolutions from the cache. Previously, once a cached
packument grew older than `minimumReleaseAge`, every subsequent install
re-validated it against the registry forever, because a 304 never
rewrites the file.

- Updated dependency ranges. Notably:

  - `@pnpm/logger` peer dependency range moved to `^1100.0.0`.
- `msgpackr` 1.11.8 → 2.0.4 (store index files remain byte-compatible in
both directions).
- `open` ^7.4.2 → ^11.0.0, `memoize` ^10 → ^11, `cli-truncate` ^5 → ^6,
`pidtree` ^0.6 → ^1.
- `@yarnpkg/core` 4.5.0 → 4.8.0, `@rushstack/worker-pool` 0.7.7 →
0.7.18, `@cyclonedx/cyclonedx-library` 10.0.0 → 10.1.0,
`@pnpm/config.nerf-dart` ^1 → ^2, `@pnpm/log.group` 3.0.2 → 4.0.1,
`@pnpm/util.lex-comparator` ^3 → ^4.

- Updated `@zkochan/cmd-shim` to v9.0.6.

- Fixed a Windows-only hang where a failed command could take 20–46
seconds to exit. On error, pnpm enumerates descendant processes (via
`pidtree`) to terminate them, which on Windows shells out to
`wmic`/PowerShell `Get-CimInstance Win32_Process` — a lookup that is
extremely slow on some machines. The lookup is now bounded by a short
timeout so it can no longer stall the process exit.

<!-- sponsors -->

#### Platinum Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes"
target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80"
alt="Bit"></a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160"
alt="OpenAI" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

#### Gold Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity"
/>
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220"
alt="Discord" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg"
width="42" alt="Vite"></a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160"
alt="SerpApi" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220"
alt="CodeRabbit" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190"
alt="Stackblitz" />
          </picture>
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190"
alt="Workleap" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

<!-- sponsors end -->

###
[`v11.6.0`](https://redirect.github.com/pnpm/pnpm/releases/tag/v11.6.0):
pnpm 11.6

[Compare
Source](https://redirect.github.com/pnpm/pnpm/compare/v11.5.3...v11.6.0)

##### ⚠️ Security fix — environment variables in a project `.npmrc`
(action may be required)

Following
[GHSA-3qhv-2rgh-x77r](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r),
pnpm no longer expands `${ENV_VAR}` placeholders that come from a
**repository-controlled** config file, because a malicious repository
could otherwise use them to leak your environment secrets (npm tokens,
CI job tokens, etc.) to an attacker-controlled registry during install.
This applies to:

- the project/workspace `.npmrc` — `registry`, `@scope:registry`, proxy
URLs, URL-scoped keys (`//host/…`), and credential values (`_authToken`,
`_auth`, `_password`, `username`, `tokenHelper`, `cert`, `key`);
- registry URLs in `pnpm-workspace.yaml`.

Environment variables are **still** expanded in trusted config: your
user-level `~/.npmrc`, the global config, CLI options, and environment
config.

**If your authentication broke after upgrading**, move the token out of
the committed `.npmrc`:

```sh

# Writes to your user/global config, not the repository:
pnpm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
```

Or keep the `${NPM_TOKEN}` line but put it in your user-level `~/.npmrc`
instead of the repo. In **GitHub Actions**, `actions/setup-node` with
`registry-url` already writes a user-level `.npmrc`, so
`NODE_AUTH_TOKEN` keeps working. For other CI where editing each
pipeline is hard, set `PNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc` (or
`NPM_CONFIG_USERCONFIG=.npmrc`) in the CI environment to declare the
project `.npmrc` trusted.

See <https://pnpm.io/npmrc> for full migration details.

#### Minor Changes

- `pnpm install` completes without re-resolving when `pnpm-lock.yaml`
was deleted but `node_modules` is intact: the up-to-date check now
treats the current lockfile (`node_modules/.pnpm/lock.yaml`) — the
record of what the previous install materialized — as the wanted
lockfile, verifies the manifests still match it, restores
`pnpm-lock.yaml` from it, and reports "Already up to date". Previously
this scenario triggered a full resolution and a re-verification of every
locked package against the registry.

- [`615c669`](https://redirect.github.com/pnpm/pnpm/commit/615c669):
Added support for configuring URL-scoped registry settings through
`npm_config_//…` and `pnpm_config_//…` environment variables, for
example:

  ```text
  npm_config_//registry.npmjs.org/:_authToken=<token>
  pnpm_config_//registry.npmjs.org/:_authToken=<token>
  ```

This provides a file-free way to supply registry authentication. Because
the registry a value applies to is encoded in the (trusted) environment
variable name, it is host-scoped by construction and cannot be
redirected to another registry by repository-controlled config. The
environment value is treated as trusted config: it takes precedence over
a project/workspace `.npmrc` but is still overridden by command-line
options. When the same key is provided through both prefixes,
`pnpm_config_` wins.

- Raised the default network concurrency from `min(64, max(cpuCores * 3,
16))` to `min(96, max(cpuCores * 3, 64))`. Package downloads are
I/O-bound, not CPU-bound, so deriving the floor from the core count left
machines with few cores (for example 4-vCPU CI runners) downloading only
16 tarballs at a time and unable to saturate a low-latency registry. The
`networkConcurrency` setting still overrides the default.

#### Patch Changes

- Improved the warning printed when a project `.npmrc` uses an
environment variable in a registry/proxy URL or in registry credentials.
The message now explains why the setting was ignored and how to migrate
it to a trusted source — for example by moving the line to the
user-level `~/.npmrc` or running `pnpm config set "<key>" <value>` —
with a link to <https://pnpm.io/npmrc>. The `pnpm config set` example is
only suggested when the key has no `${...}` placeholder, so the snippet
is always safe to copy-paste.
- Print a "Lockfile passes supply-chain policies (verified 2h ago)"
message when lockfile verification is skipped because a cached verdict
for the same lockfile content and policy is reused. Previously the
cached short-circuit was completely silent, which made it look like the
policy gate never ran
[#&#8203;12324](https://redirect.github.com/pnpm/pnpm/issues/12324).
- Platform-specific optional dependencies are now skipped even when
their `os`/`cpu`/`libc` fields are missing from the registry metadata or
the lockfile. Some registries strip these fields from the package
metadata, which made pnpm download and install the binaries of every
platform regardless of `supportedArchitectures`. The missing platform
fields of an optional dependency are now inferred from its name (e.g.
`@nx/nx-win32-arm64-msvc` → `os: win32`, `cpu: arm64`), so
foreign-platform binaries are skipped without even downloading them
[#&#8203;11702](https://redirect.github.com/pnpm/pnpm/issues/11702).

<!-- sponsors -->

#### Platinum Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes"
target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80"
alt="Bit"></a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160"
alt="OpenAI" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

#### Gold Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity"
/>
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220"
alt="Discord" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg"
width="42" alt="Vite"></a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160"
alt="SerpApi" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220"
alt="CodeRabbit" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a
href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190"
alt="Stackblitz" />
          </picture>
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190"
alt="Workleap" />
          </picture>
        </a>
      </td>
      <td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes"
target="_blank">
          <picture>
<source media="(prefers-color-scheme: light)"
srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)"
srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

<!-- sponsors end -->

</details>

<details>
<summary>vitest-dev/vitest (vitest)</summary>

###
[`v4.1.9`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v4.1.9)

[Compare
Source](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.8...v4.1.9)

##### 🐞 Bug Fixes

- Fix `importOriginal` with optimizer and query import \[backport to v4]
- by **Hiroshi Ogawa**, **David Harris**, **Codex**and **Vladimir** in
[#&#8203;10546](https://redirect.github.com/vitest-dev/vitest/issues/10546)
[<samp>(a5180)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/a5180190c)
- **browser**:
- Wait for orchestrator readiness before resolving browser sessions
\[backport to v4] - by **Vladimir** and **Séamus O'Connor** in
[#&#8203;10555](https://redirect.github.com/vitest-dev/vitest/issues/10555)
[<samp>(7fb29)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/7fb29651a)
- Wait for iframe tester readiness before preparing \[backport to v4] -
by **Vladimir** and **Séamus O'Connor** in
[#&#8203;10497](https://redirect.github.com/vitest-dev/vitest/issues/10497)
and
[#&#8203;10556](https://redirect.github.com/vitest-dev/vitest/issues/10556)
[<samp>(fbc62)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/fbc626c40)
- **mocker**:
- Hoist vi.mock() for vite-plus/test imports \[backport to v4] - by
**Hiroshi Ogawa**, **LongYinan**, **Claude Opus 4.8** and **Vladimir**
in
[#&#8203;10548](https://redirect.github.com/vitest-dev/vitest/issues/10548)
[<samp>(2c955)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/2c9559c02)
- **pool**:
- Prevent test run hang on worker crash \[backport to v4] - by **Ari
Perkkiö** and **Jattioui Ismail** in
[#&#8203;10543](https://redirect.github.com/vitest-dev/vitest/issues/10543)
and
[#&#8203;10564](https://redirect.github.com/vitest-dev/vitest/issues/10564)
[<samp>(934b0)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/934b0f587)

##### [View changes on
GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.8...v4.1.9)

</details>

<details>
<summary>vuejs/core (vue)</summary>

###
[`v3.5.38`](https://redirect.github.com/vuejs/core/blob/HEAD/CHANGELOG.md#3538-2026-06-11)

[Compare
Source](https://redirect.github.com/vuejs/core/compare/v3.5.37...v3.5.38)

</details>

<details>
<summary>vuejs/language-tools (vue-tsc)</summary>

###
[`v3.3.5`](https://redirect.github.com/vuejs/language-tools/blob/HEAD/CHANGELOG.md#335-2026-06-13)

[Compare
Source](https://redirect.github.com/vuejs/language-tools/compare/v3.3.4...v3.3.5)

##### language-core

- **fix:** include event modifiers in duplicate listener checks
([#&#8203;6097](https://redirect.github.com/vuejs/language-tools/issues/6097))
- Thanks to [@&#8203;KazariEX](https://redirect.github.com/KazariEX)!

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2026-06-22 08:39:44 +00:00
bircniandGitHub 649cb6ff3e fix(actions): show run index in run view and fix summary graph height (#38165)
- Display the per-repository run number as `#N` next to the run title in
the run view, matching the runs list and GitHub
- Add the run `Index` to the run view API response (and the devtest
mock) to support that
- Restore the summary panel's `flex: 1` so the workflow graph fills the
right-column height even when a run has no job summaries
- Keep the job-summary section content-sized so it doesn't compete with
the graph for height
- Gate the devtest mock job summaries to a subset of runs so the devtest
page also exercises the no-summary layout

<img width="521" height="232" alt="image"
src="https://github.com/user-attachments/assets/a1f2f20b-65bd-4d98-ba6a-b8135580a6de"
/>
2026-06-22 06:16:09 +00:00
puni9869andGitHub a4781dde89 fix(indexer): fix assignee filters in issue search (#38021)
fix(indexer): fix assignee filters in issue search (#38021)

Issue search filtering still relied on the legacy single-assignee field,
so searches such as "Assigned to you" could miss issues when a keyword
query was used.

Index all issue assignee IDs and add an explicit no_assignee field so
specific, any-assignee, and no-assignee filters work consistently across
Bleve, Elasticsearch, and Meilisearch.

Fixes #36299.
2026-06-22 07:45:24 +02:00
7684221ed4 feat(actions): implement jobs.<job_id>.continue-on-error (#38100)
Support `continue-on-error` for workflow jobs when aggregating an
Actions workflow run status.

Previously, `continue-on-error` was parsed from workflow YAML but was
not persisted or used when calculating the overall run result. As a
result, a failed job could incorrectly fail the entire workflow even
when the workflow explicitly allowed that job to fail.

This PR stores the parsed `continue-on-error` value on each action run
job and treats failed jobs with `continue-on-error: true` as successful
when computing the workflow run status, matching GitHub Actions
behavior.

## Changes

- Add `ContinueOnError` to `jobparser.Job`.
- Add `continue_on_error` to `ActionRunJob` with a `NOT NULL DEFAULT
FALSE` migration.
- Populate `ActionRunJob.ContinueOnError` when creating workflow run
jobs.
- Update workflow status aggregation so failed `continue-on-error` jobs
do not fail the overall run.
- Leave `resolveCheckNeeds` unchanged so dependent jobs still see the
job result as `failure` and are skipped by default.

## Compatibility

This is backward compatible.

If only the runner or only the server is updated, `continue-on-error`
continues to degrade to the previous behavior and is effectively ignored
until both sides support it.

Related runner PR: https://gitea.com/gitea/runner/pulls/1032

---------

Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-06-22 04:51:16 +00:00
2c2611eab9 chore(deps): update dependency djlint to v1.39.2 (#38192)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [djlint](https://redirect.github.com/djlint/djLint) | `==1.39.0` →
`==1.39.2` |
![age](https://developer.mend.io/api/mc/badges/age/pypi/djlint/1.39.2?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/djlint/1.39.0/1.39.2?slim=true)
|

---

### Release Notes

<details>
<summary>djlint/djLint (djlint)</summary>

###
[`v1.39.2`](https://redirect.github.com/djlint/djLint/blob/HEAD/CHANGELOG.md#1392---2026-06-11)

[Compare
Source](https://redirect.github.com/djlint/djLint/compare/v1.39.0...v1.39.2)

v1.39.1 was not published due to mypyc compilation error.

##### Packaging

- Fix mypyc compilation.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Only on Monday (`* * * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://redirect.github.com/renovatebot/renovate).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: bircni <bircni@icloud.com>
2026-06-22 04:08:48 +00:00
bircniandGitHub 685b62c60f fix(api): don't expose private org membership via public_members (#38145) 2026-06-22 05:50:02 +02:00
e5891263f8 docs: update changelog for 1.26.3 & 1.26.4 (#38178)
Front port changelog for 1.26.3 & 1.26.4

---------

Co-authored-by: bircni <bircni@icloud.com>
2026-06-21 17:13:18 +00:00
180af33f86 perf: Various performance regression fixes (#38078)
Fixes five N+1 / O(n) query patterns found across common user paths.
Each uses a bulk query that already existed elsewhere in the codebase.

| Location | Problem | Introduced in |
| -------------------------------- |
-------------------------------------------------------------------------------------------------------------------------------
| ------------- |
| `IssueList.LoadIsRead` | `.In("issue_id")` missing its arg — xorm
generates `WHERE 0=1`, so `IsRead` is **never** set; every issue always
appears unread | #29515 |
| `ParseCommitsWithStatus` | `GetLatestCommitStatus` called once per
commit (O(n) queries on commit list / PR commits tab) | #33605 |
| `getReleaseInfos` (release list) | `GetLatestCommitStatus` called once
per release for CI badges | #29149 |
| User milestone dashboard | O(n×m) nested loop matching milestones to
repos | #26300 |
| `findCodeComments` (PR diff) | `LoadResolveDoer` + `LoadReactions`
called per inline comment — up to ~150 queries on a PR with 50 comments
| #20821 |

---------

Co-authored-by: Lauris B <lauris@nix.lv>
2026-06-21 14:34:07 +00:00
wxiaoguangandGitHub ceec230fc0 fix: walk git log context error handling (#38182)
Fix #38177

Make WalkGitLog can handle EOF and context errors correctly, and don't
export these private functions & methods & structs.
2026-06-21 13:57:22 +02:00
804b9bf120 chore: upgrade eslint plugins, remove eslint-plugin-github (#38046)
- Bump `eslint`, `typescript-eslint` and `eslint-plugin-unicorn` (to
v68), and configure the rules added in unicorn v66/v67/v68.
- Remove `eslint-plugin-github` and its workarounds (rules, type stub,
pnpm peer override, in-code `eslint-disable` comments); the rules worth
keeping are covered by `unicorn` equivalents.
- Apply the resulting fixes and autofixes across the JS codebase.

_Prepared with Claude (Opus 4.8)._

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-21 08:07:13 +02:00
bircniandGitHub 5368542f8e fix(cli): default must-change-password to false for bot users (#38175) 2026-06-20 22:26:22 +02:00
TheFox0x7andGitHub 645b10087d fix(hostmacher): patch incorrect private list (#38170)
regression from #38039
2026-06-19 21:18:17 +00:00
SEONGHYUN HONGandGitHub a12f980793 docs: fix duplicated word in foreachref doc comment (#38161)
The Format doc comment read "See See git-for-each-ref(1)" — removed the
duplicated "See" (the sibling field comments use a single "See").

Signed-off-by: s3onghyun <s3onghyun.hong@gmail.com>
2026-06-18 20:48:27 +00:00
wxiaoguangandGitHub 21bcca798b fix: csp (#38162)
ref:
https://github.com/go-gitea/gitea/issues/8707#issuecomment-4741577316
2026-06-18 18:21:41 +00:00
316 changed files with 9512 additions and 4960 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ runs:
using: composite
steps:
- uses: ./.github/actions/free-disk-space
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
+1 -1
View File
@@ -9,7 +9,7 @@ inputs:
runs:
using: composite
steps:
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- if: ${{ inputs.cache == 'true' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
gobuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: TAGS="bindata" make backend
@@ -59,7 +59,7 @@ jobs:
include:
- { tags: "bindata", target: "lint-backend" }
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
+2 -2
View File
@@ -12,8 +12,8 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
+2 -2
View File
@@ -20,8 +20,8 @@ jobs:
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
+2 -2
View File
@@ -12,8 +12,8 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
with:
upload_sources: true
upload_translations: false
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
e2e: ${{ steps.changes.outputs.e2e }}
shell: ${{ steps.changes.outputs.shell }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
steps:
# pull_request_review runs without repository secrets on fork PRs, so fall
# back to the workflow token for the non-backport checks handled here.
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
with:
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
+5 -5
View File
@@ -19,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
@@ -31,7 +31,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
with:
cache: "false"
@@ -62,7 +62,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
@@ -72,7 +72,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/node-setup
- run: make deps-frontend
- run: make lint-frontend
@@ -85,7 +85,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
+7 -7
View File
@@ -42,7 +42,7 @@ jobs:
ports:
- "9000:9000"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/pgsql-shard
with:
@@ -78,7 +78,7 @@ jobs:
ports:
- "9000:9000"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/pgsql-shard
with:
@@ -90,7 +90,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend
- run: make backend
@@ -131,7 +131,7 @@ jobs:
ports:
- "7700:7700"
redis:
image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
@@ -151,7 +151,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
@@ -208,7 +208,7 @@ jobs:
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
@@ -241,7 +241,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/amd64
@@ -31,7 +31,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/arm64
@@ -41,7 +41,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/riscv64
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/node-setup
- run: make deps-frontend
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
pull-requests: write
steps:
# Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -17,11 +17,15 @@ jobs:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Authenticate snapcraft
shell: bash
run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS")
- name: Remote build
run: |
snapcraft remote-build \
+8 -4
View File
@@ -13,16 +13,17 @@ jobs:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
@@ -33,6 +34,8 @@ jobs:
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
@@ -45,6 +48,7 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
@@ -75,7 +79,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+8 -4
View File
@@ -14,16 +14,17 @@ jobs:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
@@ -34,6 +35,8 @@ jobs:
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
@@ -46,6 +49,7 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
@@ -86,7 +90,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+8 -4
View File
@@ -17,16 +17,17 @@ jobs:
permissions:
contents: read
packages: write # to publish to ghcr.io
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
@@ -37,6 +38,8 @@ jobs:
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
@@ -49,6 +52,7 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
@@ -89,7 +93,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+1
View File
@@ -107,6 +107,7 @@ linters:
- -QF1008
testifylint:
disable:
- empty
- go-require
- require-error
usetesting:
+50
View File
@@ -4,6 +4,56 @@ 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.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21
* SECURITY
* fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009) (#38183)
* BUGFIXES
* fix: walk git log context error handling (#38182) (#38185)
## [1.26.3](https://github.com/go-gitea/gitea/releases/tag/1.26.3) - 2026-06-18
* BREAKING
* fix(actions)!: require merged PR to bypass fork PR approval gate (#38010) (#38041)
* SECURITY
* fix(hostmatcher): patch incorrect private list (#38170) (#38173)
* fix: Various security fixes (#38103) (#38151)
* fix: Various sec fixes (#38108) (#38147)
* fix: allow git clone of private repos with anonymous code access (#38074) (#38146)
* fix(auth): ignore stale OIDC external login links to organizations (#37875) (#38141)
* fix(hostmatcher): block reserved IP ranges from external/private filters (#38039) (#38059)
* fix(lfs): require Code-unit access for cross-repo LFS object reuse (#38006) (#38050)
* fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) (#38015)
* fix: bound CODEOWNERS regex match time (#38011) (#38025)
* fix: bound debian ParseControlFile to a single control stanza (#38044) (#38055)
* fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829)
* API
* feat(api): add Link header in ListForks (#38052) (#38063)
* BUGFIXES
* fix: Fix the panic when ssh remote lfs endpoint parsing failure (#38026) (#38158)
* fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115)
* fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) (#38096)
* fix: parse HEAD ref (#38119)
* fix: git cmd (#38084) (#38087)
* fix(releases): generate notes for initial tag (#37697) (#37986)
* fix(actions): return 404 when job log blob is missing (#38003) (#38004)
* fix(actions): exclude `workflow_call` from workflow trigger detection (#37894) (#37899)
* fix(actions): keep action run title clickable when commit subject is a URL (#37867) (#37898)
* fix(actions): reject workflow_dispatch for workflows without that trigger (#37660) (#37895)
* fix(actions): ack re-sent `UpdateLog` finalize idempotently (#37885) (#37892)
* fix: http content file render (#37850) (#37856)
* fix(issues): clear stale ReviewTypeRequest when submitting pending review (#37809) (#37815)
* fix: Fix issue target branch selection for non-collaborators (#36916) (#38164)
* BUILD
* fix(deps): update `@playwright/test` to 1.60.0 (#38144)
* ci: add `tools/ci-tools.ts` for the PR labeler workflow (#37831)
* fix(build): swagger css import (#37801) (#37803)
## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20
* SECURITY
+5 -4
View File
@@ -18,7 +18,7 @@ GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
@@ -127,6 +127,7 @@ BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* mo
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
SVG_DEST_DIR := public/assets/img/svg
SVG_DEST_DIRS := $(SVG_DEST_DIR) options/fileicon
AIR_TMP_DIR := .air
@@ -633,10 +634,10 @@ svg: node_modules ## build svg files
.PHONY: svg-check
svg-check: svg
@git add $(SVG_DEST_DIR)
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
@git add $(SVG_DEST_DIRS)
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIRS)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIRS)' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
+2 -1
View File
@@ -158,7 +158,8 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
}
isAdmin := c.Bool("admin")
mustChangePassword := true // always default to true
// Only local, existing, regular users should be forced to update their password. Bot users for example are non-interactive
mustChangePassword := userType == user_model.UserTypeIndividual
if c.IsSet("must-change-password") {
if userType != user_model.UserTypeIndividual {
return errors.New("must-change-password flag can only be set for individual users")
+1
View File
@@ -63,6 +63,7 @@ func TestAdminUserCreate(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd)
assert.False(t, u.MustChangePassword, "bot users should not be forced to change password")
})
t.Run("AccessToken", func(t *testing.T) {
+12 -1
View File
@@ -203,7 +203,7 @@ func TestCliCmdError(t *testing.T) {
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
r, err = runTestApp(app, "./gitea", "test-cmd")
@@ -235,3 +235,14 @@ func TestCliCmdBefore(t *testing.T) {
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
assert.Equal(t, "/dev/null", configValues["action"])
}
func TestCliCmdCompletion(t *testing.T) {
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error { return nil },
})
res, err := runTestApp(app, "./gitea", "completion", "bash", "--nonexist")
assert.Error(t, err)
assert.Equal(t, 1, res.ExitCode)
assert.Equal(t, "", res.Stdout)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -nonexist\n", res.Stderr)
}
+25 -4
View File
@@ -5,10 +5,10 @@ package cmd
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
@@ -154,16 +154,37 @@ func NewMainApp(appVer AppVersion) *cli.Command {
return app
}
// usageErr marks a usage error already reported by cliOnUsageError, so RunMainApp does not print it again.
type usageErr struct{ err error }
func (e usageErr) Error() string { return e.err.Error() }
func (e usageErr) Unwrap() error { return e.err }
// cliOnUsageError reports usage errors itself instead of letting urfave/cli dump the full help to stdout (since urfave/cli v3.10).
func cliOnUsageError(_ context.Context, cmd *cli.Command, err error, _ bool) error {
_, _ = fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n", err.Error())
return usageErr{err}
}
func setCLIOnUsageError(cmd *cli.Command) {
_ = cmd.Walk(func(c *cli.Command) error {
c.OnUsageError = cliOnUsageError
return nil
})
}
func RunMainApp(app *cli.Command, args ...string) error {
ctx, cancel := installSignals()
defer cancel()
setCLIOnUsageError(app)
// the completion subcommands are built during app.Run, after the Walk above, so cover them via this hook
app.ConfigureShellCompletionCommand = setCLIOnUsageError
err := app.Run(ctx, args)
if err == nil {
return nil
}
if strings.HasPrefix(err.Error(), "flag provided but not defined:") {
// the cli package should already have output the error message, so just exit
cli.OsExiter(1)
if _, ok := errors.AsType[usageErr](err); ok {
cli.OsExiter(1) // cliOnUsageError already reported it
return err
}
_, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err)
+10 -1
View File
@@ -762,7 +762,12 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Git config options
;; This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`.
;; The format is `some.configKey = value`.
;; These options will be written into the gitconfig file under "[git] HOME_PATH" when Gitea web starts.
;; ATTENTION:
;; * It only does "set" config, a removed config key from this section won't be removed from git config automatically.
;; * Some config options might affect the behavior of git and fail Gitea's git operation,
;; make sure you know what you are doing before making changes.
;[git.config]
;diff.algorithm = histogram
;core.logAllRefUpdates = true
@@ -2997,6 +3002,10 @@ LEVEL = Info
;; Comma-separated list of workflow directories, the first one to exist
;; in a repo is used to find Actions workflow files
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
;; Comma-separated list of scoped workflow directories in a source repository, the first one to exist is used.
;; Files here are picked up only when the repo is registered as a scoped-workflow source; in any other repo they neither run repo-level nor scope-level.
;; Must not overlap with WORKFLOW_DIRS. Leave empty so no directory is scanned; no scoped workflows are found or run.
;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows
;; Maximum number of attempts a single workflow run can have. Default value is 50.
;MAX_RERUN_ATTEMPTS = 50
+185 -50
View File
@@ -1,7 +1,6 @@
import arrayFunc from 'eslint-plugin-array-func';
import comments from '@eslint-community/eslint-plugin-eslint-comments';
import deMorgan from 'eslint-plugin-de-morgan';
import github from 'eslint-plugin-github';
import globals from 'globals';
import importPlugin from 'eslint-plugin-import-x';
import playwright from 'eslint-plugin-playwright';
@@ -72,7 +71,6 @@ export default defineConfig([
regexp,
sonarjs,
unicorn,
github,
wc,
},
settings: {
@@ -86,7 +84,6 @@ export default defineConfig([
'@eslint-community/eslint-comments/no-duplicate-disable': [2],
'@eslint-community/eslint-comments/no-restricted-disable': [0],
'@eslint-community/eslint-comments/no-unlimited-disable': [2],
'@eslint-community/eslint-comments/no-unused-disable': [2],
'@eslint-community/eslint-comments/no-unused-enable': [2],
'@eslint-community/eslint-comments/no-use': [0],
'@eslint-community/eslint-comments/require-description': [0],
@@ -193,7 +190,6 @@ export default defineConfig([
'@typescript-eslint/no-duplicate-type-constituents': [2, {ignoreUnions: true}],
'@typescript-eslint/no-dynamic-delete': [0],
'@typescript-eslint/no-empty-function': [0],
'@typescript-eslint/no-empty-interface': [0],
'@typescript-eslint/no-empty-object-type': [2],
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/no-extra-non-null-assertion': [2],
@@ -206,7 +202,6 @@ export default defineConfig([
'@typescript-eslint/no-invalid-this': [0],
'@typescript-eslint/no-invalid-void-type': [0],
'@typescript-eslint/no-loop-func': [0],
'@typescript-eslint/no-loss-of-precision': [0],
'@typescript-eslint/no-magic-numbers': [0],
'@typescript-eslint/no-meaningless-void-operator': [0],
'@typescript-eslint/no-misused-new': [2],
@@ -235,7 +230,7 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-assignment': [0],
'@typescript-eslint/no-unsafe-call': [0],
'@typescript-eslint/no-unsafe-declaration-merging': [2],
'@typescript-eslint/no-unsafe-enum-comparison': [2],
'@typescript-eslint/no-unsafe-enum-comparison': [0],
'@typescript-eslint/no-unsafe-function-type': [2],
'@typescript-eslint/no-unsafe-member-access': [0],
'@typescript-eslint/no-unsafe-return': [0],
@@ -279,7 +274,6 @@ export default defineConfig([
'@typescript-eslint/strict-void-return': [0],
'@typescript-eslint/switch-exhaustiveness-check': [0],
'@typescript-eslint/triple-slash-reference': [2],
'@typescript-eslint/typedef': [0],
'@typescript-eslint/unbound-method': [0], // too many false-positives
'@typescript-eslint/unified-signatures': [2],
'accessor-pairs': [2],
@@ -312,32 +306,9 @@ export default defineConfig([
'func-names': [0],
'func-style': [0],
'getter-return': [2],
'github/a11y-aria-label-is-well-formatted': [0],
'github/a11y-no-title-attribute': [0],
'github/a11y-no-visually-hidden-interactive-element': [0],
'github/a11y-role-supports-aria-props': [0],
'github/a11y-svg-has-accessible-name': [0],
'github/array-foreach': [0],
'github/async-currenttarget': [2],
'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599
'github/authenticity-token': [0],
'github/get-attribute': [0],
'github/js-class-name': [0],
'github/no-blur': [0],
'github/no-d-none': [0],
'github/no-dataset': [2],
'github/no-dynamic-script-tag': [2],
'github/no-implicit-buggy-globals': [2],
'github/no-inner-html': [0],
'github/no-innerText': [2],
'github/no-then': [2],
'github/no-useless-passive': [2],
'github/prefer-observers': [0],
'github/require-passive-events': [2],
'gitea/unescaped-html-literal': [2],
'grouped-accessor-pairs': [2],
'guard-for-in': [0],
'id-blacklist': [0],
'id-denylist': [0],
'id-length': [0],
'id-match': [0],
'import-x/consistent-type-specifier-style': [0],
@@ -384,7 +355,6 @@ export default defineConfig([
'import-x/prefer-default-export': [0],
'import-x/unambiguous': [0],
'init-declarations': [0],
'line-comment-position': [0],
'logical-assignment-operators': [0],
'max-classes-per-file': [0],
'max-depth': [0],
@@ -393,14 +363,12 @@ export default defineConfig([
'max-nested-callbacks': [0],
'max-params': [0],
'max-statements': [0],
'multiline-comment-style': [0],
'new-cap': [0],
'no-alert': [0],
'no-array-constructor': [0], // handled by @typescript-eslint/no-array-constructor
'no-async-promise-executor': [0],
'no-await-in-loop': [0],
'no-bitwise': [0],
'no-buffer-constructor': [0],
'no-caller': [2],
'no-case-declarations': [2],
'no-class-assign': [2],
@@ -557,12 +525,11 @@ export default defineConfig([
'no-nested-ternary': [0],
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
'no-new-native-nonconstructor': [2],
'no-new-object': [2],
'no-new-symbol': [0], // handled by no-new-native-nonconstructor
'no-new-wrappers': [2],
'no-new': [0],
'no-nonoctal-decimal-escape': [2],
'no-obj-calls': [2],
'no-object-constructor': [2],
'no-octal-escape': [2],
'no-octal': [2],
'no-param-reassign': [0],
@@ -622,10 +589,8 @@ export default defineConfig([
'no-warning-comments': [0],
'no-with': [0], // handled by no-restricted-syntax
'object-shorthand': [2, 'always'],
'one-var-declaration-per-line': [0],
'one-var': [0],
'operator-assignment': [2, 'always'],
'operator-linebreak': [0], // handled by @stylistic/operator-linebreak
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
'prefer-destructuring': [0],
@@ -761,139 +726,309 @@ export default defineConfig([
'strict': [0],
'symbol-description': [2],
'unicode-bom': [2, 'never'],
'unicorn/better-regex': [0],
'unicorn/better-dom-traversing': [2],
'unicorn/catch-error-name': [0],
'unicorn/class-reference-in-static-methods': [2],
'unicorn/comment-content': [0],
'unicorn/consistent-assert': [0],
'unicorn/consistent-boolean-name': [0],
'unicorn/consistent-class-member-order': [0],
'unicorn/consistent-compound-words': [0], // too opinionated
'unicorn/consistent-conditional-object-spread': [2],
'unicorn/consistent-date-clone': [2],
'unicorn/consistent-destructuring': [2],
'unicorn/consistent-empty-array-spread': [2],
'unicorn/consistent-template-literal-escape': [2],
'unicorn/consistent-existence-index-check': [0],
'unicorn/consistent-export-decorator-position': [2],
'unicorn/consistent-function-scoping': [0],
'unicorn/consistent-function-style': [2],
'unicorn/consistent-json-file-read': [2],
'unicorn/consistent-optional-chaining': [2],
'unicorn/consistent-template-literal-escape': [2],
'unicorn/custom-error-definition': [0],
'unicorn/default-export-style': [2],
'unicorn/dom-node-dataset': [2, {preferAttributes: true}],
'unicorn/empty-brace-spaces': [2],
'unicorn/error-message': [0],
'unicorn/escape-case': [0],
'unicorn/expiring-todo-comments': [0],
'unicorn/explicit-length-check': [0],
'unicorn/explicit-timer-delay': [2],
'unicorn/filename-case': [0],
'unicorn/import-index': [0],
'unicorn/id-match': [2],
'unicorn/import-style': [0],
'unicorn/isolated-functions': [2, {functions: []}],
'unicorn/logical-assignment-operators': [0],
'unicorn/max-nested-calls': [0],
'unicorn/name-replacements': [0],
'unicorn/new-for-builtins': [2],
'unicorn/no-abusive-eslint-disable': [0],
'unicorn/no-accessor-recursion': [2],
'unicorn/no-accidental-bitwise-operator': [2],
'unicorn/no-anonymous-default-export': [0],
'unicorn/no-array-callback-reference': [0],
'unicorn/no-array-for-each': [2],
'unicorn/no-array-concat-in-loop': [2],
'unicorn/no-array-fill-with-reference-type': [2],
'unicorn/no-array-from-fill': [2],
'unicorn/no-array-front-mutation': [0],
'unicorn/no-array-method-this-argument': [2],
'unicorn/no-array-push-push': [2],
'unicorn/no-array-reduce': [2],
'unicorn/no-array-reverse': [0],
'unicorn/no-array-sort': [0],
'unicorn/no-array-sort-for-min-max': [2],
'unicorn/no-array-splice': [0],
'unicorn/no-asterisk-prefix-in-documentation-comments': [0],
'unicorn/no-await-expression-member': [0],
'unicorn/no-await-in-promise-methods': [2],
'unicorn/no-blob-to-file': [2],
'unicorn/no-boolean-sort-comparator': [2],
'unicorn/no-break-in-nested-loop': [0],
'unicorn/no-canvas-to-image': [2],
'unicorn/no-chained-comparison': [2],
'unicorn/no-collection-bracket-access': [2],
'unicorn/no-computed-property-existence-check': [0],
'unicorn/no-confusing-array-splice': [2],
'unicorn/no-confusing-array-with': [2],
'unicorn/no-console-spaces': [0],
'unicorn/no-constant-zero-expression': [2],
'unicorn/no-declarations-before-early-exit': [0],
'unicorn/no-document-cookie': [2],
'unicorn/no-double-comparison': [2],
'unicorn/no-duplicate-if-branches': [2],
'unicorn/no-duplicate-logical-operands': [2],
'unicorn/no-duplicate-loops': [0],
'unicorn/no-duplicate-set-values': [2],
'unicorn/no-empty-file': [2],
'unicorn/no-error-property-assignment': [2],
'unicorn/no-exports-in-scripts': [2],
'unicorn/no-for-each': [2],
'unicorn/no-for-loop': [0],
'unicorn/no-hex-escape': [0],
'unicorn/no-global-object-property-assignment': [0],
'unicorn/no-immediate-mutation': [0],
'unicorn/no-instanceof-array': [0],
'unicorn/no-impossible-length-comparison': [2],
'unicorn/no-incorrect-query-selector': [2],
'unicorn/no-incorrect-template-string-interpolation': [0],
'unicorn/no-instanceof-builtins': [2],
'unicorn/no-invalid-argument-count': [0],
'unicorn/no-invalid-character-comparison': [2],
'unicorn/no-invalid-fetch-options': [2],
'unicorn/no-invalid-file-input-accept': [2],
'unicorn/no-invalid-remove-event-listener': [2],
'unicorn/no-keyword-prefix': [0],
'unicorn/no-length-as-slice-end': [2],
'unicorn/no-late-current-target-access': [2],
'unicorn/no-lonely-if': [2],
'unicorn/no-loop-iterable-mutation': [2],
'unicorn/no-magic-array-flat-depth': [0],
'unicorn/no-manually-wrapped-comments': [0], // too opinionated
'unicorn/no-mismatched-map-key': [2],
'unicorn/no-misrefactored-assignment': [2],
'unicorn/no-named-default': [2],
'unicorn/no-negated-array-predicate': [2],
'unicorn/no-negated-comparison': [2],
'unicorn/no-negated-condition': [0],
'unicorn/no-negation-in-equality-check': [2],
'unicorn/no-nested-ternary': [0],
'unicorn/no-new-array': [0],
'unicorn/no-new-buffer': [0],
'unicorn/no-non-function-verb-prefix': [0],
'unicorn/no-nonstandard-builtin-properties': [2],
'unicorn/no-null': [0],
'unicorn/no-object-as-default-parameter': [0],
'unicorn/no-object-methods-with-collections': [2],
'unicorn/no-optional-chaining-on-undeclared-variable': [2],
'unicorn/no-process-exit': [0],
'unicorn/no-redundant-comparison': [2],
'unicorn/no-return-array-push': [2],
'unicorn/no-selector-as-dom-name': [2],
'unicorn/no-single-promise-in-promise-methods': [2],
'unicorn/no-static-only-class': [2],
'unicorn/no-subtraction-comparison': [2],
'unicorn/no-thenable': [2],
'unicorn/no-this-assignment': [2],
'unicorn/no-this-outside-of-class': [0], // gitea uses `this` in non-class functions
'unicorn/no-top-level-assignment-in-function': [0],
'unicorn/no-top-level-side-effects': [0],
'unicorn/no-typeof-undefined': [2],
'unicorn/no-uncalled-method': [2],
'unicorn/no-undeclared-class-members': [2],
'unicorn/no-unnecessary-array-flat-depth': [2],
'unicorn/no-unnecessary-array-splice-count': [2],
'unicorn/no-unnecessary-await': [2],
'unicorn/no-unnecessary-boolean-comparison': [2],
'unicorn/no-unnecessary-global-this': [0],
'unicorn/no-unnecessary-nested-ternary': [2],
'unicorn/no-unnecessary-polyfills': [2],
'unicorn/no-unnecessary-slice-end': [2],
'unicorn/no-unnecessary-splice': [2],
'unicorn/no-unreadable-array-destructuring': [0],
'unicorn/no-unreadable-for-of-expression': [0],
'unicorn/no-unreadable-iife': [0],
'unicorn/no-unreadable-new-expression': [0],
'unicorn/no-unreadable-object-destructuring': [0],
'unicorn/no-unsafe-buffer-conversion': [2],
'unicorn/no-unsafe-dom-html': [0],
'unicorn/no-unsafe-property-key': [0],
'unicorn/no-unsafe-string-replacement': [0],
'unicorn/no-unused-array-method-return': [2],
'unicorn/no-unused-properties': [2],
'unicorn/no-useless-boolean-cast': [2],
'unicorn/no-useless-coercion': [2],
'unicorn/no-useless-collection-argument': [2],
'unicorn/no-useless-compound-assignment': [2],
'unicorn/no-useless-concat': [2],
'unicorn/no-useless-continue': [2],
'unicorn/no-useless-delete-check': [2],
'unicorn/no-useless-else': [0],
'unicorn/no-useless-error-capture-stack-trace': [2],
'unicorn/no-useless-fallback-in-spread': [2],
'unicorn/no-useless-iterator-to-array': [2],
'unicorn/no-useless-length-check': [2],
'unicorn/no-useless-logical-operand': [2],
'unicorn/no-useless-override': [2],
'unicorn/no-useless-promise-resolve-reject': [2],
'unicorn/no-useless-recursion': [0],
'unicorn/no-useless-spread': [2],
'unicorn/no-useless-switch-case': [2],
'unicorn/no-useless-template-literals': [2],
'unicorn/no-useless-undefined': [0],
'unicorn/no-xor-as-exponentiation': [2],
'unicorn/no-zero-fractions': [2],
'unicorn/number-literal-case': [0],
'unicorn/numeric-separators-style': [0],
'unicorn/operator-assignment': [2],
'unicorn/prefer-add-event-listener': [2],
'unicorn/prefer-add-event-listener-options': [2],
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
'unicorn/prefer-array-flat': [2],
'unicorn/prefer-array-flat-map': [2],
'unicorn/prefer-array-from-async': [2],
'unicorn/prefer-array-from-map': [2],
'unicorn/prefer-array-index-of': [2],
'unicorn/prefer-array-iterable-methods': [2],
'unicorn/prefer-array-last-methods': [2],
'unicorn/prefer-array-slice': [2],
'unicorn/prefer-array-some': [2],
'unicorn/prefer-at': [0],
'unicorn/prefer-await': [2],
'unicorn/prefer-bigint-literals': [2],
'unicorn/prefer-blob-reading-methods': [2],
'unicorn/prefer-boolean-return': [2],
'unicorn/prefer-class-fields': [2],
'unicorn/prefer-classlist-toggle': [2],
'unicorn/prefer-code-point': [0],
'unicorn/prefer-continue': [0],
'unicorn/prefer-date-now': [2],
'unicorn/prefer-default-parameters': [0],
'unicorn/prefer-direct-iteration': [2],
'unicorn/prefer-dispose': [2],
'unicorn/prefer-dom-node-append': [2],
'unicorn/prefer-dom-node-dataset': [0],
'unicorn/prefer-dom-node-html-methods': [0],
'unicorn/prefer-dom-node-remove': [2],
'unicorn/prefer-dom-node-text-content': [2],
'unicorn/prefer-early-return': [0],
'unicorn/prefer-else-if': [2],
'unicorn/prefer-event-target': [2],
'unicorn/prefer-export-from': [0],
'unicorn/prefer-flat-math-min-max': [2],
'unicorn/prefer-get-or-insert-computed': [2],
'unicorn/prefer-global-number-constants': [2],
'unicorn/prefer-global-this': [0],
'unicorn/prefer-has-check': [2],
'unicorn/prefer-hoisting-branch-code': [2],
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
'unicorn/prefer-identifier-import-export-specifiers': [2],
'unicorn/prefer-import-meta-properties': [2],
'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes
'unicorn/prefer-json-parse-buffer': [0],
'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated
'unicorn/prefer-iterable-in-constructor': [2],
'unicorn/prefer-iterator-concat': [0], // too opinionated
'unicorn/prefer-iterator-to-array': [2],
'unicorn/prefer-iterator-to-array-at-end': [2],
'unicorn/prefer-keyboard-event-key': [2],
'unicorn/prefer-location-assign': [2],
'unicorn/prefer-logical-operator-over-ternary': [2],
'unicorn/prefer-map-from-entries': [0],
'unicorn/prefer-math-abs': [2],
'unicorn/prefer-math-constants': [2],
'unicorn/prefer-math-min-max': [2],
'unicorn/prefer-math-trunc': [2],
'unicorn/prefer-minimal-ternary': [0],
'unicorn/prefer-modern-dom-apis': [0],
'unicorn/prefer-modern-math-apis': [2],
'unicorn/prefer-module': [2],
'unicorn/prefer-native-coercion-functions': [2],
'unicorn/prefer-negative-index': [2],
'unicorn/prefer-node-protocol': [2],
'unicorn/prefer-number-coercion': [0],
'unicorn/prefer-number-is-safe-integer': [0],
'unicorn/prefer-number-properties': [0],
'unicorn/prefer-object-define-properties': [2],
'unicorn/prefer-object-destructuring-defaults': [2],
'unicorn/prefer-object-from-entries': [2],
'unicorn/prefer-object-has-own': [0],
'unicorn/prefer-object-iterable-methods': [2],
'unicorn/prefer-optional-catch-binding': [2],
'unicorn/prefer-path2d': [2],
'unicorn/prefer-private-class-fields': [0],
'unicorn/prefer-promise-with-resolvers': [2],
'unicorn/prefer-prototype-methods': [0],
'unicorn/prefer-query-selector': [2],
'unicorn/prefer-queue-microtask': [2],
'unicorn/prefer-reflect-apply': [0],
'unicorn/prefer-regexp-escape': [0],
'unicorn/prefer-regexp-test': [2],
'unicorn/prefer-response-static-json': [2],
'unicorn/prefer-scoped-selector': [0],
'unicorn/prefer-set-has': [0],
'unicorn/prefer-set-size': [2],
'unicorn/prefer-short-arrow-method': [2],
'unicorn/prefer-simple-condition-first': [0],
'unicorn/prefer-simple-sort-comparator': [2],
'unicorn/prefer-single-array-predicate': [2],
'unicorn/prefer-single-call': [2],
'unicorn/prefer-single-object-destructuring': [2],
'unicorn/prefer-single-replace': [2],
'unicorn/prefer-smaller-scope': [2],
'unicorn/prefer-split-limit': [0], // too opinionated
'unicorn/prefer-spread': [0],
'unicorn/prefer-string-match-all': [2],
'unicorn/prefer-string-pad-start-end': [2],
'unicorn/prefer-string-raw': [0],
'unicorn/prefer-string-repeat': [2],
'unicorn/prefer-string-replace-all': [0],
'unicorn/prefer-string-slice': [0],
'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with
'unicorn/prefer-string-trim-start-end': [2],
'unicorn/prefer-structured-clone': [2],
'unicorn/prefer-switch': [0],
'unicorn/prefer-temporal': [0],
'unicorn/prefer-ternary': [0],
'unicorn/prefer-top-level-await': [0],
'unicorn/prefer-type-error': [0],
'unicorn/prefer-type-literal-last': [0],
'unicorn/prefer-uint8array-base64': [0],
'unicorn/prefer-unary-minus': [2],
'unicorn/prefer-unicode-code-point-escapes': [0],
'unicorn/prefer-url-can-parse': [2],
'unicorn/prefer-url-href': [2],
'unicorn/prefer-while-loop-condition': [2],
'unicorn/prevent-abbreviations': [0],
'unicorn/relative-url-style': [2],
'unicorn/require-array-join-separator': [2],
'unicorn/require-array-sort-compare': [0],
'unicorn/require-css-escape': [2],
'unicorn/require-module-attributes': [2],
'unicorn/require-module-specifiers': [0],
'unicorn/require-number-to-fixed-digits-argument': [2],
'unicorn/require-passive-events': [2],
'unicorn/require-post-message-target-origin': [0],
'unicorn/require-proxy-trap-boolean-return': [2],
'unicorn/string-content': [0],
'unicorn/switch-case-braces': [0],
'unicorn/switch-case-break-position': [2],
'unicorn/template-indent': [2],
'unicorn/text-encoding-identifier-case': [0],
'unicorn/throw-new-error': [2],
'unicorn/try-complexity': [0],
'use-isnan': [2],
'valid-typeof': [2, {requireStringLiterals: true}],
'vars-on-top': [0],
@@ -956,6 +1091,7 @@ export default defineConfig([
languageOptions: {globals: globals.vitest},
rules: {
'gitea/unescaped-html-literal': [0],
'unicorn/no-error-property-assignment': [0],
'vitest/consistent-test-filename': [0],
'vitest/consistent-test-it': [0],
'vitest/expect-expect': [0],
@@ -967,7 +1103,6 @@ export default defineConfig([
'vitest/no-conditional-in-test': [0],
'vitest/no-conditional-tests': [0],
'vitest/no-disabled-tests': [0],
'vitest/no-done-callback': [0],
'vitest/no-duplicate-hooks': [0],
'vitest/no-focused-tests': [2],
'vitest/no-hooks': [0],
+10 -10
View File
@@ -23,17 +23,17 @@ require (
github.com/ProtonMail/go-crypto v1.4.1
github.com/PuerkitoBio/goquery v1.12.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
github.com/alecthomas/chroma/v2 v2.26.1
github.com/alecthomas/chroma/v2 v2.27.0
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.6.0
github.com/bohde/codel v0.2.0
github.com/buildkite/terminal-to-html/v3 v3.16.8
github.com/caddyserver/certmagic v0.25.3
github.com/caddyserver/certmagic v0.25.4
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635
github.com/chi-middleware/proxy v1.1.1
github.com/dlclark/regexp2/v2 v2.2.1
github.com/dlclark/regexp2/v2 v2.2.2
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
@@ -73,7 +73,7 @@ require (
github.com/lib/pq v1.12.3
github.com/markbates/goth v1.82.0
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-sqlite3 v1.14.45
github.com/mattn/go-sqlite3 v1.14.47
github.com/meilisearch/meilisearch-go v0.36.3
github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27
@@ -86,7 +86,7 @@ require (
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/quasoft/websspi v1.1.2
github.com/redis/go-redis/v9 v9.20.0
github.com/redis/go-redis/v9 v9.21.0
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/sassoftware/go-rpmutils v0.4.0
@@ -96,15 +96,15 @@ require (
github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.15
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.9.1
github.com/urfave/cli/v3 v3.10.0
github.com/wneessen/go-mail v0.7.3
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0
go.yaml.in/yaml/v4 v4.0.0-rc.5
golang.org/x/crypto v0.53.0
golang.org/x/image v0.42.0
golang.org/x/image v0.43.0
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/oauth2 v0.36.0
@@ -114,7 +114,7 @@ require (
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.3
modernc.org/sqlite v1.52.0
modernc.org/sqlite v1.53.0
mvdan.cc/xurls/v2 v2.6.0
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
xorm.io/builder v0.3.13
@@ -273,7 +273,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.73.0 // indirect
modernc.org/libc v1.73.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
+20 -20
View File
@@ -76,8 +76,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqK
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -187,8 +187,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
@@ -240,8 +240,8 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0=
github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -514,8 +514,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
@@ -618,8 +618,8 @@ github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4Ul
github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY=
github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w=
@@ -709,8 +709,8 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.9.1 h1:OLU13atWZ0M+a4xmyBuBNOLZsSRYXyPeMeNjOvgYP54=
github.com/urfave/cli/v3 v3.9.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0=
github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk=
@@ -740,8 +740,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0 h1:gZSMTTnLcUeY5mH4z3G6GEzbaBTOCUfBCAJXMRyuzEM=
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0 h1:Bq5YIYgUJVbt4Hbh7ibBwNR4SNEafsyDVhIXl7dXDdg=
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
@@ -780,8 +780,8 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -940,8 +940,8 @@ modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.0 h1:Y/KmTxbIN5T3x+NFjYOzV/+Ha7wKClfIecmTCTuYlqQ=
modernc.org/libc v1.73.0/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -950,8 +950,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+21 -10
View File
@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
@@ -21,6 +22,8 @@ import (
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
webhook_module "gitea.dev/modules/webhook"
"xorm.io/builder"
)
// ActionRun represents a run of a workflow file
@@ -48,6 +51,13 @@ type ActionRun struct {
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
RawConcurrency string // raw concurrency
// WorkflowRepoID/WorkflowCommitSHA record the (repo, commit) the run's workflow file content came from.
// Always filled (repo-level run = the repo itself; scoped run = the source repo).
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"` // IsScopedRun explicitly classifies scoped runs.
// Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced.
// When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops.
Started timeutil.TimeStamp
@@ -86,7 +96,11 @@ func (run *ActionRun) WorkflowLink() string {
if run.Repo == nil {
return ""
}
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
// A scoped run's workflow is disambiguated by its source repo, so carry scoped_workflow_source_repo_id back to the run list
if run.IsScopedRun {
return fmt.Sprintf("%s/actions/?workflow=%s&scoped_workflow_source_repo_id=%d", run.Repo.Link(), url.QueryEscape(run.WorkflowID), run.WorkflowRepoID)
}
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), url.QueryEscape(run.WorkflowID))
}
// RefLink return the url of run's ref
@@ -264,11 +278,7 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
}
func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) {
run := &ActionRun{
RepoID: repoID,
Index: runIndex,
}
has, err := db.GetEngine(ctx).Get(run)
run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex})
if err != nil {
return nil, err
} else if !has {
@@ -279,9 +289,7 @@ func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionR
}
func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
run := &ActionRun{
RepoID: repoID,
}
run := &ActionRun{}
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Desc("index").Get(run)
if err != nil {
return nil, err
@@ -295,7 +303,10 @@ func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branc
var run ActionRun
q := db.GetEngine(ctx).Where("repo_id=?", repoID).
And("ref = ?", branch).
And("workflow_id = ?", workflowFile)
And("workflow_id = ?", workflowFile).
// TODO: the badge only reflects the repo's own (repo-level) runs; a same-named scoped run must not leak in.
// Support a scoped-workflow badge later by making this source-aware.
And("is_scoped_run = ?", false)
if event != "" {
q.And("event = ?", event)
}
+48 -10
View File
@@ -108,6 +108,10 @@ type ActionRunJob struct {
// ParentJobID scopes `Needs` resolution: name lookups happen only among rows sharing the same ParentJobID. 0 for top-level rows.
ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
// ContinueOnError mirrors the job-level continue-on-error field from the workflow YAML.
// When true, a failure of this job does not fail the overall workflow run.
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
@@ -357,6 +361,14 @@ func CollectAllDescendantJobs(parent *ActionRunJob, allJobs []*ActionRunJob) []*
return out
}
// hasWaitingJobsToPick reports whether any waiting, unclaimed, non-reusable job
// remains in the repo, i.e. work that an idle runner could still pick up.
func hasWaitingJobsToPick(ctx context.Context, repoID int64) (bool, error) {
return db.GetEngine(ctx).
Where("repo_id = ? AND task_id = ? AND status = ? AND is_reusable_caller = ?", repoID, 0, StatusWaiting, false).
Exist(&ActionRunJob{})
}
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
e := db.GetEngine(ctx)
@@ -381,14 +393,6 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return affected, nil
}
// Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump.
if statusUpdated && job.Status.IsWaiting() && !job.IsReusableCaller {
// if the status of job changes to waiting again, increase tasks version.
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err
}
}
if job.RunID == 0 {
var err error
if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil {
@@ -396,6 +400,37 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
}
}
// Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump.
if statusUpdated && !job.IsReusableCaller {
switch {
case job.Status.IsWaiting():
// A job returning to the waiting queue is work a runner can pick up, so bump the
// version to wake idle runners whose tasksVersion already equals latestVersion.
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err
}
case job.Status.IsDone():
// When a job finishes, bump the version so that idle runners — whose
// tasksVersion already equals the current latestVersion — learn that
// remaining waiting jobs are still available and attempt PickTask again.
// Without this bump, runners that completed their tasks would see
// tasksVersion==latestVersion and skip PickTask, leaving the other jobs
// permanently unassigned until the version changes for another reason.
// Only bump when waiting work actually remains for this repo, otherwise
// every job completion would needlessly bump the global version and wake
// every idle runner instance-wide for nothing.
hasWaiting, err := hasWaitingJobsToPick(ctx, job.RepoID)
if err != nil {
return 0, err
}
if hasWaiting {
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err
}
}
}
}
if statusUpdated && job.ParentJobID > 0 {
// Reusable workflow caller's children cascade their status changes upward to the parent caller.
parent, err := GetRunJobByRunAndID(ctx, job.RunID, job.ParentJobID)
@@ -500,9 +535,12 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
allSkipped := len(jobs) != 0
var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked bool
for _, job := range jobs {
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
// A failed job with continue-on-error:true does not fail the workflow run.
// It counts as a "continued failure" and is treated like success for aggregation.
isContinuedFailure := job.ContinueOnError && job.Status == StatusFailure
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped || isContinuedFailure)
allSkipped = allSkipped && job.Status == StatusSkipped
hasFailure = hasFailure || job.Status == StatusFailure
hasFailure = hasFailure || (job.Status == StatusFailure && !job.ContinueOnError)
hasCancelled = hasCancelled || job.Status == StatusCancelled
hasCancelling = hasCancelling || job.Status == StatusCancelling
hasWaiting = hasWaiting || job.Status == StatusWaiting
+31 -2
View File
@@ -10,6 +10,7 @@ import (
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/translation"
webhook_module "gitea.dev/modules/webhook"
@@ -61,7 +62,9 @@ type FindRunOptions struct {
RepoID int64
OwnerID int64
WorkflowID string
Ref string // the commit/tag/… that caused this workflow
WorkflowRepoID int64 // source-aware filter: the repo a run's workflow content came from (0 = any)
IsScopedRun optional.Option[bool] // is the run from a scoped workflow
Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Status []Status
@@ -77,6 +80,12 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.WorkflowID != "" {
cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID})
}
if opts.WorkflowRepoID > 0 {
cond = cond.And(builder.Eq{"`action_run`.workflow_repo_id": opts.WorkflowRepoID})
}
if opts.IsScopedRun.Has() {
cond = cond.And(builder.Eq{"`action_run`.is_scoped_run": opts.IsScopedRun.Value()})
}
if opts.TriggerUserID > 0 {
cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
}
@@ -106,6 +115,15 @@ func (opts FindRunOptions) ToJoins() []db.JoinFunc {
}
func (opts FindRunOptions) ToOrders() string {
// When scoped to a repo, sort by `index`: it reuses the unique
// `repo_index` (repo_id, index) index, so the query seeks repo_id and
// walks index descending instead of filesorting all matching rows.
// Within a repo `index` is co-monotonic with `id`, so the order is the same.
if opts.RepoID > 0 {
return "`action_run`.`index` DESC"
}
// `index` is scoped per repo, so it is meaningless across repos. With no
// RepoID, sort by the global, PK-indexed `id` for a deterministic order.
return "`action_run`.`id` DESC"
}
@@ -147,9 +165,20 @@ func GetRunBranches(ctx context.Context, repoID int64) ([]string, error) {
// GetRunWorkflowIDs returns all distinct WorkflowIDs that have at least
// one ActionRun in the given repo.
func GetRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
return getRunWorkflowIDs(ctx, repoID, builder.NewCond())
}
// GetRepoRunWorkflowIDs returns all distinct WorkflowIDs that have at least
// one repo-level ActionRun in the given repo.
func GetRepoRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
return getRunWorkflowIDs(ctx, repoID, builder.Eq{"is_scoped_run": false})
}
func getRunWorkflowIDs(ctx context.Context, repoID int64, extraCond builder.Cond) ([]string, error) {
ids := make([]string, 0, 10)
cond := builder.Eq{"repo_id": repoID}
return ids, db.GetEngine(ctx).Table("action_run").
Where(builder.Eq{"repo_id": repoID}).
Where(cond.And(extraCond)).
Distinct("workflow_id").
Cols("workflow_id").
Asc("workflow_id").
+125
View File
@@ -6,10 +6,13 @@ package actions
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/optional"
"gitea.dev/modules/translation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetRunWorkflowIDs(t *testing.T) {
@@ -24,6 +27,46 @@ func TestGetRunWorkflowIDs(t *testing.T) {
assert.Empty(t, ids)
}
func TestGetRepoRunWorkflowIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
repoWorkflowID = "repo-orphan.yaml"
scopedWorkflowID = "scoped-only.yaml"
sharedWorkflowID = "shared-name.yaml"
scopedWorkflowRepo = int64(111)
)
for _, spec := range []struct {
id int64
workflowID string
workflowRepoID int64
isScopedRun bool
}{
{99811, repoWorkflowID, repoID, false},
{99812, scopedWorkflowID, scopedWorkflowRepo, true},
{99813, sharedWorkflowID, repoID, false},
{99814, sharedWorkflowID, scopedWorkflowRepo, true},
} {
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: spec.id,
Index: spec.id,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: spec.workflowID,
WorkflowRepoID: spec.workflowRepoID,
IsScopedRun: spec.isScopedRun,
}))
}
ids, err := GetRepoRunWorkflowIDs(t.Context(), repoID)
require.NoError(t, err)
assert.Contains(t, ids, repoWorkflowID)
assert.Contains(t, ids, sharedWorkflowID)
assert.NotContains(t, ids, scopedWorkflowID)
}
func TestGetStatusInfoList(t *testing.T) {
statusInfoList := GetStatusInfoList(t.Context(), translation.MockLocale{})
@@ -35,3 +78,85 @@ func TestGetStatusInfoList(t *testing.T) {
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
}, statusInfoList)
}
// TestFindRunOptions_WorkflowRepoID: two runs share the bare WorkflowID but come from different content-source repos;
// the source-aware WorkflowRepoID filter must separate them.
func TestFindRunOptions_WorkflowRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
sourceA = int64(111)
sourceB = int64(222)
workflowID = "u3-shared.yaml"
)
for _, spec := range []struct{ id, workflowRepoID int64 }{
{99801, sourceA},
{99802, sourceB},
} {
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: spec.id,
Index: spec.id,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
WorkflowRepoID: spec.workflowRepoID,
IsScopedRun: true,
}))
}
// no source filter -> both
all, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID})
require.NoError(t, err)
assert.Len(t, all, 2)
// filter by source A -> only the run whose content came from A
onlyA, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceA})
require.NoError(t, err)
require.Len(t, onlyA, 1)
assert.EqualValues(t, 99801, onlyA[0].ID)
// filter by source B -> only the run whose content came from B
onlyB, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceB})
require.NoError(t, err)
require.Len(t, onlyB, 1)
assert.EqualValues(t, 99802, onlyB[0].ID)
}
func TestFindRunOptions_IsScopedRun(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
workflowID = "scoped-flag.yaml"
)
for _, spec := range []struct {
id int64
scoped bool
}{
{99821, false},
{99822, true},
} {
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: spec.id,
Index: spec.id,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
WorkflowRepoID: repoID,
IsScopedRun: spec.scoped,
}))
}
repoLevel, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(false)})
require.NoError(t, err)
require.Len(t, repoLevel, 1)
assert.EqualValues(t, 99821, repoLevel[0].ID)
scoped, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(true)})
require.NoError(t, err)
require.Len(t, scoped, 1)
assert.EqualValues(t, 99822, scoped[0].ID)
}
+55
View File
@@ -13,6 +13,7 @@ import (
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateRepoRunsNumbers(t *testing.T) {
@@ -44,3 +45,57 @@ func TestActionRun_Duration_NonNegative(t *testing.T) {
}
assert.Equal(t, time.Duration(0), run.Duration())
}
func TestActionRun_WorkflowLink(t *testing.T) {
repo := &repo_model.Repository{OwnerName: "org", Name: "consumer"}
// a repo-level run links by file name only
repoLevel := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: repo.ID}
assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml", repoLevel.WorkflowLink())
// a scoped run carries its source repo id back, so the list stays filtered to that source
scoped := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: 42, IsScopedRun: true}
assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml&scoped_workflow_source_repo_id=42", scoped.WorkflowLink())
}
func TestGetWorkflowLatestRun_RepoLevelOnly(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
workflowID = "badge-source-aware.yaml"
ref = "refs/heads/main"
)
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: 99811,
Index: 99811,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
Ref: ref,
Event: "push",
Status: StatusSuccess,
WorkflowRepoID: repoID,
WorkflowCommitSHA: "repo-level-sha",
}))
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: 99812,
Index: 99812,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
Ref: ref,
Event: "push",
Status: StatusFailure,
WorkflowRepoID: 111,
WorkflowCommitSHA: "scoped-sha",
IsScopedRun: true,
}))
run, err := GetWorkflowLatestRun(t.Context(), repoID, workflowID, ref, "push")
require.NoError(t, err)
assert.EqualValues(t, 99811, run.ID)
assert.False(t, run.IsScopedRun)
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"fmt"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ActionScopedWorkflowSource registers a repository as a source of scoped workflows, either for an owner (user/org) or for the whole instance.
type ActionScopedWorkflowSource struct {
ID int64 `xorm:"pk autoincr"`
// OwnerID is the scope the source applies to: a user/org ID (applies to that owner's repos), or 0 for instance-level (applies to every repo).
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
// SourceRepoID is the source repository providing the workflow files; always non-zero.
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
// WorkflowConfigs maps a workflow ID (entry name) to its merge-gate config.
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
// ScopedWorkflowConfig is one scoped workflow's config within a source registration.
type ScopedWorkflowConfig struct {
Required bool `json:"required"`
Patterns []string `json:"patterns"` // the status-check patterns that must be present and pass, only effective when Required is true
}
func init() {
db.RegisterModel(new(ActionScopedWorkflowSource))
}
// IsWorkflowRequired reports whether the given workflow ID (entry name) is marked required in this source.
func (s *ActionScopedWorkflowSource) IsWorkflowRequired(workflowID string) bool {
c, ok := s.WorkflowConfigs[workflowID]
return ok && c.Required
}
type FindScopedWorkflowSourceOpts struct {
db.ListOptions
OwnerIDs []int64
SourceRepoID int64
}
func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond {
cond := builder.NewCond()
if len(opts.OwnerIDs) > 0 {
cond = cond.And(builder.In("owner_id", opts.OwnerIDs))
}
if opts.SourceRepoID != 0 {
cond = cond.And(builder.Eq{"source_repo_id": opts.SourceRepoID})
}
return cond
}
// GetEffectiveScopedWorkflowSources returns the scoped-workflow sources effective for a repo owned by repoOwnerID:
// the owner's own sources plus instance-level (owner_id=0) sources.
func GetEffectiveScopedWorkflowSources(ctx context.Context, repoOwnerID int64) ([]*ActionScopedWorkflowSource, error) {
owners := []int64{0}
if repoOwnerID != 0 {
owners = append(owners, repoOwnerID)
}
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners})
}
// IsScopedWorkflowSourceEffective reports whether sourceRepoID is a scoped-workflow source effective for a repo owned by repoOwnerID.
func IsScopedWorkflowSourceEffective(ctx context.Context, repoOwnerID, sourceRepoID int64) (bool, error) {
owners := []int64{0}
if repoOwnerID != 0 {
owners = append(owners, repoOwnerID)
}
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners, SourceRepoID: sourceRepoID}.ToConds())
}
// IsWorkflowRequiredInSources reports whether workflowID from sourceRepoID is required by any of the given sources.
func IsWorkflowRequiredInSources(sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool {
for _, s := range sources {
if s.SourceRepoID == sourceRepoID && s.IsWorkflowRequired(workflowID) {
return true
}
}
return false
}
// ScopedStatusContextPrefix returns the source-repo prefix that makes a scoped run's commit-status context distinct from same-named workflows.
func ScopedStatusContextPrefix(ctx context.Context, sourceRepoID int64) string {
if sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID); err == nil {
return sourceRepo.FullName()
}
return fmt.Sprintf("scoped:%d", sourceRepoID)
}
// IsScopedWorkflowRequired reports whether workflowID from sourceRepoID is required for a repo owned by consumerOwnerID.
func IsScopedWorkflowRequired(ctx context.Context, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) {
sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID)
if err != nil {
return false, err
}
return IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID), nil
}
// IsScopedWorkflowOptedOutloads the consumer's effective sources then calls ScopedWorkflowOptedOut
func IsScopedWorkflowOptedOut(ctx context.Context, cfg *repo_model.ActionsConfig, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) {
if !cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID) {
return false, nil
}
sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID)
if err != nil {
return false, err
}
return ScopedWorkflowOptedOut(cfg, sources, sourceRepoID, workflowID), nil
}
// ScopedWorkflowOptedOut reports whether a consumer's opt-out of (sourceRepoID, workflowID) is in effect.
func ScopedWorkflowOptedOut(cfg *repo_model.ActionsConfig, sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool {
return !IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID) && cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID)
}
// GetScopedWorkflowSourcesByOwner returns the sources an owner (user/org, or 0 for instance) registered.
func GetScopedWorkflowSourcesByOwner(ctx context.Context, ownerID int64) ([]*ActionScopedWorkflowSource, error) {
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{ownerID}})
}
// GetScopedWorkflowSource returns the (owner, repo) source registration or a NotExist error.
func GetScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) (*ActionScopedWorkflowSource, error) {
src := &ActionScopedWorkflowSource{}
has, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Get(src)
if err != nil {
return nil, err
}
if !has {
return nil, util.NewNotExistErrorf("scoped workflow source (owner %d, repo %d) does not exist", ownerID, repoID)
}
return src, nil
}
// AddScopedWorkflowSource registers repoID as a source for ownerID (no-op if already registered).
func AddScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error {
exists, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource))
if err != nil {
return err
}
if exists {
return nil
}
if err := db.Insert(ctx, &ActionScopedWorkflowSource{OwnerID: ownerID, SourceRepoID: repoID}); err != nil {
// Re-check and treat an already-present row as the intended no-op.
if exists, existErr := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource)); existErr == nil && exists {
return nil
}
return err
}
return nil
}
// SetScopedWorkflowSourceConfigs replaces the per-workflow merge-gate configs (workflow ID -> config).
func SetScopedWorkflowSourceConfigs(ctx context.Context, ownerID, repoID int64, configs map[string]*ScopedWorkflowConfig) error {
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).
Cols("workflow_configs").
Update(&ActionScopedWorkflowSource{WorkflowConfigs: configs})
return err
}
// RemoveScopedWorkflowSource removes the (owner, repo) source registration.
func RemoveScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error {
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Delete(new(ActionScopedWorkflowSource))
return err
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestScopedWorkflowSource_IsWorkflowRequired(t *testing.T) {
src := &ActionScopedWorkflowSource{WorkflowConfigs: map[string]*ScopedWorkflowConfig{
"a.yml": {Required: true, Patterns: []string{"p"}},
"b.yml": {Required: true, Patterns: []string{"p"}},
"c.yml": {Required: false, Patterns: []string{"p"}}, // patterns kept as history, not required
}}
assert.True(t, src.IsWorkflowRequired("a.yml"))
assert.True(t, src.IsWorkflowRequired("b.yml"))
assert.False(t, src.IsWorkflowRequired("c.yml"), "config kept as history but not required")
assert.False(t, src.IsWorkflowRequired("d.yml"))
empty := &ActionScopedWorkflowSource{}
assert.False(t, empty.IsWorkflowRequired("a.yml"))
}
func TestIsWorkflowRequiredInSources(t *testing.T) {
// repo 100 registered twice (org optional + instance required).
sources := []*ActionScopedWorkflowSource{
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil},
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}},
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}},
}
assert.True(t, IsWorkflowRequiredInSources(sources, 100, "a.yml"), "required at instance level wins over org optional")
assert.False(t, IsWorkflowRequiredInSources(sources, 100, "z.yml"))
assert.False(t, IsWorkflowRequiredInSources(sources, 200, "a.yml"), "a.yml is required for repo 100, not repo 200")
assert.True(t, IsWorkflowRequiredInSources(sources, 200, "b.yml"))
assert.False(t, IsWorkflowRequiredInSources(sources, 999, "a.yml"), "unknown source repo")
}
func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
rows := []*ActionScopedWorkflowSource{
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, // org 2 registers repo 100 (optional)
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, // instance also registers repo 100 (required)
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, // instance source 200
{OwnerID: 3, SourceRepoID: 300, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true, Patterns: []string{"p"}}}}, // a different owner's source
}
for _, r := range rows {
require.NoError(t, db.Insert(ctx, r))
}
// owner 2 sees its own sources plus instance-level ones, but not owner 3's.
owner2, err := GetEffectiveScopedWorkflowSources(ctx, 2)
require.NoError(t, err)
assert.Len(t, owner2, 3)
required, err := IsScopedWorkflowRequired(ctx, 2, 100, "a.yml")
require.NoError(t, err)
assert.True(t, required, "instance marks a.yml required → required for owner 2 even though org left it optional")
required, err = IsScopedWorkflowRequired(ctx, 2, 100, "x.yml")
require.NoError(t, err)
assert.False(t, required)
required, err = IsScopedWorkflowRequired(ctx, 2, 200, "b.yml")
require.NoError(t, err)
assert.True(t, required)
// owner 3's source must not be effective for owner 2.
required, err = IsScopedWorkflowRequired(ctx, 2, 300, "c.yml")
require.NoError(t, err)
assert.False(t, required)
// IsScopedWorkflowSourceEffective: owner-level and instance-level sources are effective; another owner's is not.
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 100)
require.NoError(t, err)
assert.True(t, effective, "owner 2's own source")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 200)
require.NoError(t, err)
assert.True(t, effective, "instance-level source is effective for any owner")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 300)
require.NoError(t, err)
assert.False(t, effective, "owner 3's source is not effective for owner 2")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 999)
require.NoError(t, err)
assert.False(t, effective, "unknown source repo")
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 300)
require.NoError(t, err)
assert.True(t, effective, "owner 3's own source is effective for owner 3")
}
func TestScopedWorkflowSourceCRUD(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
// add is idempotent
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
sources, err := GetScopedWorkflowSourcesByOwner(ctx, 5)
require.NoError(t, err)
assert.Len(t, sources, 1)
// set the per-workflow configs (entry name -> {required, patterns}); a.yml required, b.yml kept as history (not required)
configs := map[string]*ScopedWorkflowConfig{
"a.yml": {Required: true, Patterns: []string{"src: a.yml / *"}},
"b.yml": {Required: false, Patterns: []string{"src: b.yml / build (push)"}},
}
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, configs))
src, err := GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, err)
assert.Equal(t, configs, src.WorkflowConfigs)
// clearing the configs works
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, nil))
src, err = GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, err)
assert.Empty(t, src.WorkflowConfigs)
// remove
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 10))
_, err = GetScopedWorkflowSource(ctx, 5, 10)
assert.ErrorIs(t, err, util.ErrNotExist)
sources, err = GetScopedWorkflowSourcesByOwner(ctx, 5)
require.NoError(t, err)
assert.Empty(t, sources)
}
+54
View File
@@ -48,3 +48,57 @@ func TestStatusFromResult(t *testing.T) {
assert.Equal(t, tt.want, StatusFromResult(tt.result), "result=%s", tt.result)
}
}
func newJob(status Status, continueOnError bool) *ActionRunJob {
return &ActionRunJob{Status: status, ContinueOnError: continueOnError}
}
func TestAggregateJobStatusContinueOnError(t *testing.T) {
cases := []struct {
name string
jobs []*ActionRunJob
want Status
}{
{
name: "all success",
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusSuccess, false)},
want: StatusSuccess,
},
{
name: "one failure without continue-on-error",
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, false)},
want: StatusFailure,
},
{
name: "one failure with continue-on-error",
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, true)},
want: StatusSuccess,
},
{
name: "only continued-failure",
jobs: []*ActionRunJob{newJob(StatusFailure, true)},
want: StatusSuccess,
},
{
name: "continued-failure plus real failure",
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusFailure, false)},
want: StatusFailure,
},
{
name: "all skipped",
jobs: []*ActionRunJob{newJob(StatusSkipped, false), newJob(StatusSkipped, false)},
want: StatusSkipped,
},
{
name: "continued-failure plus skipped counts as success",
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusSkipped, false)},
want: StatusSuccess,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, AggregateJobStatus(tt.jobs))
})
}
}
+137 -71
View File
@@ -227,13 +227,18 @@ func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
return util.EllipsisDisplayString(name, limit) // database column has a length limit
}
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, false, err
}
defer committer.Close()
// errJobAlreadyClaimed is a sentinel used inside claimJobForRunner to signal that
// another runner won the optimistic-lock race; it is never returned to callers.
var errJobAlreadyClaimed = errors.New("job already claimed by another runner")
// CreateTaskForRunner finds a waiting job that matches the runner's labels and
// atomically claims it. It iterates through all matching jobs so that a
// concurrent claim by another runner (which would lose the optimistic lock on
// job #1) does not leave the remaining jobs permanently unassigned.
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
if db.InTransaction(ctx) {
return nil, false, errors.New("CreateTaskForRunner must not be called within a database transaction")
}
e := db.GetEngine(ctx)
jobCond := builder.NewCond()
@@ -254,83 +259,144 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
}
// TODO: a more efficient way to filter labels
var job *ActionRunJob
log.Trace("runner labels: %v", runner.AgentLabels)
for _, v := range jobs {
if runner.CanMatchLabels(v.RunsOn) {
job = v
break
if !runner.CanMatchLabels(v.RunsOn) {
continue
}
}
if job == nil {
return nil, false, nil
}
if err := job.LoadAttributes(ctx); err != nil {
return nil, false, err
}
now := timeutil.TimeStampNow()
job.Started = now
job.Status = StatusRunning
task := &ActionTask{
JobID: job.ID,
Attempt: job.Attempt,
RunnerID: runner.ID,
Started: now,
Status: StatusRunning,
RepoID: job.RepoID,
OwnerID: job.OwnerID,
CommitSHA: job.CommitSHA,
IsForkPullRequest: job.IsForkPullRequest,
}
task.GenerateAndFillToken()
workflowJob, err := job.ParseJob()
if err != nil {
return nil, false, fmt.Errorf("load job %d: %w", job.ID, err)
}
if _, err := e.Insert(task); err != nil {
return nil, false, err
}
task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID)
if err := UpdateTask(ctx, task, "log_filename"); err != nil {
return nil, false, err
}
if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps {
steps[i] = &ActionTaskStep{
Name: makeTaskStepDisplayName(v, 255),
TaskID: task.ID,
Index: int64(i),
RepoID: task.RepoID,
Status: StatusWaiting,
}
}
if _, err := e.Insert(steps); err != nil {
task, ok, err := claimJobForRunner(ctx, runner, v)
if err != nil {
return nil, false, err
}
task.Steps = steps
if ok {
return task, true, nil
}
// Another runner claimed this job concurrently; try the next one.
}
return nil, false, nil
}
job.TaskID = task.ID
if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil {
return nil, false, err
} else if n != 1 {
// claimJobForRunner attempts to atomically claim job for runner inside its own
// transaction. Returns (task, true, nil) on success, or (nil, false, nil) when
// another runner wins the optimistic-lock race (the caller should try the next
// candidate job).
func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (*ActionTask, bool, error) {
var resultTask *ActionTask
err := db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
if err := job.LoadAttributes(ctx); err != nil {
return err
}
now := timeutil.TimeStampNow()
job.Started = now
job.Status = StatusRunning
task := &ActionTask{
JobID: job.ID,
Attempt: job.Attempt,
RunnerID: runner.ID,
Started: now,
Status: StatusRunning,
RepoID: job.RepoID,
OwnerID: job.OwnerID,
CommitSHA: job.CommitSHA,
IsForkPullRequest: job.IsForkPullRequest,
}
task.GenerateAndFillToken()
workflowJob, err := job.ParseJob()
if err != nil {
return fmt.Errorf("load job %d: %w", job.ID, err)
}
if _, err := e.Insert(task); err != nil {
return err
}
task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID)
if err := UpdateTask(ctx, task, "log_filename"); err != nil {
return err
}
if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps {
steps[i] = &ActionTaskStep{
Name: makeTaskStepDisplayName(v, 255),
TaskID: task.ID,
Index: int64(i),
RepoID: task.RepoID,
Status: StatusWaiting,
}
}
if _, err := e.Insert(steps); err != nil {
return err
}
task.Steps = steps
}
job.TaskID = task.ID
n, err := UpdateRunJob(ctx, job, builder.And(builder.Eq{"task_id": 0}, builder.Eq{"status": StatusWaiting}))
if err != nil {
return err
}
if n != 1 {
// Another runner claimed this job between our scan and this update;
// signal the outer loop to move on without treating this as an error.
return errJobAlreadyClaimed
}
task.Job = job
resultTask = task
return nil
})
if errors.Is(err, errJobAlreadyClaimed) {
return nil, false, nil
}
task.Job = job
if err := committer.Commit(); err != nil {
if err != nil {
return nil, false, err
}
return resultTask, true, nil
}
return task, true, nil
// ReleaseTaskForRunner reverts a freshly-claimed but undelivered task: it deletes
// the task together with its steps and returns the job to the waiting queue. It is
// used when assembling the runner response fails after the job was already claimed,
// so the job is not stranded in running state with no runner ever executing it.
func ReleaseTaskForRunner(ctx context.Context, task *ActionTask) error {
return db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
job, err := GetRunJobByRepoAndID(ctx, task.RepoID, task.JobID)
if err != nil {
return err
}
job.Status = StatusWaiting
job.Started = 0
job.TaskID = 0
// Guard on task_id and status so we only release while the job still
// references this task and has not progressed past running.
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": task.ID, "status": StatusRunning}, "status", "started", "task_id")
if err != nil {
return err
}
if n != 1 {
return fmt.Errorf("release task %d: job %d no longer references it", task.ID, task.JobID)
}
if _, err := e.Delete(&ActionTaskStep{TaskID: task.ID}); err != nil {
return err
}
if _, err := e.ID(task.ID).Delete(&ActionTask{}); err != nil {
return err
}
return nil
})
}
func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
+65
View File
@@ -306,3 +306,68 @@ func TestStopTaskCancellingFallsBackForMissingRunner(t *testing.T) {
assert.Equal(t, StatusCancelled, jobAfterStop.Status)
assert.NotZero(t, jobAfterStop.Stopped)
}
// TestReleaseTaskForRunner verifies that releasing a freshly-claimed task returns
// its job to the waiting queue and deletes the task and its steps, so a failure
// while assembling the runner response cannot strand the job in running state.
func TestReleaseTaskForRunner(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
run := &ActionRun{
Title: "release-task-test-run",
RepoID: 1,
OwnerID: 2,
WorkflowID: "test.yaml",
Index: 9902,
TriggerUserID: 2,
Ref: "refs/heads/main",
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
Event: "push",
TriggerEvent: "push",
Status: StatusWaiting,
}
require.NoError(t, db.Insert(t.Context(), run))
job := &ActionRunJob{
RunID: run.ID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: "release-job",
Attempt: 1,
JobID: "release-job",
Status: StatusWaiting,
RunsOn: []string{"ubuntu-latest"},
WorkflowPayload: []byte("on: push\njobs:\n release-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"),
}
require.NoError(t, db.Insert(t.Context(), job))
runner := &ActionRunner{
UUID: "release-runner-uuid",
Name: "release-runner",
AgentLabels: []string{"ubuntu-latest"},
}
runner.GenerateAndFillToken()
require.NoError(t, db.Insert(t.Context(), runner))
task, ok, err := CreateTaskForRunner(t.Context(), runner)
require.NoError(t, err)
require.True(t, ok)
require.NotNil(t, task)
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
require.Equal(t, StatusRunning, claimed.Status)
require.Equal(t, task.ID, claimed.TaskID)
require.NoError(t, ReleaseTaskForRunner(t.Context(), task))
// Job is back in the waiting queue with no task assigned.
released := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
assert.Equal(t, StatusWaiting, released.Status)
assert.Zero(t, released.TaskID)
assert.Zero(t, released.Started)
// The task and its steps are gone.
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
}
+5 -7
View File
@@ -18,6 +18,8 @@ import (
"gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// Task represents a task
@@ -172,17 +174,13 @@ func (err ErrTaskDoesNotExist) Unwrap() error {
// GetMigratingTask returns the migrating task by repo's id
func GetMigratingTask(ctx context.Context, repoID int64) (*Task, error) {
task := Task{
RepoID: repoID,
Type: structs.TaskTypeMigrateRepo,
}
has, err := db.GetEngine(ctx).Get(&task)
task, has, err := db.Get[Task](ctx, builder.Eq{"repo_id": repoID, "`type`": structs.TaskTypeMigrateRepo})
if err != nil {
return nil, err
} else if !has {
return nil, ErrTaskDoesNotExist{0, repoID, task.Type}
return nil, ErrTaskDoesNotExist{0, repoID, structs.TaskTypeMigrateRepo}
}
return &task, nil
return task, nil
}
// CreateTask creates a task on database
+7 -5
View File
@@ -20,6 +20,7 @@ import (
"gitea.dev/modules/setting"
"strk.kbt.io/projects/go/libravatar"
"xorm.io/builder"
)
const (
@@ -99,12 +100,13 @@ func HashEmail(email string) string {
// GetEmailForHash converts a provided md5sum to the email
func GetEmailForHash(ctx context.Context, md5Sum string) (string, error) {
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
emailHash := EmailHash{
Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
emailHash, has, err := db.Get[EmailHash](ctx, builder.Eq{"`hash`": strings.ToLower(strings.TrimSpace(md5Sum))})
if err != nil {
return "", err
} else if !has {
return "", nil
}
_, err := db.GetEngine(ctx).Get(&emailHash)
return emailHash.Email, err
return emailHash.Email, nil
})
}
+1 -7
View File
@@ -323,13 +323,7 @@ type RenamedBranch struct {
// FindRenamedBranch check if a branch was renamed
func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) {
branch = &RenamedBranch{
RepoID: repoID,
From: from,
}
exist, err = db.GetEngine(ctx).Get(branch)
return branch, exist, err
return db.Get[RenamedBranch](ctx, builder.Eq{"repo_id": repoID, "`from`": from})
}
// RenameBranch rename a branch
+1 -2
View File
@@ -126,8 +126,7 @@ func GetLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (*LFSM
return nil, ErrLFSObjectNotExist
}
m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID}
has, err := db.GetEngine(ctx).Get(m)
m, has, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": oid})
if err != nil {
return nil, err
} else if !has {
+3 -2
View File
@@ -13,6 +13,8 @@ import (
"gitea.dev/models/organization"
"gitea.dev/modules/glob"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// ProtectedTag struct
@@ -111,8 +113,7 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
// GetProtectedTagByNamePattern gets protected tag by name_pattern
func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) {
tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(tag)
tag, has, err := db.Get[ProtectedTag](ctx, builder.Eq{"name_pattern": pattern, "repo_id": repoID})
if err != nil {
return nil, err
}
+8 -8
View File
@@ -79,6 +79,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
return nil, err
}
if err := comments.loadResolveDoers(ctx); err != nil {
return nil, err
}
if err := comments.loadReactions(ctx, issue.Repo); err != nil {
return nil, err
}
// Find all reviews by ReviewID
reviews := make(map[int64]*Review)
ids := make([]int64, 0, len(comments))
@@ -107,14 +115,6 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
comments[n] = comment
n++
if err := comment.LoadResolveDoer(ctx); err != nil {
return nil, err
}
if err := comment.LoadReactions(ctx, issue.Repo); err != nil {
return nil, err
}
var err error
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+68
View File
@@ -11,6 +11,7 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
// CommentList defines a list of comments
@@ -444,6 +445,73 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
return nil
}
// loadResolveDoers bulk-loads the resolve doer for all code comments that have one.
func (comments CommentList) loadResolveDoers(ctx context.Context) error {
resolveDoerIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
return c.ResolveDoerID, c.ResolveDoerID != 0 && c.Type == CommentTypeCode
})
if len(resolveDoerIDs) == 0 {
return nil
}
userMaps, err := user_model.GetUsersMapByIDs(ctx, resolveDoerIDs)
if err != nil {
return err
}
for _, comment := range comments {
if comment.ResolveDoerID == 0 || comment.Type != CommentTypeCode {
continue
}
if u, ok := userMaps[comment.ResolveDoerID]; ok {
comment.ResolveDoer = u
} else {
comment.ResolveDoer = user_model.NewGhostUser()
}
}
return nil
}
// loadReactions bulk-loads reactions for all comments in the list.
func (comments CommentList) loadReactions(ctx context.Context, repo *repo_model.Repository) error {
if len(comments) == 0 {
return nil
}
commentIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
return c.ID, c.Reactions == nil
})
if len(commentIDs) == 0 {
return nil
}
var allReactions ReactionList
if err := db.GetEngine(ctx).
Where("`comment_id` > 0").
In("comment_id", commentIDs).
In("`type`", setting.UI.Reactions).
Asc("issue_id", "comment_id", "created_unix", "id").
Find(&allReactions); err != nil {
return err
}
if _, err := allReactions.LoadUsers(ctx, repo); err != nil {
return err
}
reactByComment := make(map[int64]ReactionList, len(commentIDs))
for _, r := range allReactions {
reactByComment[r.CommentID] = append(reactByComment[r.CommentID], r)
}
for _, comment := range comments {
if comment.Reactions == nil {
comment.Reactions = reactByComment[comment.ID]
}
}
return nil
}
// LoadAttributes loads attributes of the comments, except for attachments and
// comments
func (comments CommentList) LoadAttributes(ctx context.Context) (err error) {
+3 -7
View File
@@ -361,8 +361,8 @@ func (issue *Issue) ResetAttributesLoaded() {
// GetIsRead load the `IsRead` field of the issue
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {
issueUser, has, err := db.Get[IssueUser](ctx, builder.Eq{"issue_id": issue.ID, "uid": userID})
if err != nil {
return err
} else if !has {
issue.IsRead = false
@@ -499,11 +499,7 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
if index < 1 {
return nil, ErrIssueNotExist{}
}
issue := &Issue{
RepoID: repoID,
Index: index,
}
has, err := db.GetEngine(ctx).Get(issue)
issue, has, err := db.Get[Issue](ctx, builder.Eq{"repo_id": repoID, "`index`": index})
if err != nil {
return nil, err
} else if !has {
+4 -1
View File
@@ -591,9 +591,12 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev
func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {
issueIDs := issues.getIssueIDs()
if len(issueIDs) == 0 {
return nil
}
issueUsers := make([]*IssueUser, 0, len(issueIDs))
if err := db.GetEngine(ctx).Where("uid =?", userID).
In("issue_id").
In("issue_id", issueIDs).
Find(&issueUsers); err != nil {
return err
}
+17
View File
@@ -11,6 +11,7 @@ import (
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIssueList_LoadRepositories(t *testing.T) {
@@ -30,6 +31,22 @@ func TestIssueList_LoadRepositories(t *testing.T) {
}
}
func TestIssueList_LoadIsRead(t *testing.T) {
// Regression: In("issue_id") was missing the issueIDs argument, causing
// xorm to generate "0=1" and never mark any issue as read.
require.NoError(t, unittest.PrepareTestDatabase())
issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
// Fixture: uid=1 has is_read=true on issue 1 only.
issueList := issues_model.IssueList{issue1, issue2}
require.NoError(t, issueList.LoadIsRead(t.Context(), 1))
assert.True(t, issue1.IsRead, "issue 1 should be marked read for user 1")
assert.False(t, issue2.IsRead, "issue 2 should not be marked read for user 1")
}
func TestIssueList_LoadAttributes(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
setting.Service.EnableTimetracking = true
+1 -1
View File
@@ -599,7 +599,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
continue
}
has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
has, err := db.Exist[organization.TeamUnit](ctx, builder.Eq{"org_id": issue.Repo.Owner.ID, "team_id": team.ID, "`type`": unittype})
if err != nil {
return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
}
+6 -4
View File
@@ -13,6 +13,8 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/references"
"xorm.io/builder"
)
type crossReference struct {
@@ -189,11 +191,11 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository,
ref references.IssueReference,
) (*Issue, references.XRefAction, error) {
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
refAction := ref.Action
e := db.GetEngine(stdCtx)
if has, _ := e.Get(refIssue); !has {
refIssue, has, err := db.Get[Issue](stdCtx, builder.Eq{"repo_id": repo.ID, "`index`": ref.Index})
if err != nil {
return nil, references.XRefActionNone, err
} else if !has {
return nil, references.XRefActionNone, nil
}
if err := refIssue.LoadRepo(stdCtx); err != nil {
+1 -5
View File
@@ -535,12 +535,8 @@ func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullReque
if index < 1 {
return nil, ErrPullRequestNotExist{}
}
pr := &PullRequest{
BaseRepoID: repoID,
Index: index,
}
has, err := db.GetEngine(ctx).Get(pr)
pr, has, err := db.Get[PullRequest](ctx, builder.Eq{"base_repo_id": repoID, "`index`": index})
if err != nil {
return nil, err
} else if !has {
+3
View File
@@ -417,6 +417,9 @@ func prepareMigrationTasks() []*migration {
newMigration(337, "Add visibility to team", v1_27.AddVisibilityToTeam),
newMigration(338, "Expand legacy MSSQL issue/comment long-text columns", v1_27.ExpandIssueAndCommentLongTextFieldsForMSSQL),
newMigration(339, "Extend action c_u index to include created_unix for faster dashboard feed queries", v1_27.AddCreatedUnixToActionUserIsDeletedIndex),
newMigration(340, "Add ContinueOnError column to ActionRunJob", v1_27.AddContinueOnErrorToActionRunJob),
newMigration(341, "Convert legacy MSSQL DATETIME columns to DATETIME2", v1_27.FixLegacyMSSQLDateTimeColumns),
newMigration(342, "Add scoped workflows schema", v1_27.AddScopedWorkflowsSchema),
}
return preparedMigrations
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
// AddContinueOnErrorToActionRunJob adds the ContinueOnError column to ActionRunJob,
// storing the job-level continue-on-error value from the workflow YAML.
func AddContinueOnErrorToActionRunJob(x db.EngineMigration) error {
type ActionRunJob struct {
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRunJob))
return err
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"fmt"
"strings"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"xorm.io/xorm/schemas"
)
// legacyDateTimeColumns are the persisted real datetime columns that old Gitea
// versions created as MSSQL DATETIME. Every other time value is stored as a
// unix timestamp integer, so these are the only columns affected.
var legacyDateTimeColumns = []struct {
bean any
column string
}{
{new(externalLoginUserWithExpiresAt), "expires_at"},
{new(lfsLockWithCreated), "created"},
}
type externalLoginUserWithExpiresAt struct {
ExpiresAt time.Time
}
func (externalLoginUserWithExpiresAt) TableName() string {
return "external_login_user"
}
type lfsLockWithCreated struct {
Created time.Time `xorm:"created"`
}
func (lfsLockWithCreated) TableName() string {
return "lfs_lock"
}
// FixLegacyMSSQLDateTimeColumns converts legacy locale-dependent DATETIME columns
// to DATETIME2. Databases created by old Gitea versions stored these columns as
// DATETIME, which fails to parse ISO datetime strings ('YYYY-MM-DD HH:MM:SS')
// when the MSSQL session language is not English, breaking external account
// linking and LFS lock creation. New installs already use DATETIME2, so only
// legacy MSSQL columns need converting.
func FixLegacyMSSQLDateTimeColumns(x db.EngineMigration) error {
if x.Dialect().URI().DBType != schemas.MSSQL {
return nil
}
for _, c := range legacyDateTimeColumns {
table, err := x.TableInfo(c.bean)
if err != nil {
return err
}
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table.Name, c.column).Get(&dataType)
if err != nil {
return err
}
if !has || !strings.EqualFold(dataType, "datetime") {
continue
}
column := table.GetColumn(c.column)
if column == nil {
return fmt.Errorf("column %s does not exist in table %s", c.column, table.Name)
}
if err := base.ModifyColumn(x, table.Name, column); err != nil {
return fmt.Errorf("modify %s.%s: %w", table.Name, c.column, err)
}
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/require"
)
type externalLoginUserBeforeDateTimeMigration struct {
ExternalID string `xorm:"pk NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
ExpiresAt time.Time // sync creates DATETIME2; downgraded to legacy DATETIME via raw SQL below
}
func (externalLoginUserBeforeDateTimeMigration) TableName() string {
return "external_login_user"
}
type lfsLockBeforeDateTimeMigration struct {
ID int64 `xorm:"pk autoincr"`
Created time.Time `xorm:"created"`
}
func (lfsLockBeforeDateTimeMigration) TableName() string {
return "lfs_lock"
}
func Test_FixLegacyMSSQLDateTimeColumns(t *testing.T) {
if !setting.Database.Type.IsMSSQL() {
t.Skip("Only MSSQL needs to convert the legacy locale-dependent DATETIME columns")
}
x, deferrable := migrationtest.PrepareTestEnv(t, 0,
new(externalLoginUserBeforeDateTimeMigration),
new(lfsLockBeforeDateTimeMigration),
)
defer deferrable()
// Force the legacy DATETIME column type that old Gitea versions created.
_, err := x.Exec("ALTER TABLE [external_login_user] ALTER COLUMN [expires_at] DATETIME")
require.NoError(t, err)
_, err = x.Exec("ALTER TABLE [lfs_lock] ALTER COLUMN [created] DATETIME")
require.NoError(t, err)
require.Equal(t, "datetime", mssqlColumnType(t, x, "external_login_user", "expires_at"))
require.Equal(t, "datetime", mssqlColumnType(t, x, "lfs_lock", "created"))
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x))
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x)) // idempotent
require.Equal(t, "datetime2", mssqlColumnType(t, x, "external_login_user", "expires_at"))
require.Equal(t, "datetime2", mssqlColumnType(t, x, "lfs_lock", "created"))
// Inserting an ISO-formatted datetime must succeed even under a non-English
// locale, which is the failure the legacy DATETIME columns produced. The
// SET LANGUAGE and INSERT run in one Exec so they share a single connection.
_, err = x.Exec("SET LANGUAGE German; " +
"INSERT INTO [external_login_user] ([external_id], [login_source_id], [expires_at]) " +
"VALUES ('ext-id', 1, '2026-06-25 11:58:39')")
require.NoError(t, err)
_, err = x.Exec("SET LANGUAGE German; " +
"INSERT INTO [lfs_lock] ([created]) VALUES ('2026-06-25 11:58:39')")
require.NoError(t, err)
}
func mssqlColumnType(t *testing.T, x db.EngineMigration, table, column string) string {
t.Helper()
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table, column).Get(&dataType)
require.NoError(t, err)
require.True(t, has)
return dataType
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"xorm.io/xorm"
)
func AddScopedWorkflowsSchema(x db.EngineMigration) error {
// Create the action_scoped_workflow_source table
type ScopedWorkflowConfig struct {
Required bool `json:"required"`
Patterns []string `json:"patterns"`
}
type ActionScopedWorkflowSource struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
if err := x.Sync(new(ActionScopedWorkflowSource)); err != nil {
return err
}
// Add the columns that record where a run's workflow content came from
type ActionRun struct {
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRun))
return err
}
+2 -5
View File
@@ -411,11 +411,8 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
if len(name) == 0 {
return nil, ErrOrgNotExist{0, name}
}
u := &Organization{
LowerName: strings.ToLower(name),
Type: user_model.UserTypeOrganization,
}
has, err := db.GetEngine(ctx).Get(u)
u, has, err := db.Get[Organization](ctx, builder.Eq{"lower_name": strings.ToLower(name), "`type`": user_model.UserTypeOrganization})
if err != nil {
return nil, err
} else if !has {
+3 -4
View File
@@ -58,7 +58,7 @@ func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*P
// GetPropertiesByName gets all properties with a specific name
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
pps := make([]*PackageProperty, 0, 10)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND `name` = ?", refType, refID, name).OrderBy("id").Find(&pps)
}
// UpdateProperty updates a property
@@ -68,13 +68,12 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error {
}
func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error {
pp := PackageProperty{RefType: refType, RefID: refID, Name: name}
ok, err := db.GetEngine(ctx).Get(&pp)
pp, ok, err := db.Get[PackageProperty](ctx, builder.Eq{"ref_type": refType, "ref_id": refID, "`name`": name})
if err != nil {
return err
}
if ok {
_, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value})
_, err = db.GetEngine(ctx).ID(pp.ID).Cols("value").Update(&PackageProperty{Value: value})
return err
}
_, err = InsertProperty(ctx, refType, refID, name, value)
+1 -3
View File
@@ -164,9 +164,7 @@ func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error {
// HasVersionFileReferences checks if there are associated files
func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) {
return db.GetEngine(ctx).Get(&PackageFile{
VersionID: versionID,
})
return db.Exist[PackageFile](ctx, builder.Eq{"version_id": versionID})
}
// SearchValue describes a value to search
@@ -95,6 +95,40 @@ func TestGetActionsUserRepoPermission(t *testing.T) {
assert.False(t, perm.CanRead(unit.TypeCode))
})
t.Run("CollaborativeOwner_ForkPR_Denied", func(t *testing.T) {
// Target repo15 trusts repo2's owner as a collaborative owner.
repo15ActionsUnit := repo15.MustGetUnit(ctx, unit.TypeActions)
repo15ActionsUnit.ActionsConfig().AddCollaborativeOwner(owner2.ID)
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
// Owner cross-repo policy does not allow repo15, so the only branch that
// could grant access is the collaborative-owner one.
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, actions_model.OwnerActionsConfig{}))
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
// Non-fork task is legitimately granted code-read via collaborative owner.
task53.IsForkPullRequest = false
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
require.NoError(t, err)
assert.True(t, perm.CanRead(unit.TypeCode))
// Fork PR must NOT be able to read a third private repo through the
// collaborative-owner branch.
task53.IsForkPullRequest = true
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
perm, err = GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
require.NoError(t, err)
assert.False(t, perm.CanRead(unit.TypeCode))
// Restore state for subsequent subtests.
task53.IsForkPullRequest = false
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
repo15ActionsUnit.ActionsConfig().RemoveCollaborativeOwner(owner2.ID)
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
})
t.Run("Inheritance_And_Clamping", func(t *testing.T) {
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
task53.IsForkPullRequest = false
+3 -1
View File
@@ -369,7 +369,9 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
// 2. The Actions Bot user has been explicitly granted access and repository is private
// 3. The repository is public (handled by botPerm above)
if taskRepo.IsPrivate {
// Fork PRs are never allowed cross-repo access to other private repositories,
// matching the discriminator enforced by checkSameOwnerCrossRepoAccess above.
if taskRepo.IsPrivate && !task.IsForkPullRequest {
actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
return maxPerm, nil
+10 -3
View File
@@ -11,6 +11,8 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// ViewedState stores for a file in which state it is currently viewed
@@ -66,9 +68,14 @@ func (rs *ReviewState) GetViewedFileCount() int {
// If the review didn't exist before in the database, it won't afterwards either.
// The returned boolean shows whether the review exists in the database
func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, bool, error) {
review := &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA}
has, err := db.GetEngine(ctx).Get(review)
return review, has, err
review, has, err := db.Get[ReviewState](ctx, builder.Eq{"user_id": userID, "pull_id": pullID, "commit_sha": commitSHA})
if err != nil {
return nil, false, err
}
if review == nil {
review = &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA}
}
return review, has, nil
}
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
+3 -2
View File
@@ -17,6 +17,8 @@ import (
"gitea.dev/modules/storage"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// Attachment represent a attachment of issue/comment/release.
@@ -156,8 +158,7 @@ func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachm
// GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName.
func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) {
attach := &Attachment{ReleaseID: releaseID, Name: fileName}
has, err := db.GetEngine(ctx).Get(attach)
attach, has, err := db.Get[Attachment](ctx, builder.Eq{"release_id": releaseID, "`name`": fileName})
if err != nil {
return nil, err
} else if !has {
+6 -19
View File
@@ -102,20 +102,13 @@ func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*C
// GetCollaboration get collaboration for a repository id with a user id
func GetCollaboration(ctx context.Context, repoID, uid int64) (*Collaboration, error) {
collaboration := &Collaboration{
RepoID: repoID,
UserID: uid,
}
has, err := db.GetEngine(ctx).Get(collaboration)
if !has {
collaboration = nil
}
collaboration, _, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": uid})
return collaboration, err
}
// IsCollaborator check if a user is a collaborator of a repository
func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": userID})
}
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
@@ -126,13 +119,7 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
}
return db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: uid,
}
has, err := e.Get(collaboration)
collaboration, has, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": uid})
if err != nil {
return fmt.Errorf("get collaboration: %w", err)
} else if !has {
@@ -144,12 +131,12 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
}
collaboration.Mode = mode
if _, err = e.
if _, err = db.GetEngine(ctx).
ID(collaboration.ID).
Cols("mode").
Update(collaboration); err != nil {
return fmt.Errorf("update collaboration: %w", err)
} else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
} else if _, err = db.Exec(ctx, "UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
return fmt.Errorf("update access table: %w", err)
}
@@ -174,5 +161,5 @@ func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int
return true, nil
}
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repo.ID, UserID: userID})
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": userID})
}
+3 -2
View File
@@ -12,6 +12,8 @@ import (
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrMirrorNotExist mirror does not exist error
@@ -76,8 +78,7 @@ func (m *Mirror) ScheduleNextUpdate() {
// GetMirrorByRepoID returns mirror information of a repository.
func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) {
m := &Mirror{RepoID: repoID}
has, err := db.GetEngine(ctx).Get(m)
m, has, err := db.Get[Mirror](ctx, builder.Eq{"repo_id": repoID})
if err != nil {
return nil, err
} else if !has {
+4 -2
View File
@@ -10,6 +10,8 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrRedirectNotExist represents a "RedirectNotExist" kind of error.
@@ -52,8 +54,8 @@ func init() {
// LookupRedirect look up if a repository has a redirect name
func LookupRedirect(ctx context.Context, ownerID int64, repoName string) (int64, error) {
repoName = strings.ToLower(repoName)
redirect := &Redirect{OwnerID: ownerID, LowerName: repoName}
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"owner_id": ownerID, "lower_name": repoName})
if err != nil {
return 0, err
} else if !has {
return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName}
+1 -2
View File
@@ -216,8 +216,7 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
// GetRelease returns release by given ID.
func GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) {
rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
has, err := db.GetEngine(ctx).Get(rel)
rel, has, err := db.Get[Release](ctx, builder.Eq{"repo_id": repoID, "lower_tag_name": strings.ToLower(tagName)})
if err != nil {
return nil, err
} else if !has {
+3 -3
View File
@@ -849,9 +849,9 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos
}
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
return db.GetEngine(ctx).Get(&Repository{
OwnerID: u.ID,
LowerName: strings.ToLower(repoName),
return db.Exist[Repository](ctx, builder.Eq{
"owner_id": u.ID,
"lower_name": strings.ToLower(repoName),
})
}
+25
View File
@@ -70,6 +70,8 @@ func MakeRestrictedPermissions() ActionsTokenPermissions {
type ActionsConfig struct {
DisabledWorkflows []string
// DisabledScopedWorkflows maps a scoped workflow's source repository ID to the entry names opted out of in this repository.
DisabledScopedWorkflows map[int64][]string
// CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos.
// Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions.
CollaborativeOwnerIDs []int64
@@ -98,6 +100,29 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) {
cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) IsScopedWorkflowDisabled(sourceRepoID int64, workflowID string) bool {
return slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
}
func (cfg *ActionsConfig) DisableScopedWorkflow(sourceRepoID int64, workflowID string) {
if slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) {
return
}
if cfg.DisabledScopedWorkflows == nil {
cfg.DisabledScopedWorkflows = make(map[int64][]string)
}
cfg.DisabledScopedWorkflows[sourceRepoID] = append(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
}
func (cfg *ActionsConfig) EnableScopedWorkflow(sourceRepoID int64, workflowID string) {
workflowIDs := util.SliceRemoveAll(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
if len(workflowIDs) == 0 {
delete(cfg.DisabledScopedWorkflows, sourceRepoID)
return
}
cfg.DisabledScopedWorkflows[sourceRepoID] = workflowIDs
}
func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) {
if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) {
cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID)
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsConfig_ScopedWorkflowOptOut(t *testing.T) {
cfg := &ActionsConfig{}
assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
cfg.DisableScopedWorkflow(100, "ci.yml")
assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
// idempotent
cfg.DisableScopedWorkflow(100, "ci.yml")
assert.Len(t, cfg.DisabledScopedWorkflows, 1)
// keyed by source repo: the same filename from a different source repo is independent
assert.False(t, cfg.IsScopedWorkflowDisabled(200, "ci.yml"))
// must not collide with the repo-level DisabledWorkflows list (bare filename)
assert.False(t, cfg.IsWorkflowDisabled("ci.yml"))
cfg.DisableWorkflow("ci.yml")
assert.True(t, cfg.IsWorkflowDisabled("ci.yml"))
assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"), "repo-level disable must not touch the scoped entry")
cfg.EnableScopedWorkflow(100, "ci.yml")
assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
assert.True(t, cfg.IsWorkflowDisabled("ci.yml"), "enabling the scoped entry must not touch the repo-level disable")
}
func TestActionsConfig_ScopedWorkflowSerialization(t *testing.T) {
cfg := &ActionsConfig{}
cfg.DisableScopedWorkflow(100, "ci.yml")
cfg.DisableWorkflow("repo.yml")
bs, err := cfg.ToDB()
require.NoError(t, err)
got := &ActionsConfig{}
require.NoError(t, got.FromDB(bs))
assert.True(t, got.IsScopedWorkflowDisabled(100, "ci.yml"))
assert.True(t, got.IsWorkflowDisabled("repo.yml"))
}
+3 -1
View File
@@ -9,6 +9,8 @@ import (
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// Star represents a starred repo by a user.
@@ -68,7 +70,7 @@ func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star
// IsStaring checks if user has starred given repository.
func IsStaring(ctx context.Context, userID, repoID int64) bool {
has, _ := db.GetEngine(ctx).Get(&Star{UID: userID, RepoID: repoID})
has, _ := db.Exist[Star](ctx, builder.Eq{"uid": userID, "repo_id": repoID})
return has
}
+8 -4
View File
@@ -10,6 +10,8 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// WatchMode specifies what kind of watch the user has on a repository
@@ -41,12 +43,14 @@ func init() {
}
// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) {
watch := Watch{UserID: userID, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(&watch)
func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) {
watch, has, err := db.Get[Watch](ctx, builder.Eq{"user_id": userID, "repo_id": repoID})
if err != nil {
return watch, err
}
if watch == nil {
watch = &Watch{UserID: userID, RepoID: repoID}
}
if !has {
watch.Mode = WatchModeNone
}
@@ -64,7 +68,7 @@ func IsWatching(ctx context.Context, userID, repoID int64) bool {
return err == nil && IsWatchMode(watch.Mode)
}
func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) {
func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
if watch.Mode == mode {
return nil
}
+3 -3
View File
@@ -7,6 +7,8 @@ import (
"context"
"gitea.dev/models/db"
"xorm.io/builder"
)
// AppState represents a state record in database
@@ -44,9 +46,7 @@ func SaveAppStateContent(ctx context.Context, key, content string) error {
// GetAppStateContent gets an app state from database
func GetAppStateContent(ctx context.Context, key string) (content string, err error) {
e := db.GetEngine(ctx)
appState := &AppState{ID: key}
has, err := e.Get(appState)
appState, has, err := db.Get[AppState](ctx, builder.Eq{"id": key})
if err != nil {
return "", err
} else if !has {
+2 -2
View File
@@ -348,8 +348,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
data := makeTimeLimitCodeHashData(opts, user)
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
emailAddress := &EmailAddress{UID: user.ID, Email: email}
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
emailAddress, has, _ := db.Get[EmailAddress](ctx, builder.Eq{"uid": user.ID, "email": email})
if has {
return emailAddress
}
}
+3 -1
View File
@@ -8,6 +8,8 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// Follow represents relations of user and their followers.
@@ -24,7 +26,7 @@ func init() {
// IsFollowing returns true if user is following followID.
func IsFollowing(ctx context.Context, userID, followID int64) bool {
has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID})
has, _ := db.Exist[Follow](ctx, builder.Eq{"user_id": userID, "follow_id": followID})
return has
}
+3 -1
View File
@@ -9,6 +9,8 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// UserOpenID is the list of all OpenID identities of a user.
@@ -43,7 +45,7 @@ func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
return true, nil
}
return db.GetEngine(ctx).Get(&UserOpenID{URI: uri})
return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri})
}
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
+4 -2
View File
@@ -10,6 +10,8 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error.
@@ -50,8 +52,8 @@ func init() {
// LookupUserRedirect look up userID if a user has a redirect name
func LookupUserRedirect(ctx context.Context, userName string) (int64, error) {
userName = strings.ToLower(userName)
redirect := &Redirect{LowerName: userName}
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"lower_name": userName})
if err != nil {
return 0, err
} else if !has {
return 0, ErrUserRedirectNotExist{Name: userName}
+1 -2
View File
@@ -125,8 +125,7 @@ func GetUserSetting(ctx context.Context, userID int64, key string, def ...string
return "", err
}
setting := &Setting{UserID: userID, SettingKey: key}
has, err := db.GetEngine(ctx).Get(setting)
setting, has, err := db.Get[Setting](ctx, builder.Eq{"user_id": userID, "setting_key": key})
if err != nil {
return "", err
}
+21 -6
View File
@@ -1276,8 +1276,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
email = strings.ToLower(email)
// Otherwise, check in alternative list for activated email addresses
emailAddress := &EmailAddress{LowerEmail: email, IsActivated: true}
has, err := db.GetEngine(ctx).Get(emailAddress)
emailAddress, has, err := db.Get[EmailAddress](ctx, builder.Eq{"lower_email": email, "is_activated": true})
if err != nil {
return nil, err
}
@@ -1297,13 +1296,29 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
return nil, ErrUserNotExist{Name: email}
}
func GetIndividualUser(ctx context.Context, user *User) (bool, error) {
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
has, err := db.GetEngine(ctx).Get(user)
func GetIndividualUserByPrimaryEmail(ctx context.Context, email string) (*User, error) {
email = strings.ToLower(strings.TrimSpace(email))
if len(email) == 0 {
return nil, ErrUserNotExist{Name: email}
}
user, has, err := db.Get[User](ctx, builder.Eq{"email": email, "type": UserTypeIndividual})
if err != nil {
return nil, err
}
if !has {
return nil, ErrUserNotExist{Name: email}
}
return user, nil
}
func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, loginSource int64, loginName string) (*User, bool, error) {
user, has, err := db.Get[User](ctx, builder.Eq{"login_type": loginType, "login_source": loginSource, "login_name": loginName})
if has && user.Type != UserTypeIndividual {
has = false
user = nil
}
return has, err
return user, has, err
}
// GetUserByOpenID returns the user object by given OpenID if exists.
+3
View File
@@ -69,6 +69,9 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
runsOn[i] = evaluator.Interpolate(v)
}
job.RawRunsOn = encodeRunsOn(runsOn)
if err := evaluator.EvaluateYamlNode(&job.RawContinueOnError); err != nil {
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", id, err)
}
swf := &SingleWorkflow{
Name: workflow.Name,
RawOn: workflow.RawOn,
@@ -58,6 +58,11 @@ func TestParse(t *testing.T) {
options: nil,
wantErr: false,
},
{
name: "continue_on_error_expr",
options: nil,
wantErr: false,
},
}
invalidFileTests := []struct {
name string
+49 -34
View File
@@ -79,23 +79,37 @@ func (w *SingleWorkflow) Marshal() ([]byte, error) {
}
type Job struct {
Name string `yaml:"name,omitempty"`
RawNeeds yaml.Node `yaml:"needs,omitempty"`
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
Env yaml.Node `yaml:"env,omitempty"`
If yaml.Node `yaml:"if,omitempty"`
Steps []*Step `yaml:"steps,omitempty"`
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
Strategy Strategy `yaml:"strategy,omitempty"`
RawContainer yaml.Node `yaml:"container,omitempty"`
Defaults Defaults `yaml:"defaults,omitempty"`
Outputs map[string]string `yaml:"outputs,omitempty"`
Uses string `yaml:"uses,omitempty"`
With map[string]any `yaml:"with,omitempty"`
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
Name string `yaml:"name,omitempty"`
RawNeeds yaml.Node `yaml:"needs,omitempty"`
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
Env yaml.Node `yaml:"env,omitempty"`
If yaml.Node `yaml:"if,omitempty"`
Steps []*Step `yaml:"steps,omitempty"`
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"`
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
Strategy Strategy `yaml:"strategy,omitempty"`
RawContainer yaml.Node `yaml:"container,omitempty"`
Defaults Defaults `yaml:"defaults,omitempty"`
Outputs map[string]string `yaml:"outputs,omitempty"`
Uses string `yaml:"uses,omitempty"`
With map[string]any `yaml:"with,omitempty"`
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
}
// GetContinueOnError decodes the continue-on-error field to a bool.
// The field may be a literal bool or an already-evaluated expression node.
func (j *Job) GetContinueOnError() bool {
if j.RawContinueOnError.Kind == 0 {
return false
}
var v bool
if err := j.RawContinueOnError.Decode(&v); err != nil {
return false
}
return v
}
func (j *Job) Clone() *Job {
@@ -103,23 +117,24 @@ func (j *Job) Clone() *Job {
return nil
}
return &Job{
Name: j.Name,
RawNeeds: j.RawNeeds,
RawRunsOn: j.RawRunsOn,
Env: j.Env,
If: j.If,
Steps: j.Steps,
TimeoutMinutes: j.TimeoutMinutes,
Services: j.Services,
Strategy: j.Strategy,
RawContainer: j.RawContainer,
Defaults: j.Defaults,
Outputs: j.Outputs,
Uses: j.Uses,
With: j.With,
RawSecrets: j.RawSecrets,
RawConcurrency: j.RawConcurrency,
RawPermissions: j.RawPermissions,
Name: j.Name,
RawNeeds: j.RawNeeds,
RawRunsOn: j.RawRunsOn,
Env: j.Env,
If: j.If,
Steps: j.Steps,
TimeoutMinutes: j.TimeoutMinutes,
RawContinueOnError: j.RawContinueOnError,
Services: j.Services,
Strategy: j.Strategy,
RawContainer: j.RawContainer,
Defaults: j.Defaults,
Outputs: j.Outputs,
Uses: j.Uses,
With: j.With,
RawSecrets: j.RawSecrets,
RawConcurrency: j.RawConcurrency,
RawPermissions: j.RawPermissions,
}
}
+46
View File
@@ -336,6 +336,52 @@ func TestSingleWorkflow_SetJob(t *testing.T) {
})
}
func TestGetContinueOnError(t *testing.T) {
tests := []struct {
name string
yaml string
want bool
}{
{
name: "absent",
yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n steps:\n - run: echo hi\n",
want: false,
},
{
name: "static true",
yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n continue-on-error: true\n steps:\n - run: echo hi\n",
want: true,
},
{
name: "static false",
yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n continue-on-error: false\n steps:\n - run: echo hi\n",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse([]byte(tt.yaml))
require.NoError(t, err)
require.Len(t, got, 1)
_, job := got[0].Job()
assert.Equal(t, tt.want, job.GetContinueOnError())
})
}
// Expression case: ${{ matrix.experimental }} must resolve per matrix variant.
t.Run("matrix expression", func(t *testing.T) {
content := ReadTestdata(t, "continue_on_error_expr.in.yaml")
got, err := Parse(content)
require.NoError(t, err)
require.Len(t, got, 2)
// sorted by matrix name: (false) before (true)
_, jobFalse := got[0].Job()
_, jobTrue := got[1].Job()
assert.False(t, jobFalse.GetContinueOnError())
assert.True(t, jobTrue.GetContinueOnError())
})
}
func TestParseMappingNode(t *testing.T) {
tests := []struct {
input string
@@ -0,0 +1,10 @@
name: test
jobs:
job1:
strategy:
matrix:
experimental: [false, true]
runs-on: ubuntu-22.04
continue-on-error: ${{ matrix.experimental }}
steps:
- run: echo hi
@@ -0,0 +1,25 @@
name: test
jobs:
job1:
name: job1 (false)
runs-on: ubuntu-22.04
steps:
- run: echo hi
continue-on-error: false
strategy:
matrix:
experimental:
- false
---
name: test
jobs:
job1:
name: job1 (true)
runs-on: ubuntu-22.04
steps:
- run: echo hi
continue-on-error: true
strategy:
matrix:
experimental:
- true
+17 -13
View File
@@ -15,9 +15,11 @@ import (
type UsesKind int
const (
// UsesKindLocalSameRepo is "./.gitea/workflows/foo.yml" - a path inside the calling repository.
// UsesKindLocalSameRepo is "./<dir>/foo.yml" - a path inside the calling repository.
// For example: "./.gitea/workflows/foo.yml"
UsesKindLocalSameRepo UsesKind = iota + 1
// UsesKindLocalCrossRepo is "owner/repo/.gitea/workflows/foo.yml@ref" - a workflow in another repo on the same instance.
// UsesKindLocalCrossRepo is "owner/repo/<dir>/foo.yml@ref" - a workflow in another repo on the same instance.
// For example: "owner/repo/.gitea/workflows/foo.yml@ref"
UsesKindLocalCrossRepo
)
@@ -31,14 +33,16 @@ type UsesRef struct {
}
var (
reLocalSameRepo = regexp.MustCompile(`^\./\.(gitea|github)/workflows/([^@]+\.ya?ml)$`)
reLocalCrossRepo = regexp.MustCompile(`^([-.\w]+)/([-.\w]+)/\.(gitea|github)/workflows/([^@]+\.ya?ml)@(.+)$`)
reLocalSameRepo = regexp.MustCompile(`^\./([^@]+\.ya?ml)$`)
reLocalCrossRepo = regexp.MustCompile(`^([-.\w]+)/([-.\w]+)/([^@]+\.ya?ml)@(.+)$`)
)
// ParseUses parses a reusable workflow "uses:" value.
// Only two forms are supported:
// - "./.gitea/workflows/foo.yml" (UsesKindLocalSameRepo, no @ref)
// - "OWNER/REPO/.gitea/workflows/foo.yml@REF" (UsesKindLocalCrossRepo)
// ParseUses parses the SYNTAX of a reusable workflow "uses:" value into a UsesRef. Two forms are supported:
// - "./<dir>/foo.yml" (UsesKindLocalSameRepo, no @ref)
// - "OWNER/REPO/<dir>/foo.yml@REF" (UsesKindLocalCrossRepo)
//
// It deliberately does NOT validate that <dir> is an allowed workflow directory: the allowed directories are instance-configurable (WORKFLOW_DIRS / SCOPED_WORKFLOW_DIRS).
// The caller (services/actions.ResolveUses) enforces the directory allowlist. The returned Path is the cleaned, repo-relative file path.
func ParseUses(s string) (*UsesRef, error) {
s = strings.TrimSpace(s)
if s == "" {
@@ -48,9 +52,9 @@ func ParseUses(s string) (*UsesRef, error) {
if strings.HasPrefix(s, "./") {
m := reLocalSameRepo.FindStringSubmatch(s)
if m == nil {
return nil, fmt.Errorf(`invalid local "uses:" %q (expect ./.gitea/workflows/<file>.yml)`, s)
return nil, fmt.Errorf(`invalid local "uses:" %q (expect ./<dir>/<file>.yml)`, s)
}
p := fmt.Sprintf(".%s/workflows/%s", m[1], m[2])
p := m[1]
if path.Clean(p) != p {
return nil, fmt.Errorf("invalid workflow path %q", s)
}
@@ -59,9 +63,9 @@ func ParseUses(s string) (*UsesRef, error) {
m := reLocalCrossRepo.FindStringSubmatch(s)
if m == nil {
return nil, fmt.Errorf(`invalid cross-repo "uses:" %q (expect owner/repo/.gitea/workflows/<file>.yml@ref)`, s)
return nil, fmt.Errorf(`invalid cross-repo "uses:" %q (expect owner/repo/<dir>/<file>.yml@ref)`, s)
}
p := fmt.Sprintf(".%s/workflows/%s", m[3], m[4])
p := m[3]
if path.Clean(p) != p {
return nil, fmt.Errorf("invalid workflow path %q", s)
}
@@ -70,6 +74,6 @@ func ParseUses(s string) (*UsesRef, error) {
Owner: m[1],
Repo: m[2],
Path: p,
Ref: m[5],
Ref: m[4],
}, nil
}
+23 -4
View File
@@ -42,6 +42,17 @@ func TestParseUses(t *testing.T) {
in: "./.gitea/workflows/sub/build.yml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/sub/build.yml"},
},
{
// ParseUses is dir-agnostic; the allowed directories (WORKFLOW_DIRS / SCOPED_WORKFLOW_DIRS) are enforced by ResolveUses.
name: "scoped workflows dir parses",
in: "./.gitea/scoped_workflows/lib.yml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/scoped_workflows/lib.yml"},
},
{
name: "non-default dir parses (allowlist enforced downstream)",
in: "./.gitea/custom_workflows/x.yaml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/custom_workflows/x.yaml"},
},
{
name: "leading/trailing whitespace is trimmed",
in: " ./.gitea/workflows/build.yml ",
@@ -118,6 +129,17 @@ func TestParseUses(t *testing.T) {
Ref: "v1",
},
},
{
name: "scoped workflows dir parses (allowlist enforced by ResolveUses)",
in: "owner/repo/.gitea/scoped_workflows/lib.yml@v1",
want: UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: "owner",
Repo: "repo",
Path: ".gitea/scoped_workflows/lib.yml",
Ref: "v1",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -136,23 +158,20 @@ func TestParseUses(t *testing.T) {
{name: "empty string", in: ""},
{name: "whitespace only", in: " "},
// Same-repo malformed
// Same-repo malformed (note: a wrong *directory* parses and should be rejected by the caller)
{name: "same-repo with @ref", in: "./.gitea/workflows/build.yml@v1"},
{name: "same-repo wrong directory", in: "./not-workflows/build.yml"},
{name: "same-repo wrong extension", in: "./.gitea/workflows/build.txt"},
{name: "same-repo missing extension", in: "./.gitea/workflows/build"},
{name: "same-repo absolute path", in: "/.gitea/workflows/build.yml"},
{name: "same-repo path traversal", in: "./.gitea/workflows/../escape.yml"},
{name: "same-repo double slash", in: "./.gitea/workflows//build.yml"},
{name: "same-repo redundant ./", in: "./.gitea/workflows/./build.yml"},
{name: "same-repo no filename", in: "./.gitea/workflows/.yml"},
// Cross-repo malformed
{name: "cross-repo missing @ref", in: "owner/repo/.gitea/workflows/build.yml"},
{name: "cross-repo empty ref", in: "owner/repo/.gitea/workflows/build.yml@"},
{name: "cross-repo missing owner", in: "/repo/.gitea/workflows/build.yml@v1"},
{name: "cross-repo missing repo", in: "owner//.gitea/workflows/build.yml@v1"},
{name: "cross-repo wrong workflows dir", in: "owner/repo/workflows/build.yml@v1"},
{name: "cross-repo wrong extension", in: "owner/repo/.gitea/workflows/build.txt@v1"},
{name: "cross-repo path traversal", in: "owner/repo/.gitea/workflows/../escape.yml@v1"},
{name: "cross-repo double slash in path", in: "owner/repo/.gitea/workflows//build.yml@v1"},
+83
View File
@@ -0,0 +1,83 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
webhook_module "gitea.dev/modules/webhook"
)
// ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit.
func ListScopedWorkflows(commit *git.Commit) (string, git.Entries, error) {
return listWorkflowsInDirs(commit, setting.Actions.ScopedWorkflowDirs)
}
// ParsedScopedWorkflow is one scoped workflow's source-side parse result
type ParsedScopedWorkflow struct {
EntryName string
DisplayName string // the workflow `name:` or base file name
Content []byte // raw content of the workflow file
Events []*jobparser.Event // decoded `on:` events
}
// ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS).
func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) {
_, entries, err := ListScopedWorkflows(sourceCommit)
if err != nil {
return nil, err
}
parsed := make([]*ParsedScopedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(entry)
if err != nil {
return nil, err
}
// one workflow may have multiple events
events, err := GetEventsFromContent(content)
if err != nil {
log.Warn("ignore invalid scoped workflow %q: %v", entry.Name(), err)
continue
}
parsed = append(parsed, &ParsedScopedWorkflow{
EntryName: entry.Name(),
DisplayName: WorkflowDisplayName(entry.Name(), content),
Content: content,
Events: events,
})
}
return parsed, nil
}
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event, returning those whose `on:` matches.
func MatchScopedWorkflows(
parsed []*ParsedScopedWorkflow,
consumerGitRepo *git.Repository,
consumerCommit *git.Commit,
triggedEvent webhook_module.HookEventType,
payload api.Payloader,
) []*DetectedWorkflow {
workflows := make([]*DetectedWorkflow, 0, len(parsed))
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,
})
}
}
}
return workflows
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsWorkflowInDirs(t *testing.T) {
tests := []struct {
name string
dirs []string
path string
expected bool
}{
{
name: "default scoped dir with yml",
dirs: []string{".gitea/scoped_workflows", ".github/scoped_workflows"},
path: ".gitea/scoped_workflows/security.yml",
expected: true,
},
{
name: "default scoped dir with yaml",
dirs: []string{".gitea/scoped_workflows", ".github/scoped_workflows"},
path: ".github/scoped_workflows/lint.yaml",
expected: true,
},
{
name: "normal workflow path is not a scoped workflow",
dirs: []string{".gitea/scoped_workflows"},
path: ".gitea/workflows/ci.yml",
expected: false,
},
{
name: "non-yaml file",
dirs: []string{".gitea/scoped_workflows"},
path: ".gitea/scoped_workflows/readme.md",
expected: false,
},
{
name: "feature disabled (no scoped dirs)",
dirs: []string{},
path: ".gitea/scoped_workflows/security.yml",
expected: false,
},
{
name: "directory boundary",
dirs: []string{".gitea/scoped_workflows"},
path: ".gitea/scoped_workflows2/security.yml",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, isWorkflowInDirs(tt.path, tt.dirs))
})
}
}
+51 -2
View File
@@ -5,6 +5,8 @@ package actions
import (
"bytes"
"fmt"
"path"
"slices"
"strings"
@@ -38,11 +40,20 @@ func init() {
}
func IsWorkflow(path string) bool {
return isWorkflowInDirs(path, setting.Actions.WorkflowDirs)
}
// IsWorkflowOrScopedWorkflow reports whether path is a workflow file under WORKFLOW_DIRS or SCOPED_WORKFLOW_DIRS.
func IsWorkflowOrScopedWorkflow(path string) bool {
return isWorkflowInDirs(path, setting.Actions.WorkflowDirs) || isWorkflowInDirs(path, setting.Actions.ScopedWorkflowDirs)
}
func isWorkflowInDirs(path string, dirs []string) bool {
if (!strings.HasSuffix(path, ".yaml")) && (!strings.HasSuffix(path, ".yml")) {
return false
}
for _, workflowDir := range setting.Actions.WorkflowDirs {
for _, workflowDir := range dirs {
if strings.HasPrefix(path, workflowDir+"/") {
return true
}
@@ -51,10 +62,14 @@ func IsWorkflow(path string) bool {
}
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
return listWorkflowsInDirs(commit, setting.Actions.WorkflowDirs)
}
func listWorkflowsInDirs(commit *git.Commit, dirs []string) (string, git.Entries, error) {
var tree *git.Tree
var err error
var workflowDir string
for _, workflowDir = range setting.Actions.WorkflowDirs {
for _, workflowDir = range dirs {
tree, err = commit.SubTree(workflowDir)
if err == nil {
break
@@ -117,6 +132,40 @@ func ValidateWorkflowContent(content []byte) error {
return err
}
// WorkflowDisplayName returns a workflow's display name: its `name:` if non-blank, otherwise the base file name.
// This is the value used as the workflow segment of its commit-status context.
func WorkflowDisplayName(file string, content []byte) string {
displayName := path.Base(file)
if wfs, err := jobparser.Parse(content); err == nil && len(wfs) > 0 {
if name := strings.TrimSpace(wfs[0].Name); name != "" {
displayName = name
}
}
return displayName
}
// WorkflowStatusContextName builds a workflow job's commit-status context name: "<display> / <job> (<event>)".
func WorkflowStatusContextName(displayName, jobName, event string) string {
return strings.TrimSpace(fmt.Sprintf("%s / %s (%s)", displayName, jobName, event))
}
// ScopedWorkflowStatusContextName prefixes a scoped run's status-check context with its source repo, set off by a colon: "<prefix>: <display> / <job> (<event>)".
func ScopedWorkflowStatusContextName(prefix, displayName, jobName, event string) string {
return strings.TrimSpace(fmt.Sprintf("%s: %s", prefix, WorkflowStatusContextName(displayName, jobName, event)))
}
// ShouldEventCreateCommitStatus reports whether a run triggered by the given workflow `on:` event posts a commit status,
// so its context can serve as a required status check.
// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted.
// The two are kept in sync by hand and can drift; unify them into a single source so adding a status-producing event in one place automatically updates the other.
func ShouldEventCreateCommitStatus(event string) bool {
switch event {
case "push", "pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment", "release":
return true
}
return false
}
func DetectWorkflows(
gitRepo *git.Repository,
commit *git.Commit,
+1 -1
View File
@@ -106,7 +106,7 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
// GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits
revs, err := WalkGitLog(ctx, commit.repo, commit, treePath, paths...)
revs, err := walkGitLog(ctx, commit.repo, commit, treePath, paths...)
if err != nil {
return nil, err
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
"context"
"path/filepath"
"testing"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
require.NoError(t, err)
defer repo.Close()
commit, err := repo.GetCommit("feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
require.NoError(t, err)
entries, err := commit.Tree.ListEntries()
require.NoError(t, err)
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
for _, info := range infos {
nilCommits += util.Iif(info.Commit == nil, 1, 0)
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
}
return nilCommits, nonNilCommits
}
ctx, cancel := context.WithCancel(t.Context())
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
walkGitLogDebugBeforeNext = cancel
commitInfos, _, err := entries.GetCommitsInfo(ctx, "/any/repo-link", commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
assert.Equal(t, 3, nilCommits)
walkGitLogDebugBeforeNext = nil
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, "")
assert.NoError(t, err)
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
assert.Equal(t, 3, nonNilCommits)
assert.Equal(t, 0, nilCommits)
}
+11 -9
View File
@@ -21,14 +21,6 @@ func syncGitConfig(ctx context.Context) (err error) {
return fmt.Errorf("unable to prepare git home directory %s, err: %w", gitcmd.HomeDir(), err)
}
// first, write user's git config options to git config file
// user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes
for k, v := range setting.GitConfig.Options {
if err = configSet(ctx, strings.ToLower(k), v); err != nil {
return err
}
}
// Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults"
// TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used.
// If these values are not really used, then they can be set (overwritten) directly without considering about existence.
@@ -111,8 +103,18 @@ func syncGitConfig(ctx context.Context) (err error) {
}
err = configUnsetAll(ctx, "uploadpack.allowAnySHA1InWant", "true")
}
if err != nil {
return err
}
return err
// Apply user's git config options last so they take precedence over builtin defaults
for k, v := range setting.GitConfig.Options {
if err = configSet(ctx, strings.ToLower(k), v); err != nil {
return err
}
}
return nil
}
func configSet(ctx context.Context, key, value string) error {
+10 -5
View File
@@ -10,6 +10,7 @@ import (
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
@@ -55,14 +56,18 @@ func TestGitConfig(t *testing.T) {
assert.False(t, gitConfigContains("key-x = *"))
}
func TestSyncConfig(t *testing.T) {
oldGitConfig := setting.GitConfig
defer func() {
setting.GitConfig = oldGitConfig
}()
func TestSyncGitConfig(t *testing.T) {
defer test.MockVariableValue(&setting.GitConfig)()
assert.Empty(t, setting.GitConfig.Options)
assert.NoError(t, syncGitConfig(t.Context()))
assert.True(t, gitConfigContains("commitGraph = true")) // builtin default config
setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA"
setting.GitConfig.Options["core.commitgraph"] = "false"
assert.NoError(t, syncGitConfig(t.Context()))
assert.True(t, gitConfigContains("[sync-test]"))
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
assert.False(t, gitConfigContains("commitGraph")) // builtin default config can be overridden
assert.True(t, gitConfigContains("commitgraph = false")) // git config key is case-insensitive
}
+1 -1
View File
@@ -16,7 +16,7 @@ var (
)
// Format supports specifying and parsing an output format for 'git
// for-each-ref'. See See git-for-each-ref(1) for available fields.
// for-each-ref'. See git-for-each-ref(1) for available fields.
type Format struct {
// fieldNames hold %(fieldname)s to be passed to the '--format' flag of
// for-each-ref. See git-for-each-ref(1) for available fields.
+1 -1
View File
@@ -32,7 +32,7 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string
entryPaths[i] = entry.Name()
}
_, err = WalkGitLog(ctx, c.repo, c, treePath, entryPaths...)
_, err = walkGitLog(ctx, c.repo, c, treePath, entryPaths...)
if err != nil {
return err
}
@@ -1,6 +1,8 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (
@@ -18,10 +20,8 @@ import (
"gitea.dev/modules/log"
)
// LogNameStatusRepo opens git log --raw in the provided repo and returns a stdin pipe, a stdout reader and cancel function
func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) (*bufio.Reader, func()) {
// Lets also create a context so that we can absolutely ensure that the command should die when we're done
// logNameStatusRepo opens git log --raw in the provided repo and returns a parser
func logNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) *logNameStatusRepoParser {
cmd := gitcmd.NewCommand()
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head)
@@ -54,77 +54,62 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p
ctx, ctxCancel := context.WithCancel(ctx)
go func() {
err := cmd.WithDir(repository).RunWithStderr(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
log.Error("Unable to run git command %v: %v", cmd.LogString(), err)
}
}()
bufReader := bufio.NewReaderSize(stdoutReader, 32*1024)
return bufReader, func() {
ctxCancel()
stdoutReaderClose()
return &logNameStatusRepoParser{
treepath: treepath,
paths: paths,
rd: bufReader,
close: func() {
ctxCancel()
stdoutReaderClose()
},
}
}
// LogNameStatusRepoParser parses a git log raw output from LogRawRepo
type LogNameStatusRepoParser struct {
// logNameStatusRepoParser parses a git log raw output from LogRawRepo
type logNameStatusRepoParser struct {
treepath string
paths []string
next []byte
buffull bool
rd *bufio.Reader
cancel func()
close func()
}
// NewLogNameStatusRepoParser returns a new parser for a git log raw output
func NewLogNameStatusRepoParser(ctx context.Context, repository, head, treepath string, paths ...string) *LogNameStatusRepoParser {
rd, cancel := LogNameStatusRepo(ctx, repository, head, treepath, paths...)
return &LogNameStatusRepoParser{
treepath: treepath,
paths: paths,
rd: rd,
cancel: cancel,
}
}
// LogNameStatusCommitData represents a commit artefact from git log raw
type LogNameStatusCommitData struct {
// logNameStatusCommitData represents a commit artifact from git log raw
type logNameStatusCommitData struct {
CommitID string
ParentIDs []string
Paths []bool
}
// Next returns the next LogStatusCommitData
func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int, changed []bool, maxpathlen int) (*LogNameStatusCommitData, error) {
// walkNext returns the next LogStatusCommitData
func (g *logNameStatusRepoParser) walkNext(treepath string, paths2ids map[string]int, changed []bool, maxpathlen int) (*logNameStatusCommitData, error) {
var err error
if len(g.next) == 0 {
g.buffull = false
g.next, err = g.rd.ReadSlice('\x00')
if err != nil {
switch err {
case bufio.ErrBufferFull:
g.buffull = true
case io.EOF:
return nil, nil //nolint:nilnil // return nil to signal EOF
default:
return nil, err
}
switch {
case errors.Is(err, bufio.ErrBufferFull):
g.buffull = true
case err != nil:
return nil, err
}
}
ret := LogNameStatusCommitData{}
ret := logNameStatusCommitData{}
if bytes.Equal(g.next, []byte("commit\000")) {
g.next, err = g.rd.ReadSlice('\x00')
if err != nil {
switch err {
case bufio.ErrBufferFull:
g.buffull = true
case io.EOF:
return nil, nil //nolint:nilnil // return nil to signal EOF
default:
return nil, err
}
switch {
case errors.Is(err, bufio.ErrBufferFull):
g.buffull = true
case err != nil:
return nil, err
}
}
@@ -273,13 +258,10 @@ diffloop:
}
}
// Close closes the parser
func (g *LogNameStatusRepoParser) Close() {
g.cancel()
}
var walkGitLogDebugBeforeNext func() // is used to simulate various edge git process cases
// WalkGitLog walks the git log --name-status for the head commit in the provided treepath and files
func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
// walkGitLog walks the git log --name-status for the head commit in the provided treepath and files
func walkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
headRef := head.ID.String()
tree, err := head.SubTree(treepath)
@@ -322,11 +304,9 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st
}
}
g := NewLogNameStatusRepoParser(ctx, repo.Path, head.ID.String(), treepath, paths...)
// don't use defer g.Close() here as g may change its value - instead wrap in a func
defer func() {
g.Close()
}()
g := logNameStatusRepo(ctx, repo.Path, head.ID.String(), treepath, paths...)
// don't use defer g.cancel() here as g may change its value - instead wrap in a func
defer func() { g.close() }()
results := make([]string, len(paths))
remaining := len(paths)
@@ -340,25 +320,16 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st
heaploop:
for {
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
break heaploop
}
g.Close()
return nil, ctx.Err()
default:
if walkGitLogDebugBeforeNext != nil {
walkGitLogDebugBeforeNext()
}
current, err := g.Next(treepath, path2idx, changed, maxpathlen)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
break heaploop
}
g.Close()
return nil, err
}
if current == nil {
break heaploop
current, err := g.walkNext(treepath, path2idx, changed, maxpathlen)
if ctx.Err() != nil {
break heaploop // context is either canceled or deadline exceeded - break the loop and return what we have so far
} else if errors.Is(err, io.EOF) {
break heaploop // reached to the end of log output
} else if err != nil {
return nil, err // other unknown errors
}
parentRemaining.Remove(current.CommitID)
for i, found := range current.Paths {
@@ -395,14 +366,14 @@ heaploop:
if remaining <= nextRestart {
commitSinceNextRestart++
if 4*commitSinceNextRestart > 3*commitSinceLastEmptyParent {
g.Close()
remainingPaths := make([]string, 0, len(paths))
for i, pth := range paths {
if results[i] == "" {
remainingPaths = append(remainingPaths, pth)
}
}
g = NewLogNameStatusRepoParser(ctx, repo.Path, lastEmptyParent, treepath, remainingPaths...)
g.close()
g = logNameStatusRepo(ctx, repo.Path, lastEmptyParent, treepath, remainingPaths...)
parentRemaining = make(container.Set[string])
nextRestart = (remaining * 3) / 4
continue heaploop
@@ -410,7 +381,6 @@ heaploop:
}
parentRemaining.AddMultiple(current.ParentIDs...)
}
g.Close()
resultsMap := map[string]string{}
for i, pth := range paths {
+9 -6
View File
@@ -228,13 +228,16 @@ func (ref RefName) RefWebLinkPath() string {
return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName())
}
func ParseRefSuffix(ref string) (string, string) {
func ParseRefSuffix(ref string) (refName, refSuffix string) {
// Partially support https://git-scm.com/docs/gitrevisions
if idx := strings.Index(ref, "@{"); idx != -1 {
return ref[:idx], ref[idx:]
suffixIdx := -1 // earliest suffix mark, so a combined suffix like "main~2^" stays intact
for _, mark := range []string{"@{", "^", "~"} {
if idx := strings.Index(ref, mark); idx != -1 && (suffixIdx == -1 || idx < suffixIdx) {
suffixIdx = idx
}
}
if idx := strings.Index(ref, "^"); idx != -1 {
return ref[:idx], ref[idx:]
if suffixIdx == -1 {
return ref, ""
}
return ref, ""
return ref[:suffixIdx], ref[suffixIdx:]
}
+19
View File
@@ -37,3 +37,22 @@ func TestRefWebLinkPath(t *testing.T) {
assert.Equal(t, "tag/foo", RefName("refs/tags/foo").RefWebLinkPath())
assert.Equal(t, "commit/c0ffee", RefName("c0ffee").RefWebLinkPath())
}
func TestParseRefSuffix(t *testing.T) {
cases := []struct {
ref, name, suffix string
}{
{"main", "main", ""},
{"main^", "main", "^"},
{"main^2", "main", "^2"},
{"main~3", "main", "~3"},
{"main@{yesterday}", "main", "@{yesterday}"},
{"main~2^", "main", "~2^"},
{"main^~2", "main", "^~2"},
}
for _, c := range cases {
name, suffix := ParseRefSuffix(c.ref)
assert.Equal(t, c.name, name, "ref: %s", c.ref)
assert.Equal(t, c.suffix, suffix, "ref: %s", c.ref)
}
}

Some files were not shown because too many files have changed in this diff Show More