Addresses a batch of privately reported security issues, grouped by
area:
- **SSRF** - migration PR-patch/asset fetches, OAuth2 avatar & OpenID
discovery, pull-mirror URL re-validation, and the outbound proxy path.
- **Access-token scope** - prevent scope escalation on token creation;
keep public-only tokens confined (feeds, packages, Actions listings,
star/watch lists, limited/private owners).
- **Access control / disclosure** - go-get default-branch leak, webhook
authorization-header leak, watch clearing on private transitions,
label/attachment scoping.
- **Denial of service** - input bounds for npm dist-tags, Debian control
files, Arch file lists, and SSH keys.
### 📌 Attention for site admins
Not breaking - existing configs keep working - but two changes are worth
a look:
- **New SSRF protection** Outbound requests (migrations, OAuth2 avatars,
OpenID discovery, pull mirrors, proxy path) are now validated against
the allow/block host lists. If your instance legitimately reaches
internal hosts, you may need to add them to
`[security].ALLOWED_HOST_LIST` (and the relevant `ALLOW_LOCALNETWORKS`
settings).
- **Deprecation** `[webhook].ALLOWED_HOST_LIST` is deprecated and will
be removed in a future release. Use `[security].ALLOWED_HOST_LIST`
instead; the old key still works for now.
---------
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
The **Status** filter dropdown on the repository Actions run list does
not let you filter for **Blocked** runs (nor **Cancelled** or
**Skipped**). These statuses are missing from the dropdown even though a
run can legitimately end up in any of them.
A run's status is computed by `aggregateJobStatus`, which can return
`Blocked`, `Cancelled` and `Skipped`. Because the filter dropdown only
offered Success, Failure, Waiting, Running and Cancelling, runs in those
other states existed but were impossible to filter for.
## 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>
The `index` column is unique per repo, but the `id` column is scoped to
the whole table
240d0efa7e/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>
`ifNeedApproval` in `services/actions/notifier_helper.go` decided
whether a
fork PR's workflow run had to wait for maintainer approval. The bypass
clause
counted any prior `approved_by > 0` run for `(repo_id,
trigger_user_id)`, so
the very first Approve-and-run click on a contributor's fork PR
permanently
trusted that user for every future fork PR in the same repository —
including
PRs whose only change is the workflow YAML itself.
Approving a workflow *run* is not the same as merging *code*. This
change
aligns the gate with GitHub Actions' first-time-contributor model: trust
is
granted only after the user has had a pull request merged in the repo.
## Behavior change
- **Before**: one approval = permanent trust for that user in that repo.
- **After**: every fork PR is gated until the contributor has at least
one
merged PR in the repo.
Existing already-approved runs and merged PRs continue to work; only the
trust criterion for *future* fork PRs changes. Maintainers who rely on
the
implicit "approve once" trust will see the approval banner reappear
until
they merge a PR from that contributor.
## Summary
- Add a Branch filter dropdown to the repo Actions run list web UI
- Wire `?branch=` query param through the web handler, matching the
existing REST API filter behavior
- Source the Branch dropdown from the indexed `branch` table (filtering
out deleted branches) instead of scanning `action_run.ref`, addressing
review feedback about unindexed columns
The Event filter was dropped after review: a static list of supported
events was noisy as UX, and querying distinct values from
`action_run.trigger_event` is slow because the column is not indexed.
`FindRunOptions.TriggerEvent` is kept for the REST API.
Closes#25042
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Fixes#36983
## Summary
1. Add transitional `Cancelling` status (between `Running` and
`Cancelled`); cancel flow marks active tasks `Cancelling`, runner
finalizes to `Cancelled` on terminal result.
2. Taskless jobs cancel directly (no runner to finalize).
3. Runner-protocol responses map `Cancelling` → `RESULT_CANCELLED`.
4. Run/job aggregation treats `Cancelling` as active.
5. Status mapping/aggregation tests + en-US locale added.
**Problem**
When a workflow was cancelled from the UI, jobs were marked cancelled
immediately, which could skip post-run cleanup behavior.
## Solution
Use a transitional status path:
Running → Cancelling → Cancelled
This allows runner finalization and cleanup path execution before final
terminal state.
**Testing**
> 1. go test -tags "sqlite sqlite_unlock_notify" ./models/actions -run
"TestAggregateJobStatus|TestStatusAsResult|TestStatusFromResult"
> 2. go run
github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 run
./models/actions/... ./routers/api/actions/runner/...
## Related
- act_runner: https://gitea.com/gitea/act_runner/pulls/825 —
independent; this PR's capability gate keeps legacy runners on the
immediate-cancel path. The new flow activates only for runners that
advertise the `cancelling` capability.
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Avoid per-item DB queries in ListRuns, ListJobs, and ListActionTasks by
batch-loading trigger users, repositories, and task attributes before
the conversion loop. Remove ReferencesGitRepo from the /actions route
group since no task/run endpoints use it.
Added tests for these endpoints as well.
---------
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This PR introduces a new `ActionRunAttempt` model and makes Actions
execution attempt-scoped.
**Main Changes**
- Each workflow run trigger generates a new `ActionRunAttempt`. The
triggered jobs are then associated with this new `ActionRunAttempt`
record.
- Each rerun now creates:
- a new `ActionRunAttempt` record for the workflow run
- a full new set of `ActionRunJob` records for the new
`ActionRunAttempt`
- For jobs that need to be rerun, the new job records are created as
runnable jobs in the new attempt.
- For jobs that do not need to be rerun, new job records are still
created in the new attempt, but they reuse the result of the previous
attempt instead of executing again.
- Introduce `rerunPlan` to manage each rerun and refactored rerun flow
into a two-phase plan-based model:
- `buildRerunPlan`
- `execRerunPlan`
- `RerunFailedWorkflowRun` and `RerunFailed` no longer directly derives
all jobs that need to be rerun; this step is now handled by
`buildRerunPlan`.
- Converted artifacts from run-scoped to attempt-scoped:
- uploads are now associated with `RunAttemptID`
- listing, download, and deletion resolve against the current attempt
- Added attempt-aware web Actions views:
- the default run page shows the latest attempt
(`/actions/runs/{run_id}`)
- previous attempt pages show jobs and artifacts for that attempt
(`/actions/runs/{run_id}/attempts/{attempt_num}`)
- New APIs:
- `/repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt}`
- `/repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt}/jobs`
- New configuration `MAX_RERUN_ATTEMPTS`
- https://gitea.com/gitea/docs/pulls/383
**Compatibility**
- Existing legacy runs use `LatestAttemptID = 0` and legacy jobs use
`RunAttemptID = 0`. Therefore, these fields can be used to identify
legacy runs and jobs and provide backward compatibility.
- If a legacy run is rerun, an `ActionRunAttempt` with `attempt=1` will
be created to represent the original execution. Then a new
`ActionRunAttempt` with `attempt=2` will be created for the real rerun.
- Existing artifact records are not backfilled; legacy artifacts
continue to use `RunAttemptID = 0`.
**Improvements**
- It is now easier to inspect and download logs from previous attempts.
-
[`run_attempt`](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context)
semantics are now aligned with GitHub.
- > A unique number for each attempt of a particular workflow run in a
repository. This number begins at 1 for the workflow run's first
attempt, and increments with each re-run.
- Rerun behavior is now clearer and more explicit.
- Instead of mutating the status of previous jobs in place, each rerun
creates a new attempt with a full new set of job records.
- Artifacts produced by different reruns can now be listed separately.
Signed-off-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Fix#28157
This PR fix the possible bugs about actions schedule.
## The Changes
- Move `UpdateRepositoryUnit` and `SetRepoDefaultBranch` from models to
service layer
- Remove schedules plan from database and cancel waiting & running
schedules tasks in this repository when actions unit has been disabled
or global disabled.
- Remove schedules plan from database and cancel waiting & running
schedules tasks in this repository when default branch changed.
- cancel running jobs if the event is push
- Add a new function `CancelRunningJobs` to cancel all running jobs of a
run
- Update `FindRunOptions` struct to include `Ref` field and update its
condition in `toConds` function
- Implement auto cancellation of running jobs in the same workflow in
`notify` function
related task: https://github.com/go-gitea/gitea/pull/22751/
---------
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>