Compare commits

..

88 Commits

Author SHA1 Message Date
bircni
a62dfffbe7 docs: Clean up build section in CHANGELOG.md (#38671) 2026-07-27 20:43:03 +02:00
bircni
035dd58664 docs: update changelog for 1.27.1 (#38668)
added the changelog for 1.27.1

---------

Signed-off-by: bircni <bircni@icloud.com>
2026-07-27 20:08:22 +02:00
Giteabot
0d9ce64f76 fix: skip OIDC end-session after password login for OAuth2 users (#38439) (#38666)
Backport #38439 by @Otto-Deviant1904

Fixes #38209

OAuth2-linked accounts that sign in via the password form were still
redirected to the provider end_session_endpoint on logout because the
redirect was keyed off account LoginType.

Store the session sign-in method (password vs oauth2) and only use
RP-initiated OIDC logout when this session was authenticated via OAuth2.
Sessions without the new key keep the previous LoginType behavior.


Co-authored-by: Harsh Satyajit Thakur <f20240223@goa.bits-pilani.ac.in>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-27 16:23:49 +00:00
Giteabot
4a77fbce28 ci: set AWS_REGION for Cloudflare R2 upload steps (#38658) (#38665)
Backport #38658 by @lunny

## What to build

Fix the Cloudflare R2 upload steps added in #38635, which currently fail
on every release run:

```
fatal error: An error occurred (InvalidRegionName) when calling the ListObjectsV2 operation:
The region name '***' is not valid. Must be one of: wnam, enam, weur, eeur, apac, oc, auto
```

### Root cause

The `configure-aws-credentials` step earlier in the same job exports
**both** `AWS_DEFAULT_REGION` and `AWS_REGION` into `$GITHUB_ENV`
(verified in `exportRegion()` at the pinned SHA `517a711`), so
`secrets.AWS_REGION` (the real AWS region) stays set for every later
step in that job.

The AWS CLI v2 region resolution order is `--region` > `AWS_REGION` >
`AWS_DEFAULT_REGION`. The R2 step only set `AWS_DEFAULT_REGION: auto`,
so the leaked `AWS_REGION` won and R2 rejected it, since R2 only accepts
`wnam`, `enam`, `weur`, `eeur`, `apac`, `oc` or `auto`.

The failure log shows both `AWS_DEFAULT_REGION: auto` and `AWS_REGION:
***` in the step env, which confirms the leak.

### Fix

Set `AWS_REGION: auto` explicitly in the R2 upload step env of all three
release workflows. Step-level `env:` is applied after
`GITHUB_ENV`-derived variables, so this reliably overrides the leaked
value.

No other variable leaks from `configure-aws-credentials` matter here:
`AWS_SESSION_TOKEN` is only exported when a session token exists, and
these workflows use static access keys without `role-to-assume`;
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are already overridden
in the R2 step.

Consolidating the three duplicated upload steps into a composite action
remains a follow-up, as noted in #38635.

## Acceptance criteria

- [x] `AWS_REGION: auto` set in the R2 upload step of
`release-nightly.yml`, `release-tag-rc.yml` and
`release-tag-version.yml`
- [x] No other behaviour changed; the existing S3 upload steps are
untouched
- [x] All three workflows still pass `actionlint` and YAML parsing
- [ ] The next nightly release run syncs to R2 without
`InvalidRegionName`

## Blocked by

- None -- can start immediately.

---
Generated by Codet

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-07-27 15:24:53 +00:00
Giteabot
2b37732d5f fix: make Actions log parser support multiple line message encoding (#38659) (#38664)
Backport #38659

fix #38652


UI part (`.log-msg`) uses "white-space: break-spaces;" so the new line
can be correctly rendered.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-27 14:47:14 +00:00
Giteabot
2695b47887 fix(actions): use base branch ref for pull_request_target context (#38636) (#38657)
Backport #38636 by @SudhanshuMatrix

Fixes a bug in Actions context generation for `pull_request_target`
workflows where `github.ref` / `gitea.ref` was incorrectly populated
with `refs/heads/owner:branch` instead of `refs/heads/branch`.

### Problem Statement
In `services/actions/context.go`, when constructing the `ref` string for
`pull_request_target` events:
```go
ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name

Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-27 07:35:03 +00:00
Giteabot
9c685dedbb fix(actions): skip already-approved runs in ApproveRuns (#38653) (#38654)
Backport #38653 by @Zettat123

The handler of `/actions/runs/{run}/approve` doesn't check if the run is
already approved. If a run is re-approved, its jobs' status will be
reset to `StatusWaiting`, causing incorrect job status.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-27 06:35:25 +00:00
Giteabot
e9b3917042 chore(build): upload release to Cloudflare R2 (#38635) (#38651)
Backport #38635

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-07-27 13:07:23 +08:00
wxiaoguang
d7bc52beea refactor: git patch apply (#38637) (#38638)
Backport #38637
2026-07-26 17:32:02 +00:00
Giteabot
ccd38f9a70 fix: orgmode render include path (#38642) (#38645)
Backport #38642

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-07-26 16:58:22 +00:00
Giteabot
f47e3d930d fix(actions): cancel tasks immediately when the runner stopped reporting (#38616) (#38644)
Backport #38616 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-26 14:38:54 +00:00
Giteabot
9972bee41f fix(issues): fix label bulk-load key and reduce log noise in LoadLabel (#38632) (#38643)
Backport #38632 by @eliroca

CommentList.loadLabels keyed the result map by label.ID but looked up by
comment.ID, so every label event fell back to individual DB queries.

This caused log spam for any comment where the label was deleted, since
ToTimelineComment calls LoadLabel unconditionally for all comment types
including those with LabelID=0.

Fix the map key, skip the query when LabelID=0, demote the now rarely
triggered orphaned-label log to Debug, and fix a typo ("Commit" ->
"Comment") in that message.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Elisei Roca <eroca@suse.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-26 13:20:17 +00:00
Giteabot
375e6ea038 fix(actions): improve runner list status sorting, labels and task job links (#38586) (#38633)
Backport #38586 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-25 22:02:40 +00:00
Giteabot
31c435454b fix(actions): correctness and hardening fixes (#38518) (#38631)
Backport #38518 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-25 19:49:33 +02:00
wxiaoguang
a54324e2b7 fix(repo): prevent double-write redirect collisions on dependency errors, fix ui (#38627) (#38628)
Backport #38627

Co-authored-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
2026-07-25 08:48:56 -07:00
Giteabot
d141dc729c fix: delete repo-scoped rows of seven more tables when deleting a repository (#38534) (#38618)
Backport #38534 by @luuuc

Fixes #38494

`DeleteRepositoryDirectly` left rows behind in seven registered tables
carrying a repo-scoped key: `action_variable`, `action_run_attempt`,
`action_tasks_version`, `renamed_branch`, `commit_status_summary`,
`commit_status_index` and `repo_transfer`. Repository IDs are `pk
autoincr` and never reissued, so the orphaned rows were unreachable, but
they accumulated forever (unbounded table growth, referential
inconsistency; not a security issue, see the issue discussion).

This adds all seven to the `deleteBeans` cascade, each placed next to
its sibling bean
(`CommitStatus`/`CommitStatusIndex`/`CommitStatusSummary`,
`Branch`/`RenamedBranch`, `Secret`/`ActionVariable`,
`ActionRun`/`ActionRunAttempt`). `repo_transfer` goes through the same
cascade rather than `DeleteRepositoryTransfer` so teardown stays one
mechanism; the existing reaper remains for the transfer flows.

The test inserts one row per previously-orphaned table (the fixture
files ask test cases to prepare their own data), deletes the repository,
and asserts each table is purged. Without the fix it fails on all seven
tables.

As discussed in the issue, this is the first of the two suggested steps;
the reflection-driven enforcement sweep over registered repo-keyed
models (with an explicit exemption list, e.g. head-repo PRs) can follow
in a separate PR.

PS: this was found while benchmarking
[Sense](https://github.com/luuuc/sense) against the gitea codebase (the
cross-reference method is described in the issue).

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Signed-off-by: Luc <luuuc@users.noreply.github.com>
Co-authored-by: Luc <luuuc@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-24 21:25:31 +02:00
Giteabot
27fed5a83a fix(webhook): remove slack channel name check (#38608) (#38612)
Backport #38608 by @chlee1001

Users should know what they are doing, don't check everything,
especially for that we don't understand.

Fixes #23840

Co-authored-by: Chaehyeon Lee <chlee1001@naver.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-24 08:09:05 +00:00
Giteabot
6cdd02bdad fix: download dropdown menu clipped on the branches page (#38604) (#38609)
Backport #38604

Fixes #38603

Co-authored-by: Richard Mahn <richmahn@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-24 12:47:37 +08:00
Giteabot
cc4ee6387b fix(oauth2): enforce mandatory 2FA policy on OAuth2 authorize/grant endpoints (#38591) (#38606)
Backport #38591

Co-authored-by: Mitrahsoft <bala.c@mitrahsoft.in>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-23 20:13:13 -07:00
Giteabot
4780cffe08 fix(project): prevent database mutations on invalid MoveIssues payload (#38600) (#38602)
Backport #38600

Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
2026-07-23 16:14:57 +00:00
Giteabot
483fb19b37 fix(actions): make SingleWorkflow.Marshal round-trip multi-line run blocks (stop silent job stranding) (#38520) (#38599)
Backport #38520 by @darklight147

## Problem
Jobs that call a reusable workflow (`uses:`) whose steps contain a `run:
|` block that **starts with blank lines** never start — the child jobs
stay `Blocked` forever, the run never finishes, and nothing is shown to
the user (the error is only logged at DEBUG). The reusable is valid YAML
and worked on 1.26.

## Root cause
`jobparser` serializes each expanded job via `SingleWorkflow.SetJob()`,
which uses `yaml.NewEncoder(...).SetIndent(2)`, but
`SingleWorkflow.Marshal()` used `yaml.Marshal`, whose default
indentation is **4**. Re-emitting a multi-line literal block scalar at a
different indentation makes the encoder write a wrong explicit
indentation indicator (`run: |4`) whose declared indent doesn't match
the actual content indent. The stored `workflow_payload` is then
unparseable:

```
run: |4


                while ...
```

`jobparser.Parse` / `model.ReadWorkflow` (both go.yaml.in/yaml/v4)
reject it: `did not find expected key`. This surfaces in
`services/actions/job_emitter.go` `resolve()` →
`updateConcurrencyEvaluationForJobWithNeeds` → `ParseJob`, where the
error is swallowed at `log.Debug` and the job is left `Blocked`.

Encoding at indent 4 triggers the bad indicator; indent 2 does not —
matching the value already used by `SetJob`.

## Fix
Encode `SingleWorkflow.Marshal()` with `SetIndent(2)` so both encoders
agree and the serialized single workflow round-trips. Adds a regression
test (`Parse → Marshal → Parse` on a `run:` block with leading blank
lines) that fails before the change with `did not find expected key`.

## Notes
- This is the correctness fix. Related: #37116 added
`ValidateWorkflowContent` to *report* such content for top-level
workflows, but the reusable-expansion/concurrency-eval path is not
covered and strands silently — this fix removes the failure mode
entirely.
- Consider a follow-up to elevate the swallowed error in
`job_emitter.resolve()` from `log.Debug` to a user-visible job failure
(there is an existing `// TODO`).

Signed-off-by: quasimodo <mohamed.belkamel@intelcia.com>
Co-authored-by: Mohamed Belkamel <39389636+darklight147@users.noreply.github.com>
Co-authored-by: quasimodo <mohamed.belkamel@intelcia.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-23 14:23:32 +00:00
Giteabot
560535a97f fix(api): align Swagger schemas for UserSettings and TopicListResponse (#38590) (#38592)
Backport #38590

Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
2026-07-23 07:27:47 +00:00
Giteabot
62c61aa8ce fix(file-tree): handle submodule links and missing view container (#38033) (#38589)
Backport #38033

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: yszl666 <941131649@qq.com>
Co-authored-by: dzf <douzf@sparkspacetech.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-23 10:17:34 +08:00
Giteabot
337fa5a950 fix(actions): fail unexpandable reusable workflow callers and decouple the job emitter's cross-run processing (#38565) (#38587)
Backport #38565 by @Zettat123

## Changes

### 1. Handle reusable workflow expansion failures

If a reusable workflow caller job is invalid (e.g. uses a workflow with
syntax error), the job emitter should mark it as failed instead of
retrying.

Related:
https://github.com/go-gitea/gitea/pull/38518#discussion_r3608882095

### 2. No longer process concurrent run inline

**Before**: If a run(R1)'s status change unlocks another blocked run(R2)
via concurrency group, the job emitter will process R2 in R1's
transaction.

**Current**: No longer process R2 inline and emit the ID of R2 to let
another pass process it.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-22 23:35:28 +02:00
Giteabot
ad84c14d53 fix: keep serving valid ACME cert when renewal fails at startup (#38554) (#38583)
Backport #38554 by @Otto-Deviant1904

Fix #38519

When `ENABLE_ACME` renew fails during startup (e.g. CA unreachable),
`ManageSync` currently aborts even if a still-valid certificate is on
disk, so HTTPS never comes up.

If `CacheManagedCertificate` finds a non-expired cert, log the manage
error, continue with that cert, and kick `ManageAsync` for background
retries. First-time install / expired-or-missing cert still fails
closed.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Harsh Satyajit Thakur <f20240223@goa.bits-pilani.ac.in>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-22 19:59:56 +00:00
Giteabot
65ea4079ce fix: branch protection user list (#38570) (#38584)
Backport #38570 by @wxiaoguang

fix #38569, also fix the UI

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-22 21:36:12 +02:00
Giteabot
1cf54fed70 fix(pulls): respect diff.orderFile in diff file tree (#38566) (#38578)
Backport #38566 by @eliroca

Co-authored-by: Elisei Roca <eroca@suse.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-22 18:14:16 +00:00
Giteabot
1ae686696e fix(issue): make issue action (issue list batch operation) elements have correct attributes (#38575) (#38580)
Backport #38575 by @SudhanshuMatrix

The "Clear projects" action in the issue list batch operations doesn't
work. When users select multiple issues and choose to clear their
project assignments, the operation fails because:

1. The frontend sends a project ID of `0` to represent "no project"
2. The backend passes this invalid ID directly to
`IssueAssignOrRemoveProject` without filtering
3. The backend tries to look up a project with ID `0`, which doesn't
exist, resulting in a `project 0 not found` error
4. Selected issues remain assigned to their projects instead of being
removed

Fixes #38571 

## Root Cause

The issue is a regression from the multi-project feature (#36784). The
frontend was using `data-element-id="0"` to represent "clear" actions,
but the backend doesn't filter out this invalid ID before validation.

## Solution

### Template Changes (`templates/repo/issue/filter_actions.tmpl`)
- Changed `data-element-id="0"` to `data-element-id=""` for the "Clear
projects" action (line 78)
- Changed `data-element-id="0"` to `data-element-id=""` for the "Clear
milestone" action (line 47)
- Removed the duplicate "no select" assignee option that was using
`data-element-id="0"` (lines 116-118)

### Frontend Logic Changes (`web_src/js/features/repo-issue-list.ts`)
- Made `elementId` a `const` instead of `let` (line 60) to prevent
mutations
- Removed the workaround code that was trying to handle
`data-element-id="0"` for assignees (lines 65-69)
- Updated comment from "for toggle" to "for label toggle" for clarity
(line 71)

Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-22 17:33:43 +00:00
Giteabot
e2f0358368 enhance: improve diff contrast in light and dark themes (#37477) (#38574)
Backport #37477 by @cyphercodes

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

Adjust the diff stat counter and syntax colors in both light and dark
themes to github-like colors that meet a >= 5:1 contrast floor (>= 7:1
for syntax names on diff rows), and make the counters semibold.

Signed-off-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Rayan Salhab <r.salhab@aiyexpertsolutions.com>
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent (GPT-5.5) <hermes-agent@nousresearch.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Hermes Agent <hermes@noreply.local>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-22 12:07:42 +00:00
Giteabot
d88bbfd0db fix(actions): support matrix when evaluating workflow if expression (#38474) (#38557)
Backport #38474 by @Zettat123

Partially fixes #38466

Added `matrix` to the evaluator for `if` expressions

Signed-off-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-07-21 13:28:58 +00:00
Giteabot
5e494f9cad fix(actions): align status icon span for Safari rendering (#38558) (#38562)
Backport #38558

Fixes #38553

Co-authored-by: okxint <130782884+okxint@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-21 18:43:11 +08:00
wxiaoguang
9731ad7c3c fix: revert git clone http redirection forbidden (#38530) (#38545)
backport #38530
2026-07-20 17:21:17 +02:00
Giteabot
148d528814 fix: clean up orphaned user-keyed tables in deleteUser (#38511) (#38514) 2026-07-20 08:41:59 +00:00
Giteabot
1af5277aba fix(actions): coerce workflow_dispatch boolean inputs to native types (#38472) (#38521)
Backport #38472 by @bircni

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

Co-authored-by: bircni <bircni@icloud.com>
2026-07-18 23:49:43 +02:00
Giteabot
6d41731184 fix: make the merge box button red if some checks fail (#38508) (#38516)
Backport #38508

fix #38506

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-18 04:10:50 -07:00
Eyüp Can Akman
cfc9f4c685 fix(pull): sign the commit when updating a branch by merge (#38441) (#38499)
Updating a branch by merge produced an unsigned commit even when merges
are configured to be signed. Update by rebase was unaffected.

`Update()` builds a fake reverse PR to switch head and base, and it has
no `Index`, so `pr.GetGitHeadRefName()` resolves `refs/pull/0/head`.
Since #36186 `SignMerge` looks that ref up in the base repository
instead of the temporary merge repo. That lookup fails. The caller
dropped the error, so `sign` stayed false.

Sync fork goes through the same fake-PR path.

Pass both sides of the merge to `SignMerge` as refs and evaluate them in
the temp repo, where `base` and `tracking` always exist.

Tests cover a signed and an unsigned update by merge.

Fixes #38066
Backport #38441
2026-07-17 11:05:05 +00:00
Giteabot
895d848ff4 fix: make commit message merge correctly (#38490) (#38502)
Backport #38490

fix #38487

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
2026-07-17 08:09:01 +00:00
Giteabot
7199547218 fix(actions): explain why a blocked or waiting job has not started (#38476) (#38498)
Backport #38476 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-16 21:45:11 +00:00
Giteabot
7cb4201f8e fix(actions): make cancelled() work in job if evaluation (#38495) (#38497)
Backport #38495 by @Zettat123

Fix #38485

#37478 moved `if` evaluation from runner to server. But `NewInterpeter`
always provides a nil `JobContext` for the evaluator, which makes
[`cancelled()`](ad967330a8/act/exprparser/functions.go (L299))
panic on `Job.Status` because `Job` is a nil pointer.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-16 20:54:26 +00:00
Giteabot
5f017302bf fix(actions): show retention info on hover for expired artifacts (#38477) (#38493)
Backport #38477 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-16 17:01:56 +00:00
Giteabot
59c619660c fix(actions): group reusable-workflow matrix legs in the workflow graph (#38475) (#38492)
Backport #38475

Co-authored-by: bircni <bircni@icloud.com>
2026-07-16 15:58:06 +00:00
Giteabot
8599289459 fix: full file highlighting for git diff with CR char (#38484) (#38491)
Backport #38484

fix #38481

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-16 06:11:20 -07:00
Giteabot
da9f0a3726 fix(packages): serve noarch Alpine index for any requested architecture (#38479) (#38486)
Backport #38479 by @gaurav0107

Fixes #38456

## Problem
The Alpine package registry serves one `APKINDEX.tar.gz` per
architecture. When a repository contains only `noarch` packages (no
architecture-specific packages), only the `noarch` index is built.
Because `apk` substitutes `$ARCH` with the host architecture and
requests e.g. `x86_64/APKINDEX.tar.gz`, such a repository returned HTTP
404 and was unusable, matching the report in #38456.

## Fix
`GetRepositoryFile` now falls back to the `noarch` index when the
requested architecture has no index of its own, mirroring the fallback
already present in the sibling `DownloadPackageFile` handler. `noarch`
packages are installable on every architecture, so serving them for any
requested architecture is correct. The index-build side is unchanged;
only the serving path gains the fallback, so mixed repositories (which
already merge `noarch` into each per-architecture index) are unaffected.

## AI assistance disclosure
This change was implemented with the help of an AI coding assistant,
which gitea's CONTRIBUTING.md explicitly welcomes when disclosed. I have
reviewed the change, understand it, and can explain and defend it.

## Tests
Added a `NoArchOnly` subtest to `TestPackageAlpine` that publishes only
a `noarch` package to a fresh repository and asserts that `GET
.../x86_64/APKINDEX.tar.gz` now returns `200` (previously `404`) and
that the served index lists the noarch package. Verified locally with
`go build`/`go vet` on the changed package and a compile of the
integration test package (`go test -c`); the full integration run relies
on CI.

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Gaurav Dubey <gauravdubey0107@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-16 12:28:39 +02:00
Giteabot
08fd59959e fix: 500 error when updating user visibility (#38480) (#38483)
Backport #38480 by @Zettat123

## How to reproduce this bug

1. Create a user with `visibility=public`
2. Update `ALLOWED_USER_VISIBILITY_MODES` setting to `limited, private`
3. Modify any field other than "User visibility" (e.g. "Full Name")
4. UI shows 500 error

## Fix

Only update the visibility field when it actually changes.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-16 11:10:54 +02:00
Giteabot
d60215c2a2 fix(actions): make job list item fully clickable (#38462) (#38471)
Backport #38462 by @SudhanshuMatrix

Clicking the empty space to the right of a job in the Actions sidebar
didn't switch jobs: the interactive `<a>`/`<button>` used `display:
contents` and so generated no clickable box.

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

Drop the wrapper `div` and `display: contents`, moving the `.item` and
layout styles directly onto the `<a>`/`<button>` so the whole row is
clickable. Reusable-caller `<button>` rows also get `width: 100%` and
`line-height: inherit` to match the `<a>` rows.

Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-15 17:18:05 +00:00
wxiaoguang
bf594690db fix: mail template for push event (#38467) (#38468)
Backport #38467
2026-07-15 18:57:03 +02:00
Giteabot
5cb7ec9304 fix: make "test push webhook" always work (#38425) (#38455)
Backport #38425

* fix #38309
* fix #26238
* fix #37886

Co-authored-by: Harsh Satyajit Thakur <f20240223@goa.bits-pilani.ac.in>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-15 05:51:44 +00:00
Giteabot
6e86c4cde8 fix(actions): prevent bulk actions from affecting all runners (#38453) (#38457)
Backport #38453

Co-authored-by: xkm <fzc_study@163.com>
2026-07-14 20:06:17 -07:00
Giteabot
c32af046a2 fix(org): align follow button and wrap description (#38448) (#38454)
Backport #38448

Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-15 10:12:39 +08:00
Giteabot
a98468da30 fix(actions): populate github.event for scheduled runs (#38446) (#38452)
Backport #38446 by @silverwind

Scheduled runs stored a `null` event payload, so
`github.event.repository`/`sender`/`organization` were empty in
scheduled workflows. Every other event carries them, and so does GitHub
Actions. `CreateScheduleTask` now synthesizes them.

Co-authored-by: silverwind <me@silverwind.io>
2026-07-14 19:32:08 +02:00
bircni
b969123b7f chore: don't auto refresh the merge box when user has interacted with it (#38436)
Backport #38435

Otherwise, the user just isn't able to use "auto merge" form
2026-07-13 02:54:47 -07:00
bircni
c6627af968 docs: update changelog for 1.27.0 (#38427) 2026-07-13 06:31:20 +02:00
Giteabot
de4b8277e9 fix: various security fixes (#38406) (#38426)
Backport #38406 by @bircni

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: bircni <bircni@icloud.com>
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>
2026-07-12 17:41:58 +00:00
Giteabot
acbc75e2bd fix(util): reject invalid characters between time-estimate units (#38416) (#38423)
Backport #38416 by @TowyTowy

### What / why

`TimeEstimateParse` (used by the issue time-estimate form) only checked
that the first token starts at the beginning of the string and the last
token ends at its end, but never checked the gaps between consecutive
tokens. Non-whitespace garbage embedded between two valid units was
silently dropped and the string accepted with a wrong value instead of
being reported as invalid.

Examples that were wrongly accepted before this change:

- `1h 2x 3m` → 3780s (parsed as 1h3m)
- `1h_2m`    → 3720s
- `1h,1m`    → 3660s

All three now return an "invalid time string" error, while valid inputs
such as `1h 1m 1s` and `1h1m1s` keep working.

### How

Reject any non-whitespace content between two matched units.

Signed-off-by: TowyTowy <towy@airreps.link>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: TowyTowy <85077986+TowyTowy@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 09:59:28 -07:00
Giteabot
c3325be816 fix: represent a deleted assignee team as a Ghost team (#38413) (#38419)
Backport #38413

Fixes #35472.

Co-authored-by: Harsh Satyajit Thakur <f20240223@goa.bits-pilani.ac.in>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 13:06:24 +00:00
Giteabot
d4250bafd9 fix(turnstile): route CAPTCHA verification through the configured proxy (#38412) (#38420)
Backport #38412

Fixes #38217

Co-authored-by: Lovepreet Singh <lovepreet.singh61182@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 05:39:14 -07:00
Giteabot
6815d1646c fix: refresh pull request merge box when the commit status is pending (#38410) (#38411)
Backport #38410

Before: the merge box is only refreshed when the PR is being
conflict-checked.

After: also refresh the merge box when the commit status is pending
(e.g.: Action task is running)

By the way, slightly increase the refresh interval to 5s

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-12 07:15:25 +08:00
Giteabot
8ba4d4ebec fix: actions task state concurrent update (#38405) (#38409)
Backport #38405

fix #38333

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-11 16:51:32 +02:00
Giteabot
b2794f96b9 fix(actions): keep workflow run trailing on one row with long branch names (#38382) (#38403)
Backport #38382 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-10 22:08:24 +00:00
Giteabot
b6aa2b61c6 fix(web): use locale-aware date formatting for contribution calendar tooltips (#38398) (#38401)
Backport #38398 by @SudhanshuMatrix

The contribution calendar manually constructed localized date strings
instead of using the browser's locale-aware date formatting. Replace
this with `toLocaleDateString()` to correctly format dates for all
locales and calendar systems, including non-Gregorian calendars.

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

### Before and After Comparison (Persian `fa-IR`)

Before (Buggy) 
 
<img width="1246" height="651" alt="image"
src="https://github.com/user-attachments/assets/670698e6-dd4a-4c7e-b7fb-23849090d1b9"
/>
 
After (Fixed) 
<img width="1246" height="651" alt="image"
src="https://github.com/user-attachments/assets/9b65c647-876e-475b-98d1-614ef316fd6f"
/>

Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
2026-07-10 21:28:54 +00:00
Giteabot
74ad781db9 fix(pull): re-evaluate review official flag on target branch change (#38319) (#38402)
Backport #38319 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-10 19:42:00 +00:00
Giteabot
ed493f0684 fix(security): harden access checks and migration validation (#38324) (#38400) 2026-07-10 20:14:38 +02:00
Giteabot
af7ba2edef fix: enforce public-only token scope and harden push options / locale parsing (#38323) (#38399)
Backport #38323 by @bircni

Three independent security hardening fixes, each with a regression test:

- **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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-10 19:07:04 +02:00
Giteabot
8b4252e7f6 fix: co-author detection (#38392) (#38397)
Backport #38392

Fix #38384

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-10 11:15:20 +00:00
Giteabot
500a09e044 fix(api): stop leaking private repo metadata after access revocation (#38321) (#38390)
Backport #38321 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-10 07:40:02 +00:00
Giteabot
bfebc4b0e1 fix: incorrect co-author detection on commit page (#38386) (#38387)
Backport #38386 by @bircni


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

Co-authored-by: bircni <bircni@icloud.com>
2026-07-09 18:29:28 -07:00
Giteabot
d9534e7bfa fix(lfs): require proof of possession for cross-repo objects (#38322) (#38389)
Backport #38322 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-09 22:18:07 +00:00
Giteabot
532828cc82 enhance(actions): only create filtered-out workflow commit status for required contexts (#38371) (#38385)
Backport #38371 by @Zettat123

Follow #38237

#38237 posts "skipped" commit statuses for every workflow that is not
triggered due to a filter (e.g. `paths` or `branches`) mismatch.
However, for non-required workflows, creating "skipped" commit statuses
for them would generate a lot of noise.

To address this issue, this PR adds a check before creating commit
status:

- For the context that matches any required status check patterns, a
"skipped" commit status will be created. The `Required` label can inform
users that this status check is required, but has been skipped because
of a filter mismatch.

<img width="800" alt="image"
src="https://github.com/user-attachments/assets/264fd716-413c-457d-a5a6-6717175d3807"
/>

- For a non-required context, nothing will be created.

NOTE: Reducing noise is a best-effort approach and isn't entirely
accurate. When creating commit statuses, it is impossible to predict
which branch protection rule will take effect. Therefore, we have to
compare the commit status context against the required patterns from all
rules. If any rule matches, the context is considered "required".
 
Closes https://github.com/go-gitea/gitea/issues/38351

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-09 15:58:46 +00:00
Giteabot
8cf988f0a6 fix(ui): restore commits table column widths (#38379) (#38383)
Backport #38379 by @silverwind

https://github.com/go-gitea/gitea/pull/37594 widened the author column
from `three wide` to `four wide`, leaving a large empty gap around the
SHA column in the common single-author case. The old author cell was
capped at 180px, so the wider column only adds whitespace: avatar-stack
names ellipsize at 240px each, and wider multi-author rows still expand
naturally in the auto-layout table. Restore the pre-existing
`three`/`eight` widths.

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
2026-07-09 17:07:20 +02:00
Giteabot
04a26f40a0 test(e2e): fix race in pdf file render test (#38380) (#38381)
Backport #38380 by @silverwind

`data-render-name` is set before the plugin's async render runs, so
measuring the container height right after the attribute appears can
observe the pre-render 48px height when the `pdfobject` chunk loads
slowly (flaked in CI). Poll for the height instead, like the asciicast
test in the same file does.

Co-authored-by: silverwind <me@silverwind.io>
2026-07-09 13:57:50 +00:00
Giteabot
fc4eac390a fix: golang html template url escaping (#38363) (#38369)
Backport #38363

fix #38362

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-08 12:10:26 +08:00
Giteabot
64d559a8e6 perf(actions): debounce runner heartbeat writes and throttle task picks (#38281) (#38368)
Backport #38281 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-07 16:27:00 -07:00
Giteabot
03372836ab fix(mirror): disable HTTP redirects on pull mirror sync (#38320) (#38367)
Backport #38320 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-07 17:59:33 +00:00
Giteabot
1af1e06ac4 fix: minio init check (#38355) (#38361)
Backport #38355

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-07 14:41:57 +02:00
Giteabot
e07a5de53f fix: org project view assignee list (#38357) (#38360)
Backport #38357

fix #38129

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-07 07:07:30 +00:00
Giteabot
d4d81af583 fix(actions): release claimed task if context is cancelled during FetchTask (#38343) (#38347) 2026-07-06 07:38:47 +02:00
Giteabot
6cd2c647ec test: compare key file contents instead of FileInfo in TestInitKeys (#38330) (#38331)
Backport #38330 by @Zettat123

`TestInitKeys` verified whether a host key file was regenerated by
comparing `os.FileInfo` before and after a `InitDefaultHostKeys` call.
On systems where the OS clock / filesystem timestamp granularity is
coarse, both writes land in the same tick and get an identical mtime.
The resulting `FileInfo` is then byte-for-byte equal even though the key
was actually regenerated, so `assert.NotEqual` fails.

Since a regenerated key always produces different random bytes,
comparing the content can reliably detect regeneration.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2026-07-04 20:52:52 +00:00
Giteabot
37c8604105 fix(release): validate web attachment renames against allowed types (#38314) (#38328)
Backport #38314 by @lunny

This fixes the web release edit flow so renamed release attachments are
validated against `[repository.release] ALLOWED_TYPES`.

Previously, the API attachment edit endpoint already enforced release
attachment type restrictions, but the web release edit form passed
`attachment-edit-*` values into `release_service.UpdateRelease`, which
updated attachment names directly without validating the new filename
against `setting.Repository.Release.AllowedTypes`.

As a result, a user with repository write access could rename an
existing release attachment to a disallowed extension through the web
UI.

- validate edited release attachment names in
`release_service.UpdateRelease`
- reject forbidden attachment renames using
  `setting.Repository.Release.AllowedTypes`
- re-render the web release edit page with a validation error instead of
  returning an internal server error
- add regression coverage for both the service layer and the web flow

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-04 17:45:11 +02:00
Giteabot
a9814e789c fix(actions): make runner list pagination order deterministic (#38313) (#38327)
Backport #38313 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-03 20:59:33 +00:00
Giteabot
ab10e37acf fix(release): gate draft release attachments on web download endpoints (#38318) (#38325)
Backport #38318 by @bircni

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.

This extends `ServeAttachment` to require write access to releases when
the attachment belongs to a draft release, mirroring the existing
API-side `canAccessReleaseDraft` gate. A regression test is included.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-03 20:34:37 +02:00
Giteabot
f7bc6b89c1 fix: Improve since/until when counting commits for X-Total-Count (#38243) (#38304)
Backport #38243 by @puni9869

Follow up for https://github.com/go-gitea/gitea/pull/38204.

Signed-off-by: puni9869 <80308335+puni9869@users.noreply.github.com>
Co-authored-by: puni9869 <80308335+puni9869@users.noreply.github.com>
2026-07-02 10:07:29 +00:00
Giteabot
fabc5e6fcb fix(actions): prevent chevron overlap with log text when timestamps are enabled (#38227) (#38307)
Backport #38227 by @SudhanshuMatrix

### 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.

Fixes #38222.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-02 11:22:26 +02:00
Giteabot
a016d678db fix(workflows): branch protection status checks fail when workflow uses on: paths filter (#38237) (#38302) 2026-07-01 20:10:11 +00:00
Giteabot
b6e409badd fix(oauth2): persist linkAccountData during auto-link 2FA flow (#38274) (#38295)
Backport #38274 by @afahey03

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.

Created the test, `TestOAuth2AutoLinkWithTwoFactor`, which verifies that
automatic account linking completes after the user passes local 2FA when
an OIDC identity matches an existing account.

DISCLAIMER: I used AI to create the test

Closes #38171

Co-authored-by: Aidan Fahey <afahey2003@yahoo.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-01 13:18:37 +02:00
Giteabot
2734504cfc fix(actions): allow Actions bot to push to protected branches (#38284) (#38293)
Backport #38284 by @bircni

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: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-07-01 11:46:17 +02:00
Giteabot
eee7967b81 fix(actions): include all aggregable run statuses in status filter (#38280) (#38287)
Backport #38280 by @bircni

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.

Co-authored-by: bircni <bircni@icloud.com>
2026-07-01 10:12:11 +02:00
Giteabot
1df8f91691 fix(archiver): use serializable repo-archive queue payload (#38273) (#38283)
Backport #38273 by @Vinod-OAI

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: Vinod-OAI <venkat.vinod@observe.ai>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-30 19:17:29 +02:00
bircni
4654e7eccd docs: Changelog for 1.27.0-rc0 (#38247)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-06-29 21:06:07 +02:00
3551 changed files with 127650 additions and 138710 deletions

View File

@@ -11,7 +11,7 @@ include_file = ["main.go"]
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
exclude_dir = [
"models/fixtures",
"modelmigration/fixtures",
"models/migrations/fixtures",
"modules/avatar/identicon/testdata",
"modules/avatar/testdata",
"modules/git/tests",

59
.changelog.yml Normal file
View File

@@ -0,0 +1,59 @@
# The full repository name
repo: go-gitea/gitea
# Service type (gitea or github)
service: github
# Base URL for Gitea instance if using gitea service type (optional)
# Default: https://gitea.com
base-url:
# Changelog groups and which labeled PRs to add to each group
groups:
-
name: BREAKING
labels:
- pr/breaking
-
name: SECURITY
labels:
- topic/security
-
name: FEATURES
labels:
- type/feature
-
name: ENHANCEMENTS
labels:
- type/enhancement
-
name: PERFORMANCE
labels:
- performance/memory
- performance/speed
- performance/bigrepo
- performance/cpu
-
name: BUGFIXES
labels:
- type/bug
-
name: TESTING
labels:
- type/testing
-
name: BUILD
labels:
- topic/build
- topic/code-linting
-
name: DOCS
labels:
- type/docs
-
name: MISC
default: true
# regex indicating which labels to skip for the changelog
skip-labels: skip-changelog|backport\/.+

View File

@@ -1,6 +1,6 @@
{
"name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.27-trixie",
"image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
"containerEnv": {
// override "local" from packaged version
"GOTOOLCHAIN": "auto"

View File

@@ -63,6 +63,7 @@ cpu.out
/public/assets/fonts
/public/assets/img/avatar
/vendor
/VERSION
/.air
/.go-licenses
/Dockerfile

View File

@@ -18,10 +18,13 @@ indent_style = tab
[templates/custom/*.tmpl]
insert_final_newline = false
[templates/swagger/*.generated.json]
[templates/swagger/*_json.tmpl]
indent_style = space
insert_final_newline = false
[templates/user/auth/oidc_wellknown.tmpl]
indent_style = space
[templates/shared/actions/runner_badge_*.tmpl]
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
charset = unset

7
.gitattributes vendored
View File

@@ -2,12 +2,11 @@
*.tmpl linguist-language=Handlebars
*.pb.go linguist-generated
/assets/*.json linguist-generated
/public/assets/emoji.json linguist-generated
/public/assets/img/svg/*.svg linguist-generated
/templates/swagger/*.generated.json linguist-generated
/templates/swagger/v1_json.tmpl linguist-generated
/templates/swagger/v1_openapi3_json.tmpl linguist-generated
/options/fileicon/** linguist-generated
/services/migrations/_mock_data/** linguist-generated
/tests/integration/_mock_data/** linguist-generated
/vendor/** -text -eol linguist-vendored
/web_src/js/vendor/** -text -eol linguist-vendored
Dockerfile.* linguist-language=Dockerfile
Makefile.* linguist-language=Makefile

View File

@@ -1,59 +0,0 @@
name: Release
description: Track a Gitea release (for release managers).
title: "Release Gitea "
body:
- type: markdown
attributes:
value: |
Follow the [release management guide](https://github.com/go-gitea/gitea/blob/main/docs/release-management.md).
Replace the examples below and mark inapplicable tasks as such.
CI signs the tag, generates release notes, and publishes binaries and containers. Track verification here; no manual changelog PR or release upload is needed.
- type: input
id: version
attributes:
label: Version
placeholder: "28.0.1"
validations:
required: true
- type: input
id: branch
attributes:
label: Release branch
placeholder: "release/v28"
validations:
required: true
- type: textarea
id: checklist
attributes:
label: Release checklist
description: Keep workflow runs, release URLs, and follow-up PRs alongside the relevant tasks.
value: |
### Preparation
- [ ] Resolve release blockers and confirm milestone issues and PRs are resolved or deferred.
- [ ] Confirm required backports are merged and release branch CI passes.
- [ ] For a new release line, create the release branch and tag its fork point on main with the next version's -dev tag.
### Release
- [ ] Run https://github.com/go-gitea/gitea/actions/workflows/release-create-tag.yml on the release branch with the selected version and obtain maintainer approval.
- [ ] Confirm https://github.com/go-gitea/gitea/actions/workflows/release-tag-version.yml succeeds for the new tag (binaries and containers).
- [ ] Verify the public GitHub release, generated notes, binary attachments, and signatures at https://github.com/go-gitea/gitea/releases.
- [ ] Verify binaries and signatures at https://dl.gitea.com/gitea/ for this version.
- [ ] Verify versioned regular and rootless images on Docker Hub and GHCR, and smoke-test the release.
### Follow-up
- [ ] Verify the automated https://dl.gitea.com/gitea/version.json update, where applicable to this release line.
- [ ] Verify automated Helm chart and Terraform provider update PRs and follow up if needed.
- [ ] Check Homebrew and Snap availability; record any outstanding packaging follow-up.
- [ ] Confirm documentation reflects the release, where applicable.
- [ ] Confirm and merge the release blog post, if planned: https://gitea.com/gitea/blog.
- [ ] Announce the release in Discord #announcements.
validations:
required: true
- type: textarea
id: notes
attributes:
label: Blockers and notes
description: Link outstanding work or release-specific checks using full URLs. Do not include undisclosed security details.

View File

@@ -0,0 +1,29 @@
name: docker-dryrun
description: Composite action that performs the container build steps for a single platform.
inputs:
platform:
description: "The target platform: linux/amd64, linux/arm64, linux/riscv64."
required: true
runs:
using: composite
steps:
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Build regular image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: ${{ inputs.platform }}
push: false
file: Dockerfile
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: ${{ inputs.platform }}
push: false
file: Dockerfile.rootless
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless

View File

@@ -7,11 +7,6 @@ runs:
steps:
- shell: bash
run: |
avail=$(df --output=avail -BG / | tr -dc '0-9')
if [ "$avail" -ge 50 ]; then
echo "skipping cleanup, ${avail}G free"
exit 0
fi
echo "free space before cleanup:"
df -h /
for dir in /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet; do

View File

@@ -2,18 +2,12 @@ name: go-caches
description: Restore the go module, build, and golangci-lint caches. Save only on the cache-seeder workflow.
# Only the cache-seeder workflow saves; rename requires updating cache-seeder.yml.
# The lint job restores but does not save the gobuild and gomod caches, so every key
# has a single writer and there is no contention on it.
# The lint job restores but does not save the gobuild cache, so only one writer
# (the gobuild job) populates it and there is no contention on the cache key.
# Seeder restores by exact key only (no restore-keys) so each go.sum seeds a clean
# cache and size stays bounded; do not add restore-keys here. PR runs keep them.
# The go version is part of the gobuild and golint keys because a toolchain bump
# invalidates every entry in them, so a cross-version fallback would restore
# gigabytes that no build can use. The module cache is version-independent.
inputs:
go-version:
description: Go version the build caches belong to
required: true
lint-cache:
description: Restore (and save in cache-seeder) ~/.cache/golangci-lint
default: "false"
@@ -21,36 +15,36 @@ inputs:
runs:
using: composite
steps:
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- if: ${{ github.workflow == 'cache-seeder' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- if: ${{ github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}-
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum') }}
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/golangci-lint
key: golint-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum', '.golangci.yml') }}
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/golangci-lint
key: golint-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}

View File

@@ -13,8 +13,7 @@ runs:
using: composite
steps:
- uses: ./.github/actions/free-disk-space
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
id: setup-go
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
@@ -22,5 +21,4 @@ runs:
- if: ${{ inputs.cache == 'true' }}
uses: ./.github/actions/go-cache
with:
go-version: ${{ steps.setup-go.outputs.go-version }}
lint-cache: ${{ inputs.lint-cache }}

View File

@@ -1,30 +1,22 @@
name: node-setup
description: Set up pnpm and node and restore the pnpm store cache
description: Set up pnpm and node and restore caches
inputs:
cache:
description: Restore the pnpm store cache
description: Cache pnpm downloads
default: "true"
runs:
using: composite
steps:
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: 26
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- if: ${{ inputs.cache == 'true' }}
id: store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- if: ${{ inputs.cache == 'true' && github.workflow == 'cache-seeder' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
path: ${{ steps.store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
- if: ${{ inputs.cache == 'true' && github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- if: ${{ inputs.cache != 'true' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
path: ${{ steps.store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-${{ runner.arch }}-
node-version: 24

View File

@@ -34,7 +34,7 @@ runs:
env:
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
GOTEST_FLAGS: -race -timeout=40m
TAGS: bindata
TAGS: bindata gogit
TEST_LDAP: 1
TEST_SHARD: ${{ inputs.shard }}
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}

View File

@@ -1,10 +0,0 @@
name: python-setup
description: Set up uv and python and restore the uv cache
runs:
using: composite
steps:
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
python-version: 3.15 # renovate: datasource=python-version depName=python versioning=python
save-cache: ${{ github.workflow == 'cache-seeder' }}

View File

@@ -1,48 +0,0 @@
name: cache-prune
# Keeps the repository's total cache size below a fixed limit, so that GitHub never
# rejects a save for being over the allowance.
on:
schedule:
- cron: "37 */6 * * *" # every six hours at :37
workflow_dispatch:
workflow_call:
permissions: {}
concurrency:
group: cache-prune
jobs:
prune:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
actions: write # to delete caches
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
# Keep the newest generation per key, restores never reach the older ones.
- name: delete superseded caches
run: |
gh cache list --limit 1000 --json id,key,ref,createdAt |
jq -r 'group_by([.ref, (.key | sub("(-go[0-9.]+)?-[0-9a-f]{40,64}(-[0-9]+-[0-9]+)?$"; ""))])[]
| sort_by(.createdAt)[:-1][] | "\(.id) \(.key)"' |
while read -r id key; do
echo "deleting $key"
gh cache delete "$id" || true
done
# Deletes least recently used first, the order GitHub itself evicts in.
- name: delete caches over the size limit
run: |
caches=$(gh cache list --limit 1000 --sort last_accessed_at --order asc --json id,key,sizeInBytes)
size=$(jq '[.[].sizeInBytes] | add // 0' <<< "$caches")
echo "cache usage: $((size / 1000000)) MB"
while [ "$size" -gt 6000000000 ] && read -r id bytes key; do
echo "deleting $key"
gh cache delete "$id" || true
size=$((size - bytes))
done <<< "$(jq -r '.[] | "\(.id) \(.sizeInBytes) \(.key)"' <<< "$caches")"

View File

@@ -1,25 +1,21 @@
# Populates main's cache scope so PR runs warm-start from it. Caches are ref-scoped:
# PR runs read their own scope then fall back to the base branch, and only this
# workflow saves, so push-to-main is the only chance to populate the fallback scope.
# Populates main's cache scope so PR runs warm-start from it. Saves the go
# module, go build (incl. test compile), and golangci-lint caches.
#
# Caches are ref-scoped: PR runs read their own scope then fall back to the
# base branch. Per .github/actions/go-cache/action.yml, PRs are restore-only,
# so push-to-main is the only opportunity to populate the fallback scope.
name: cache-seeder
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "go.mod" # a toolchain bump invalidates the build caches
- "go.sum"
- ".golangci.yml"
- "pnpm-lock.yaml"
- "pyproject.toml"
- "uv.lock"
- ".github/actions/go-cache/action.yml"
- ".github/actions/go-setup/action.yml"
- ".github/actions/node-setup/action.yml"
- ".github/actions/python-setup/action.yml"
- ".github/workflows/cache-seeder.yml"
concurrency:
@@ -33,19 +29,27 @@ jobs:
gobuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
- name: warm test compile cache (bindata)
env:
TAGS: bindata
GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend
- name: warm test compile cache (bindata gogit)
env:
TAGS: bindata gogit
GOEXPERIMENT:
GOTEST_FLAGS: -race -list=^$$ -count=1
run: make test-backend
- name: warm integration compile cache
run: |
TAGS="bindata" make test-integration-compile
TAGS="bindata" GOTEST_FLAGS="-race" make test-integration-compile
TAGS="bindata gogit" GOEXPERIMENT="" make test-integration-compile
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
lint:
runs-on: ubuntu-latest
@@ -55,7 +59,7 @@ jobs:
include:
- { tags: "bindata", target: "lint-backend" }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
@@ -63,28 +67,6 @@ jobs:
- run: make generate-go
env:
TAGS: ${{ matrix.tags }}
- run: make "$TARGET"
- run: make ${{ matrix.target }}
env:
TAGS: ${{ matrix.tags }}
TARGET: ${{ matrix.target }}
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/node-setup
- run: make deps-frontend
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/python-setup
- run: make deps-py
# reclaims the caches this run superseded, so the next save still fits in the allowance
prune:
needs: [gobuild, lint, frontend, python]
permissions:
actions: write
uses: ./.github/workflows/cache-prune.yml

View File

@@ -5,8 +5,6 @@ on:
# - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch:
permissions: {}
jobs:
cron-licenses:
runs-on: ubuntu-latest
@@ -14,12 +12,11 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- run: make generate-gitignore
timeout-minutes: 40
- name: push translations to repo

View File

@@ -9,7 +9,7 @@ concurrency:
group: cron-renovate
env:
RENOVATE_VERSION: 44.210.0 # renovate: datasource=npm depName=renovate
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
permissions:
contents: read
@@ -20,14 +20,13 @@ jobs:
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: renovatebot/github-action@1c0cf989c02653fba0efe7ca5772338860ca71dc # v46.3.12
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }}
env:
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg|generate-codemirror-languages)$"]'
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]'
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
RENOVATE_DOCKER_MAX_PAGES: 10 # https://github.com/renovatebot/renovate/issues/46925

View File

@@ -5,8 +5,6 @@ on:
- cron: "7 0 * * *" # every day at 00:07 UTC
workflow_dispatch:
permissions: {}
jobs:
crowdin-pull:
runs-on: ubuntu-latest
@@ -14,8 +12,8 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: crowdin/github-action@5b9790df740c85af5412a3a3a91b24988d5d324f # v3.5.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
with:
upload_sources: true
upload_translations: false

View File

@@ -15,6 +15,8 @@ on:
value: ${{ jobs.detect.outputs.templates }}
docker:
value: ${{ jobs.detect.outputs.docker }}
dockerfile:
value: ${{ jobs.detect.outputs.dockerfile }}
swagger:
value: ${{ jobs.detect.outputs.swagger }}
yaml:
@@ -40,21 +42,22 @@ jobs:
actions: ${{ steps.changes.outputs.actions }}
templates: ${{ steps.changes.outputs.templates }}
docker: ${{ steps.changes.outputs.docker }}
dockerfile: ${{ steps.changes.outputs.dockerfile }}
swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
json: ${{ steps.changes.outputs.json }}
e2e: ${{ steps.changes.outputs.e2e }}
shell: ${{ steps.changes.outputs.shell }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: |
backend:
- "**/*.go"
- "templates/**/*.tmpl"
- "public/assets/emoji.json"
- "assets/emoji.json"
- "go.mod"
- "go.sum"
- "Makefile"
@@ -70,11 +73,10 @@ jobs:
frontend:
- "*.ts"
- "web_src/**"
- "tools/**/*.ts"
- "tools/**/*.json"
- "tools/playwright.sh"
- "tools/generate-svg.ts"
- "tools/generate-svg-vscode-extensions.json"
- "tsconfig.json"
- "public/assets/emoji.json"
- "assets/emoji.json"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
@@ -100,13 +102,19 @@ jobs:
docker:
- ".github/workflows/pull-docker-dryrun.yml"
- ".github/actions/docker-dryrun/**"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
- "Makefile"
dockerfile:
- "Dockerfile"
- "Dockerfile.rootless"
swagger:
- "templates/swagger/*.json"
- "templates/swagger/v1_json.tmpl"
- "templates/swagger/v1_input.json"
- "Makefile"
- "package.json"
- "pnpm-lock.yaml"
@@ -127,7 +135,6 @@ jobs:
e2e:
- "tests/e2e/**"
- "tools/test-e2e.sh"
- "tools/playwright.sh"
- "playwright.config.ts"
shell:

View File

@@ -4,14 +4,13 @@ on:
push:
branches:
- main
- release/v*
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
@@ -20,8 +19,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
checks: backport,backport_conflict_free
checks: backport

View File

@@ -1,19 +0,0 @@
name: giteabot-review
# Relays PR reviews to giteabot.yml through its workflow_run trigger, because review
# runs on fork PRs get no secrets and a read-only token. The job itself does nothing.
on:
pull_request_review:
types:
- submitted
- edited
- dismissed
permissions: {}
jobs:
relay:
runs-on: ubuntu-latest
steps:
- run: "true"

View File

@@ -1,11 +1,16 @@
name: giteabot
on:
# When main advances, rerun merge queue maintenance so the oldest
# reviewed/wait-merge PR can be updated against the new base promptly.
push:
branches:
- main
# pull_request_target gives this workflow access to GITEABOT_TOKEN on PRs from
# forks, which the bot needs to write labels, statuses and comments. Safe here
# because the job only runs a pinned action and never checks out PR HEAD.
# These PR lifecycle events drive label maintenance, lgtm, and explicit bot
# actions triggered by relevant label changes.
# These PR lifecycle events drive label maintenance, queue maintenance, and
# explicit bot actions triggered by relevant label changes.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
@@ -15,13 +20,15 @@ on:
- closed
- review_requested
- review_request_removed
# Reviews arrive through giteabot-review because fork PR review runs get no secrets
workflow_run:
workflows:
- giteabot-review
# Review events keep review-derived state such as lgtm labels and status checks
# in sync after approvals, edits, or dismissals.
pull_request_review:
types:
- requested
# Daily housekeeping like locks, stale feedback issues, last calls and milestones
- submitted
- edited
- dismissed
# Periodic maintenance is still useful as a backstop for queue cleanup and
# other housekeeping, even though main pushes now trigger it promptly.
schedule:
- cron: "15 3 * * *"
# Allow maintainers to rerun selected checks manually when debugging bot
@@ -31,26 +38,27 @@ on:
checks:
description: Comma-separated list of non-backport checks to run
required: false
default: backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
default: labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
permissions: {}
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
concurrency:
group: ${{ format('{0}-{1}', github.workflow, github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && format('review-{0}', github.event.workflow_run.head_sha) || 'maintenance') }}
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }}
cancel-in-progress: false
jobs:
giteabot:
if: github.repository == 'go-gitea/gitea' && (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request_review')
if: github.repository == 'go-gitea/gitea'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
steps:
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
# pull_request_review runs without repository secrets on fork PRs, so fall
# back to the workflow token for the non-backport checks handled here.
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
checks: ${{ github.event.inputs.checks || 'backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}

View File

@@ -1,16 +0,0 @@
name: merge-group
# Reports the required giteabot/lgtm check on merge queue commits. giteabot only sets it
# on PR heads, which already passed it before entering the queue.
on:
merge_group:
permissions: {}
jobs:
lgtm:
name: giteabot/lgtm
runs-on: ubuntu-latest
steps:
- run: "true"

View File

@@ -2,7 +2,6 @@ name: compliance
on:
pull_request:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -20,19 +19,19 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
- run: make deps-backend deps-tools
- run: TAGS="bindata" make generate-go lint-backend # lint-go can lint with "bindata" tags
- run: TAGS="bindata" make generate-go # lint-go also lints with "bindata" tags which requires "_bindata.go"
- run: make lint-backend
lint-on-demand:
if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true' || needs.files-changed.outputs.shell == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
with:
cache: "false"
@@ -40,8 +39,12 @@ jobs:
with:
cache: "false"
- run: make lint-spell
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
uses: ./.github/actions/python-setup
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: 3.14
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
run: make deps-py lint-templates lint-yaml
@@ -59,7 +62,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
@@ -69,12 +72,11 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/node-setup
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
- run: make playwright
- run: make test-frontend
- run: make frontend
@@ -83,27 +85,25 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
- name: build-backend-arm64
run: go build -ldflags '-s -w' -o gitea-linux-arm64
run: go build -o gitea_linux_arm64
env:
GOOS: linux
GOARCH: arm64
TAGS: bindata
TAGS: bindata gogit
- name: build-backend-windows
run: go build -ldflags '-s -w' -o gitea-windows
run: go build -o gitea_windows
env:
GOOS: windows
GOARCH: amd64
TAGS: bindata
TAGS: bindata gogit
- name: build-backend-386
run: go build -ldflags '-s -w' -o gitea-linux-386
run: go build -o gitea_linux_386
env:
GOOS: linux
GOARCH: 386
- name: binary-sizes
run: ls -l gitea-*

View File

@@ -2,7 +2,6 @@ name: db-tests
on:
pull_request:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -22,12 +21,10 @@ jobs:
timeout-minutes: 50
services:
pgsql:
image: postgres:14@sha256:156f0b253fd61366d5fc2107ad45955027d5612f695a8436ce20167f3fa79bff
image: postgres:14
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
ports:
- "5432:5432"
ldap:
@@ -38,14 +35,14 @@ jobs:
minio:
# as github actions doesn't support "entrypoint", we need to use a non-official image
# that has a custom entrypoint set to "minio server /data"
image: bitnamilegacy/minio:2025.7.23@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7
image: bitnamilegacy/minio:2025.7.23
env:
MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678
ports:
- "9000:9000"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/pgsql-shard
with:
@@ -60,12 +57,10 @@ jobs:
timeout-minutes: 50
services:
pgsql:
image: postgres:14@sha256:156f0b253fd61366d5fc2107ad45955027d5612f695a8436ce20167f3fa79bff
image: postgres:14
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
ports:
- "5432:5432"
ldap:
@@ -76,14 +71,14 @@ jobs:
minio:
# as github actions doesn't support "entrypoint", we need to use a non-official image
# that has a custom entrypoint set to "minio server /data"
image: bitnamilegacy/minio:2025.7.23@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7
image: bitnamilegacy/minio:2025.7.23
env:
MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678
ports:
- "9000:9000"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/pgsql-shard
with:
@@ -95,22 +90,24 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
TAGS: bindata gogit
GOEXPERIMENT:
- run: GITEA_TEST_DATABASE=sqlite make test-migration
env:
TAGS: bindata
TAGS: bindata gogit
- name: run tests
run: GITEA_TEST_DATABASE=sqlite make test-integration
timeout-minutes: 50
env:
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
GOTEST_FLAGS: -timeout=40m
TAGS: bindata
TAGS: bindata gogit
GOEXPERIMENT:
test-unit:
if: needs.files-changed.outputs.backend == 'true'
@@ -118,7 +115,7 @@ jobs:
runs-on: ubuntu-latest
services:
elasticsearch:
image: elasticsearch:8.19.23@sha256:ef5cafd961d7e273f13fed8def82e63c5ae0491106b5eecb5a69aa12d64f089b
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15
env:
discovery.type: single-node
xpack.security.enabled: false
@@ -128,13 +125,13 @@ jobs:
ports:
- "9200:9200"
meilisearch:
image: getmeili/meilisearch:v1@sha256:88f2a08eb9532cc54a60d8fd014a23bf5cfda33d0c01ecbbaca9b4b463e17466
image: getmeili/meilisearch:v1
env:
MEILI_ENV: development # disable auth
ports:
- "7700:7700"
redis:
image: redis:8@sha256:298e5b3bc566bade82f46ad5511777a4a07a294097ce16ada2f6a42be5239df5
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
@@ -143,18 +140,18 @@ jobs:
ports:
- 6379:6379
minio:
image: bitnamilegacy/minio:2025.7.23@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7
image: bitnamilegacy/minio:2025.7.23
env:
MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678
ports:
- "9000:9000"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:f9791d0a0613e93057c06ae31f7ac0c0222f0530590e67e020b0986c66a36fa6
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4
ports:
- 10000:10000
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
@@ -168,6 +165,14 @@ jobs:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: make test-backend
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata gogit
GOEXPERIMENT:
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
GITEA_TEST_CI_SKIP_EXTERNAL: true
- run: make test-check
test-mysql:
@@ -177,7 +182,7 @@ jobs:
services:
mysql:
# the bitnami mysql image has more options than the official one, it's easier to customize
image: bitnamilegacy/mysql:8.4@sha256:7089d796fc9b4629a628bd445e4afabe607351ee665444c3197bdeaed812ea65
image: bitnamilegacy/mysql:8.4
env:
ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: testgitea
@@ -186,7 +191,7 @@ jobs:
options: >-
--mount type=tmpfs,destination=/bitnami/mysql/data
elasticsearch:
image: elasticsearch:8.19.23@sha256:ef5cafd961d7e273f13fed8def82e63c5ae0491106b5eecb5a69aa12d64f089b
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15
env:
discovery.type: single-node
xpack.security.enabled: false
@@ -203,7 +208,7 @@ jobs:
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
@@ -216,6 +221,7 @@ jobs:
run: GITEA_TEST_DATABASE=mysql make test-integration
env:
TAGS: bindata
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql:
if: needs.files-changed.outputs.backend == 'true'
@@ -223,7 +229,7 @@ jobs:
runs-on: ubuntu-latest
services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest@sha256:07d7dedecdbfcf6776035dc5eefb5bfd9aa1804b2f6b99f7dedb397d8a599069
image: mcr.microsoft.com/mssql/server:2019-latest
env:
ACCEPT_EULA: Y
MSSQL_PID: Standard
@@ -231,11 +237,11 @@ jobs:
ports:
- "1433:1433"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:f9791d0a0613e93057c06ae31f7ac0c0222f0530590e67e020b0986c66a36fa6
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4
ports:
- 10000:10000
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'

View File

@@ -14,26 +14,34 @@ jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
container:
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
# Run amd64 when any docker-related files change, which is fast (4 minutes).
container-amd64:
if: needs.files-changed.outputs.docker == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/docker-dryrun
with:
cache-image: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Build regular image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
platform: linux/amd64
container-arm64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/docker-dryrun
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
platform: linux/arm64
container-riscv64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/docker-dryrun
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
file: Dockerfile.rootless
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
platform: linux/riscv64

View File

@@ -2,7 +2,6 @@ name: e2e-tests
on:
pull_request:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -20,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/node-setup
- run: make deps-frontend

View File

@@ -10,8 +10,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
labeler:
runs-on: ubuntu-latest
@@ -19,7 +17,7 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
sync-labels: true
@@ -32,12 +30,12 @@ jobs:
pull-requests: write
steps:
# Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
node-version: 24
# Labels are only synced after the title lints, so an invalid title never reaches the label diff.
- run: node ./tools/ci-tools.ts lint-pr-title
env:
@@ -45,6 +43,5 @@ jobs:
- run: node ./tools/ci-tools.ts set-pr-labels
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_TITLE_BEFORE: ${{ github.event.changes.title.from }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -1,34 +0,0 @@
name: release-create-tag
run-name: Release v${{ inputs.version }} from ${{ github.ref_name }}
on:
workflow_dispatch:
inputs:
version:
description: Version to release, for example 28.0.1
required: true
permissions: {}
jobs:
tag:
runs-on: ubuntu-latest
environment:
name: release-signing
deployment: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
token: ${{ secrets.RELEASE_TOKEN }}
- uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
git_user_signingkey: true
git_committer_email: teabot@gitea.io
- env:
VERSION: ${{ inputs.version }}
run: |
[[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
git tag -s -m "v$VERSION" "v$VERSION"
git push origin tag "v$VERSION"

View File

@@ -0,0 +1,45 @@
name: release-nightly-snapcraft
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
build-and-publish:
runs-on: ubuntu-latest
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Authenticate snapcraft
shell: bash
run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS")
- name: Remote build
run: |
snapcraft remote-build \
--launchpad-accept-public-upload \
--build-for=amd64,arm64,armhf
- name: List built snaps
run: find . -maxdepth 1 -type f -name '*.snap' -print
- name: Upload and release snapcraft nightly build
run: |
set -euo pipefail
for snap in ./*.snap; do
echo "Uploading $snap to edge"
snapcraft upload --release="latest/edge" "$snap"
done

View File

@@ -1,66 +1,39 @@
name: release-nightly
on:
schedule:
- cron: "17 */12 * * *"
workflow_dispatch:
push:
branches: [main, release/v*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
branches:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
branches: ${{ steps.changed.outputs.branches }}
steps:
- id: changed
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
since=$(gh run list --workflow release-nightly.yml --status success --limit 1 --json createdAt -q '.[0].createdAt')
branches=$(gh api --paginate "repos/$GH_REPO/branches" -q '.[].name | select(. == "main" or startswith("release/v"))' |
while read -r branch; do
if [ -z "$since" ] || [ -n "$(gh api "repos/$GH_REPO/commits?sha=$branch&since=$since&per_page=1" -q '.[].sha')" ]; then
echo "$branch"
fi
done | jq -Rnc '[inputs]')
echo "branches=$branches" >> "$GITHUB_OUTPUT"
nightly-binary:
needs: branches
if: needs.branches.outputs.branches != '[]'
strategy:
matrix:
branch: ${{ fromJSON(needs.branches.outputs.branches) }}
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ matrix.branch }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/node-setup
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
- run: make release GITHUB_REF_NAME="$BRANCH"
# xgo build
- run: make release
env:
BRANCH: ${{ matrix.branch }}
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
@@ -78,20 +51,34 @@ jobs:
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in the object storage
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
env:
REF: ${{ matrix.branch }}
REF: ${{ github.ref }}
run: |
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
- name: upload binaries to cloudflare r2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
@@ -99,37 +86,27 @@ jobs:
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
nightly-container:
needs: branches
if: needs.branches.outputs.branches != '[]'
strategy:
matrix:
branch: ${{ fromJSON(needs.branches.outputs.branches) }}
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ matrix.branch }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
cache-image: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Get cleaned branch name
id: clean_name
env:
REF: ${{ matrix.branch }}
REF: ${{ github.ref }}
run: |
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta
with:
context: git
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
@@ -137,10 +114,9 @@ jobs:
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta_rootless
with:
context: git
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
@@ -152,18 +128,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -173,7 +149,7 @@ jobs:
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -183,47 +159,3 @@ jobs:
annotations: ${{ steps.meta_rootless.outputs.annotations }}
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless,mode=max
nightly-snap:
needs: branches
if: contains(fromJSON(needs.branches.outputs.branches), 'main')
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.runner }}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: main
fetch-depth: 0
- name: Build snap
id: build
env:
SNAPCRAFT_IMAGE_INFO: '{"build_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
run: |
# snapcraft installs build-snaps and apt packages system-wide, which needs root
sudo snap install --classic snapcraft
sudo SNAPCRAFT_BUILD_INFO=1 SNAPCRAFT_IMAGE_INFO="$SNAPCRAFT_IMAGE_INFO" snapcraft pack --destructive-mode
echo "snap=$(ls ./*.snap)" >> "$GITHUB_OUTPUT"
# retry snapcraft uploads which can be flaky
- name: Publish snap
run: |
# part-gitea-pull.sh decides what is built, see the comments there:
# * grade devel (main or a prerelease tag) goes to latest/edge
# * grade stable (a new release tag) goes to latest/stable, and to latest/candidate which marks it as released
channel=latest/edge
if unsquashfs -cat "$SNAP" meta/snap.yaml | grep -qx 'grade: stable'; then
channel=latest/stable,latest/candidate
fi
for attempt in 1 2 3 4 5; do
snapcraft upload "$SNAP" --release "$channel" && exit 0
echo "::warning::snap upload attempt $attempt failed, retrying in 15s"
sleep 15
done
exit 1
env:
SNAP: ${{ steps.build.outputs.snap }}
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}

166
.github/workflows/release-tag-rc.yml vendored Normal file
View File

@@ -0,0 +1,166 @@
name: release-tag-rc
on:
push:
tags:
- "v1*-rc*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
env:
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
env:
REF: ${{ github.ref }}
run: |
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
- name: upload binaries to cloudflare r2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with:
gh-cli-version: 2.39.1
- name: create github release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
gh release create "$TAG" --title "$TAG" --draft --notes-from-tag dist/release/*
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
flavor: |
latest=false
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
latest=false
suffix=-rootless
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}

View File

@@ -3,15 +3,14 @@ name: release-tag-version
on:
push:
tags:
- "v[0-9]*"
- "!v[0-9]*-*"
- "v1.*"
- "!v1*-rc*"
- "!v1*-dev"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions: {}
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
@@ -20,18 +19,25 @@ jobs:
packages: write # to publish to ghcr.io
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/node-setup
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
@@ -49,7 +55,7 @@ jobs:
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in the object storage
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
env:
@@ -58,35 +64,40 @@ jobs:
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
- name: upload binaries to cloudflare r2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
BRANCH: ${{ steps.clean_name.outputs.branch }}
run: |
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
- id: range
run: |
previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one
echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT"
- name: skip reverts and reverted commits in changelog
env:
RANGE: ${{ steps.range.outputs.range }}
run: ./tools/generate-cliffignore.sh "$RANGE" | sort -u > .cliffignore
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with:
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
gh-cli-version: 2.39.1
- name: create github release
env:
GH_CLI_VERSION: 2.103.3 # renovate: datasource=github-releases depName=cli/cli extractVersion=^v(?<version>.+)$
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.tar.gz" | tar xz --strip-components=2 "gh_${GH_CLI_VERSION}_linux_amd64/bin/gh"
./gh release create "$TAG" --title "$TAG" --notes-file git-cliff/CHANGELOG.md dist/release/*
gh release create "$TAG" --title "$TAG" --notes-from-tag dist/release/*
container:
runs-on: namespace-profile-gitea-release-docker
@@ -94,15 +105,13 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
cache-image: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta
with:
images: |-
@@ -119,7 +128,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta_rootless
with:
images: |-
@@ -140,18 +149,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -159,7 +168,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64

9
.github/zizmor.yml vendored
View File

@@ -1,9 +0,0 @@
rules:
anonymous-definition:
disable: true
undocumented-permissions:
disable: true
concurrency-limits:
disable: true
self-repository:
disable: true # actionlint rejects "$/" uses, https://github.com/rhysd/actionlint/issues/711

View File

@@ -10,9 +10,7 @@ linters:
- depguard
- dupl
- errcheck
- exhaustruct_v5
- forbidigo
- forcetypeassert
- gocheckcompilerdirectives
- gocritic
- govet
@@ -53,23 +51,14 @@ linters:
desc: do not use the go-chi cache package, use gitea's cache system
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
- pkg: gitea.com/go-chi/binding
desc: use our wrapper
migrations:
files:
- '**/modelmigration/**/*.go'
- '**/models/migrations/**/*.go'
deny:
- pkg: gitea.dev/models
desc: "migrations must not depend on the models package. HINT: MIGRATION-STRUCT-FROZEN"
- pkg: gitea.dev/models$
desc: migrations must not depend on the models package
- pkg: gitea.dev/modules/structs
desc: "migrations must not depend on modules/structs. HINT: MIGRATION-STRUCT-FROZEN"
forbidigo:
forbid:
- pattern: '^(fmt\.Print(|f|ln)|print|println)$' # default
- pattern: '^exec\.CommandContext$' # use our wrapper for graceful termination
modernize:
disable:
- embedlit
desc: migrations must not depend on modules/structs (API structures change over time)
nolintlint:
allow-unused: false
require-explanation: true
@@ -98,11 +87,6 @@ linters:
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: package-naming
arguments:
- convention-name-check-regex: '^[a-z][a-z0-9_]*$' # underscores are used by migration packages
skip-default-bad-name-check: true
skip-collision-with-common-std: true
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
@@ -112,7 +96,7 @@ linters:
arguments:
- [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
- [] # DenyList
- - skip-initialism-name-checks: true
- - skip-package-name-checks: true # supress errors from underscore in migration packages
staticcheck:
checks:
- all
@@ -134,8 +118,6 @@ linters:
enable:
- nilness
- unusedwrite
exhaustruct_v5:
explicit-mode: true
exclusions:
generated: lax
presets:
@@ -147,13 +129,16 @@ linters:
- linters:
- dupl
- errcheck
- forbidigo
- staticcheck
- unparam
path: _test\.go
- linters:
- dupl
path: modelmigration/v
- errcheck
path: models/migrations/v
- linters:
- forbidigo
path: cmd
- linters:
- dupl
text: (?i)webhook
@@ -170,10 +155,13 @@ linters:
- gocritic
text: '(?i)exitAfterDefer:'
paths:
- ^node_modules/
- ^\.venv/
- ^public/
- ^web_src/
- node_modules
- .venv
- public
- web_src
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
@@ -190,15 +178,14 @@ formatters:
- blank
- default
gofumpt:
extra:
group-params: true
extra-rules: true
exclusions:
generated: lax
paths:
- ^node_modules/
- ^\.venv/
- ^public/
- ^web_src/
- node_modules
- .venv
- public
- web_src
run:
timeout: 10m

View File

@@ -1,2 +1 @@
# SC2153: false-alert "Possible misspelling: TAGS may not be assigned. Did you mean tags?". We already use strict mode.
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2153,SC2154,SC2155,SC2164,SC2181,SC2207
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2154,SC2155,SC2164,SC2181,SC2207

View File

@@ -4,4 +4,9 @@ rules:
info-contact: off
oas2-api-host: off
oas2-parameter-description: off
oas2-schema: off
oas2-valid-schema-example: off
openapi-tags: off
operation-description: off
operation-singular-tag: off
operation-tag-defined: off

View File

@@ -1,26 +1,20 @@
- Never assume, verify before claiming
- List development targets with `make help`
- Before working on code, read the `docs` guides for every area touched: `guidelines-backend.md` for Go, `guidelines-frontend.md` for TS, CSS and Vue, both for templates, `testing.md` for tests, `development.md` for config options and migrations, `guidelines-refactoring.md` for refactors
- Before designing a feature or behavior, check prior art in GitHub and GitLab
- Don't add code for theoretical edge cases, only for ones that can realistically occur. Ask humans to make decisions if the edge case matters, comment the edge case for why it can be ignored if it doesn't matter
- Write docs concisely and user-facing, skip implementation details
- PR descriptions: minimal, only what and why, no task or file listings, no headings. Aim for less than 1000 characters. References go at the end of the description like `Fixes: <url>`.
- PR descriptions and update comments: Include screenshots for UI changes, before and after side by side. Take them at 4x scale in dark theme, cropped to the changes with some padding, and embed them at half CSS size
- Reference issues and PRs by full URL, not by number
- Use Conventional Commits for commit messages and PR titles, plus Gitea's `enhance` type for user-facing enhancements
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages and PR descriptions, never `Co-Authored-By` or `Signed-off-by`
- Attribute agent authorship on one trailing line in issue and PR comments, never as a PR description section
- Never rewrite git history unless asked, update PRs with new commits and normal push
- Comments: write almost none, short and preferably same-line, explaining why for a future reader. Never narrate code, the change or the prompt. Preserve existing ones that still apply. If you need to write a paragraph-long comment, rethink your implementation, it is likely too complicated
- Add the current year to copyright headers of new `.go` files
- In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically
- In TS, use `!` instead of `?.`/`??` when a value always exists
- In Go, prefer to use modern language features wherever possible
- In Go, function-name prefixes in errors like `fmt.Errorf("Foo: %w", err)` must always name the function they are in
- Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10`
- Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important`
- Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates`
- Fix the cause rather than disabling a linter or weakening a test. Where unavoidable, use the narrowest scope with a trailing comment giving the reason
- Run single tests with `go test -run '^TestName$' ./modulepath/` for Go, `pnpm exec vitest <path-filter>` for TS and `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e` for e2e
- Write the fewest, fastest tests covering the behavior, extending an existing one where possible. Prefer unit tests where logic is testable in isolation
- Aim for sub-2s per integration test and sub-4s per e2e test. Wait on a deterministic condition rather than `sleep`, and prefer semantic locators in e2e tests
- Use `make help` to find available development targets
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
- Run `make lint-js` to lint `.ts` files
- Run `make tidy` after any `go.mod` changes
- Run single go tests with `go test -run '^TestName$' ./modulepath/`
- Run single js test files with `pnpm exec vitest <path-filter>`
- Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e`
- Add the current year into the copyright header of new `.go` files
- Ensure no trailing whitespace in edited files
- Use Conventional Commits for commit messages and PR titles, e.g. `type(scope): subject`; `!` before the colon if breaking. Use `test` type for test-only changes.
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
- Preserve existing code comments, do not remove or rewrite comments that are still relevant
- Keep comments short, prefer same-line, explain why, never narrate code
- Prefer unit tests over integration tests when logic is testable in isolation
- Aim for sub-2s local runtime for integration and e2e tests
- In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist
- For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important`
- Include authorship attribution in issue and pull request comments
- Always add `Assisted-By` trailers to commit messages in format `Assisted-by: AGENT_NAME:MODEL_VERSION`
- Never add `Co-Authored-By` `Signed-off-by` trailer to commit messages. Sign off must be done by a human.

5223
CHANGELOG-archived.md Normal file

File diff suppressed because it is too large Load Diff

6274
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -58,8 +58,7 @@ Contributions made with the assistance of AI tools are welcome, but contributors
2. Manually test the changes and add appropriate automated tests where feasible.
3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review.
4. Disclose AI-assisted content clearly.
5. Do not rely solely on AI to generate replies or engage in discussions.
Ensure your responses are concise, accurate, and thoughtful, avoiding generic or low-quality AI-generated content.
5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model.
6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit.
Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves.
@@ -112,7 +111,7 @@ If further discussion is needed, we encourage you to open a new issue instead an
## Building Gitea
See [docs/build-setup.md](docs/build-setup.md) for prerequisites and [docs/development.md](docs/development.md)
See [docs/setup.md](docs/setup.md) for prerequisites and [docs/development.md](docs/development.md)
for building Gitea and the development workflow.
## Styleguide
@@ -170,13 +169,13 @@ In the PR title, describe the problem you are fixing, not how you are fixing it.
Use the first comment as a summary of your PR. \
In the PR summary, you can describe exactly how you are fixing this problem.
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message and release notes entry:
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message:
```text
type(scope)!: subject
```
The scope in parentheses is optional. A `!` immediately before the colon marks a [breaking change](https://www.conventionalcommits.org/en/v1.0.0/#summary): either `type!:` or `type(scope)!:` (not `type!(scope):`). Do not mark `build`, `chore`, `ci`, `docs`, `style`, or `test` as breaking.
The scope in parentheses is optional. A `!` immediately before the colon marks a [breaking change](https://www.conventionalcommits.org/en/v1.0.0/#summary): either `type!:` or `type(scope)!:` (not `type!(scope):`).
Use one of these types:

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS frontend-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
COPY --exclude=.git/ . .
RUN make frontend
# Cross-compile backend on the native platform to avoid slow QEMU emulation
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
@@ -28,15 +28,11 @@ RUN go mod download
# ".git" directory is mounted separately later only for version data extraction.
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=bind,source=".git/",target=".git/" \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
make backend
COPY docker/root /tmp/local
@@ -52,7 +48,7 @@ FROM docker.io/library/alpine:3.24 AS gitea
EXPOSE 22 3000
RUN apk --no-cache upgrade && apk --no-cache add \
RUN apk --no-cache add \
bash \
ca-certificates \
curl \

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS frontend-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
COPY --exclude=.git/ . .
RUN make frontend
# Cross-compile backend on the native platform
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
@@ -27,15 +27,11 @@ RUN go mod download
# See the comments in Dockerfile
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=bind,source=".git/",target=".git/" \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
make backend
COPY docker/rootless /tmp/local
@@ -47,7 +43,7 @@ FROM docker.io/library/alpine:3.24 AS gitea-rootless
EXPOSE 2222 3000
RUN apk --no-cache upgrade && apk --no-cache add \
RUN apk --no-cache add \
bash \
ca-certificates \
dumb-init \

View File

@@ -41,6 +41,7 @@ Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
Leon Hofmeister <dev.lh@web.de> (@delvh)
Wim <wim@42.be> (@42wim)
Jason Song <i@wolfogre.com> (@wolfogre)
Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
Yu Tian <zettat123@gmail.com> (@Zettat123)
Dong Ge <gedong_1994@163.com> (@sillyguodong)
Xinyi Gong <hestergong@gmail.com> (@HesterG)
@@ -64,6 +65,3 @@ Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
Nicolas <bircni@icloud.com> (@bircni)
Minjie Fang <wingsallen@gmail.com> (@wingsallen)
Shudhanshu Singh <sudhanshuwriterblc@gmail.com> (@SudhanshuMatrix)
Shivansh Garg <shivanshgarg587@gmail.com> (@SHIVANSHGARG07)

224
Makefile
View File

@@ -1,23 +1,29 @@
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
HASH := \#
# By default use go's 1.25 experimental json v2 library when building
# TODO: remove when no longer experimental
export GOEXPERIMENT ?= jsonv2
GO ?= go
SHASUM ?= shasum -a 256
COMMA := ,
AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.7 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.9.0 # renovate: datasource=go
XGO_VERSION := go-1.26.x
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
CONTAINER_RUNTIME ?= $(shell hash docker >/dev/null 2>&1 && echo docker || echo podman)
PLAYWRIGHT_BROWSERS ?= chromium firefox webkit
PLAYWRIGHT_FLAGS ?=
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
ifeq ($(HAS_GO), yes)
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
@@ -36,13 +42,18 @@ endif
TAGS ?=
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
CGO_TAGS := sqlite_mattn pam
CGO_ENABLED ?= 0
ifneq ($(strip $(filter $(CGO_TAGS),$(TAGS))),)
ifneq (,$(findstring sqlite_mattn,$(TAGS))$(findstring pam,$(TAGS)))
CGO_ENABLED = 1
endif
STATIC ?=
EXTLDFLAGS ?=
ifneq ($(STATIC),)
EXTLDFLAGS = -extldflags "-static"
endif
ifeq ($(GOOS),windows)
IS_WINDOWS := yes
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
@@ -51,13 +62,14 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
endif
endif
# GOFLAGS and EXTRA_GOFLAGS are for the 'go build' command only
ifeq ($(IS_WINDOWS),yes)
GOFLAGS := -v -buildmode=exe
EXECUTABLE ?= gitea.exe
else
GOFLAGS := -v
EXECUTABLE ?= gitea
endif
# EXTRA_GOFLAGS is for the 'go build' command only
EXTRA_GOFLAGS ?=
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
@@ -68,27 +80,20 @@ endif
# GOTEST_FLAGS is for unit test and integration test
GOTEST_FLAGS ?= -timeout 40m
# disable race detection for the packages that slow down the tests
GOTEST_FLAGS := -gcflags=modernc.org/sqlite/lib=-race=false $(GOTEST_FLAGS)
STORED_VERSION_FILE := VERSION
GITHUB_REF_TYPE ?= branch
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
# VERSION: the name for the build and filenames, e.g.: "1.2.3" for tag "v1.2.3", "main-nightly" for branch "main"
# GITEA_VERSION: the Gitea's internal version for display, e.g. "1.28.0+dev-356-ge47d0b66ea"
ifeq ($(GITHUB_REF_TYPE),tag)
# convert tag "v1.2.3" to "1.2.3"
ifneq ($(GITHUB_REF_TYPE),branch)
VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
GITEA_VERSION ?= $(VERSION)
else ifeq ($(GITHUB_REF_TYPE),branch)
else
ifneq ($(GITHUB_REF_NAME),)
# convert branch "release/v1.2" to "1.2-nightly"
VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
else
# no branch name info, use git ref name "HEAD" instead
VERSION ?= HEAD
VERSION ?= main
endif
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
@@ -97,15 +102,19 @@ else ifeq ($(GITHUB_REF_TYPE),branch)
else
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif
else
$(error unsupported ref type $(GITHUB_REF_TYPE))
endif
# if version = "main" then update version to "nightly"
ifeq ($(VERSION),main)
VERSION := main-nightly
endif
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
RELEASE_ENV = GO="$(GO)" TAGS="$(TAGS)" LDFLAGS="$(LDFLAGS)" DIST="$(DIST)" VERSION="$(VERSION)"
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list gitea.dev/modelmigration/...) gitea.dev/tests/integration/migration-test gitea.dev/tests gitea.dev/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list gitea.dev/modelmigration/...)
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list gitea.dev/models/migrations/...) gitea.dev/tests/integration/migration-test gitea.dev/tests gitea.dev/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list gitea.dev/models/migrations/...)
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
@@ -126,10 +135,12 @@ GO_LICENSE_FILE := assets/go-licenses.json
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
GO_DIRS := build cmd modelmigration models modules routers services tests tools
GO_DIRS := build cmd models modules routers services tests tools
WEB_DIRS := web_src/js web_src/css
ESLINT_FILES := web_src/js tools *.ts tests/e2e
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json
GO_SOURCES := $(wildcard *.go)
@@ -137,12 +148,11 @@ GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
GO_SOURCES += $(GENERATED_GO_DEST)
ESLINT_CONCURRENCY ?= 2
ESLINT_ARGS := --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY)
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
SWAGGER_EXCLUDE := gitea.dev/sdk
SWAGGER_SPEC_INPUT := templates/swagger/v1-input.json
SWAGGER_SPEC := templates/swagger/v1-swagger.generated.json
OPENAPI3_SPEC := templates/swagger/v1-openapi3.generated.json
OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl
TEST_MYSQL_HOST ?= mysql:3306
TEST_MYSQL_DBNAME ?= testgitea
@@ -201,7 +211,7 @@ fmt: ## format the Go and template code
.PHONY: fmt-check
fmt-check: fmt
@diff=$$(git diff --color=always $(GO_SOURCES) templates); \
@diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
printf "%s" "$${diff}"; \
@@ -221,9 +231,7 @@ endif
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
@output="$$($(GO) run $(SWAGGER_PACKAGE) generate spec --enable-allof-compounding --skip-enum-desc --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' 2>&1)" || { printf '%s\n' "$$output" >&2; exit 1; }; \
warnings="$$(printf '%s\n' "$$output" | grep -v '^go: ')"; \
if [ -n "$$warnings" ]; then printf '%s\n' "$$warnings" >&2; exit 1; fi
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
.PHONY: swagger-check
swagger-check: generate-swagger
@@ -236,10 +244,11 @@ swagger-check: generate-swagger
.PHONY: swagger-validate
swagger-validate: ## check if the swagger spec is valid
@# ensure no warnings
@output="$$($(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' 2>&1)"; status=$$?; \
printf '%s\n' "$$output" | grep -v '^go: '; \
case "$$output" in *WARNING:*) exit 1;; esac; exit $$status
@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
@# FIXME: there are some warnings
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
.PHONY: generate-openapi3
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
@@ -266,10 +275,10 @@ checks-frontend: lockfile-check svg-check ## check frontend files
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
.PHONY: lint
lint: lint-frontend lint-backend lint-templates lint-swagger lint-md lint-actions lint-json lint-yaml lint-shell ## lint everything
lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml lint-shell ## lint everything
.PHONY: lint-fix
lint-fix: lint-frontend-fix lint-backend-fix ## lint everything and fix issues
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
.PHONY: lint-frontend
lint-frontend: lint-js lint-css ## lint frontend files
@@ -285,12 +294,12 @@ lint-backend-fix: lint-go-fix lint-editorconfig ## lint backend files and fix is
.PHONY: lint-js
lint-js: node_modules ## lint js and ts files
pnpm exec eslint $(ESLINT_ARGS) $(ESLINT_FILES)
pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES)
pnpm exec vue-tsc
.PHONY: lint-js-fix
lint-js-fix: node_modules ## lint js and ts files and fix issues
pnpm exec eslint $(ESLINT_ARGS) $(ESLINT_FILES) --fix
pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) --fix
pnpm exec vue-tsc
.PHONY: lint-css
@@ -303,7 +312,7 @@ lint-css-fix: node_modules ## lint css files and fix issues
.PHONY: lint-swagger
lint-swagger: node_modules ## lint swagger files
pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) $(OPENAPI3_SPEC)
pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC)
.PHONY: lint-md
lint-md: node_modules ## lint markdown files
@@ -313,6 +322,14 @@ lint-md: node_modules ## lint markdown files
lint-md-fix: node_modules ## lint markdown files and fix issues
pnpm exec markdownlint --fix *.md
.PHONY: lint-spell
lint-spell: ## lint spelling
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error
.PHONY: lint-spell-fix
lint-spell-fix: ## lint spelling and fix issues
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -w
.PHONY: lint-go
lint-go: ## lint go files
GO=$(GO) GOLANGCI_LINT_PACKAGE=$(GOLANGCI_LINT_PACKAGE) $(GO) run ./tools/lint-go-all.go
@@ -329,7 +346,7 @@ lint-editorconfig:
.PHONY: lint-actions
lint-actions: .venv ## lint action workflow files
@$(GO) run $(ACTIONLINT_PACKAGE)
@uv run --frozen zizmor --quiet --persona=pedantic --min-confidence=medium .github
@uv run --frozen zizmor --quiet --min-confidence=medium .github
.PHONY: lint-shell
lint-shell: ## lint shell scripts
@@ -346,11 +363,11 @@ lint-yaml: .venv ## lint yaml files
.PHONY: lint-json
lint-json: node_modules ## lint json files
pnpm exec eslint -c eslint.json.config.ts $(ESLINT_ARGS)
pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY)
.PHONY: lint-json-fix
lint-json-fix: node_modules ## lint and fix json files
pnpm exec eslint -c eslint.json.config.ts $(ESLINT_ARGS) --fix
pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) --fix
.PHONY: watch
watch: ## watch everything and continuously rebuild
@@ -370,7 +387,7 @@ test-backend: ## test backend files
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES)
.PHONY: test-frontend
test-frontend: playwright ## test frontend files
test-frontend: node_modules ## test frontend files
pnpm exec vitest
.PHONY: test-check
@@ -385,11 +402,10 @@ test-check:
exit 1; \
fi
TEST_BACKEND_GOALS := $(filter test-backend$(HASH)%,$(MAKECMDGOALS))
.PHONY: $(TEST_BACKEND_GOALS)
$(TEST_BACKEND_GOALS): test-backend$(HASH)%:
.PHONY: test-backend\#%
test-backend\#%:
@echo "Running go test with -tags '$(TAGS)'..."
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run '$*' $(GO_TEST_PACKAGES)
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
.PHONY: coverage
coverage:
@@ -405,12 +421,7 @@ unit-test-coverage:
.PHONY: tidy
tidy: ## run go mod tidy
$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
$(eval GO_TOOLCHAIN := $(shell grep -Eo '^toolchain\s+go[0-9.]+' go.mod | cut -d' ' -f2))
$(GO) mod tidy -compat=$(MIN_GO_VERSION)
@# workaround https://github.com/golang/go/issues/75331: restore toolchain if tidy dropped it
@if [ -n "$(GO_TOOLCHAIN)" ] && ! grep -qE '^toolchain\s' go.mod; then \
$(GO) mod edit -toolchain=$(GO_TOOLCHAIN); \
fi
@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
vendor: go.mod go.sum
@@ -433,7 +444,7 @@ $(GO_LICENSE_FILE): go.mod go.sum
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
.PHONY: test-integration
test-integration: $(EXECUTABLE)
test-integration:
@# Use a compiled binary: testlogger forwards gitea logs to t.Log, so `go test -v`
@# would flood output per passing test. testcache can't help these tests anyway —
@# they mutate the work directory, so cache inputs change between runs.
@@ -444,10 +455,9 @@ test-integration: $(EXECUTABLE)
test-integration-compile:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null gitea.dev/tests/integration
TEST_INTEGRATION_GOALS := $(filter test-integration$(HASH)%,$(MAKECMDGOALS))
.PHONY: $(TEST_INTEGRATION_GOALS)
$(TEST_INTEGRATION_GOALS): test-integration$(HASH)%: $(EXECUTABLE)
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run '$*' gitea.dev/tests/integration
.PHONY: test-integration\#%
test-integration\#%:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run $(subst .,/,$*) gitea.dev/tests/integration
.PHONY: test-migration
test-migration: migrations.integration.test migrations.individual.test
@@ -463,15 +473,15 @@ migrations.individual.test:
.PHONY: migrations.individual.test\#%
migrations.individual.test\#%:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' gitea.dev/modelmigration/$*
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' gitea.dev/models/migrations/$*
.PHONY: playwright
playwright: deps-frontend
@./tools/playwright.sh $(PLAYWRIGHT_FLAGS) $(PLAYWRIGHT_BROWSERS)
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/test-e2e.sh install
.PHONY: test-e2e
test-e2e: playwright frontend backend
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS)
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS)
.PHONY: build
build: frontend backend ## build everything
@@ -496,38 +506,38 @@ generate-go: $(TAGS_PREREQ)
.PHONY: security-check
security-check:
go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build -v $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
$(error pam support set via TAGS does not support static builds)
endif
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
.PHONY: release
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
$(DIST_DIRS):
mkdir -p $(DIST_DIRS)
# Release builds always use Go's native cross compilation. To cross-compile with CGO,
# use "build" target with proper TAGS/LDFLAGS/CGO_CFLAGS to make "$(EXECUTABLE)" target run the "go build" command.
.PHONY: release
release: frontend release-binaries release-copy release-compress vendor release-sources release-check
.PHONY: release-binaries
release-binaries: | $(DIST_DIRS)
@$(RELEASE_ENV) ./tools/build-release.sh
.PHONY: release-windows
release-windows: | $(DIST_DIRS)
@$(RELEASE_ENV) ./tools/build-release.sh windows
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq (,$(findstring gogit,$(TAGS)))
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
endif
.PHONY: release-linux
release-linux: | $(DIST_DIRS)
@$(RELEASE_ENV) ./tools/build-release.sh linux
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
.PHONY: release-darwin
release-darwin: | $(DIST_DIRS)
@$(RELEASE_ENV) ./tools/build-release.sh darwin
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
.PHONY: release-freebsd
release-freebsd: | $(DIST_DIRS)
@$(RELEASE_ENV) ./tools/build-release.sh freebsd
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
.PHONY: release-copy
release-copy: | $(DIST_DIRS)
@@ -543,11 +553,11 @@ release-compress: | $(DIST_DIRS)
.PHONY: release-sources
release-sources: | $(DIST_DIRS)
echo $(GITEA_VERSION) > $(STORED_VERSION_FILE)
echo $(VERSION) > $(STORED_VERSION_FILE)
# bsdtar needs a ^ to prevent matching subdirectories
$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '|^./|gitea-src-$(VERSION)/|'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
rm -f $(STORED_VERSION_FILE)
@@ -565,14 +575,16 @@ deps-backend: ## install backend dependencies
$(GO) mod download
.PHONY: deps-tools
deps-tools: ## fetch and compile tool dependencies
$(GO) run -exec true $(AIR_PACKAGE) & \
$(GO) run -exec true $(EDITORCONFIG_CHECKER_PACKAGE) & \
$(GO) run -exec true $(GOLANGCI_LINT_PACKAGE) & \
$(GO) run -exec true $(GXZ_PACKAGE) & \
$(GO) run -exec true $(SWAGGER_PACKAGE) & \
$(GO) run -exec true $(GOVULNCHECK_PACKAGE) & \
$(GO) run -exec true $(ACTIONLINT_PACKAGE) & \
deps-tools: ## install tool dependencies
$(GO) install $(AIR_PACKAGE) & \
$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
$(GO) install $(GXZ_PACKAGE) & \
$(GO) install $(MISSPELL_PACKAGE) & \
$(GO) install $(SWAGGER_PACKAGE) & \
$(GO) install $(XGO_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
wait
node_modules: pnpm-lock.yaml
@@ -583,6 +595,28 @@ node_modules: pnpm-lock.yaml
uv sync
@touch .venv
.PHONY: update
update: update-go update-js update-py ## update dependencies
.PHONY: update-go
update-go: ## update go dependencies
$(GO) get -u ./...
$(MAKE) tidy
.PHONY: update-js
update-js: node_modules ## update js dependencies
pnpm exec updates -u -f package.json
rm -rf node_modules pnpm-lock.yaml
pnpm install
@touch node_modules
.PHONY: update-py
update-py: node_modules ## update py dependencies
pnpm exec updates -u -f pyproject.toml
rm -rf .venv uv.lock
uv sync
@touch .venv
.PHONY: vite
vite: $(FRONTEND_DEST) ## build vite files
@@ -623,10 +657,6 @@ lockfile-check:
generate-gitignore: ## update gitignore files
$(GO) run build/generate-gitignores.go
.PHONY: generate-emoji
generate-emoji: ## update emoji data from Unicode
$(GO) run build/generate-emoji.go
.PHONY: generate-images
generate-images: | node_modules ## generate images
cd tools && node generate-images.ts $(TAGS)

View File

@@ -2,6 +2,7 @@
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/gitea.dev)](https://goreportcard.com/report/gitea.dev "Go Report Card")
[![](https://pkg.go.dev/badge/gitea.dev?status.svg)](https://pkg.go.dev/gitea.dev "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
@@ -62,13 +63,13 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language, ask one of the managers in the Crowdin project to add a new language there.
You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord.
You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty, but we hope to fill it as questions pop up.
Get more information from [the translation section of our contributing guide](CONTRIBUTING.md#translation).
Get more information from [documentation](https://docs.gitea.com/contributing/localization).
## Official and Third-Party Projects
We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [gt](https://gitea.com/gitea/cli) and an [action runner](https://gitea.com/gitea/runner) for Gitea Action.
We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action.
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more.
@@ -115,12 +116,12 @@ Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea"
For dynamic config options, you can change it on your admin panel's configuration section.
For static config options, you can edit your `app.ini` file and restart the instance.
For static config options, you can edit your `app.ini` file and resart the instance.
See [app.example.ini](https://github.com/go-gitea/gitea/blob/main/custom/conf/app.example.ini) or [configuration documentation](https://docs.gitea.com/administration/config-cheat-sheet) for more details.
**Where can I find the security patches?**
Check the [release notes](https://github.com/go-gitea/gitea/releases) and [security advisories](https://github.com/go-gitea/gitea/security/advisories) for security patches.
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
(more FAQs are listed in [FAQ documentation](https://docs.gitea.com/help/faq))

View File

@@ -2,6 +2,7 @@
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/gitea.dev)](https://goreportcard.com/report/gitea.dev "Go Report Card")
[![](https://pkg.go.dev/badge/gitea.dev?status.svg)](https://pkg.go.dev/gitea.dev "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
@@ -72,11 +73,11 @@
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
更多信息请参阅 [文件](https://docs.gitea.com/zh-cn/contributing/localization)。
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
## 官方和第三方项目
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [gt](https://gitea.com/gitea/cli) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/runner)。
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
@@ -125,7 +126,7 @@ Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea
**在哪里可以找到安全补丁?**
在 [发布日志](https://github.com/go-gitea/gitea/releases) 中,搜索关键词 `SECURITY` 以找到安全补丁。
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
## 许可证

View File

@@ -2,6 +2,7 @@
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/gitea.dev)](https://goreportcard.com/report/gitea.dev "Go Report Card")
[![](https://pkg.go.dev/badge/gitea.dev?status.svg)](https://pkg.go.dev/gitea.dev "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
@@ -72,11 +73,11 @@
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
更多信息請參閱 [文件](https://docs.gitea.com/zh-tw/contributing/localization)。
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
## 官方和第三方項目
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [gt](https://gitea.com/gitea/cli) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/runner)。
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
@@ -125,7 +126,7 @@ Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea
**在哪裡可以找到安全補丁?**
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
## 許可證

View File

@@ -6,46 +6,20 @@ If you discover a security issue, please bring it to their attention right away!
Previous vulnerabilities are listed at https://about.gitea.com/security.
## Supported Versions
Security fixes follow our [support policy](docs/release-management.md#end-of-life-eol): only the most recent major release and the `main` branch receive them.
Reports must be filed against a supported version. Before reporting, verify that the issue reproduces on a supported version; any report that only affects an unsupported version will be closed as out of scope.
## Reporting a Vulnerability
Please **DO NOT** file a public issue. Instead, report the vulnerability privately by opening a [GitHub security advisory](https://github.com/go-gitea/gitea/security/advisories/new), or optionally by sending your report to `security@gitea.io`. Former is preferred if given a choice.
If reporting via GitHub, open an advisory per found issue. You can group the found issues if they relate to a single area (same feature, same fault present in multiple routes) but do not group unrelated reports into one as it makes it much harder to assess and track for duplicates.
Do not use LLM to write the reports. It's meant for people to read so take your time and use your own words to write it. If you have used LLM to find the bug, be transparent about it and verify that it is not hallucination before submitting the report.
If your report turns out to be a duplicate, you will be credited in the original report.
### Report requirements
When filing the report please document it. Reports must include:
- Version or a range of versions it was successfully tested against. If reproducible against the `main` branch, a commit hash is also acceptable. Ambiguous versions like `latest` are not acceptable.
- Settings used, especially when they diverge from the defaults
- Reproduction steps from freshly installed instance. If the fault is only reproducible through upgrade from older version, reproduction should state that.
Take your time to format it properly as it'll be much easier to read. Prefer a concise writing style.
When submitting an advisory, it should have the `gitea.dev` go package as package `name.cd`
### Triage process
Github does not have any search or tagging in GHSA so in most cases your report will leave triage state when we have a fix for it in works for next release.
This helps us keep track of items fixed and pending release and separate them from things that we either haven't looked at yet or have not fixed.
Please **DO NOT** file a public issue, instead send your report privately to `security@gitea.io`.
## Protecting Security Information
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
The PGP key is valid until July 23, 2027.
The PGP key is valid until July 4, 2026.
```
Key ID: 6FCD2D5B
Key Type: RSA
Expires: 7/23/2027
Expires: 7/4/2026
Key Size: 4096/4096
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
```
@@ -68,18 +42,18 @@ lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
LVsFAmpi3IkFCQmOqAsACgkQqv0jgW/NLVsgyxAAuTyj4+PImOFr7ZkEJcseWUy+
gWQrJDagj+Ks2SKGneVroJ63q5Vngx44b7JNabauKel4IJJCScLdwA7k0DKFHPOM
rUS6aavW5+961CMQxJ13vWz7qxLxhI4FXle255xFSXgOo9T1W7+wtm/wtze8+sws
8XKAVBZGuVI1/KzVpE6bWxpIIYK79DC07NWGDAZzMbuO92TFHBRblXsicPWExBjC
/oFmT70YMcA0wOwU/YKYMg2eK2XYUr5+uYX6FnP7iUhDj1vZmwkF33Cj9v9D3Hv9
NzAquj97uLht4wL2sqsIgZd4Vc37uipPDFaddzTb/lon7lPbJPMLObcd4HjSTkIo
nEMM0wdOQZU7mIIgRM9o3mr/gyVigl3uARmUKaP9PhBaOGTjkni0nrv90hSHBHeu
ayM76vl2tzAB2KpDxHIHbI2SEdytV28l/rxWrsbk2kG7s7QB6VKiThTatYevh1kg
6Ujgl2xNSntG6t6PWjQ65OMSq5Iiwe7RfOZkNmPRGP/rbzMbgWXB3iCR2gVQvsxA
ctYdDZdUYAeQ5g6xSKJBloygDQdLzk9m0hqNUyl6gxhRNRw69hqp9E4mxFF3L1Gi
Z2LspLX1ZZWMh0bp0AniWzZuzLuNI0KYhlok6fnshjidJ1qYM/lbz5eJTYWxoJZW
rczn8gB3+MZbULc/iiO5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
+ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
@@ -91,20 +65,21 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
WwUCamLcngUJCY6oIAAKCRCq/SOBb80tWxilD/41xF1pwD2lGqjWLmlXcd0Ok5xm
67TSGiKiFZUgI3ftkRVlqmeJqjVmRjpc2dLfCuVBzlaP0Oyv5Jn+2CPqgmWIk5tw
pqlxqmsXiIZ9Uv2wWItxrXIwQ6DHc1qcIOu7feTnt1HVhxp2GqlXl3tA/+MTTDmn
4rqrHffI7ps9eCbK/SfSuoQk3+x/x4GPqWDbsNw1E9dDjPPhsKfdwzZ6dFR2Yhzu
2U0GDc2aeBA0Gjy6VFBj1ksmGxQCL3TqtGEHOonoPpTIjmDd35Xq6BSu119onOQS
THZ7+3JQRpizBVrIklKf33UOikZy9WHuffFddP46m8qC2Alh1aB7xcD8K3P9gzd5
OJO9Zr8JR+rvi8eOsCHvJqGhe0JMMf9bgXKjQ1tMTDTe6r0Jnw7HbkDghjpj9xG7
GYmDB0CL4kP5nAZ1OPfIasIu3Llhcy6mS52ECw0X6PNwUINwBn7F6EKAiciw8/OS
CXoGJDI5alwNhh+3DL4oBH1aq1lnmxmhFNpwpRDvtbRGmZeeASTeC4LMAtqP85Er
8W9ulIgQ4Ub8QuoxvE5hzWEB1QUkJgozI8vO4DoGlrsz7P8cPzrOT7WqUfUPTYE6
oZEfEYDSj1IZHnPYPS+yLBJMJ/Lc1h8OCetmbJZx4QWaX3Fgar7Zks8FfJQOTN6i
HnOlrsaMDYNyO76uBQ==
=4kVJ
WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
9M2VbqL9C51z/wyHLg==
=SfZA
-----END PGP PUBLIC KEY BLOCK-----
```
Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.

View File

@@ -436,7 +436,6 @@
"jsonl",
"mcmeta",
"sarif",
"slnlaunch",
"tact",
"tfstate",
"topojson",
@@ -453,8 +452,6 @@
".htmlhintrc",
".imgbotconfig",
".nycrc",
".releaserc",
".secrets.baseline",
".tern-config",
".tern-project",
".watchmanconfig",
@@ -694,17 +691,10 @@
"extensions": [
"ini",
"cnf",
"container",
"dof",
"lektorproject",
"mount",
"network",
"prefs",
"properties",
"service",
"socket",
"target",
"timer",
"url",
"conf"
],
@@ -712,7 +702,6 @@
".buckconfig",
".coveragerc",
".flake8",
".gitreview",
".pylintrc",
"HOSTS",
"buildozer.spec",
@@ -787,12 +776,9 @@
"extensions": [
"r",
"rd",
"rhistory",
"rsx"
],
"filenames": [
".Rapp.history",
".Rhistory",
".Rprofile",
"expr-dist"
]
@@ -912,7 +898,6 @@
"bats",
"command",
"ksh",
"pacscript",
"sbatch",
"slurm",
"tmux",
@@ -1176,7 +1161,6 @@
"kml",
"launch",
"mdpolicy",
"meta4",
"mjml",
"mod",
"mojo",
@@ -1197,7 +1181,6 @@
"pt",
"pubxml",
"qhelp",
"rbxmx",
"rdf",
"res",
"resx",
@@ -1284,9 +1267,7 @@
".clang-format",
".clang-tidy",
".clangd",
".eslintrc",
".gemrc",
".releaserc",
"CITATION.cff",
"glide.lock",
"pixi.lock",

11483
assets/emoji.json generated Normal file

File diff suppressed because it is too large Load Diff

307
assets/go-licenses.json generated

File diff suppressed because one or more lines are too long

21
assets/misspellings.csv Normal file
View File

@@ -0,0 +1,21 @@
acounts,accounts
canidate,candidate
comfirm,confirm
converage,coverage
currrently,currently
delimeter,delimiter
differrent,different
exclusing,excluding
finshed,finished
formated,formatted
inderect,indirect
insuficient,insufficient
likly,likely
mergable,mergeable
overrided,overridden
priortized,prioritized
registeration,registration
reuqest,request
reviwer,reviewer
superceded,superseded
underlaying,underlying
1 acounts accounts
2 canidate candidate
3 comfirm confirm
4 converage coverage
5 currrently currently
6 delimeter delimiter
7 differrent different
8 exclusing excluding
9 finshed finished
10 formated formatted
11 inderect indirect
12 insuficient insufficient
13 likly likely
14 mergable mergeable
15 overrided overridden
16 priortized prioritized
17 registeration registration
18 reuqest request
19 reviwer reviewer
20 superceded superseded
21 underlaying underlying

View File

@@ -7,129 +7,213 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"go/format"
"io"
"log"
"net/http"
"os"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"sync"
"unicode/utf8"
"gitea.dev/modules/json"
)
const (
emojiTestURL = "https://www.unicode.org/Public/17.0.0/emoji/emoji-test.txt"
jsonFile = "public/assets/emoji.json"
gemojiURL = "https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json"
maxUnicodeVersion = 16
)
type emoji struct {
Emoji string `json:"emoji"`
Aliases []string `json:"aliases"`
description string
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
// Gemoji is a set of emoji data.
type Gemoji []Emoji
// Emoji represents a single emoji and associated data.
type Emoji struct {
Emoji string `json:"emoji"`
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases"`
UnicodeVersion string `json:"unicode_version,omitempty"`
SkinTones bool `json:"skin_tones,omitempty"`
}
var slugRe = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`[^a-z0-9]+`)
})
// Don't include some fields in JSON
func (e Emoji) MarshalJSON() ([]byte, error) {
type emoji Emoji
x := emoji(e)
x.UnicodeVersion = ""
x.Description = ""
x.SkinTones = false
return json.Marshal(x)
}
func main() {
if err := generate(); err != nil {
log.Fatal(err)
}
}
flag.Parse()
func generate() error {
// the existing file is also the alias source, so existing aliases stay stable
existing, err := os.ReadFile(jsonFile)
// generate data
buf, err := generate()
if err != nil {
return err
}
var existingEmojis []*emoji
if err := json.Unmarshal(existing, &existingEmojis); err != nil {
return err
}
existingAliases := make(map[string][]string, len(existingEmojis))
for _, e := range existingEmojis {
existingAliases[e.Emoji] = e.Aliases
log.Fatalf("generate err: %v", err)
}
emojis, err := fetchEmojis(existingAliases)
// write
err = os.WriteFile(*flagOut, buf, 0o644)
if err != nil {
return err
log.Fatalf("WriteFile err: %v", err)
}
lines := make([]string, len(emojis))
for i, e := range emojis {
line, err := json.Marshal(e)
if err != nil {
return err
}
lines[i] = string(line)
}
return os.WriteFile(jsonFile, []byte("[\n"+strings.Join(lines, ",\n")+"\n]\n"), 0o644)
}
func isSkinTone(r rune) bool {
return r >= 0x1f3fb && r <= 0x1f3ff
}
var replacer = strings.NewReplacer(
"main.Gemoji", "Gemoji",
"main.Emoji", "\n",
"}}", "},\n}",
", Description:", ", ",
", Aliases:", ", ",
", UnicodeVersion:", ", ",
", SkinTones:", ", ",
)
func fetchEmojis(existingAliases map[string][]string) ([]*emoji, error) {
res, err := http.Get(emojiTestURL)
var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
func generate() ([]byte, error) {
// load gemoji data
res, err := http.Get(gemojiURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetching %s: %s", emojiTestURL, res.Status)
}
var emojis []*emoji
scanner := bufio.NewScanner(res.Body)
for scanner.Scan() {
// e.g. "1F44D ; fully-qualified # 👍 E0.6 thumbs up"
_, rest, _ := strings.Cut(scanner.Text(), ";")
status, comment, _ := strings.Cut(rest, "#")
if strings.TrimSpace(status) != "fully-qualified" {
continue
}
fields := strings.SplitN(strings.TrimSpace(comment), " ", 3)
if strings.ContainsFunc(fields[0], isSkinTone) {
continue
}
emojis = append(emojis, &emoji{Emoji: fields[0], description: fields[2]})
}
if err := scanner.Err(); err != nil {
// read all
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var errs []error
aliasOwners := map[string]string{}
for _, e := range emojis {
e.Aliases = existingAliases[e.Emoji]
if e.Aliases == nil {
e.Aliases = []string{strings.Trim(slugRe().ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
}
delete(existingAliases, e.Emoji)
for _, alias := range e.Aliases {
if owner, ok := aliasOwners[alias]; ok {
errs = append(errs, fmt.Errorf("alias %q used by both %q and %q", alias, owner, e.Emoji))
}
aliasOwners[alias] = e.Emoji
}
}
for code, aliases := range existingAliases {
errs = append(errs, fmt.Errorf("emoji %q with aliases %v is missing from Unicode data", code, aliases))
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
// unmarshal
var data Gemoji
err = json.Unmarshal(body, &data)
if err != nil {
return nil, err
}
slices.SortFunc(emojis, func(a, b *emoji) int {
return strings.Compare(a.Aliases[0], b.Aliases[0])
skinTones := make(map[string]string)
skinTones["\U0001f3fb"] = "Light Skin Tone"
skinTones["\U0001f3fc"] = "Medium-Light Skin Tone"
skinTones["\U0001f3fd"] = "Medium Skin Tone"
skinTones["\U0001f3fe"] = "Medium-Dark Skin Tone"
skinTones["\U0001f3ff"] = "Dark Skin Tone"
var tmp Gemoji
// filter out emoji that require greater than max unicode version
for i := range data {
val, _ := strconv.ParseFloat(data[i].UnicodeVersion, 64)
if int(val) <= maxUnicodeVersion {
tmp = append(tmp, data[i])
}
}
data = tmp
sort.Slice(data, func(i, j int) bool {
return data[i].Aliases[0] < data[j].Aliases[0]
})
return emojis, nil
aliasMap := make(map[string]int, len(data))
for i, e := range data {
if e.Emoji == "" || len(e.Aliases) == 0 {
continue
}
for _, a := range e.Aliases {
if a == "" {
continue
}
aliasMap[a] = i
}
}
// gitea customizations
i, ok := aliasMap["tada"]
if ok {
data[i].Aliases = append(data[i].Aliases, "hooray")
}
i, ok = aliasMap["laughing"]
if ok {
data[i].Aliases = append(data[i].Aliases, "laugh")
}
// write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet)
file, _ := json.MarshalIndent(data, "", " ")
_ = os.WriteFile("assets/emoji.json", append(file, '\n'), 0o644)
// Add skin tones to emoji that support it
var (
s []string
newEmoji string
newDescription string
newData Emoji
)
for i := range data {
if data[i].SkinTones {
for k, v := range skinTones {
s = strings.Split(data[i].Emoji, "")
if utf8.RuneCountInString(data[i].Emoji) == 1 {
s = append(s, k)
} else {
// insert into slice after first element because all emoji that support skin tones
// have that modifier placed at this spot
s = append(s, "")
copy(s[2:], s[1:])
s[1] = k
}
newEmoji = strings.Join(s, "")
newDescription = data[i].Description + ": " + v
newAlias := data[i].Aliases[0] + "_" + strings.ReplaceAll(v, " ", "_")
newData = Emoji{newEmoji, newDescription, []string{newAlias}, "12.0", false}
data = append(data, newData)
}
}
}
sort.Slice(data, func(i, j int) bool {
return data[i].Aliases[0] < data[j].Aliases[0]
})
// add header
str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data))
// change the format of the unicode string
str = emojiRE.ReplaceAllStringFunc(str, func(s string) string {
var err error
s, err = strconv.Unquote(s[len("{Emoji:"):])
if err != nil {
panic(err)
}
return "{" + strconv.QuoteToASCII(s)
})
// format
return format.Source([]byte(str))
}
const hdr = `
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package emoji
// Code generated by build/generate-emoji.go. DO NOT EDIT.
// Sourced from %s
var GemojiData = %#v
`

View File

@@ -15,24 +15,17 @@ import (
"slices"
"sort"
"strings"
"sync"
)
var globalVars = sync.OnceValue(func() (ret struct {
licenseRe *regexp.Regexp
primaryLicenseRe *regexp.Regexp
},
) {
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
ret.licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
ret.primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
return ret
})
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
// ignoredNames are LicenseEntry.Name values to exclude from the output.
var ignoredNames = map[string]bool{
@@ -151,7 +144,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
continue
}
name := entry.Name()
if !globalVars().licenseRe.MatchString(name) {
if !licenseRe.MatchString(name) {
continue
}
if excludedExt[strings.ToLower(filepath.Ext(name))] {
@@ -188,7 +181,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
var primary []LicenseEntry
for _, e := range entries {
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
if globalVars().primaryLicenseRe.MatchString(fileName) {
if primaryLicenseRe.MatchString(fileName) {
primary = append(primary, e)
}
}

View File

@@ -10,7 +10,7 @@
// cleaner SDK output with proper enum types instead of anonymous strings.
//
// Run: go run build/generate-openapi.go
// Output: templates/swagger/v1-openapi3.generated.json
// Output: templates/swagger/v1_openapi3_json.tmpl
//go:build ignore
@@ -21,21 +21,35 @@ import (
"fmt"
"log"
"os"
"regexp"
"sort"
"strings"
"gitea.dev/build/openapi3gen"
"github.com/getkin/kin-openapi/openapi3"
)
const (
swaggerSpecPath = "templates/swagger/v1-swagger.generated.json"
openapi3OutPath = "templates/swagger/v1-openapi3.generated.json"
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
appVerVar = "{{.SwaggerAppVer}}"
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
appVerPlaceholder = "0.0.0-gitea-placeholder"
)
var enumScanDirs = []string{
"modules/structs",
"modules/commitstatus",
}
var (
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
enumScanDirs = []string{
"modules/structs",
"modules/commitstatus",
}
)
func main() {
astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs)
@@ -54,17 +68,28 @@ func main() {
log.Fatalf("reading swagger spec: %v", err)
}
oas3, err := openapi3gen.Convert(data, astEnumMap)
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
if err != nil {
log.Fatalf("converting to openapi 3.0: %v", err)
}
oas3.Servers = openapi3.Servers{
{URL: appSubUrlPlaceholder + "/api/v1"},
}
out, err := json.MarshalIndent(oas3, "", " ")
if err != nil {
log.Fatalf("marshaling openapi 3.0: %v", err)
}
if err := os.WriteFile(openapi3OutPath, out, 0o644); err != nil {
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
result = strings.TrimSpace(result)
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
log.Fatalf("writing openapi 3.0 spec: %v", err)
}

View File

@@ -8,7 +8,6 @@ import (
"regexp"
"sort"
"strings"
"sync"
"gitea.dev/modules/json"
@@ -17,19 +16,11 @@ import (
"github.com/getkin/kin-openapi/openapi3"
)
var globalVars = sync.OnceValue(func() (ret struct {
rxDeprecated *regexp.Regexp
rxSwaggerEnum *regexp.Regexp
},
) {
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
ret.rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
ret.rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
return ret
})
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
// Gitea-specific post-processing: file-schema fixups, URI formats,
@@ -167,7 +158,7 @@ func addDeprecatedFlags(doc *openapi3.T) {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if globalVars().rxDeprecated.MatchString(propRef.Value.Description) {
if rxDeprecated.MatchString(propRef.Value.Description) {
propRef.Value.Deprecated = true
}
}

View File

@@ -42,19 +42,27 @@ func TestDeriveEnumName_miss(t *testing.T) {
func TestExtractSharedEnums_usesASTMap(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
Schemas: openapi3.Schemas{},
},
}
for _, name := range []string{"A", "B"} {
doc.Components.Schemas[name] = &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
Schemas: openapi3.Schemas{
"A": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
}},
},
}},
"B": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
}},
},
}},
},
}}
},
}
astMap := map[string][]string{EnumKey([]any{"red", "green", "blue"}): {"Color"}}
if err := extractSharedEnums(doc, astMap); err != nil {
@@ -182,19 +190,27 @@ func TestExtractEnumTypeName_rejectsIncidentalPrefix(t *testing.T) {
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
Schemas: openapi3.Schemas{},
},
}
for _, name := range []string{"A", "B"} {
doc.Components.Schemas[name] = &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
Schemas: openapi3.Schemas{
"A": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
}},
},
}},
"B": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
}},
},
}},
},
}}
},
}
if err := extractSharedEnums(doc, map[string][]string{}); err == nil {
t.Fatal("expected miss error")

View File

@@ -13,6 +13,7 @@ import (
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -30,6 +31,8 @@ func EnumKey(values []any) string {
return strings.Join(strs, "|")
}
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
// a canonical value-set key (see EnumKey) to the Go type names declared with
// // swagger:enum TypeName. Multiple type names per key are allowed (e.g.
@@ -143,7 +146,7 @@ func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes m
if doc == nil {
return nil
}
matches := globalVars().rxSwaggerEnum.FindStringSubmatch(doc.Text())
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
if len(matches) < 2 {
return nil
}

View File

@@ -33,17 +33,9 @@ func TestEnumKey_handlesNonStringValues(t *testing.T) {
}
}
func TestScanSwaggerEnumTypes(t *testing.T) {
scan := func(t *testing.T, filename, src string) (map[string][]string, error) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, filename), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
return ScanSwaggerEnumTypes([]string{dir})
}
t.Run("basic", func(t *testing.T) {
got, err := scan(t, "color.go", `package fixture
func TestScanSwaggerEnumTypes_basic(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// Color is a primary color.
// swagger:enum Color
@@ -54,18 +46,24 @@ const (
ColorGreen Color = "green"
ColorBlue Color = "blue"
)
`)
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"red", "green", "blue"})
if single(got, wantKey) != "Color" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
}
})
`
if err := os.WriteFile(filepath.Join(dir, "color.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
t.Run("orphanAnnotation", func(t *testing.T) {
_, err := scan(t, "typo.go", `package fixture
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"red", "green", "blue"})
if single(got, wantKey) != "Color" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
}
}
func TestScanSwaggerEnumTypes_orphanAnnotation(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Sttype
type StateType string
@@ -73,17 +71,23 @@ type StateType string
const (
StateOpen StateType = "open"
)
`)
if err == nil {
t.Fatal("expected error for annotation referencing a non-matching type name")
}
if !strings.Contains(err.Error(), "Sttype") {
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
}
})
`
if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
t.Run("collision", func(t *testing.T) {
got, err := scan(t, "dup.go", `package fixture
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected error for annotation referencing a non-matching type name")
}
if !strings.Contains(err.Error(), "Sttype") {
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
}
}
func TestScanSwaggerEnumTypes_collision(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Alpha
type Alpha string
@@ -98,90 +102,52 @@ const (
BetaX Beta = "x"
BetaY Beta = "y"
)
`)
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
key := EnumKey([]any{"x", "y"})
names := got[key]
if !slices.Equal(names, []string{"Alpha", "Beta"}) {
t.Fatalf("map[%q] = %v, want [Alpha Beta]", key, names)
}
})
`
if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
t.Run("parseFailure", func(t *testing.T) {
_, err := scan(t, "bad.go", "package fixture\nfunc Foo() {")
if err == nil {
t.Fatal("expected parse error, got nil")
}
})
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
key := EnumKey([]any{"x", "y"})
names := got[key]
if !slices.Equal(names, []string{"Alpha", "Beta"}) {
t.Fatalf("map[%q] = %v, want [Alpha Beta]", key, names)
}
}
t.Run("annotationWithoutConsts", func(t *testing.T) {
_, err := scan(t, "lonely.go", `package fixture
func TestScanSwaggerEnumTypes_parseFailure(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "bad.go"), []byte("package fixture\nfunc Foo() {"), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected parse error, got nil")
}
}
func TestScanSwaggerEnumTypes_annotationWithoutConsts(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Lonely
type Lonely string
`)
if err == nil {
t.Fatal("expected error for annotation without consts")
}
if !strings.Contains(err.Error(), "Lonely") {
t.Fatalf("error %q should mention Lonely", err.Error())
}
})
`
if err := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
t.Run("constsBeforeType", func(t *testing.T) {
got, err := scan(t, "shade.go", `package fixture
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
// swagger:enum Shade
type Shade string
`)
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"dark", "light"})
if single(got, wantKey) != "Shade" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
}
})
t.Run("groupedTypeDecl", func(t *testing.T) {
got, err := scan(t, "grouped.go", `package fixture
type (
// swagger:enum Color
Color string
// swagger:enum Shade
Shade string
)
const (
ColorRed Color = "red"
ColorBlue Color = "blue"
)
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
`)
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
colorKey := EnumKey([]any{"red", "blue"})
shadeKey := EnumKey([]any{"dark", "light"})
if single(got, colorKey) != "Color" {
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
}
if single(got, shadeKey) != "Shade" {
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
}
})
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected error for annotation without consts")
}
if !strings.Contains(err.Error(), "Lonely") {
t.Fatalf("error %q should mention Lonely", err.Error())
}
}
func TestScanSwaggerEnumTypes_constsAndTypeInDifferentFiles(t *testing.T) {
@@ -216,3 +182,68 @@ type Hue string
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue")
}
}
func TestScanSwaggerEnumTypes_constsBeforeType(t *testing.T) {
dir := t.TempDir()
src := `package fixture
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
// swagger:enum Shade
type Shade string
`
if err := os.WriteFile(filepath.Join(dir, "shade.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"dark", "light"})
if single(got, wantKey) != "Shade" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
}
}
func TestScanSwaggerEnumTypes_groupedTypeDecl(t *testing.T) {
dir := t.TempDir()
src := `package fixture
type (
// swagger:enum Color
Color string
// swagger:enum Shade
Shade string
)
const (
ColorRed Color = "red"
ColorBlue Color = "blue"
)
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
`
if err := os.WriteFile(filepath.Join(dir, "grouped.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
colorKey := EnumKey([]any{"red", "blue"})
shadeKey := EnumKey([]any{"dark", "light"})
if single(got, colorKey) != "Color" {
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
}
if single(got, shadeKey) != "Shade" {
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
}
}

View File

@@ -1,10 +0,0 @@
[git]
commit_parsers = [
{ message = "^(chore|ci)(\\([\\w/.-]+\\))?!?: ", skip = true },
{ message = "^feat", group = "Features" },
{ message = "^enhance", group = "Enhancements" },
{ message = "^perf", group = "Performance" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^docs", group = "Documentation" },
{ message = ".*", group = "Miscellaneous" },
]

View File

@@ -5,6 +5,7 @@ package cmd
import (
"context"
"fmt"
"gitea.dev/modules/private"
"gitea.dev/modules/setting"
@@ -48,6 +49,6 @@ func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
if extra.HasError() {
return handleCliResponseExtra(extra)
}
cprintln(c, respText.Text)
_, _ = fmt.Printf("%s\n", respText.Text)
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
repo_module "gitea.dev/modules/repository"
@@ -54,9 +55,6 @@ func newAuthCommand() *cli.Command {
return &cli.Command{
Name: "auth",
Usage: "Modify external auth providers",
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
return cliAuditContext(ctx), nil
},
Commands: []*cli.Command{
microcmdAuthAddOauth(),
microcmdAuthUpdateOauth(),
@@ -130,8 +128,8 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
}
log.Trace("Processing next %d repos of %d", len(repos), count)
for _, repo := range repos {
log.Trace("Synchronizing repo %s", repo.FullName())
gitRepo, err := git.OpenRepository(ctx, repo)
log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath())
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Warn("OpenRepository: %v", err)
continue

View File

@@ -10,7 +10,6 @@ import (
"gitea.dev/models/auth"
"gitea.dev/modules/util"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/auth/source/ldap"
"github.com/urfave/cli/v3"
@@ -49,11 +48,11 @@ func commonLdapCLIFlags() []cli.Flag {
},
&cli.StringFlag{
Name: "host",
Usage: "Comma-separated LDAP hosts or ldap://, ldaps:// or ldap+starttls:// URLs, tried in order.",
Usage: "The address where the LDAP server can be reached.",
},
&cli.IntFlag{
Name: "port",
Usage: "The port to use when connecting to hosts given without a URL.",
Usage: "The port to use when connecting to the LDAP server.",
},
&cli.StringFlag{
Name: "user-search-base",
@@ -222,8 +221,8 @@ func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
func newAuthService() *authService {
return &authService{
initDB: initDB,
createAuthSource: auth_service.CreateSource,
updateAuthSource: auth_service.UpdateSource,
createAuthSource: auth.CreateSource,
updateAuthSource: auth.UpdateSource,
getAuthSourceByID: auth.GetSourceByID,
}
}
@@ -355,10 +354,13 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
return 0, false
}
// getAuthSourceOfType gets the login source by id.
// It returns an error if the id does not match any source or if the source is not of the expected type.
func (a *authService) getAuthSourceOfType(ctx context.Context, id int64, authType auth.Type) (*auth.Source, error) {
authSource, err := a.getAuthSourceByID(ctx, id)
// getAuthSource gets the login source by its id defined in the command line flags.
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
if err := argsSet(c, "id"); err != nil {
return nil, err
}
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return nil, err
}
@@ -370,15 +372,6 @@ func (a *authService) getAuthSourceOfType(ctx context.Context, id int64, authTyp
return authSource, nil
}
// getAuthSource gets the login source by its id defined in the command line flags.
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
if err := argsSet(c, "id"); err != nil {
return nil, err
}
return a.getAuthSourceOfType(ctx, c.Int64("id"), authType)
}
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
@@ -388,17 +381,16 @@ func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
return err
}
ldapConfig := &ldap.Source{
Enabled: true, // always true
}
authSource := &auth.Source{
Type: auth.LDAP,
IsActive: true, // active by default
Cfg: ldapConfig,
Cfg: &ldap.Source{
Enabled: true, // always true
},
}
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, ldapConfig); err != nil {
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
@@ -415,10 +407,9 @@ func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) erro
if err != nil {
return err
}
ldapConfig := auth.MustSourceCfg[*ldap.Source](authSource)
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, ldapConfig); err != nil {
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
@@ -435,17 +426,16 @@ func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) err
return err
}
ldapConfig := &ldap.Source{
Enabled: true, // always true
}
authSource := &auth.Source{
Type: auth.DLDAP,
IsActive: true, // active by default
Cfg: ldapConfig,
Cfg: &ldap.Source{
Enabled: true, // always true
},
}
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, ldapConfig); err != nil {
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
@@ -462,10 +452,9 @@ func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command)
if err != nil {
return err
}
ldapConfig := auth.MustSourceCfg[*ldap.Source](authSource)
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, ldapConfig); err != nil {
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}

View File

@@ -489,20 +489,13 @@ func TestUpdateLdapBindDn(t *testing.T) {
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
// Test cases
type testCase struct {
cases := []struct {
args []string
id int64
existingAuthSource *auth.Source
authSource *auth.Source
errMsg string
}
withConfig := func(config *ldap.Source, args ...string) testCase {
return testCase{
args: append([]string{"ldap-test", "--id", "1"}, args...),
authSource: &auth.Source{Type: auth.LDAP, Cfg: config},
}
}
cases := []testCase{
}{
// case 0
{
args: []string{
@@ -581,7 +574,16 @@ func TestUpdateLdapBindDn(t *testing.T) {
},
},
// case 1
withConfig(&ldap.Source{}),
{
args: []string{
"ldap-test",
"--id", "1",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{},
},
},
// case 2
{
args: []string{
@@ -616,44 +618,215 @@ func TestUpdateLdapBindDn(t *testing.T) {
},
},
// case 4
withConfig(&ldap.Source{SecurityProtocol: ldap.SecurityProtocol(1)}, "--security-protocol", "LDAPS"),
{
args: []string{
"ldap-test",
"--id", "1",
"--security-protocol", "LDAPS",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
SecurityProtocol: ldap.SecurityProtocol(1),
},
},
},
// case 5
withConfig(&ldap.Source{SkipVerify: true}, "--skip-tls-verify"),
{
args: []string{
"ldap-test",
"--id", "1",
"--skip-tls-verify",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
SkipVerify: true,
},
},
},
// case 6
withConfig(&ldap.Source{Host: "ldap-server"}, "--host", "ldap-server"),
{
args: []string{
"ldap-test",
"--id", "1",
"--host", "ldap-server",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
Host: "ldap-server",
},
},
},
// case 7
withConfig(&ldap.Source{Port: 389}, "--port", "389"),
{
args: []string{
"ldap-test",
"--id", "1",
"--port", "389",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
Port: 389,
},
},
},
// case 8
withConfig(
&ldap.Source{UserBase: "ou=Users,dc=domain,dc=org"},
"--user-search-base", "ou=Users,dc=domain,dc=org",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--user-search-base", "ou=Users,dc=domain,dc=org",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
UserBase: "ou=Users,dc=domain,dc=org",
},
},
},
// case 9
withConfig(
&ldap.Source{Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)"},
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
},
},
},
// case 10
withConfig(
&ldap.Source{AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)"},
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
},
},
},
// case 11
withConfig(&ldap.Source{AttributeUsername: "uid"}, "--username-attribute", "uid"),
{
args: []string{
"ldap-test",
"--id", "1",
"--username-attribute", "uid",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AttributeUsername: "uid",
},
},
},
// case 12
withConfig(&ldap.Source{AttributeName: "givenName"}, "--firstname-attribute", "givenName"),
{
args: []string{
"ldap-test",
"--id", "1",
"--firstname-attribute", "givenName",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AttributeName: "givenName",
},
},
},
// case 13
withConfig(&ldap.Source{AttributeSurname: "sn"}, "--surname-attribute", "sn"),
{
args: []string{
"ldap-test",
"--id", "1",
"--surname-attribute", "sn",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AttributeSurname: "sn",
},
},
},
// case 14
withConfig(&ldap.Source{AttributeMail: "mail"}, "--email-attribute", "mail"),
{
args: []string{
"ldap-test",
"--id", "1",
"--email-attribute", "mail",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AttributeMail: "mail",
},
},
},
// case 15
withConfig(&ldap.Source{AttributesInBind: true}, "--attributes-in-bind"),
{
args: []string{
"ldap-test",
"--id", "1",
"--attributes-in-bind",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AttributesInBind: true,
},
},
},
// case 16
withConfig(&ldap.Source{AttributeSSHPublicKey: "publickey"}, "--public-ssh-key-attribute", "publickey"),
{
args: []string{
"ldap-test",
"--id", "1",
"--public-ssh-key-attribute", "publickey",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
AttributeSSHPublicKey: "publickey",
},
},
},
// case 17
withConfig(&ldap.Source{BindDN: "cn=readonly,dc=domain,dc=org"}, "--bind-dn", "cn=readonly,dc=domain,dc=org"),
{
args: []string{
"ldap-test",
"--id", "1",
"--bind-dn", "cn=readonly,dc=domain,dc=org",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
BindDN: "cn=readonly,dc=domain,dc=org",
},
},
},
// case 18
withConfig(&ldap.Source{BindPassword: "secret"}, "--bind-password", "secret"),
{
args: []string{
"ldap-test",
"--id", "1",
"--bind-password", "secret",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
BindPassword: "secret",
},
},
},
// case 19
{
args: []string{
@@ -668,7 +841,19 @@ func TestUpdateLdapBindDn(t *testing.T) {
},
},
// case 20
withConfig(&ldap.Source{SearchPageSize: 12}, "--page-size", "12"),
{
args: []string{
"ldap-test",
"--id", "1",
"--page-size", "12",
},
authSource: &auth.Source{
Type: auth.LDAP,
Cfg: &ldap.Source{
SearchPageSize: 12,
},
},
},
// case 21
{
args: []string{
@@ -779,20 +964,13 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
// Test cases
type testCase struct {
cases := []struct {
args []string
id int64
existingAuthSource *auth.Source
authSource *auth.Source
errMsg string
}
withConfig := func(config *ldap.Source, args ...string) testCase {
return testCase{
args: append([]string{"ldap-test", "--id", "1"}, args...),
authSource: &auth.Source{Type: auth.DLDAP, Cfg: config},
}
}
cases := []testCase{
}{
// case 0
{
args: []string{
@@ -842,7 +1020,16 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
},
},
// case 1
withConfig(&ldap.Source{}),
{
args: []string{
"ldap-test",
"--id", "1",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{},
},
},
// case 2
{
args: []string{
@@ -877,43 +1064,187 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
},
},
// case 4
withConfig(&ldap.Source{SecurityProtocol: ldap.SecurityProtocol(2)}, "--security-protocol", "starttls"),
{
args: []string{
"ldap-test",
"--id", "1",
"--security-protocol", "starttls",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
SecurityProtocol: ldap.SecurityProtocol(2),
},
},
},
// case 5
withConfig(&ldap.Source{SkipVerify: true}, "--skip-tls-verify"),
{
args: []string{
"ldap-test",
"--id", "1",
"--skip-tls-verify",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
SkipVerify: true,
},
},
},
// case 6
withConfig(&ldap.Source{Host: "ldap-server"}, "--host", "ldap-server"),
{
args: []string{
"ldap-test",
"--id", "1",
"--host", "ldap-server",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
Host: "ldap-server",
},
},
},
// case 7
withConfig(&ldap.Source{Port: 987}, "--port", "987"),
{
args: []string{
"ldap-test",
"--id", "1",
"--port", "987",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
Port: 987,
},
},
},
// case 8
withConfig(
&ldap.Source{UserBase: "ou=Users,dc=domain,dc=org"},
"--user-search-base", "ou=Users,dc=domain,dc=org",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--user-search-base", "ou=Users,dc=domain,dc=org",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
UserBase: "ou=Users,dc=domain,dc=org",
},
},
},
// case 9
withConfig(
&ldap.Source{Filter: "(&(objectClass=posixAccount)(cn=%s))"},
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
Filter: "(&(objectClass=posixAccount)(cn=%s))",
},
},
},
// case 10
withConfig(
&ldap.Source{AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)"},
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
},
},
},
// case 11
withConfig(&ldap.Source{AttributeUsername: "uid"}, "--username-attribute", "uid"),
{
args: []string{
"ldap-test",
"--id", "1",
"--username-attribute", "uid",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
AttributeUsername: "uid",
},
},
},
// case 12
withConfig(&ldap.Source{AttributeName: "givenName"}, "--firstname-attribute", "givenName"),
{
args: []string{
"ldap-test",
"--id", "1",
"--firstname-attribute", "givenName",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
AttributeName: "givenName",
},
},
},
// case 13
withConfig(&ldap.Source{AttributeSurname: "sn"}, "--surname-attribute", "sn"),
{
args: []string{
"ldap-test",
"--id", "1",
"--surname-attribute", "sn",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
AttributeSurname: "sn",
},
},
},
// case 14
withConfig(&ldap.Source{AttributeMail: "mail"}, "--email-attribute", "mail"),
{
args: []string{
"ldap-test",
"--id", "1",
"--email-attribute", "mail",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
AttributeMail: "mail",
},
},
},
// case 15
withConfig(&ldap.Source{AttributeSSHPublicKey: "publickey"}, "--public-ssh-key-attribute", "publickey"),
{
args: []string{
"ldap-test",
"--id", "1",
"--public-ssh-key-attribute", "publickey",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
AttributeSSHPublicKey: "publickey",
},
},
},
// case 16
withConfig(
&ldap.Source{UserDN: "cn=%s,ou=Users,dc=domain,dc=org"},
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
),
{
args: []string{
"ldap-test",
"--id", "1",
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
},
authSource: &auth.Source{
Type: auth.DLDAP,
Cfg: &ldap.Source{
UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
},
},
},
// case 17
{
args: []string{

View File

@@ -221,11 +221,12 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
return err
}
source, err := a.getAuthSourceOfType(ctx, c.Int64("id"), auth_model.OAuth2)
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
oAuth2Config := auth_model.MustSourceCfg[*oauth2.Source](source)
oAuth2Config := source.Cfg.(*oauth2.Source)
if c.IsSet("name") {
source.Name = c.String("name")

View File

@@ -16,11 +16,10 @@ import (
func TestAddOauth(t *testing.T) {
testCases := []struct {
name string
args []string
config *oauth2.Source
twoFactorPolicy string
errMsg string
name string
args []string
source *auth_model.Source
errMsg string
}{
{
name: "valid config",
@@ -30,11 +29,17 @@ func TestAddOauth(t *testing.T) {
"--key", "some_key",
"--secret", "some_secret",
},
config: &oauth2.Source{
Scopes: []string{},
Provider: "github",
ClientID: "some_key",
ClientSecret: "some_secret",
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Scopes: []string{},
Provider: "github",
ClientID: "some_key",
ClientSecret: "some_secret",
},
TwoFactorPolicy: "",
},
},
{
@@ -46,12 +51,18 @@ func TestAddOauth(t *testing.T) {
"--secret", "some_secret",
"--auto-discover-url", "https://example.com",
},
config: &oauth2.Source{
Scopes: []string{},
Provider: "openidConnect",
ClientID: "some_key",
ClientSecret: "some_secret",
OpenIDConnectAutoDiscoveryURL: "https://example.com",
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Scopes: []string{},
Provider: "openidConnect",
ClientID: "some_key",
ClientSecret: "some_secret",
OpenIDConnectAutoDiscoveryURL: "https://example.com",
},
TwoFactorPolicy: "",
},
},
{
@@ -80,30 +91,35 @@ func TestAddOauth(t *testing.T) {
"--ssh-public-key-claim-name", "attr_ssh_pub_key",
"--full-name-claim-name", "attr_full_name",
},
config: &oauth2.Source{
Provider: "gitlab",
ClientID: "some_key",
ClientSecret: "some_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://example.com/token",
AuthURL: "https://example.com/auth",
ProfileURL: "https://example.com/profile",
EmailURL: "https://example.com/email",
Tenant: "some_tenant",
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "some_key",
ClientSecret: "some_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://example.com/token",
AuthURL: "https://example.com/auth",
ProfileURL: "https://example.com/profile",
EmailURL: "https://example.com/email",
Tenant: "some_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "attr_ssh_pub_key",
FullNameClaimName: "attr_full_name",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "attr_ssh_pub_key",
FullNameClaimName: "attr_full_name",
TwoFactorPolicy: "skip",
},
twoFactorPolicy: "skip",
},
}
@@ -134,13 +150,7 @@ func TestAddOauth(t *testing.T) {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
assert.Equal(t, &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: tc.config,
TwoFactorPolicy: tc.twoFactorPolicy,
}, createdSource)
assert.Equal(t, tc.source, createdSource)
}
})
}
@@ -148,10 +158,12 @@ func TestAddOauth(t *testing.T) {
func TestUpdateOauth(t *testing.T) {
testCases := []struct {
name string
args []string
authSource *auth_model.Source
errMsg string
name string
args []string
id int64
existingAuthSource *auth_model.Source
authSource *auth_model.Source
errMsg string
}{
{
name: "missing id",
@@ -162,6 +174,19 @@ func TestUpdateOauth(t *testing.T) {
},
{
name: "valid config",
id: 1,
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "old name",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "github",
ClientID: "old_key",
ClientSecret: "old_secret",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
@@ -185,6 +210,37 @@ func TestUpdateOauth(t *testing.T) {
},
{
name: "valid config with options",
id: 1,
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "old name",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "old_key",
ClientSecret: "old_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://old.example.com/token",
AuthURL: "https://old.example.com/auth",
ProfileURL: "https://old.example.com/profile",
EmailURL: "https://old.example.com/email",
Tenant: "old_tenant",
},
IconURL: "https://old.example.com/icon",
Scopes: []string{"old_scope1", "old_scope2"},
RequiredClaimName: "old_claim_name",
RequiredClaimValue: "old_claim_value",
GroupClaimName: "old_group_name",
AdminGroup: "old_admin",
RestrictedGroup: "old_restricted",
GroupTeamMap: `{"old_group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "old_ssh_pub_key",
FullNameClaimName: "old_full_name",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",

View File

@@ -175,11 +175,12 @@ func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
return err
}
source, err := a.getAuthSourceOfType(ctx, c.Int64("id"), auth_model.SMTP)
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
smtpConfig := auth_model.MustSourceCfg[*smtp.Source](source)
smtpConfig := source.Cfg.(*smtp.Source)
if err := parseSMTPConfig(c, smtpConfig); err != nil {
return err

View File

@@ -132,10 +132,11 @@ func TestAddSMTP(t *testing.T) {
func TestUpdateSMTP(t *testing.T) {
testCases := []struct {
name string
args []string
authSource *auth_model.Source
errMsg string
name string
args []string
existingAuthSource *auth_model.Source
authSource *auth_model.Source
errMsg string
}{
{
name: "missing id",
@@ -148,6 +149,17 @@ func TestUpdateSMTP(t *testing.T) {
},
{
name: "valid config",
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "old name",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "old host",
Port: 26,
},
},
args: []string{
"--id", "1",
"--name", "test",
@@ -168,6 +180,20 @@ func TestUpdateSMTP(t *testing.T) {
},
{
name: "valid config with options",
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "old name",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "old host",
Port: 26,
HeloHostname: "old.example.com",
AllowedDomains: "old.example.com",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",

View File

@@ -4,34 +4,13 @@
package cmd
import (
"context"
"strings"
user_model "gitea.dev/models/user"
"gitea.dev/modules/util"
"github.com/urfave/cli/v3"
)
// parseUserTypeFlag parses "--user-type", keeping the lowercase "individual" and "bot" the flag shipped with
func parseUserTypeFlag(s string) (user_model.UserType, error) {
switch strings.ToLower(s) {
case "user", "individual":
return user_model.UserTypeIndividual, nil
case "bot":
return user_model.UserTypeBot, nil
default:
return 0, util.NewInvalidArgumentErrorf("invalid user type %q (expected User, Bot)", s)
}
}
func newUserCommand() *cli.Command {
return &cli.Command{
Name: "user",
Usage: "Modify users",
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
return cliAuditContext(ctx), nil
},
Commands: []*cli.Command{
microcmdUserCreate(),
newUserListCommand(),
@@ -39,8 +18,6 @@ func newUserCommand() *cli.Command {
microcmdUserDelete(),
newUserGenerateAccessTokenCommand(),
microcmdUserMustChangePassword(),
microcmdUserDisableTwoFactor(),
microcmdUserChangeType(),
},
}
}

View File

@@ -73,6 +73,6 @@ func runChangePassword(ctx context.Context, c *cli.Command) error {
}
}
cprintf(c, "%s's password has been successfully updated!\n", user.Name)
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
return nil
}

View File

@@ -1,61 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
user_service "gitea.dev/services/user"
"github.com/urfave/cli/v3"
)
func microcmdUserChangeType() *cli.Command {
return &cli.Command{
Name: "change-type",
Usage: "Convert an account between the user and bot types",
Action: runChangeUserType,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "The user to convert",
Required: true,
},
&cli.StringFlag{
Name: "user-type",
Usage: "New user type: User or Bot",
Required: true,
},
},
}
}
func runChangeUserType(ctx context.Context, c *cli.Command) error {
targetType, err := parseUserTypeFlag(c.String("user-type"))
if err != nil {
return err
}
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{UserType: optional.Some(targetType)}); err != nil {
return err
}
cprintf(c, "%s's type has been successfully changed to %s!\n", user.Name, targetType.DisplayName())
return nil
}

View File

@@ -44,8 +44,8 @@ func microcmdUserCreate() *cli.Command {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user-type",
Usage: "Set user's type: User or Bot",
Value: "User",
Usage: "Set user's type: individual or bot",
Value: "individual",
},
&cli.StringFlag{
Name: "password",
@@ -105,16 +105,20 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()
userType, err := parseUserTypeFlag(c.String("user-type"))
if err != nil {
return err
userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot,
}
userType, ok := userTypes[c.String("user-type")]
if !ok {
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
}
if userType != user_model.UserTypeIndividual {
// Some other commands like "change-password" also only support regular user accounts.
// Some other commands like "change-password" also only support individual users.
// It needs to clarify the "password" behavior for bot users in the future.
// At the moment, we do not allow setting password for bot users.
if c.IsSet("password") || c.IsSet("random-password") {
return errors.New("password can only be set for user accounts")
return errors.New("password can only be set for individual users")
}
}
@@ -148,7 +152,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
return err
}
// codeql[disable-next-line=go/clear-text-logging]
cprintf(c, "generated random password is '%s'\n", password)
fmt.Printf("generated random password is '%s'\n", password)
} else if userType == user_model.UserTypeIndividual {
return errors.New("must set either password or random-password flag")
}
@@ -158,7 +162,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
mustChangePassword := userType == user_model.UserTypeIndividual
if c.IsSet("must-change-password") {
if userType != user_model.UserTypeIndividual {
return errors.New("must-change-password flag can only be set for user accounts")
return errors.New("must-change-password flag can only be set for individual users")
}
// if the flag is set, use the value provided by the user
mustChangePassword = c.Bool("must-change-password")
@@ -224,7 +228,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err)
}
cprintf(c, "New user '%s' has been successfully created!\n", username)
fmt.Printf("New user '%s' has been successfully created!\n", username)
// create the access token
if accessTokenScope != "" {
@@ -232,7 +236,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
if err := auth_model.NewAccessToken(ctx, t); err != nil {
return err
}
cprintf(c, "Access token was successfully created... %s\n", t.Token)
fmt.Printf("Access token was successfully created... %s\n", t.Token)
}
return nil
}

View File

@@ -56,21 +56,14 @@ func TestAdminUserCreate(t *testing.T) {
t.Run("UserType", func(t *testing.T) {
reset()
assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for user accounts")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for user accounts")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--admin"), "bot user can not be a site administrator")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
assert.NoError(t, createUser("u", "--user-type", "bot"))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd)
assert.False(t, u.MustChangePassword, "bot users should not be forced to change password")
changeType := func(userType string) error {
return microcmdUserChangeType().Run(t.Context(), []string{"change-type", "--username", "u", "--user-type", userType})
}
assert.NoError(t, changeType("User"))
assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"}).IsIndividual())
})
t.Run("AccessToken", func(t *testing.T) {

View File

@@ -24,29 +24,42 @@ func TestAdminUserDelete(t *testing.T) {
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
}()
for _, tc := range []struct {
name string
args []string
}{
{"delete user by id", []string{"delete-test", "--id", ""}},
{"delete user by username", []string{"delete-test", "--username", "testuser"}},
{"delete user by email", []string{"delete-test", "--email", "testuser@gitea.local"}},
{"delete user by all 3 attributes", []string{"delete", "--id", "", "--username", "testuser", "--email", "testuser@gitea.local"}},
} {
t.Run(tc.name, func(t *testing.T) {
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
if tc.args[1] == "--id" {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
tc.args[2] = strconv.FormatInt(user.ID, 10)
}
err = microcmdUserDelete().Run(ctx, tc.args)
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
setupTestUser := func(t *testing.T) {
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
}
t.Run("delete user by id", func(t *testing.T) {
setupTestUser(t)
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by username", func(t *testing.T) {
setupTestUser(t)
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by email", func(t *testing.T) {
setupTestUser(t)
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by all 3 attributes", func(t *testing.T) {
setupTestUser(t)
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
}
func TestAdminUserDeleteFailure(t *testing.T) {

View File

@@ -1,72 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"strings"
auth_model "gitea.dev/models/auth"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"github.com/urfave/cli/v3"
)
func microcmdUserDisableTwoFactor() *cli.Command {
return &cli.Command{
Name: "disable-2fa",
Usage: "Disable two-factor authentication for a user",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username of the user to disable 2FA for",
},
&cli.Int64Flag{
Name: "id",
Usage: "ID of the user to disable 2FA for",
},
},
Action: runDisableTwoFactor,
}
}
func runDisableTwoFactor(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") && !c.IsSet("username") {
return errors.New("either --id or --username must be provided")
}
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
var user *user_model.User
var err error
if c.IsSet("id") {
user, err = user_model.GetUserByID(ctx, c.Int64("id"))
} else {
user, err = user_model.GetUserByName(ctx, c.String("username"))
}
if err != nil {
return err
}
// When both selectors are given, make sure they refer to the same user.
if c.IsSet("id") && c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
return fmt.Errorf("the user with id %d is %q, which does not match the provided username %q", user.ID, user.Name, c.String("username"))
}
totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, user.ID)
if err != nil {
return err
}
cprintf(c, "Disabled 2FA for user %q (removed %d TOTP and %d WebAuthn credential(s))\n", user.Name, totp, webAuthn)
return nil
}

View File

@@ -1,119 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"io"
"strconv"
"testing"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDisableTwoFactorCommand(t *testing.T) {
ctx := t.Context()
defer func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}, &auth_model.TwoFactor{}, &auth_model.WebAuthnCredential{}))
}()
t.Run("disable TOTP and WebAuthn", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "tfuser", "--email", "tfuser@gitea.local", "--random-password"}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "tfuser"})
// Enroll TOTP.
tf := &auth_model.TwoFactor{UID: user.ID}
require.NoError(t, tf.SetSecret("test-secret"))
_, err := tf.GenerateScratchToken()
require.NoError(t, err)
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
// Register a WebAuthn credential.
_, err = auth_model.CreateCredential(ctx, user.ID, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")})
require.NoError(t, err)
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
require.NoError(t, err)
require.True(t, has)
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "tfuser"}))
// Both factors must be gone afterwards.
has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
require.NoError(t, err)
assert.False(t, has)
})
t.Run("disable by id", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "iduser", "--email", "iduser@gitea.local", "--random-password"}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "iduser"})
tf := &auth_model.TwoFactor{UID: user.ID}
require.NoError(t, tf.SetSecret("test-secret"))
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", strconv.FormatInt(user.ID, 10)}))
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
require.NoError(t, err)
assert.False(t, has)
})
t.Run("no enrollment is a no-op", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "plainuser", "--email", "plainuser@gitea.local", "--random-password"}))
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "plainuser"}))
})
t.Run("id and username must match when both given", func(t *testing.T) {
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "matchuser", "--email", "matchuser@gitea.local", "--random-password"}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "matchuser"})
id := strconv.FormatInt(user.ID, 10)
// Matching id + username is accepted.
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", id, "--username", "matchuser"}))
// Mismatched id + username is rejected.
cmd := microcmdUserDisableTwoFactor()
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
err := cmd.Run(ctx, []string{"disable-2fa", "--id", id, "--username", "someotheruser"})
require.Error(t, err)
require.Contains(t, err.Error(), "does not match the provided username")
})
t.Run("failure cases", func(t *testing.T) {
testCases := []struct {
name string
args []string
expectedErr string
}{
{
name: "user does not exist",
args: []string{"disable-2fa", "--username", "nonexistentuser"},
expectedErr: "user does not exist",
},
{
name: "neither id nor username",
args: []string{"disable-2fa"},
expectedErr: "either --id or --username must be provided",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := microcmdUserDisableTwoFactor()
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
err := cmd.Run(ctx, tc.args)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
})
}
})
}

View File

@@ -88,9 +88,9 @@ func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
}
if c.Bool("raw") {
cprintln(c, t.Token)
fmt.Printf("%s\n", t.Token)
} else {
cprintf(c, "Access token was successfully created: %s\n", t.Token)
fmt.Printf("Access token was successfully created: %s\n", t.Token)
}
return nil

View File

@@ -33,7 +33,7 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
return err
}
users, err := user_model.GetAllUsersForAdminListing(ctx)
users, err := user_model.GetAllUsers(ctx)
if err != nil {
return err
}
@@ -41,17 +41,17 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
if c.IsSet("admin") {
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tType\n")
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
for _, u := range users {
if u.IsAdmin {
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%s\n", u.ID, u.Name, u.Email, u.IsActive, u.Type.DisplayName())
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
}
}
} else {
twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\tType\n")
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
for _, u := range users {
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\t%s\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID], u.Type.DisplayName())
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
}
}

View File

@@ -6,6 +6,7 @@ package cmd
import (
"context"
"errors"
"fmt"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
@@ -58,6 +59,6 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error {
}
// codeql[disable-next-line=go/clear-text-logging]
cprintf(c, "Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
return nil
}

View File

@@ -18,28 +18,61 @@ func TestMustChangePassword(t *testing.T) {
defer func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
}()
for _, name := range []string{"testuser", "testuserexclude"} {
require.NoError(t, microcmdUserCreate().Run(t.Context(), []string{"create", "--username", name, "--email", name + "@gitea.local", "--random-password"}))
}
for _, tc := range []struct {
args []string
assertUser, assertExcludedUser assert.BoolAssertionFunc
}{
// Reset password change flag
{[]string{"change-test", "--all", "--unset"}, assert.False, assert.False},
// Make all users change password
{[]string{"change-test", "--all"}, assert.True, assert.True},
// Reset password change flag but exclude all tested users
{[]string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"}, assert.True, assert.True},
// Reset password change flag by listing multiple users
{[]string{"change-test", "--unset", "testuser", "testuserexclude"}, assert.False, assert.False},
// Exclude a user from all user
{[]string{"change-test", "--all", "--exclude", "testuserexclude"}, assert.True, assert.False},
// Unset a flag for single user
{[]string{"change-test", "--unset", "testuser"}, assert.False, assert.False},
} {
require.NoError(t, microcmdUserMustChangePassword().Run(t.Context(), tc.args))
tc.assertUser(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}).MustChangePassword)
tc.assertExcludedUser(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}).MustChangePassword)
}
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
require.NoError(t, err)
// Reset password change flag
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
require.NoError(t, err)
testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Make all users change password
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.True(t, testUserExclude.MustChangePassword)
// Reset password change flag but exclude all tested users
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.True(t, testUserExclude.MustChangePassword)
// Reset password change flag by listing multiple users
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Exclude a user from all user
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Unset a flag for single user
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
}

View File

@@ -1,17 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
audit_model "gitea.dev/models/audit"
user_model "gitea.dev/models/user"
"gitea.dev/services/audit"
)
func cliAuditContext(ctx context.Context) context.Context {
ctx = audit.WithOrigin(ctx, audit_model.OriginCLI)
return audit.WithDoer(ctx, user_model.NewCliUser())
}

View File

@@ -76,82 +76,110 @@ func TestCliCmd(t *testing.T) {
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
env map[string]string
cmds []string
exp string
env map[string]string
cmd string
exp string
}{
// help commands
{
cmds: []string{"./gitea -h", "./gitea help"},
exp: "DEFAULT CONFIGURATION:",
cmd: "./gitea -h",
exp: "DEFAULT CONFIGURATION:",
},
{
cmds: []string{
"./gitea -c /dev/null -h",
"./gitea -c /dev/null help",
"./gitea help -c /dev/null",
"./gitea -c /dev/null test-cmd -h",
"./gitea test-cmd -c /dev/null -h",
"./gitea test-cmd -h -c /dev/null",
"./gitea -c /dev/null test-cmd help",
"./gitea test-cmd -c /dev/null help",
"./gitea test-cmd help -c /dev/null",
},
cmd: "./gitea help",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -h -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
// parse paths
{
cmds: []string{"./gitea test-cmd"},
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
cmd: "./gitea test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
},
{
cmds: []string{"./gitea -c /tmp/app.ini test-cmd"},
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
cmd: "./gitea -c /tmp/app.ini test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
cmds: []string{"./gitea test-cmd -c /tmp/app.ini"},
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
cmd: "./gitea test-cmd -c /tmp/app.ini",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmds: []string{"./gitea test-cmd"},
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmds: []string{"./gitea test-cmd --work-path /tmp/other"},
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --work-path /tmp/other",
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmds: []string{"./gitea test-cmd --config /tmp/app-other.ini"},
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
},
}
for _, c := range cases {
for _, command := range c.cmds {
t.Run(command, func(t *testing.T) {
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error {
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
},
})
for k, v := range c.env {
t.Setenv(k, v)
}
args := strings.Split(command, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...)
assert.NoError(t, err, command)
assert.NotEmpty(t, c.exp, command)
if !assert.Contains(t, r.Stdout, c.exp, command) {
t.Log("Full output:\n" + r.Stdout)
t.Log("Expected:\n" + c.exp)
}
t.Run(c.cmd, func(t *testing.T) {
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error {
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
},
})
}
for k, v := range c.env {
t.Setenv(k, v)
}
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...)
assert.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd)
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
t.Log("Full output:\n" + r.Stdout)
t.Log("Expected:\n" + c.exp)
}
})
}
}

View File

@@ -12,9 +12,9 @@ import (
"strings"
"text/tabwriter"
"gitea.dev/modelmigration"
"gitea.dev/modelmigration/base"
"gitea.dev/models/db"
"gitea.dev/models/migrations"
migrate_base "gitea.dev/models/migrations/base"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
@@ -114,8 +114,8 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
setting.Database.LogSQL = debug
if err := db.InitEngine(ctx); err != nil {
cprintln(cmd, err)
cprintln(cmd, "Check if you are using the right config file. You can use a --config directive to specify one.")
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}
@@ -129,10 +129,10 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
if err != nil {
return err
}
recreateTables := base.RecreateTables(beans...)
recreateTables := migrate_base.RecreateTables(beans...)
return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x db.EngineMigration) error {
if err := modelmigration.EnsureUpToDate(ctx, x); err != nil {
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
return err
}
return recreateTables(x)

View File

@@ -5,6 +5,7 @@ package cmd
import (
"context"
"fmt"
"gitea.dev/models/db"
"gitea.dev/modules/log"
@@ -39,15 +40,15 @@ func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
log.Fatal("Failed to convert database & table: %v", err)
return err
}
cprintln(cmd, "Converted successfully, please confirm your database's character set is now utf8mb4")
fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4")
case setting.Database.Type.IsMSSQL():
if err := db.ConvertVarcharToNVarchar(); err != nil {
log.Fatal("Failed to convert database from varchar to nvarchar: %v", err)
return err
}
cprintln(cmd, "Converted successfully, please confirm your database's all columns character is NVARCHAR now")
fmt.Println("Converted successfully, please confirm your database's all columns character is NVARCHAR now")
default:
cprintln(cmd, "This command can only be used with a MySQL or MSSQL database")
fmt.Println("This command can only be used with a MySQL or MSSQL database")
}
return nil

View File

@@ -13,11 +13,13 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/dump"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/util"
"gitea.com/go-chi/session"
"github.com/urfave/cli/v3"
)
@@ -90,7 +92,7 @@ func newDumpCommand() *cli.Command {
},
&cli.StringFlag{
Name: "type",
Usage: `Dump output format, default to the --file extension if supported, else "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
},
},
}
@@ -196,7 +198,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
}
defer func() {
_ = dbDump.Close()
if err := os.Remove(dbDump.Name()); err != nil {
if err := util.Remove(dbDump.Name()); err != nil {
log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
}
}()
@@ -247,8 +249,12 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
log.Info("Packing data directory...%s", setting.AppDataPath)
var excludes []string
if setting.SessionConfig.Provider == "file" {
excludes = append(excludes, setting.SessionConfig.ProviderConfig)
if setting.SessionConfig.OriginalProvider == "file" {
var opts session.Options
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
return err
}
excludes = append(excludes, opts.ProviderConfig)
}
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {

View File

@@ -17,7 +17,6 @@ import (
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/convert"
"gitea.dev/services/gitproxy"
"gitea.dev/services/migrations"
"github.com/urfave/cli/v3"
@@ -33,7 +32,7 @@ func newDumpRepositoryCommand() *cli.Command {
&cli.StringFlag{
Name: "git_service",
Value: "",
Usage: "Git service, git, github, gitea, forgejo, gitlab. If clone_addr could be recognized, this could be ignored.",
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
},
&cli.StringFlag{
Name: "repo_dir",
@@ -95,9 +94,6 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
if err := git.InitSimple(); err != nil {
return err
}
if err := gitproxy.Run(ctx); err != nil {
return err
}
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
@@ -117,8 +113,6 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
serviceStr = "gitlab"
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://gitea.com/") {
serviceStr = "gitea"
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://codeberg.org/") {
serviceStr = "forgejo"
}
if serviceStr == "" {
return errors.New("git_service missed or clone_addr cannot be recognized")

View File

@@ -18,6 +18,7 @@ import (
"gitea.dev/modules/public"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"github.com/urfave/cli/v3"
)
@@ -150,7 +151,7 @@ func runListDo(c *cli.Command) error {
}
for _, a := range matchedAssetFiles {
cprintln(c, a.path)
fmt.Println(a.path)
}
return nil
@@ -194,7 +195,7 @@ func runExtractDo(c *cli.Command) error {
destdir = c.String("destination")
} else if c.Bool("custom") {
destdir = setting.CustomPath
cprintln(c, "Using app.ini at", setting.CustomConf)
fmt.Println("Using app.ini at", setting.CustomConf)
}
fi, err := os.Stat(destdir)
@@ -213,13 +214,13 @@ func runExtractDo(c *cli.Command) error {
return fmt.Errorf("destination %q is not a directory", destdir)
}
cprintf(c, "Extracting to %s:\n", destdir)
fmt.Printf("Extracting to %s:\n", destdir)
overwrite := c.Bool("overwrite")
rename := c.Bool("rename")
for _, a := range matchedAssetFiles {
if err := extractAsset(c, destdir, a, overwrite, rename); err != nil {
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
// Non-fatal error
_, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err)
}
@@ -228,7 +229,7 @@ func runExtractDo(c *cli.Command) error {
return nil
}
func extractAsset(c *cli.Command, d string, a assetFile, overwrite, rename bool) error {
func extractAsset(d string, a assetFile, overwrite, rename bool) error {
dest := filepath.Join(d, filepath.FromSlash(a.path))
dir := filepath.Dir(dest)
@@ -249,23 +250,29 @@ func extractAsset(c *cli.Command, d string, a assetFile, overwrite, rename bool)
return fmt.Errorf("%s: %w", dest, err)
}
} else if !overwrite && !rename {
cprintf(c, "%s already exists; skipped.\n", dest)
fmt.Printf("%s already exists; skipped.\n", dest)
return nil
} else if !fi.Mode().IsRegular() {
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
} else if rename {
if err := os.Rename(dest, dest+".bak"); err != nil {
if err := util.Rename(dest, dest+".bak"); err != nil {
return fmt.Errorf("error creating backup for %s: %w", dest, err)
}
// Attempt to respect file permissions mask (even if user:group will be set anew)
perms = fi.Mode()
}
if err := os.WriteFile(dest, data, perms); err != nil {
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
if err != nil {
return fmt.Errorf("%s: %w", dest, err)
}
defer file.Close()
if _, err = file.Write(data); err != nil {
return fmt.Errorf("%s: %w", dest, err)
}
cprintln(c, dest)
fmt.Println(dest)
return nil
}
@@ -300,7 +307,7 @@ func compileCollectPatterns(args []string) (_ []glob.Glob, err error) {
pat := make([]glob.Glob, len(args))
for i := range args {
if pat[i], err = glob.Compile(args[i], '/'); err != nil {
return nil, fmt.Errorf("invalid glob pattern %q: %w", args[i], err)
return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err)
}
}
return pat, nil

View File

@@ -106,10 +106,10 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
return err
}
cprintf(c, "%s", internalToken)
fmt.Printf("%s", internalToken)
if isatty.IsTerminal(os.Stdout.Fd()) {
cprintf(c, "\n")
fmt.Printf("\n")
}
return nil
@@ -117,10 +117,10 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
cprintf(c, "%s", jwtSecretBase64)
fmt.Printf("%s", jwtSecretBase64)
if isatty.IsTerminal(os.Stdout.Fd()) {
cprintf(c, "\n")
fmt.Printf("\n")
}
return nil
@@ -133,10 +133,10 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
}
// codeql[disable-next-line=go/clear-text-logging]
cprintf(c, "%s", secretKey)
fmt.Printf("%s", secretKey)
if isatty.IsTerminal(os.Stdout.Fd()) {
cprintf(c, "\n")
fmt.Printf("\n")
}
return nil
@@ -168,14 +168,14 @@ func runGenerateKeyPair(_ context.Context, c *cli.Command) error {
// Check if file exists to prevent overwriting
if _, err := os.Stat(file); err == nil {
if !confirm(c.Reader, c.Writer, "%s already exists.\nOverwrite (y/n)? ", file) {
cprintln(c, "Aborting")
fmt.Println("Aborting")
return nil
}
}
bits := c.Int("bits")
err := ssh.GenKeyPair(file, generate.SSHKeyType(keyType), bits)
if err == nil {
cprintf(c, "Your SSH key has been saved in %s\n", file)
fmt.Printf("Your SSH key has been saved in %s\n", file)
}
return err
}

View File

@@ -1,6 +1,8 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package cmd provides subcommands to the gitea binary - such as "web" or
// "admin".
package cmd
import (
@@ -20,8 +22,8 @@ import (
"github.com/urfave/cli/v3"
)
// argsSet checks that all the required arguments are set.
// args is a list of arguments that must be set in the command context.
// argsSet checks that all the required arguments are set. args is a list of
// arguments that must be set in the passed Context.
func argsSet(c *cli.Command, args ...string) error {
for _, a := range args {
if !c.IsSet(a) {
@@ -139,11 +141,3 @@ func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
}
return "", true
}
func cprintf(c *cli.Command, format string, args ...any) {
_, _ = fmt.Fprintf(c.Writer, format, args...)
}
func cprintln(c *cli.Command, args ...any) {
_, _ = fmt.Fprintln(c.Writer, args...)
}

View File

@@ -169,6 +169,10 @@ func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, re
}
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
setup(ctx, c.Bool("debug"))
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
@@ -182,24 +186,23 @@ Gitea or set your environment appropriately.`, "")
// the environment is set by serv command
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
ownerName := os.Getenv(repo_module.EnvRepoUsername)
repoName := os.Getenv(repo_module.EnvRepoName)
username := os.Getenv(repo_module.EnvRepoUsername)
reponame := os.Getenv(repo_module.EnvRepoName)
userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
actionsTaskID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionsTaskID), 10, 64)
hookOptions := private.HookOptions{
IsWiki: isWiki,
UserID: userID,
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
GitPushOptions: pushOptions(),
PullRequestID: prID,
UserID: userID,
UserName: os.Getenv(repo_module.EnvPusherName),
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
PullRequestID: prID,
DeployKeyID: deployKeyID,
ActionsTaskID: actionsTaskID,
IsWiki: isWiki,
}
scanner := bufio.NewScanner(os.Stdin)
@@ -209,6 +212,7 @@ Gitea or set your environment appropriately.`, "")
refFullNames := make([]git.RefName, hookBatchSize)
count := 0
total := 0
lastline := 0
out := io.Discard
if setting.Git.VerbosePush {
@@ -221,6 +225,8 @@ Gitea or set your environment appropriately.`, "")
}
}
supportProcReceive := git.DefaultFeatures().SupportProcReceive
for scanner.Scan() {
// TODO: support news feeds for wiki
if isWiki {
@@ -233,23 +239,37 @@ Gitea or set your environment appropriately.`, "")
}
total++
oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName
count++
fmt.Fprintf(out, "*")
lastline++
if count >= hookBatchSize {
fmt.Fprintf(out, " Checking %d references\n", count)
// If the ref is a branch or tag, check if it's protected
// if supportProcReceive all ref should be checked because
// permission check was delayed
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName
count++
fmt.Fprintf(out, "*")
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
if extra.HasError() {
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
if count >= hookBatchSize {
fmt.Fprintf(out, " Checking %d references\n", count)
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
if extra.HasError() {
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
}
count = 0
lastline = 0
}
count = 0
} else {
fmt.Fprintf(out, ".")
}
if lastline >= hookBatchSize {
fmt.Fprintf(out, "\n")
lastline = 0
}
}
if err := scanner.Err(); err != nil {
@@ -263,10 +283,12 @@ Gitea or set your environment appropriately.`, "")
fmt.Fprintf(out, " Checking %d references\n", count)
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
if extra.HasError() {
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
}
} else if lastline > 0 {
fmt.Fprintf(out, "\n")
}
fmt.Fprintf(out, "Checked %d references in total\n", total)
@@ -276,6 +298,10 @@ Gitea or set your environment appropriately.`, "")
// runHookUpdate avoid to do heavy operations on update hook because it will be
// invoked for every ref update which does not like pre-receive and post-receive
func runHookUpdate(_ context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
// Update is empty and is kept only for backwards compatibility
if len(os.Args) < 3 {
return nil
@@ -296,6 +322,11 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error {
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
}
// Now if we're an internal don't do anything else
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
return fail(ctx, `Rejecting changes as Gitea environment not set.
@@ -322,20 +353,18 @@ Gitea or set your environment appropriately.`, "")
repoName := os.Getenv(repo_module.EnvRepoName)
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
pusherName := os.Getenv(repo_module.EnvPusherName)
hookOptions := private.HookOptions{
IsWiki: isWiki,
UserName: pusherName,
UserID: pusherID,
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
GitPushOptions: pushOptions(),
PullRequestID: prID,
UserID: pusherID,
UserName: os.Getenv(repo_module.EnvPusherName),
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
PullRequestID: prID,
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
IsWiki: isWiki,
}
oldCommitIDs := make([]string, 0, hookBatchSize)
@@ -443,11 +472,16 @@ Gitea or set your environment appropriately.`, "")
return nil
}
if !git.DefaultFeatures().SupportProcReceive {
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
}
reader := bufio.NewReader(os.Stdin)
repoUser := os.Getenv(repo_module.EnvRepoUsername)
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
repoName := os.Getenv(repo_module.EnvRepoName)
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
pusherName := os.Getenv(repo_module.EnvPusherName)
// 1. Version and features negotiation.
// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
@@ -519,13 +553,10 @@ Gitea or set your environment appropriately.`, "")
// S: ... ...
// S: flush-pkt
hookOptions := private.HookOptions{
IsWiki: isWiki,
UserName: pusherName,
UserID: pusherID,
GitPushOptions: make(map[string]string),
UserID: pusherID,
UserName: os.Getenv(repo_module.EnvPusherName),
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
IsWiki: isWiki,
}
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
@@ -721,7 +752,7 @@ func writeFlushPktLine(ctx context.Context, out io.Writer) error {
func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
hexchar := []byte("0123456789abcdef")
hex := func(n uint64) byte {
return hexchar[n&15]
return hexchar[(n)&15]
}
length := uint64(len(data) + 4)

View File

@@ -74,7 +74,7 @@ func runKeys(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
authorizedString, extra := private.AuthorizedPublicKeyForSSH(ctx, content)
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
// do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys
if extra.Error != nil {
return extra.Error

View File

@@ -5,6 +5,7 @@ package cmd
import (
"context"
"fmt"
"gitea.dev/modules/private"
"gitea.dev/modules/setting"
@@ -21,11 +22,11 @@ func runSendMail(ctx context.Context, c *cli.Command) error {
if !confirmSkipped {
if len(body) == 0 {
cprintln(c, "warning: Content is empty")
fmt.Println("warning: Content is empty")
}
if !confirm(c.Reader, c.Writer, "Proceed with sending email? [Y/n] ") {
cprintln(c, "The mail was not sent")
fmt.Println("The mail was not sent")
return nil
}
}
@@ -34,6 +35,6 @@ func runSendMail(ctx context.Context, c *cli.Command) error {
if extra.HasError() {
return handleCliResponseExtra(extra)
}
cprintf(c, "Sent %s email(s) to all users\n", respText.Text)
_, _ = fmt.Printf("Sent %s email(s) to all users\n", respText.Text)
return nil
}

View File

@@ -5,14 +5,60 @@ package cmd
import (
"context"
"errors"
"fmt"
"os"
"gitea.dev/modules/log"
"gitea.dev/modules/private"
"github.com/urfave/cli/v3"
)
func defaultLoggingFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "logger",
Usage: `Logger name - will default to "default"`,
},
&cli.StringFlag{
Name: "writer",
Usage: "Name of the log writer - will default to mode",
},
&cli.StringFlag{
Name: "level",
Usage: "Logging level for the new logger",
},
&cli.StringFlag{
Name: "stacktrace-level",
Aliases: []string{"L"},
Usage: "Stacktrace logging level",
},
&cli.StringFlag{
Name: "flags",
Aliases: []string{"F"},
Usage: "Flags for the logger",
},
&cli.StringFlag{
Name: "expression",
Aliases: []string{"e"},
Usage: "Matching expression for the logger",
},
&cli.StringFlag{
Name: "prefix",
Aliases: []string{"p"},
Usage: "Prefix for the logger",
},
&cli.BoolFlag{
Name: "color",
Usage: "Use color in the logs",
},
&cli.BoolFlag{
Name: "debug",
},
}
}
func newLoggingCommand() *cli.Command {
return &cli.Command{
Name: "logging",
@@ -45,6 +91,92 @@ func newLoggingCommand() *cli.Command {
},
},
Action: runReleaseReopenLogging,
}, {
Name: "remove",
Usage: "Remove a logger",
ArgsUsage: "[name] Name of logger to remove",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
}, &cli.StringFlag{
Name: "logger",
Usage: `Logger name - will default to "default"`,
},
},
Action: runRemoveLogger,
}, {
Name: "add",
Usage: "Add a logger",
Commands: []*cli.Command{
{
Name: "file",
Usage: "Add a file logger",
Flags: append(defaultLoggingFlags(), []cli.Flag{
&cli.StringFlag{
Name: "filename",
Aliases: []string{"f"},
Usage: "Filename for the logger - this must be set.",
},
&cli.BoolFlag{
Name: "rotate",
Aliases: []string{"r"},
Usage: "Rotate logs",
},
&cli.Int64Flag{
Name: "max-size",
Aliases: []string{"s"},
Usage: "Maximum size in bytes before rotation",
},
&cli.BoolFlag{
Name: "daily",
Aliases: []string{"d"},
Usage: "Rotate logs daily",
},
&cli.IntFlag{
Name: "max-days",
Aliases: []string{"D"},
Usage: "Maximum number of daily logs to keep",
},
&cli.BoolFlag{
Name: "compress",
Aliases: []string{"z"},
Usage: "Compress rotated logs",
},
&cli.IntFlag{
Name: "compression-level",
Aliases: []string{"Z"},
Usage: "Compression level to use",
},
}...),
Action: runAddFileLogger,
}, {
Name: "conn",
Usage: "Add a net conn logger",
Flags: append(defaultLoggingFlags(), []cli.Flag{
&cli.BoolFlag{
Name: "reconnect-on-message",
Aliases: []string{"R"},
Usage: "Reconnect to host for every message",
},
&cli.BoolFlag{
Name: "reconnect",
Aliases: []string{"r"},
Usage: "Reconnect to host when connection is dropped",
},
&cli.StringFlag{
Name: "protocol",
Aliases: []string{"P"},
Usage: "Set protocol to use: tcp, unix, or udp (defaults to tcp)",
},
&cli.StringFlag{
Name: "address",
Aliases: []string{"a"},
Usage: "Host address and port to connect to (defaults to :7020)",
},
}...),
Action: runAddConnLogger,
},
},
}, {
Name: "log-sql",
Usage: "Set LogSQL",
@@ -63,6 +195,107 @@ func newLoggingCommand() *cli.Command {
}
}
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
logger := c.String("logger")
if len(logger) == 0 {
logger = log.DEFAULT
}
writer := c.Args().First()
extra := private.RemoveLogger(ctx, logger, writer)
return handleCliResponseExtra(extra)
}
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "conn"
vals["net"] = "tcp"
if c.IsSet("protocol") {
switch c.String("protocol") {
case "udp":
vals["net"] = "udp"
case "unix":
vals["net"] = "unix"
}
}
if c.IsSet("address") {
vals["address"] = c.String("address")
} else {
vals["address"] = ":7020"
}
if c.IsSet("reconnect") {
vals["reconnect"] = c.Bool("reconnect")
}
if c.IsSet("reconnect-on-message") {
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
}
return commonAddLogger(ctx, c, mode, vals)
}
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "file"
if c.IsSet("filename") {
vals["filename"] = c.String("filename")
} else {
return errors.New("filename must be set when creating a file logger")
}
if c.IsSet("rotate") {
vals["rotate"] = c.Bool("rotate")
}
if c.IsSet("max-size") {
vals["maxsize"] = c.Int64("max-size")
}
if c.IsSet("daily") {
vals["daily"] = c.Bool("daily")
}
if c.IsSet("max-days") {
vals["maxdays"] = c.Int("max-days")
}
if c.IsSet("compress") {
vals["compress"] = c.Bool("compress")
}
if c.IsSet("compression-level") {
vals["compressionLevel"] = c.Int("compression-level")
}
return commonAddLogger(ctx, c, mode, vals)
}
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
if len(c.String("level")) > 0 {
vals["level"] = log.LevelFromString(c.String("level")).String()
}
if len(c.String("stacktrace-level")) > 0 {
vals["stacktraceLevel"] = log.LevelFromString(c.String("stacktrace-level")).String()
}
if len(c.String("expression")) > 0 {
vals["expression"] = c.String("expression")
}
if len(c.String("prefix")) > 0 {
vals["prefix"] = c.String("prefix")
}
if len(c.String("flags")) > 0 {
vals["flags"] = log.FlagsFromString(c.String("flags"))
}
if c.IsSet("color") {
vals["colorize"] = c.Bool("color")
}
logger := log.DEFAULT
if c.IsSet("logger") {
logger = c.String("logger")
}
writer := mode
if c.IsSet("writer") {
writer = c.String("writer")
}
extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra)
}
func runPauseLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.PauseLogging(ctx)

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
@@ -28,7 +29,6 @@ import (
"gitea.dev/modules/process"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
"gitea.dev/services/agit"
"gitea.dev/services/lfs"
"github.com/kballard/go-shellquote"
@@ -139,13 +139,13 @@ func runServ(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
if setting.SSH.Disabled {
cprintln(c, "Gitea: SSH has been disabled")
println("Gitea: SSH has been disabled")
return nil
}
if c.NArg() < 1 {
if err := cli.ShowSubcommandHelp(c); err != nil {
cprintf(c, "error showing subcommand help: %v\n", err)
fmt.Printf("error showing subcommand help: %v\n", err)
}
return nil
}
@@ -171,19 +171,15 @@ func runServ(ctx context.Context, c *cli.Command) error {
if err != nil {
return fail(ctx, "Key check failed", "Failed to check provided key: %v", err)
}
var authSuccessMsg string
switch key.Type {
case asymkey_model.KeyTypeDeploy:
authSuccessMsg = "Hi there! You've successfully authenticated with an SSH deploy key."
println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.")
case asymkey_model.KeyTypePrincipal:
authSuccessMsg = "Hi there! You've successfully authenticated with the SSH principal " + key.Content + "."
println("Hi there! You've successfully authenticated with the principal " + key.Content + ", but Gitea does not provide shell access.")
default:
authSuccessMsg = "Hi there, " + user.Name + "! You've successfully authenticated with the SSH key named " + key.Name + "."
println("Hi there, " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.")
}
_, _ = fmt.Fprintf(c.ErrWriter, "%s\n%s",
authSuccessMsg,
"Gitea does not provide shell access. If this is unexpected, please setup Gitea under another SSH user or use container to deploy.",
)
println("If this is unexpected, please log in with password and setup Gitea under another user.")
return nil
} else if c.Bool("debug") {
log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND"))
@@ -195,35 +191,45 @@ func runServ(ctx context.Context, c *cli.Command) error {
}
if len(sshCmdArgs) < 2 {
// for AGit Flow
if cmd == "ssh_info" {
cprintf(c, "%s", agit.SshInfoJson)
return nil
if git.DefaultFeatures().SupportProcReceive {
// for AGit Flow
if cmd == "ssh_info" {
fmt.Print(`{"type":"agit","version":1}`)
return nil
}
}
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
}
var reqOwnerName, reqRepoName string
{
var ok bool
reqRepoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
reqOwnerName, reqRepoName, ok = strings.Cut(reqRepoPath, "/")
if !ok {
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", reqRepoPath)
}
reqRepoName = strings.TrimSuffix(reqRepoName, ".git") // "the-repo-name" or "the-repo-name.wiki"
repoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
repoPathFields := strings.SplitN(repoPath, "/", 2)
if len(repoPathFields) != 2 {
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
}
if !repo_model.IsValidSSHAccessRepoName(reqRepoName) {
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reqRepoName)
username := repoPathFields[0]
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
if !repo_model.IsValidSSHAccessRepoName(reponame) {
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
}
if c.Bool("enable-pprof") {
stopProfiler, err := pprof.DumpPprofForUsername(setting.PprofDataPath, reqOwnerName)
if err != nil {
return fail(ctx, "Unable to start pprof profiler", "Unable to start pprof profile: %v", err)
if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil {
return fail(ctx, "Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err)
}
defer stopProfiler()
stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username)
if err != nil {
return fail(ctx, "Unable to start CPU profiler", "Unable to start CPU profile: %v", err)
}
defer func() {
stopCPUProfiler()
err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username)
if err != nil {
_ = fail(ctx, "Unable to dump Mem profile", "Unable to dump Mem Profile: %v", err)
}
}()
}
verb, lfsVerb := sshCmdArgs[0], ""
@@ -248,38 +254,39 @@ func runServ(ctx context.Context, c *cli.Command) error {
return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb)
}
results, extra := private.ServCommand(ctx, keyID, reqOwnerName, reqRepoName, requestedMode, verb, lfsVerb)
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
if extra.HasError() {
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
}
// because the original repoPath maybe redirected, we need to use the returned actual repository information
if results.IsWiki {
repoPath = repo_model.RelativeWikiPath(results.OwnerName, results.RepoName)
} else {
repoPath = repo_model.RelativePath(results.OwnerName, results.RepoName)
}
// LFS SSH protocol
if verb == git.CmdVerbLfsTransfer {
if results.IsWiki {
return fail(ctx, "LFS Transfer is not supported for wikis", "")
}
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, UserExtDoerData: results.UserExtDoerData, RepoID: results.RepoID})
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
if err != nil {
return err
}
return lfstransfer.Main(ctx, results.OwnerName, results.RepoName, lfsVerb, token)
return lfstransfer.Main(ctx, repoPath, lfsVerb, token)
}
// LFS token authentication
if verb == git.CmdVerbLfsAuthenticate {
if results.IsWiki {
return fail(ctx, "LFS Authenticate is not supported for wikis", "")
}
lfsTokenHref := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, UserExtDoerData: results.UserExtDoerData, RepoID: results.RepoID})
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
if err != nil {
return err
}
tokenAuthentication := &git_model.LFSTokenResponse{
Header: make(map[string]string),
Href: lfsTokenHref,
Href: url,
}
tokenAuthentication.Header["Authorization"] = token
@@ -291,7 +298,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
return nil
}
var command *process.Cmd
var command *exec.Cmd
gitBinPath := filepath.Dir(gitcmd.GitExecutable) // e.g. /usr/bin
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
if _, err := os.Stat(gitBinVerb); err != nil {
@@ -300,14 +307,15 @@ func runServ(ctx context.Context, c *cli.Command) error {
verbFields := strings.SplitN(verb, "-", 2)
if len(verbFields) == 2 {
// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
command = process.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], results.RepoStoragePath)
command = exec.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], repoPath)
}
}
if command == nil {
// by default, use the verb (it has been checked above by allowedCommands)
command = process.CommandContext(ctx, gitBinVerb, results.RepoStoragePath)
command = exec.CommandContext(ctx, gitBinVerb, repoPath)
}
process.SetSysProcAttribute(command)
command.Dir = setting.RepoRootPath
command.Stdout = os.Stdout
command.Stdin = os.Stdin
@@ -315,20 +323,15 @@ func runServ(ctx context.Context, c *cli.Command) error {
command.Env = append(command.Env, os.Environ()...)
command.Env = append(command.Env,
repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
repo_module.EnvRepoUsername+"="+results.OwnerName,
repo_module.EnvRepoName+"="+results.RepoName,
repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
repo_module.EnvKeyID+"="+strconv.FormatInt(results.PublicKeyID, 10),
repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
repo_module.EnvRepoUsername+"="+results.OwnerName,
repo_module.EnvPusherName+"="+results.UserName,
repo_module.EnvPusherEmail+"="+results.UserEmail,
repo_module.EnvPusherExtDoerData+"="+results.UserExtDoerData,
repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
repo_module.EnvPRID+"="+strconv.Itoa(0),
repo_module.EnvDeployKeyID+"="+strconv.FormatInt(results.DeployKeyID, 10),
repo_module.EnvKeyID+"="+strconv.FormatInt(results.KeyID, 10),
repo_module.EnvAppURL+"="+setting.AppURL,
)
// to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
@@ -340,9 +343,9 @@ func runServ(ctx context.Context, c *cli.Command) error {
}
// Update user key activity.
if results.PublicKeyID > 0 {
if err = private.UpdatePublicKeyLastUsed(ctx, results.PublicKeyID, results.RepoID); err != nil {
return fail(ctx, "Failed to update public key", "UpdatePublicKeyLastUsed: %v", err)
if results.KeyID > 0 {
if err = private.UpdatePublicKeyInRepo(ctx, results.KeyID, results.RepoID); err != nil {
return fail(ctx, "Failed to update public key", "UpdatePublicKeyInRepo: %v", err)
}
}

View File

@@ -15,7 +15,6 @@ import (
"strings"
"time"
audit_model "gitea.dev/models/audit"
"gitea.dev/modules/container"
"gitea.dev/modules/graceful"
"gitea.dev/modules/gtprof"
@@ -27,7 +26,6 @@ import (
"gitea.dev/modules/util"
"gitea.dev/routers"
"gitea.dev/routers/install"
"gitea.dev/services/audit"
"github.com/felixge/fgprof"
"github.com/urfave/cli/v3"
@@ -228,14 +226,7 @@ func serveInstalled(c *cli.Command) error {
// Set up Chi routes
webRoutes := routers.NormalRoutes()
auditCtx := cliAuditContext(context.Background())
log.Info("Audit record output: %s", setting.Audit.RecordOutput)
audit.Record(auditCtx, audit_model.SystemStartup, nil, "version", setting.AppVer)
err := listen(webRoutes, true)
audit.Record(auditCtx, audit_model.SystemShutdown, nil)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
return err
@@ -253,8 +244,7 @@ func servePprof() {
_, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true)
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it.
log.Info("Starting pprof server on localhost:6060")
server := &http.Server{Addr: "localhost:6060", Handler: mux, ReadHeaderTimeout: 10 * time.Second}
log.Info("Stopped pprof server: %v", server.ListenAndServe())
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", mux))
finished()
}

View File

@@ -83,7 +83,6 @@ func runACME(listenAddr string, m http.Handler) error {
TrustedRoots: certPool,
Email: setting.AcmeEmail,
Agreed: setting.AcmeTOS,
Profile: setting.AcmeProfile,
DisableHTTPChallenge: !enableHTTPChallenge,
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
ListenHost: setting.HTTPAddr,
@@ -100,20 +99,37 @@ func runACME(listenAddr string, m http.Handler) error {
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
// Prefer keeping the existing cert and retrying renewals asynchronously.
ctx := graceful.GetManager().ShutdownContext()
err := magic.ManageSync(ctx, []string{setting.AppDomain})
err := magic.ManageSync(ctx, []string{setting.Domain})
if err != nil {
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.Domain)
if cacheErr != nil || cert.Expired() {
return errors.Join(err, cacheErr)
}
log.Error("ACME certificate manage failed; continuing with existing certificate: %v", err)
if err := magic.ManageAsync(ctx, []string{setting.AppDomain}); err != nil {
if err := magic.ManageAsync(ctx, []string{setting.Domain}); err != nil {
log.Error("Failed to start async ACME management: %v", err)
}
}
// certmagic only advertises its own ACME challenge protocol, applyTLSSettings appends ours
tlsConfig := applyTLSSettings(magic.TLSConfig())
tlsConfig := magic.TLSConfig()
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2")
if version := toTLSVersion(setting.SSLMinimumVersion); version != 0 {
tlsConfig.MinVersion = version
}
if version := toTLSVersion(setting.SSLMaximumVersion); version != 0 {
tlsConfig.MaxVersion = version
}
// Set curve preferences
if curves := toCurvePreferences(setting.SSLCurvePreferences); len(curves) > 0 {
tlsConfig.CurvePreferences = curves
}
// Set cipher suites
if ciphers := toTLSCiphers(setting.SSLCipherSuites); len(ciphers) > 0 {
tlsConfig.CipherSuites = ciphers
}
if enableHTTPChallenge {
go func() {

View File

@@ -12,82 +12,122 @@ import (
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"github.com/klauspost/cpuid/v2"
)
var tlsVersionStringMap = map[string]uint16{
"": tls.VersionTLS12, // Default to tls.VersionTLS12
"tlsv1.0": tls.VersionTLS10,
"tlsv1.1": tls.VersionTLS11,
"tlsv1.2": tls.VersionTLS12,
"tlsv1.3": tls.VersionTLS13,
}
// toTLSVersion returns 0 when unset or unknown, which keeps the crypto/tls default
func toTLSVersion(version string) uint16 {
version = strings.TrimSpace(strings.ToLower(version))
tlsVersion, ok := tlsVersionStringMap[version]
if !ok && version != "" {
tlsVersion, ok := tlsVersionStringMap[strings.TrimSpace(strings.ToLower(version))]
if !ok {
log.Warn("Unknown tls version: %s", version)
return 0
}
return tlsVersion
}
var curveStringMap = map[string]tls.CurveID{
"x25519": tls.X25519,
"p256": tls.CurveP256,
"p384": tls.CurveP384,
"p521": tls.CurveP521,
"mlkem1024": tls.MLKEM1024,
"x25519mlkem768": tls.X25519MLKEM768,
"secp256r1mlkem768": tls.SecP256r1MLKEM768,
"secp384r1mlkem1024": tls.SecP384r1MLKEM1024,
"x25519": tls.X25519,
"p256": tls.CurveP256,
"p384": tls.CurveP384,
"p521": tls.CurveP521,
}
// lookupAll resolves configured names, warning about and skipping the ones crypto/tls does not know
func lookupAll[T comparable](names []string, table map[string]T, kind string) []T {
values := make([]T, 0, len(names))
for _, name := range names {
value, ok := table[strings.TrimSpace(strings.ToLower(name))]
func toCurvePreferences(preferences []string) []tls.CurveID {
ids := make([]tls.CurveID, 0, len(preferences))
for _, pref := range preferences {
id, ok := curveStringMap[strings.TrimSpace(strings.ToLower(pref))]
if !ok {
log.Warn("Unknown %s: %s", kind, name)
continue
log.Warn("Unknown curve: %s", pref)
}
if id != 0 {
ids = append(ids, id)
}
values = append(values, value)
}
return values
return ids
}
// cipherStringMap is derived from crypto/tls so that suites Go adds or removes are picked up automatically
var cipherStringMap = buildCipherStringMap()
var cipherStringMap = map[string]uint16{
"rsa_with_rc4_128_sha": tls.TLS_RSA_WITH_RC4_128_SHA,
"rsa_with_3des_ede_cbc_sha": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
"rsa_with_aes_128_cbc_sha": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"rsa_with_aes_256_cbc_sha": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"rsa_with_aes_128_cbc_sha256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
"rsa_with_aes_128_gcm_sha256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
"rsa_with_aes_256_gcm_sha384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
"ecdhe_ecdsa_with_rc4_128_sha": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
"ecdhe_ecdsa_with_aes_128_cbc_sha": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"ecdhe_ecdsa_with_aes_256_cbc_sha": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"ecdhe_rsa_with_rc4_128_sha": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
"ecdhe_rsa_with_3des_ede_cbc_sha": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"ecdhe_rsa_with_aes_128_cbc_sha": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"ecdhe_rsa_with_aes_256_cbc_sha": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"ecdhe_ecdsa_with_aes_128_cbc_sha256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
"ecdhe_rsa_with_aes_128_cbc_sha256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
"ecdhe_rsa_with_aes_128_gcm_sha256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"ecdhe_ecdsa_with_aes_128_gcm_sha256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"ecdhe_rsa_with_aes_256_gcm_sha384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"ecdhe_ecdsa_with_aes_256_gcm_sha384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"ecdhe_rsa_with_chacha20_poly1305_sha256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
"ecdhe_ecdsa_with_chacha20_poly1305_sha256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
"ecdhe_rsa_with_chacha20_poly1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"ecdhe_ecdsa_with_chacha20_poly1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
"aes_128_gcm_sha256": tls.TLS_AES_128_GCM_SHA256,
"aes_256_gcm_sha384": tls.TLS_AES_256_GCM_SHA384,
"chacha20_poly1305_sha256": tls.TLS_CHACHA20_POLY1305_SHA256,
}
func buildCipherStringMap() map[string]uint16 {
ciphers := map[string]uint16{
// aliases for the two suites Go later renamed with a _SHA256 suffix
"ecdhe_rsa_with_chacha20_poly1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"ecdhe_ecdsa_with_chacha20_poly1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
}
for _, cipher := range append(tls.CipherSuites(), tls.InsecureCipherSuites()...) {
ciphers[strings.ToLower(strings.TrimPrefix(cipher.Name, "TLS_"))] = cipher.ID
func toTLSCiphers(cipherStrings []string) []uint16 {
ciphers := make([]uint16, 0, len(cipherStrings))
for _, cipherString := range cipherStrings {
cipher, ok := cipherStringMap[strings.TrimSpace(strings.ToLower(cipherString))]
if !ok {
log.Warn("Unknown cipher: %s", cipherString)
}
if cipher != 0 {
ciphers = append(ciphers, cipher)
}
}
return ciphers
}
// applyTLSSettings applies the configured [server] TLS options. Every option
// left unset keeps the crypto/tls default, which Go keeps current.
func applyTLSSettings(tlsConfig *tls.Config) *tls.Config {
// the listener is already TLS wrapped when it reaches http.Server, so it never sets ALPN for us
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2", "http/1.1")
tlsConfig.MinVersion = toTLSVersion(setting.SSLMinimumVersion)
tlsConfig.MaxVersion = toTLSVersion(setting.SSLMaximumVersion)
if curves := lookupAll(setting.SSLCurvePreferences, curveStringMap, "curve"); len(curves) > 0 {
tlsConfig.CurvePreferences = curves
// defaultCiphers uses hardware support to check if AES is specifically
// supported by the CPU.
//
// If AES is supported AES ciphers will be preferred over ChaCha based ciphers
// (This code is directly inspired by the certmagic code.)
func defaultCiphers() []uint16 {
if cpuid.CPU.Supports(cpuid.AESNI) {
return defaultCiphersAESfirst
}
if ciphers := lookupAll(setting.SSLCipherSuites, cipherStringMap, "cipher suite"); len(ciphers) > 0 {
tlsConfig.CipherSuites = ciphers
}
return tlsConfig
return defaultCiphersChaChaFirst
}
var (
defaultCiphersAES = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}
defaultCiphersChaCha = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}
defaultCiphersAESfirst = append(defaultCiphersAES, defaultCiphersChaCha...)
defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...)
)
// runHTTPS listens on the provided network address and then calls
// Serve to handle requests on incoming TLS connections.
//
@@ -96,7 +136,33 @@ func applyTLSSettings(tlsConfig *tls.Config) *tls.Config {
// certFile should be the concatenation of the server's certificate followed by the
// CA's certificate.
func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
tlsConfig := applyTLSSettings(&tls.Config{})
tlsConfig := &tls.Config{}
if tlsConfig.NextProtos == nil {
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
}
if version := toTLSVersion(setting.SSLMinimumVersion); version != 0 {
tlsConfig.MinVersion = version
}
if version := toTLSVersion(setting.SSLMaximumVersion); version != 0 {
tlsConfig.MaxVersion = version
}
// Set curve preferences
tlsConfig.CurvePreferences = []tls.CurveID{
tls.X25519,
tls.CurveP256,
}
if curves := toCurvePreferences(setting.SSLCurvePreferences); len(curves) > 0 {
tlsConfig.CurvePreferences = curves
}
// Set cipher suites
tlsConfig.CipherSuites = defaultCiphers()
if ciphers := toTLSCiphers(setting.SSLCipherSuites); len(ciphers) > 0 {
tlsConfig.CipherSuites = ciphers
}
tlsConfig.Certificates = make([]tls.Certificate, 1)
certPEMBlock, err := os.ReadFile(certFile)

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