Fixes several gaps in the approval of fork pull request runs:
1. Approving a run that was cancelled while awaiting approval revived
its cancelled jobs. Such a run is no longer treated as awaiting approval
by the merge box, run page, approve actions and API, and rerunning it
approves it.
2. Approval no longer revives jobs cancelled while the run was pending,
no longer lets two jobs sharing a concurrency group cancel each other,
and re-emits the run so jobs needing a cancelled job get resolved.
3. An unapproved run applies its workflow-level concurrency only once
approved.
4. For workflows from the pull request, both the event actor and the
pull request author must be trusted to skip approval. Workflows from the
default branch, like `issue_comment`, still only check the actor.
---------
Co-authored-by: silverwind <me@silverwind.io>
Adds a read-only Actions job queue: running jobs first, then waiting
jobs in the order a runner picks them up. It is shown instance-wide in
the admin Actions section with owner, repository and status filters, and
per repository in the Actions tab. Both lists refresh in place.
Pending work is currently only visible per repository and newest-first,
so nothing shows what is queued, in which order, or what occupies the
runners. Reordering the queue will be proposed separately.
A migration adds indexes for the runner pickup query and
repository-scoped status lookups.
* Fix#34198
<img width="1345" height="451" alt="image"
src="https://github.com/user-attachments/assets/7d52ff76-81b4-44e8-b583-d7d89c9dffcd"
/>
<img width="1809" height="1134" alt="image"
src="https://github.com/user-attachments/assets/4d56c0cb-bae7-4ce2-8f3c-75163b2bc7f4"
/>
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Closes https://github.com/go-gitea/gitea/issues/33579.
Adds browser previews for Actions artifacts. Selecting an artifact opens
its file browser; selecting a file renders it in the same tab. The ZIP
download remains available separately.
Previews require sign-in and read access to the run. Text, image and PDF
files are supported; rendered HTML and JavaScript run in a sandboxed
frame and are labeled as automatically generated. The frame loads files
from a signed link that expires after an hour, because its requests
carry no session cookie. `[actions] ARTIFACT_PREVIEW_MAX_SIZE` limits
total previewable artifact size (`0` disables previews; `-1` removes the
limit); individual files also follow `[ui] MAX_DISPLAY_FILE_SIZE`.
<img width="1803" height="913" alt="image"
src="https://github.com/user-attachments/assets/a38fd704-2244-44fa-9181-c695ecbe0276"
/>
Docs: https://gitea.com/gitea/docs/pulls/533
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
https://github.com/go-gitea/gitea/pull/38406 stopped setting
`ServerName` on the internal API client, which
https://github.com/go-gitea/gitea/pull/5820 had added for ACME. Internal
requests to a local `LOCAL_ROOT_URL` now send SNI `localhost` (or none
for IPs). The ACME listener selects its certificate by SNI, finds none
and aborts the handshake with `tls: internal error`, breaking SSH access
and git hooks.
Send the `ROOT_URL` host as SNI again for local targets, and treat
unspecified addresses (`0.0.0.0`, `::`) as local since dialing them
reaches the local host. Remote targets are still verified against their
own hostname.
Fixes: https://github.com/go-gitea/gitea/issues/38903
---------
Co-authored-by: silverwind <me@silverwind.io>
Closes https://github.com/go-gitea/gitea/issues/39414
`CommentList.loadAssignees` set a Ghost assignee on every comment
without an `AssigneeID` whenever another comment in the same batch had
one. Since https://github.com/go-gitea/gitea/pull/38413 the timeline
prefers `Assignee` over `AssigneeTeam`, so team review requests, from
CODEOWNERS or added manually, rendered as "Ghost" whenever the timeline
also had a user assignee or user review request. Skip comments without
an `AssigneeID`.
Co-authored-by: silverwind <me@silverwind.io>
Require organization ownership before changing repository team
associations when team access is restricted.
---------
Co-authored-by: silverwind <me@silverwind.io>
Remove temporary recipient access after a transfer ends while preserving
existing collaboration.
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Enforce Issues and Pull Requests access for references within the
current repository.
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Bound gitignore template selections at both web and API request
boundaries before repository initialization.
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Use the canonical profile-visibility check for user API content and
prevent restricted users from enumerating public repositories owned by
limited users.
This keeps feeds, heatmaps, keys, and issue search consistent with
profile visibility.
---------
Co-authored-by: silverwind <me@silverwind.io>
Create delegate hook files and directories without group or other write
access, including correcting existing hook directories.
_Assisted-by: Codex:GPT-5_
Reject public-only tokens for repository migrations and require
repository scope for canonical organization repository creation. This
aligns both routes with the existing token authorization boundaries.
_Assisted-by: Codex:GPT-5_
Exclude public repositories owned by hidden individual accounts from
broad repository listings, while preserving visibility through explicit
access and ownership.
_Assisted-by: Codex:GPT-5_
`FullSteps` only gave the synthetic "Complete job" step the remaining
log range when the last step that had run was also the final step of the
job. A skipped step does not count as having run, so any job ending in a
skipped step left the post step with an empty range: its logs were
stored but never rendered, and the duration showed as `0s`.
Reproducible with any job whose last step is skipped, which is common
for failure notifications:
```yaml
steps:
- run: echo hello
- run: echo never
if: failure()
```
The gate now checks whether the final step is done, which preserves the
behaviour from https://github.com/go-gitea/gitea/pull/29926 of showing
the post step as waiting while steps are still pending.
--> Regression from https://github.com/go-gitea/gitea/pull/29926
---------
Signed-off-by: bircni <bircni@icloud.com>
Admin and write team authorize now grant that mode on every unit,
including units added later, instead of only rows present in
`team_unit`. Granular teams keep `authorize=none` and explicit unit
rows.
Closes the `TEAM-UNIT-PERMISSION` design gap from
https://github.com/go-gitea/gitea/pull/34128.
Maybe also fix#15962 (actually maybe it had been fixed before, the root
cause is out-of-sync "access" table)
## Screenshots
only writing selected:
<img width="1399" height="1007" alt="image"
src="https://github.com/user-attachments/assets/1d1b4c49-a59a-47b6-998f-0464a067395b"
/>
_Created with the help of AI_
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Add a GitHub-style quick repo switcher: a caret next to the owner/repo
breadcrumb
opens a dropdown that lists and searches the current owner's
repositories and
navigates to the selected one. The current repository is marked with a
check, and
private/fork repos show an icon.
Also, fix various bugs in fomtantic dropdown remote query
## Screenshots
<img width="505" height="198" alt="image"
src="https://github.com/user-attachments/assets/9f673d1b-fe60-41f0-b9e2-b00dc43720b5"
/>
Fixes#38187
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
The dashboard activity feed was paginated with `[ui.user]
REPO_PAGING_NUM`
instead of `[ui] FEED_PAGING_NUM`.
The wrong setting was picked up when the page size was hoisted into a
local
variable in https://github.com/go-gitea/gitea/pull/34994, most likely
copied
from the `dashboardRepoList` block a few lines above. `REPO_PAGING_NUM`
should
only control repository lists.
Fixes https://github.com/go-gitea/gitea/issues/38925
`github.event.inputs` must mirror the raw `workflow_dispatch` payload,
where
GitHub keeps every input as a string. Only the separate `inputs` context
preserves declared types, e.g. booleans. A previous fix coerced boolean
inputs in the single map that fed both contexts, so
`github.event.inputs.someBool` became a real boolean and comparisons
like
`== 'true'` stopped matching.
`github.event.inputs` now stays string-only again. The `inputs` context
used
for server-side `if:` evaluation of needs-gated/matrix-deferred jobs
re-coerces booleans independently, from the job's own workflow
declaration,
so that path keeps working correctly.
Fixes https://github.com/go-gitea/gitea/issues/38896
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
For a `pull_request_target` (PRT) run, Gitea loads the top-level
workflow from the trusted base branch, but any local reusable workflow
it calls (`uses: ./...`) was read from the PR **head** commit, which the
fork author controls.
## Fix
**Record the source commit where the content is read.**
`DetectedWorkflow` now carries a `SourceCommitSHA` filled in next to
`Content`, so the PRT detection pass at the base commit records the base
SHA automatically.
**Defense in depth.** `loadReusableWorkflowSource` pins the PR base
commit for a PRT run's local `uses: ./...` rather than trusting the
stored SHA. This also covers runs recorded before this change, whose
rows still hold the head SHA and would otherwise resolve from the fork
on rerun.
Existing run rows are not migrated.
---------
Co-authored-by: Zettat <zettat123@gmail.com>
Follow-up to https://github.com/go-gitea/gitea/pull/36564 (dynamic
matrix) and https://github.com/go-gitea/gitea/pull/36357 (max-parallel),
fixing issues found reviewing the two features together.
- **A placeholder could stall its run forever.** Its payload keeps the
raw matrix but loses its `needs`, so `ParseJob` re-expanded it instead
of reading it back — fatal for `include: ${{ fromJson(needs.*.outputs.*)
}}`.
- **An `if:` reading `matrix.*` skipped the whole job**, with or without
the `${{ }}`. It now reduces to the needs gate, except under
`always()`/`failure()`/`cancelled()`, and each combination is decided on
its own values once the matrix expands.
- **Dependents could be skipped before the combinations ran**, since
inserted siblings are absent from the resolver's job set. The pass now
stops after an insert and defers to the re-emit it schedules.
- **Expansion failures stranded the placeholder.** A retryable one is
returned so the queue retries it; a malformed payload fails the job
instead of requeueing forever.
- **Rerun could rewind a pass-through row** into a raw placeholder
keeping its old terminal status, which nothing expands. Now gated on the
anchor itself.
Plus: `max-parallel` distinguishes an unevaluated `${{ }}` (debug) from
a non-numeric literal (warn — it silently drops the cap).
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Fixes jobs that get stuck in `cancelling` after Gitea is restarted while
a job is running.
Reproduction:
1. Run a job with `sleep 100`
2. Stop Gitea and wait 120s (> 100s)
3. Start Gitea — the job is still `running`; cancel it, and it stays in
`cancelling`
## Cause
A cancellation is only ever delivered to a runner as the *response* to
its `UpdateTask`
RPC. A runner that has already given up on the task — it crashed, or it
failed to report
the final state while Gitea was unreachable — never calls `UpdateTask`
again, so it never
learns about the cancellation. The task then sits in `cancelling` until
`stop_zombie_tasks`
reaps it, which needs `ZOMBIE_TASK_TIMEOUT` (10m) of silence and only
runs every 5 minutes.
Cancelling actually made this worse. xorm rewrites the `updated` column
on every `UPDATE`,
even when `Cols()` restricts the update to `status`, so persisting the
`cancelling` status
reset the zombie clock. Pressing cancel pushed the cleanup a full
`ZOMBIE_TASK_TIMEOUT`
into the future instead of bringing it forward.
## Change
`StopTask` now skips the `cancelling` handshake when the task has had no
state report from
its runner for longer than `TaskReportTimeout` (1 minute) and cancels it
directly. This
joins the two existing fallbacks — runner deleted, and runner without
cancelling support —
so every cancel path (web UI, API, concurrency, rerun) is covered.
Runners report the state of a running task every few seconds, so a
minute of silence means
the runner is gone. The value is a constant rather than a setting
because it only decides
whether the runner is still reachable, not whether a task should be
killed —
`ZOMBIE_TASK_TIMEOUT` still owns that.
### Tradeoff
If a runner is alive but has been silent for over a minute and is
cancelled in that window,
it skips the graceful post-step cleanup added in #37275. No work is
lost: the runner still
learns the outcome on its next report, because `UpdateTask` returns the
task status as the
result and the runner stops there. That is the behaviour that existed
before #37275.
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Several small fixes to the Actions runner management UI.
### Runner task list links to the job, not the workflow run
Relabeled the first column from "Run" to "Job"; it now shows the job ID
and links to the specific job (`/actions/runs/{runID}/jobs/{jobID}`).
Renamed locale key `task_list.run` to `task_list.job`.
### Missing "Disabled" translation
The runner list rendered a grey label via `actions.runners.disabled`,
but that key
did not exist in `locale_en-US.json`, so the raw key string leaked into
the UI.
Replaced `"actions.runners.disabled"` with `"disabled"`.
### Status column sorting ignored active vs idle
Sorting by status ordered purely on `last_online`, but the displayed
status is
computed from both `last_online` (offline) and `last_active` (idle vs
active).
As a result idle runners were interleaved with active ones. Sorting now
ranks by
the computed status (active → idle → offline). Disabled runners sink to
the bottom
of their status group (`is_disabled` as a secondary key), with
`last_online`/`id`
as stable tiebreakers so pagination stays deterministic.
### Status label colors
Active and idle both rendered green. Idle is now yellow, active green,
and
offline/unknown grey; the separate grey "Disabled" badge is unchanged.
This keeps
connectivity visible even for disabled runners (e.g. a disabled runner
still shows
whether it is idle or offline).
<img width="715" height="406" alt="image"
src="https://github.com/user-attachments/assets/9ef06aa8-a870-4de5-9d94-603a58186908"
/>
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Various fixes to actions
1. **Cap total jobs per run in reusable-workflow expansion** — only
nesting depth was capped, so fan-out + nested reusable workflows could
explode job-row inserts and exhaust the DB from a single push. Now
enforces `MaxJobNumPerRun` in the insert path.
2. **Reject rerun-failed when a run has no failed jobs** — an empty job
list meant "re-run everything", so `rerun-failed` on a green run re-ran
all jobs. Now errors (web + API).
3. **Don't adopt external commit statuses into the legacy hash** — the
pre-#35699 Context-only hash matched API-posted statuses too, collapsing
two same-named workflows into one check. Now limited to Actions-user
rows.
4. **Don't cut post-cancel cleanup short in `StopEndlessTasks`** — the
sweep force-stopped just-cancelled jobs mid-cleanup. Now targets
`StatusRunning` only; stalled cancels stay covered by `StopZombieTasks`.
5. **Avoid redundant run reload in `GenerateGiteaContext`** — resolving
`github.triggering_actor` reloaded the run already passed in. Now loads
only the trigger user via new `ActionRunAttempt.LoadTriggerUser`.
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
A `workflow_dispatch` job that has `needs:` **and** an `if:` comparing a
boolean
input against a boolean literal never runs — it stays `Blocked` forever
even
though its needs succeed. Minimal repro:
```yaml
on:
workflow_dispatch:
inputs:
deploy:
type: boolean
default: true
jobs:
build:
runs-on: ubuntu-latest
steps: [{ run: echo build }]
deploy:
needs: build
if: ${{ inputs.deploy == true }} # never true on Gitea
runs-on: ubuntu-latest
steps: [{ run: echo deploy }]
```
On GitHub this runs; on Gitea `deploy` is stuck. Jobs **without**
`needs` are
unaffected, which makes it look like a matrix/`needs` bug — it isn't.
## Root cause
`workflow_dispatch` stores boolean inputs as the strings
`"true"`/`"false"`
(`ctx.FormBool` → `strconv.FormatBool` in the web path, plain strings in
the API
path). Since #37478 (shipped in **v1.27.0**), `evaluateJobIf` runs
**server-side**
as part of the job-emitter resolver passes. For a `needs`-gated job the
server
therefore evaluates `inputs.deploy == true` with `inputs.deploy` being
the string
`"true"`; comparing a string to a boolean coerces to `NaN == 1` →
`false`, so the
job is never dispatched.
Jobs without `needs` skip this server-side gate and are evaluated by the
runner,
which coerces the string to a real bool — that's why they keep working,
and why
the same input yields opposite results in the two paths.
## Fix
Normalize `type: boolean` dispatch inputs to native JSON booleans in the
shared
`DispatchActionWorkflow` path, so it covers both the web and API entry
points in
one place. The coerced value flows into both the run's `EventPayload`
(read back
by the server-side `if` evaluation and by the runner) and the
creation-time
parse, so all consumers agree.
This matches GitHub, whose `inputs` context "preserves Boolean values as
Booleans
instead of converting them to strings", and mirrors the native JSON
types Gitea
already sends for `workflow_call`. Only booleans are coerced; other
input types
are left untouched, and a value that is already a bool (JSON API
request) passes
through.
Note: this makes dispatch payloads carry native booleans, which the
runner
consumes correctly as of gitea/runner#1087 (it accepts a native bool and
keeps
the string fallback) — the same expectation `workflow_call` already
relies on.
Fixes https://github.com/go-gitea/gitea/issues/38466
When an Actions job is blocked or waiting, the job view only shows the
generic **Blocked** / **Waiting** label. Users have no way to tell *why*
a job is stuck — whether it's waiting on dependencies, waiting for a
runner that doesn't exist, waiting for a runner whose labels don't
match, or simply queued behind busy runners.
## Change
The current-job detail line now surfaces the actual cause:
- **Blocked** → lists the dependency jobs (`needs`) that haven't
finished yet, e.g. *"Waiting for the following jobs to complete:
build."*
- **Waiting**, no online runner → *"No runner is online to pick up this
job."*
- **Waiting**, online runners but none match `runs-on` → *"No matching
online runner with label: X"* (reuses the existing string)
- **Waiting**, a matching runner exists but hasn't claimed the job →
*"Waiting for a matching runner to become available."*
The runner lookup reuses the same available-online-runner query the run
list already performs, and only runs while a selected job is actually
pending. Dependency resolution is scoped to the same parent job and
treats matrix expansions of a `need` as pending until all of them
complete.
The artifact info hover tooltip (#37100) only worked for live artifacts.
Hovering an expired/deleted artifact showed nothing, and even on live
artifacts the retention date was missing on real runs.
Two root causes:
1. **Expired artifacts had no tooltip.** In the run view sidebar,
expired artifacts render in a separate branch that omitted
`data-tooltip-content` entirely, so there was nothing to hover.
2. **`ExpiresUnix` was no longer sent.** The `ActionRunAttempt` refactor
(#37119) dropped `ExpiresUnix` from `fillViewRunResponseArtifacts`, so
real runs always returned `0` and the tooltip fell back to size-only.
Only the devtest mock still populated it, which masked the regression.
Artifacts without a recorded expiry (`expiresUnix <= 0`) still degrade
gracefully to a size-only tooltip. Both states can be previewed on the
`/devtest/mock/*` actions run page.
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Fixes the UI issue reported in #38466: matrix jobs that invoke a
reusable workflow (`build-call (linux)`, `build-call (windows)`, …) were
rendered as separate nodes in the run's workflow graph, while an
equivalent regular matrix
(`build (linux)`, `build (windows)`, …) collapsed neatly into a single
matrix node.
Before: three loose `build-call (...)` boxes next to one grouped `build` box.
After: both matrices collapse into a single node.
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 flex-list refactor (#37505) raised the shared `.item-trailing`
selector's specificity, so its `flex-wrap: wrap` started overriding the
run list's intended `flex-wrap: nowrap` — a long branch name pushed the
trailing content past its fixed 280px width and wrapped the kebab menu
onto its own line.
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
The `official` flag of a pull request review is computed against the
target branch's protection rules at submit time and stored on the review
record. `ChangeTargetBranch` updated the base branch but never
re-evaluated it, so an `official` approval obtained against an
unprotected branch could be retargeted onto a protected branch and
satisfy its required approvals, bypassing the branch protection.
This re-evaluates the `official` flag of the latest approve/reject
reviews against the new base branch whenever the target branch changes.
- **Locale DoS:** the `Locale` middleware passed the raw
`Accept-Language` header to `ParseAcceptLanguage`, whose guard only
counts `-` while the scanner aliases `_` to `-` — a large `_`-separated
header on an unauthenticated request burned CPU. The header is now
length-bounded before parsing.
- **Public-only token scope:** `GET /teams/{id}/repos`,
`.../repos/{org}/{repo}`, `/teams/{id}/activities/feeds`, and
`/users/{username}/orgs/{org}/permissions` still returned private
repo/activity/permission data to a public-only token. They now filter
via `TokenCanAccessRepo` / `ApplyPublicOnly` and reject non-public org
permissions.
- **Push-option visibility:** `repo.private` / `repo.template` push
options were applied to any existing repo, letting an owner/admin
silently flip visibility bypassing audit, webhooks, and notifications.
They are now honored only on push-to-create.
The `/user/starred` and `/user/subscriptions` endpoints returned private
repositories a user had starred/watched even after their access to those
repositories was revoked, still exposing the repository name,
description and visibility (including later metadata changes).
Private repositories in the starred/watched queries are now gated on the
actor's current access via `AccessibleRepositoryCondition`, so users who
no longer have access no longer receive the metadata. Public
repositories and public-only tokens are unaffected.
The LFS batch and upload handlers linked an object that already existed
in the content store but was not linked to the current repo whenever the
token's user could access it in another repo. Deploy-key tokens carry
the repo owner's identity, so a single-repo write deploy key could link
and then download objects from any repo the owner can see.
This drops the cross-repo access check: the batch handler now makes the
client upload (hash-verified) any object not yet linked to the repo, and
the upload handler skips proof of possession only when the object is
already linked to the current repo.
The commit page built its "co-authored by" list from
`AllParticipantIdentities()[1:]`, which only drops the author and leaves
the committer in the list even though the committer is already shown
separately as "committed by". This caused the committer to be wrongly
rendered as a co-author (issue #38384): a commit authored by
`silverwind`, committed by `bircni`, with a `Co-authored-by: silverwind`
trailer displayed "co-authored by bircni" instead of the actual trailer
identity. This adds a `Commit.CoAuthorIdentities()` method that excludes
both the author and the committer, uses it on the commit page, and
covers the fix with a regression test.
Closes#38384
3 reductions in the DB load generated by many runners polling
`FetchTask`:
**1. Debounce runner heartbeat writes**
Every poll wrote `last_online`, and every `UpdateTask`/`UpdateLog` wrote
`last_active` — while a runner streams logs that is many writes per
second per runner. These are now persisted only when stale enough to
actually affect the active/offline status (`ShouldPersistLastOnline` /
`ShouldPersistLastActive`), using the existing columns.
**2. Throttle concurrent task picks**
A new in-process semaphore (`MAX_CONCURRENT_TASK_PICKS`) bounds how many
runners run the task-assignment transaction at once, so a fleet polling
together cannot stampede the query. Throttled polls retry on their next
poll without advancing the runner's tasks version.
**3. Paginate the task-pick query**
`CreateTaskForRunner` previously loaded every waiting job in the
runner's scope into memory on each poll (no `LIMIT`). Now it pages
through the waiting backlog oldest-first with `LIMIT`, claiming the
first label-matching job.
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Pull mirror sync ran `git fetch` / `remote update` / `remote prune`
without disabling HTTP redirects. A mirror remote that later starts
redirecting to an otherwise-blocked or internal address could be used as
an SSRF/exfiltration vector on scheduled syncs, bypassing the
allow/block validation applied at migration time.
This sets `http.followRedirects=false` on all three remote-contacting
commands in the pull mirror path, matching the existing guard already
present on the clone path.
The runner-listing API endpoints (`admin`, `org`, `user`, `repo`, and
`shared`) return runners in a non-deterministic order across pages. When
paging through runners, some runners from page 1 could reappear on page
2 (and others get skipped entirely).
The cause is in `FindRunnerOptions.ToOrders()`. Most sort modes order by
a **non-unique** column only:
- default / `online` / `offline` → `last_online`
- `alphabetically` / `reversealphabetically` → `name`
When multiple runners tie on the sort key (e.g. every offline runner
shares `last_online = 0`, or two runners have the same name), the
database is free to return the tied rows in any order between separate
queries. Combined with `LIMIT`/`OFFSET` pagination, this means the same
runner can land on more than one page.
## Fix
Append the unique primary key `id` as a stable tiebreaker to each
non-unique sort order:
The `newest`/`oldest` modes already sort by the unique `id`, so they are
left unchanged.
Draft-release access control was enforced only on the API release
endpoints (`/api/v1/repos/{owner}/{repo}/releases/...`) but not on the
UUID-based web attachment endpoints (`/attachments/{uuid}`,
`/{owner}/{repo}/attachments/{uuid}`,
`/{owner}/{repo}/releases/attachments/{uuid}`).
Anyone who obtained an attachment UUID — including unauthenticated
callers — could download files belonging to a hidden draft release,
since `ServeAttachment` only checked repo-level read permission and
never the release's draft state.
Fixes#38278
## Problem
When branch protection matches the branch an Actions workflow pushes to,
the runner's `git push` is rejected — even though the workflow token has
`contents: write` and the same push performed with a PAT (write access)
succeeds. Disabling protection or changing the pattern so it no longer
matches makes the push work.
## Root cause
In `preReceiveBranch` (`routers/private/hook_pre_receive.go`), the "can
the doer push to this protected branch" check resolves the pusher with
`user_model.GetUserByID(ctx, ctx.opts.UserID)`. For an Actions push the
user ID is `-2` (the virtual `ActionsUserID`), which has no database
row, so the lookup fails. Even past that, `CanUserPush` →
`HasAccessUnit`/whitelist membership cannot evaluate a virtual user and
returns `false`. As a result the Actions bot was rejected on every
matching protected branch, despite the earlier `assertCanWriteRef`
already confirming the token's code-write via
`GetActionsUserRepoPermission`.
This was inconsistent: a PAT with identical write access passed the
exact same check.
## Fix
Evaluate the Actions bot against its already-computed token permission
instead of a user lookup, mirroring the existing
`IsUserMergeWhitelisted` pattern:
- Add `CanActionsUserPush` / `CanActionsUserForcePush` on
`ProtectedBranch`, which take the precomputed `access_model.Permission`.
- Allow the push when push is enabled, **no** push whitelist is
enforced, and the token has code-write.
- Keep the bot blocked when a whitelist is enforced — it cannot be added
to one, so it must use a pull request. This preserves the whitelist as a
real security boundary.
Force-push, signed-commit and protected-file-path checks are untouched.
---------
Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@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.
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
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>
### 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
}
}
```
## 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.
## 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>
## 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
- 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"
/>
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>
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>
Adds `created_unix` as the third column of the `c_u` composite index on
the `action` table, changing it from `(user_id, is_deleted)` to
`(user_id, is_deleted, created_unix)`.
Migration 337 drops and recreates the index. No data is touched.
## Root causes
#32333 introduced the `c_u` index to speed up dashboard queries, but
defined it as `(user_id, is_deleted)` — without `created_unix`.
#3368 The simple query is now efficient enough for the database to
actually use `c_u`, but because `created_unix` is absent from the index,
the database must load and sort **every** matching row before returning
the first page of 20.
The existing `c_u_d` index `(created_unix, user_id, is_deleted)` does
not help because its leading column is `created_unix`, which can't be
used for an equality seek on `user_id`.
Those two caused this issue:
https://github.com/go-gitea/gitea/issues/38075
With the fix, the database seeks directly to `(user_id=X,
is_deleted=false)` and walks `created_unix` in descending order,
stopping after 20 rows.
Fixes https://github.com/go-gitea/gitea/issues/38075
- Enforce org visibility on organization label read endpoints (private
org labels no longer leak to non-members).
- Block fork sync (`merge-upstream`) when the base repo is no longer
readable (stops pulling commits after a parent goes private).
- Remove `REVERSE_PROXY_LIMIT` / `REVERSE_PROXY_TRUSTED_PROXIES` from
the Docker `app.ini` templates (the `= *` default allowed
`X-WEBAUTH-USER` impersonation; reverse-proxy auth is now opt-in and
admin-configured).
- Enforce single-use TOTP passcodes across web login, password-reset,
and Basic-Auth `X-Gitea-OTP` (fixes a TOCTOU race and a stateless
replay).
- Re-check branch write permission for every ref in a push (the
pre-receive hook cached the first ref's result, letting a per-branch
maintainer-edit grant escalate to full repo write).
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Moves the "Hacking on Gitea" page out of the documentation website and into the repository as `docs/development.md`, so contributors find build and test instructions next to the code. The content has been cleaned up and corrected for in-repo use.
---------
Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
- Enforce repository token scope on RSS/Atom feed endpoints so a PAT
without repo scope can no longer read private repo commit data.
- Block HTTP redirects during repository migration clones to prevent
SSRF reaching internal addresses via an attacker-controlled redirect.
- Redact the notification subject after repo access is revoked so
private issue/PR metadata is no longer leaked through the notification
API.
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Closes#37670.
Today, org members in Gitea only see teams they're a member of. In
larger orgs that hurts onboarding and discoverability — there's no way
to look up which team owns what without asking around. GitHub solves
this with a per-team visibility setting; this PR brings the same model
to Gitea.
## What changes
- Every team gets a `visibility` setting:
- `private` *(default)* — only team members and org owners can see the
team. Same as today's behavior.
- `limited` — listable by any member of the organization. Members and
the repos the team has access to are visible too. Non-org-members still
see nothing.
- `public` — listable by any signed-in user.
- The Owners team visibility is fixed and cannot be changed via
settings.
- Existing teams default to `private`, so this is a no-op for anyone who
doesn't change anything.
## API
- `Team`, `CreateTeamOption`, `EditTeamOption` all gain a `visibility`
field (string enum: `private` | `limited` | `public`).
- `GET /orgs/{org}/teams` and `/orgs/{org}/teams/search` now apply the
same visibility rules as the web UI:
- site admins and org owners still see every team
- other org members see their own teams plus any `limited` or `public`
team
- `private` teams are no longer leaked through these endpoints
- Swagger/OpenAPI specs regenerated.
## UI
View from admin2 (not an owner):
<img width="1669" height="726"
src="https://github.com/user-attachments/assets/daf4bccb-644b-4426-b178-71963aeaf73b"
/>
View from admin (owner):
<img width="2559" height="863"
src="https://github.com/user-attachments/assets/4f22cebc-e9df-4fd2-8ed4-724d31fadb7a"
/>
---------
Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Fixes#38079
## Regression path
The layout previously had `.commit-status-item .status-context { flex: 1
}`,
which let the context fill remaining space and ellipsize. That rule was
dropped in #37517 ("Refactor pull request view (5)") when the row markup
moved to nested `.flex-text-block` wrappers, so nothing constrained the
left block anymore.
After:
<img width="832" height="242" alt="image"
src="https://github.com/user-attachments/assets/a20019f8-6016-40f7-8901-2808280dc093"
/>
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Follow-up to #37987, addressing the unresolved review comments on the
org members search form.
And fix more trivial problems together (see the commit titles)
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
[actions-proto-go v0.6.0](https://gitea.com/gitea/actions-proto-go) adds
a
`capabilities` field to `RegisterRequest` and `DeclareRequest`. This
lets a
runner advertise the transitional `cancelling` capability directly in
the proto
message instead of through the out-of-band mechanism we used while the
proto
bump was pending.
This PR:
- Bumps `gitea.dev/actions-proto-go` to `v0.6.0`.
- Drops the forward-compat `capabilityGetter` type-assertion shim and
the
`runnerRequestHasCancellingCapability` helper, reading
`GetCapabilities()`
directly (now part of the `declareRequest` interface).
- Removes the "capability state unknown → preserve existing value"
branch.
## Why the behaviour change is correct
The shim and the `(hasSupport, known)` two-value return only existed
because the
old proto had no `capabilities` field, so we couldn't tell "runner
doesn't
support it" from "we can't see the field." With v0.6.0 the field is
always
present. Since proto3 repeated fields have no presence, "no capabilities
sent"
now unambiguously means the runner does not advertise the capability, so
a
runner that omits `cancelling` is correctly recorded as
`HasCancellingSupport =
false`.
There is no regression: prior to this bump Gitea was on `v0.5.0`, where
the
type assertion always failed and `HasCancellingSupport` was therefore
never set
from requests — so no runner relied on the preserved-unknown path.
## Compatibility
The change is wire-compatible in both directions of version skew,
because the
new field uses a previously unused field number (8 on `RegisterRequest`,
3 on
`DeclareRequest`) and the transport uses the binary protobuf codec:
- **Old runner → new Gitea:** the runner omits the field; it decodes to
an empty
capability list. Registration/declaration succeed; the runner simply
doesn't
get the cancelling feature.
- **New runner → old Gitea:** the runner sends the field; the old
server's
generated code doesn't know the field number and silently ignores it.
Registration/declaration succeed.
The feature only activates once both server and runner are on `v0.6.0`.