### Description
This PR resolves a UI alignment bug in the Gitea Actions log viewer
where the expand/collapse disclosure chevron overlaps with the log text
(specifically the timestamp) when timestamps are enabled.
### Cause
When log timestamps are enabled, the timestamp element
(`.log-time-stamp`) is rendered as the first element next to the line
number. Because it only had a default `10px` left margin, it positioned
itself exactly where the group's expand/collapse chevron is located,
causing them to overlap.
### Solution
Updated the CSS styles in `web_src/js/components/ActionRunJobView.vue`
to dynamically apply the `21px` margin to whichever element is the first
visible element after the line number:
- If the timestamp is visible, it gets the `21px` margin to clear the
chevron, and the subsequent log message gets a `10px` margin.
- If the timestamp is hidden, the log message receives the `21px`
margin.
### Before / After
**Before:**
<img width="853" height="348" alt="actions_log_before"
src="https://github.com/user-attachments/assets/d09a752e-18cb-4fe3-b749-4979cbe45240"
/>
**After:**
<img width="862" height="511" alt="actions_log_after"
src="https://github.com/user-attachments/assets/63063f05-8cd6-4986-a993-ed12f28625c8"
/>
Fixes#38222.
---------
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: silverwind <me@silverwind.io>
Updates `go-swagger` to v0.35.0 and makes swagger generation and
validation warning-free, with the build now failing on any warning
(`go-swagger` itself exits `0` on warnings).
- Generation passes `--enable-allof-compounding` (keeps `$ref` fields
bare, no spec change) and `--skip-enum-desc` (drops the enum description
that duplicates `x-go-enum-desc` and was the only source of `allOf`
noise in the OpenAPI 3.0 output).
- Fixed warnings at the source: dropped `swagger:strfmt` where it
conflicts with `required: true` (`required` kept, `time.Time` still maps
to `date-time`), fixed a malformed `units_map` example, moved the
`parameterBodies` injection hack to `swagger:parameters`, and removed
unused responses.
Fixes: https://github.com/go-gitea/gitea/issues/12508
---------
Co-authored-by: bircni <bircni@icloud.com>
The rules from `eslint-plugin-array-func` are redundant with
`eslint-plugin-unicorn`:
- `from-map` → `unicorn/prefer-array-from-map`
- `no-unnecessary-this-arg` → `unicorn/no-array-method-this-argument`
- `prefer-flat` / `prefer-flat-map` → `unicorn/prefer-array-flat` /
`unicorn/prefer-array-flat-map` (already disabled here)
The two remaining rules (`avoid-reverse`, `prefer-array-from`) are niche
and not worth carrying an extra dependency for.
Co-authored-by: bircni <bircni@icloud.com>
- bump ci, flake and `@types/node` to node 26
- regenerate flake.lock which is needed for that package
- refactor workflow to use shared composite action
Inlines the small SVG bar graph into `RepoActivityTopAuthors.vue` (its
only consumer) and drops the `vue-bar-graph` npm dependency.
- Bars render at static height (dropped the grow animation).
- Theme-aware axis color instead of a hardcoded `#555555`.
- Removed the dangling `role="img"`/`aria-labelledby` on the `<svg>`.
- Reserve the chart height so the page does not shift when the component
mounts.
<img width="416" height="110" alt="Screenshot 2026-07-01 at 11 15 25"
src="https://github.com/user-attachments/assets/b2db4d0c-20f1-4345-9951-32a908abfaba"
/>
<img width="419" height="110" alt="Screenshot 2026-07-01 at 11 15 35"
src="https://github.com/user-attachments/assets/853305a5-575f-4a26-ba3b-12fc51081324"
/>
fyi @lafriks
---------
Signed-off-by: silverwind <me@silverwind.io>
This PR adds the `gitea admin user disable-2fa` command to disable 2FA
for a user
When the only admin in the instance loses their 2FA credentials, this
command can be used to disable 2FA, allowing them to log in and reset
it.
---------
Co-authored-by: Giteabot <teabot@gitea.io>
Fixes HTTP 500 when OIDC auto account linking (`ACCOUNT_LINKING=auto`)
requires local 2FA. `oauth2LinkAccount` set `linkAccount` in the session
before redirecting to 2FA but did not persist `linkAccountData`, so
`TwoFactorPost` failed with `not in LinkAccount session`. The manual
linking flow already stored both, this aligns auto-link with that
behavior.
Closes#38171
---------
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
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>
autoplay is useless nowadays without "muted" as browsers won't autoplay
unmuted videos.
Similarly, other attributes are also commonly used and harmless to keep.
<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch
names`.
- Read the contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs
Describe your change below and link any issue it fixes.
-->
---------
Signed-off-by: Avinash Thakur <19588421+80avin@users.noreply.github.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.
After upgrading from 1.25.x to 1.26.x, `repo-archive` workers can fail
to unmarshal queued items:
```
Failed to unmarshal item from queue "repo-archive":
json: unable to unmarshal into Go convert.Conversion within "/Repo/Units/0/Config":
cannot derive concrete type for nil interface with finite type set
```
`ArchiveRequest` started embedding `*repo_model.Repository` in 1.26,
which does not round-trip through the JSON queue.
This change stores a minimal `archiveQueueItem` (`RepoID`, `Type`,
`CommitID`, `Paths`) in `repo-archive` and loads the repository in the
worker. `UnmarshalJSON` accepts legacy payloads that used `RepoID` or
embedded `Repo.id`.
Fixes#38272
<!--
Before submitting:
- Target the `main` branch; release branches are for backports only.
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch
names`.
- Read the contributing guidelines:
https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
- Documentation changes go to https://gitea.com/gitea/docs
Describe your change below and link any issue it fixes.
-->
---------
Co-authored-by: bircni <bircni@icloud.com>
Adds `make generate-codemirror-languages` to the npm group's
`postUpgradeTasks` in `renovate.json5`, so renovate regenerates
`assets/codemirror-languages.json` whenever `@codemirror/language-data`
(or any npm dep) updates — mirroring the existing `make svg` handling.
Also reformats the `fileFilters` arrays multi-line and regenerates the
asset to pick up current upstream linguist languages.
This PR contains the following updates:
| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [js-yaml](https://redirect.github.com/nodeca/js-yaml) | [`4.2.0` →
`5.1.0`](https://renovatebot.com/diffs/npm/js-yaml/4.2.0/5.1.0) |

|

|
---
### Release Notes
<details>
<summary>nodeca/js-yaml (js-yaml)</summary>
###
[`v5.1.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#510---2026-06-23)
[Compare
Source](https://redirect.github.com/nodeca/js-yaml/compare/5.0.0...5.1.0)
##### Added
- Collection tags can finalize an incrementally populated carrier into a
different result value.
##### Changed
- \[breaking] `quoteStyle` now selects the preferred quote style; use
the
restored `forceQuotes` option to force quoting non-key strings.
###
[`v5.0.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#500---2026-06-20)
[Compare
Source](https://redirect.github.com/nodeca/js-yaml/compare/4.3.0...5.0.0)
##### Added
- Added named exports for schemas, tags, parser events and AST
utilities.
- Reworked `JSON_SCHEMA` and `CORE_SCHEMA` with spec-compliant scalar
resolution
rules, and added `YAML11_SCHEMA`.
- Added `realMapTag` for lossless mappings with non-string and complex
keys.
Object-based mappings now reject complex keys instead of stringifying
them.
- Added `dump()` `transform` option for changing the generated AST
before
rendering.
- Added `dump()` options `seqInlineFirst`, `flowBracketPadding`,
`flowSkipCommaSpace`, `flowSkipColonSpace`, `quoteFlowKeys`,
`quoteStyle` and
`tagBeforeAnchor`.
- Added formal data layers (events and AST) for modular data pipelines.
- Added low-level parser (to events), presenter and visitor APIs.
- Added the [YAML Test
Suite](https://redirect.github.com/yaml/yaml-test-suite) to the
test set.
##### Changed
- See the [migration guide](docs/migrate_v4_to_v5.md) for upgrade notes.
- Rewritten in TypeScript and reorganized the public API around flat
named
exports.
- Reduced the set of exported schemas:
- YAML 1.2 schemas: `CORE_SCHEMA` (loader default), `JSON_SCHEMA`,
`FAILSAFE_SCHEMA`.
- `YAML11_SCHEMA`, a combination of all YAML 1.1 tags (YAML 1.1 does not
specify a schema, only "types").
- `load`/`dump` default behaviour is now specified exactly via schemas:
- `load` uses `CORE_SCHEMA`, without `!!merge` by default.
- `dump` uses `YAML11_SCHEMA` + `CORE_SCHEMA` for the quoting check, to
guarantee backward compatibility by default.
- `!!set` is now loaded as a JavaScript `Set`.
- Replaced the `Type` API with a tags API. Similar, but more precise and
simpler. See examples for details. Tags can be defined via
`defineScalarTag()`, `defineSequenceTag()` and `defineMappingTag()`, or
as a
spread + override of an existing tag.
- Renamed `Schema.extend()` to `Schema.withTags()`.
- Expanded YAML 1.2 conformance and improved handling of directives,
document
markers, block keys, multiline scalars, tag syntax and other things.
- `load()` now throws on empty input instead of returning `undefined`.
- Moved browser builds to the `js-yaml/browser` export.
- Deprecated the `loadAll` signature with an iterator (still works, but
is a
candidate for removal).
##### Removed
- Removed deprecated `safeLoad()`, `safeLoadAll()` and `safeDump()`
exports.
- Removed `DEFAULT_SCHEMA` and the nested `types` export.
- Removed loader options `onWarning`, `legacy` and `listener`.
- Removed dumper options `styles`, `replacer`, `noCompatMode`,
`condenseFlow`,
`quotingType` and `forceQuotes`. Renamed `noArrayIndent` to
`seqNoIndent`.
Formatting and representation are now configured through presenter
options,
schemas and tag definitions. See migration guide on how to replace.
- Removed support for importing internal files from `lib/`.
###
[`v4.3.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#430-3150---2026-06-27)
[Compare
Source](https://redirect.github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)
##### Security
- Backported `maxTotalMergeKeys` option.
</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: silverwind <me@silverwind.io>
**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>
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>
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.
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
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>
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
## 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._
## 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.
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>
## 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>
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>