Compare commits
13
Commits
a09f247290
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54eb69d1c7 | ||
|
|
0cbf267474 | ||
|
|
335a74010d | ||
|
|
4c9611a2d2 | ||
|
|
5681519b6f | ||
|
|
fd59158ed6 | ||
|
|
d9b2a4e787 | ||
|
|
c439c1b948 | ||
|
|
0fe85c67d9 | ||
|
|
09eecf5969 | ||
|
|
e444ea5936 | ||
|
|
c917e4908c | ||
|
|
ef849b085f |
+1
-1
@@ -6,7 +6,7 @@ keys:
|
|||||||
- &bfs-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
|
- &bfs-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
|
||||||
- &bfs-pol-server age1fpytf05sg9n6ywpwkmn09lhpfvgtud9h75h76jhxha475zpnasqq952rpu
|
- &bfs-pol-server age1fpytf05sg9n6ywpwkmn09lhpfvgtud9h75h76jhxha475zpnasqq952rpu
|
||||||
- &bfs-new-server age17yx98qk9gzgcf2q6zhhp05p6mmtrkgz66dvyk9gqclypvlr8rersxjy5v7
|
- &bfs-new-server age17yx98qk9gzgcf2q6zhhp05p6mmtrkgz66dvyk9gqclypvlr8rersxjy5v7
|
||||||
- &neuro-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
|
- &neuro-server age1ak7heljpr0pjr4m0rcwxgn3sp0jjxw03lxyf33r8lcemqh2u2sgqx0aplq
|
||||||
- &games-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
|
- &games-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
|
||||||
- &hectic-lab-server age13h8twnwvgxn04l5ywtru89a6psw5d0uckr2eghxsjp88a5augvsstq5ard
|
- &hectic-lab-server age13h8twnwvgxn04l5ywtru89a6psw5d0uckr2eghxsjp88a5augvsstq5ard
|
||||||
- &umbriel-bfs age1jxntjca8q2vxvf2jaal4xyvm2ae6sh62fhv897694kuzawfrk5asj00zdt
|
- &umbriel-bfs age1jxntjca8q2vxvf2jaal4xyvm2ae6sh62fhv897694kuzawfrk5asj00zdt
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
postgres-c = import ./postgres-c.nix { inherit self system pkgs; };
|
postgres-c = import ./postgres-c.nix { inherit self system pkgs; };
|
||||||
pure-c = import ./pure-c.nix { inherit self system pkgs; };
|
pure-c = import ./pure-c.nix { inherit self system pkgs; };
|
||||||
rust = import ./rust.nix { inherit self system pkgs; };
|
rust = import ./rust.nix { inherit self system pkgs; };
|
||||||
|
ratatui = import ./ratatui.nix { inherit self system pkgs; };
|
||||||
haskell = import ./haskell.nix { inherit self system pkgs; };
|
haskell = import ./haskell.nix { inherit self system pkgs; };
|
||||||
neuro = import ./neuro.nix { inherit self system pkgs; };
|
neuro = import ./neuro.nix { inherit self system pkgs; };
|
||||||
xmpp = import ./xmpp.nix { inherit self system pkgs; };
|
xmpp = import ./xmpp.nix { inherit self system pkgs; };
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
self,
|
||||||
|
pkgs,
|
||||||
|
system
|
||||||
|
}: let
|
||||||
|
rustToolchain =
|
||||||
|
if builtins.pathExists ./rust-toolchain.toml
|
||||||
|
then pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml
|
||||||
|
else pkgs.pkgsBuildHost.rust-bin.stable."1.81.0".default;
|
||||||
|
in
|
||||||
|
self.devShells.${system}.default
|
||||||
|
// (pkgs.mkShell {
|
||||||
|
nativeBuildInputs = [
|
||||||
|
rustToolchain
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.gcc
|
||||||
|
pkgs.gnumake
|
||||||
|
];
|
||||||
|
})
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
- [Using the `hectic` Attic Cache](./attic-cache.md)
|
- [Using the `hectic` Attic Cache](./attic-cache.md)
|
||||||
|
- [Minecraft incident log](./minecraft-incidents.md)
|
||||||
|
- [Project Zomboid backups](./project-zomboid-backups.md)
|
||||||
|
|
||||||
## Gitea runner labels
|
## Gitea runner labels
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Minecraft incident log
|
||||||
|
|
||||||
|
This file records only observed evidence, actions, and verification results.
|
||||||
|
An entity appearing in a stack trace is a trigger-path observation, not a
|
||||||
|
proven root cause.
|
||||||
|
|
||||||
|
## 2026-09-19 — WorldOfSosal crashes in Sable block-change handling
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
|
||||||
|
- `minecraft-server-wowMineMap.service` terminates while a player is online.
|
||||||
|
- Public Minecraft endpoint is `store.hectic-lab.com:25568`.
|
||||||
|
- Server is intentionally stopped after the latest crash to prevent repeated
|
||||||
|
crash-save cycles while recovery is investigated.
|
||||||
|
|
||||||
|
### Observed evidence
|
||||||
|
|
||||||
|
All crash reports contain `sable@2.0.5` in
|
||||||
|
`LevelAccelerator.getBlockState`, followed by
|
||||||
|
`ArrayIndexOutOfBoundsException` where the requested section index exceeds
|
||||||
|
the world section array length of `24`.
|
||||||
|
|
||||||
|
| UTC timestamp | Crash report | Observed trigger path | Exception |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 18:47:03 | `crash-2026-09-19_18.47.03-server.txt` | `EnderMan$EndermanTakeBlockGoal.tick` | index `38` / length `24` |
|
||||||
|
| 18:52:17 | `crash-2026-09-19_18.52.17-server.txt` | `GlowSquid.aiStep` → `RedStoneOreBlock.stepOn` | index `33` / length `24` |
|
||||||
|
| 19:14:46 | `crash-2026-09-19_19.14.46-server.txt` | `Skeleton.tick` → `RedStoneOreBlock.stepOn` | index `34` / length `24` |
|
||||||
|
|
||||||
|
Evidence locations on `neuro`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/srv/minecraft/wowMineMap/crash-reports/
|
||||||
|
/srv/minecraft/wowMineMap/logs/latest.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actions performed
|
||||||
|
|
||||||
|
| UTC timestamp | Action | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 17:51 | Archived current world before recovery | Archive checksum recorded |
|
||||||
|
| 18:08 | Set `randomTickSpeed=0` | Server started, but later crashed from an entity block change |
|
||||||
|
| 18:48 | Set `mobGriefing=false` | Prevented Enderman block pickup only; later crashes still occurred |
|
||||||
|
| 18:54 | Archived post-crash world | Archive checksum recorded |
|
||||||
|
| 19:00 | Moved Boss offline player NBT from `(3299.067, 142.630, 8613.742)` to `(3296, 500, 8608)` in `crafting_azeroth:azeroth` | Only `Pos` and `Dimension` changed; later crash still occurred |
|
||||||
|
| after 19:14 crash | Stopped `minecraft-server-wowMineMap.service` | Prevented further automatic crash/restart saves |
|
||||||
|
|
||||||
|
### Recovery artifacts
|
||||||
|
|
||||||
|
```text
|
||||||
|
/srv/minecraft/backups/wowMineMap-before-sable-recovery-20260919T175139Z.tar.zst
|
||||||
|
/srv/minecraft/backups/wowMineMap-after-sable-crashes-20260919T185445Z.tar.zst
|
||||||
|
/srv/minecraft/wowMineMap/world/playerdata/1c189af5-2713-3fa6-bcc4-893dfadedfa4.dat.before-relocation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conclusions supported by evidence
|
||||||
|
|
||||||
|
- Public proxy and reverse tunnel are not the failure point: server-list ping
|
||||||
|
succeeded before later in-world crashes.
|
||||||
|
- The failure is not limited to Endermen, random ticks, or one player
|
||||||
|
position.
|
||||||
|
- Sable's block-change callback is present in every captured crash.
|
||||||
|
|
||||||
|
### Not established
|
||||||
|
|
||||||
|
- Exact corrupt chunk, block, or mod data.
|
||||||
|
- Whether world data is corrupt, Sable itself is defective, or another mod is
|
||||||
|
supplying incompatible world state.
|
||||||
|
- Whether deleting any chunk, region, or Sable state would be safe.
|
||||||
|
|
||||||
|
### External research
|
||||||
|
|
||||||
|
No exact upstream match was found for Sable `2.0.5` on NeoForge `1.21.1` with
|
||||||
|
`LevelAccelerator.getBlockState` and a requested section index of `33`, `34`,
|
||||||
|
or `38` against a section array of length `24`.
|
||||||
|
|
||||||
|
Related but non-identical upstream reports:
|
||||||
|
|
||||||
|
- [Sable #776](https://github.com/ryanhcode/sable/issues/776) documents an
|
||||||
|
`ArrayIndexOutOfBoundsException` associated with unusual dimension height
|
||||||
|
bounds. This is relevant to section-coordinate handling, but is an older
|
||||||
|
version and different stack trace.
|
||||||
|
- [Sable #1087](https://github.com/ryanhcode/sable/issues/1087) documents a
|
||||||
|
`LevelAccelerator.getBlockState` recursion during block-shape processing.
|
||||||
|
The failure type differs.
|
||||||
|
- [Sable #820](https://github.com/ryanhcode/sable/issues/820) documents a
|
||||||
|
ticking-entity block-change crash. The reported downgrade to `1.1.3` helped
|
||||||
|
that distinct recursive-update failure; it is not evidence for this crash.
|
||||||
|
- [Sable #1223](https://github.com/ryanhcode/sable/issues/1223) documents a
|
||||||
|
different `ArrayIndexOutOfBoundsException` in voxel-neighborhood handling.
|
||||||
|
Its suggested Lithium setting only reduced crashes for some reporters and is
|
||||||
|
not a verified mitigation here.
|
||||||
|
|
||||||
|
Sable `2.0.4` and `2.0.5` release notes mention other block or contraption
|
||||||
|
crash fixes, but not this exception. No version upgrade or downgrade is
|
||||||
|
currently evidence-backed as a production fix.
|
||||||
|
|
||||||
|
### Next recovery step
|
||||||
|
|
||||||
|
Use a disposable full-world copy to test a supported Sable/physics integration
|
||||||
|
mitigation. Do not restart production, delete region files, or overwrite a
|
||||||
|
backup until that test gives reproducible evidence.
|
||||||
+55
-9
@@ -1,7 +1,7 @@
|
|||||||
# WorldOfSosal: Prism automatic updates
|
# WorldOfSosal: Prism automatic updates
|
||||||
|
|
||||||
The published client entry points are:
|
The published client entry points are:
|
||||||
- https://bfs.band/minecraft/ (BFS / Element host)
|
- https://store.bfs.band/minecraft/ (BFS / Element host)
|
||||||
- https://store.hectic-lab.com/minecraft/world-of-sosal/ (hectic-lab)
|
- https://store.hectic-lab.com/minecraft/world-of-sosal/ (hectic-lab)
|
||||||
|
|
||||||
Each site provides its own Prism ZIP with that site's update URL and matching
|
Each site provides its own Prism ZIP with that site's update URL and matching
|
||||||
@@ -95,8 +95,8 @@ Both services and firewall rules are in Nix and start on boot. The SSH client
|
|||||||
uses an explicit AES-CTR / HMAC-SHA256-ETM / curve25519 transport profile with
|
uses an explicit AES-CTR / HMAC-SHA256-ETM / curve25519 transport profile with
|
||||||
IPQoS=none, tested on the neuro-to-lab route. The default profile stalled after
|
IPQoS=none, tested on the neuro-to-lab route. The default profile stalled after
|
||||||
the handshake on this route. Both ends check peer liveness so stale listeners
|
the handshake on this route. Both ends check peer liveness so stale listeners
|
||||||
are eventually released. Minecraft retains
|
are eventually released. Minecraft initially used `online-mode=true`. It now uses offline mode at the
|
||||||
`online-mode=true`, requiring normal authenticated Minecraft accounts.
|
owner's request; see the RCON and authentication section below.
|
||||||
|
|
||||||
For a temporary direct local tunnel, use:
|
For a temporary direct local tunnel, use:
|
||||||
|
|
||||||
@@ -156,22 +156,22 @@ SHA-256 was verified before extraction.
|
|||||||
|
|
||||||
## Independent BFS entry point (2026-09-19)
|
## Independent BFS entry point (2026-09-19)
|
||||||
|
|
||||||
- Server: `bfs.band:25568`; downloads: https://bfs.band/minecraft/.
|
- Server: `wow.bfs.band`; downloads: https://store.bfs.band/minecraft/.
|
||||||
- BFS is `bfs.poland.xray` (91.198.166.181), the host of Element.
|
- BFS is `bfs.poland.xray` (91.198.166.181), the host of Element.
|
||||||
- `minecraft-wow-tunnel-bfs` connects neuro directly to BFS. The BFS path does
|
- `minecraft-wow-tunnel-bfs` connects neuro directly to BFS. The BFS path does
|
||||||
not transit hectic-lab; both tunnels have independent reconnecting services.
|
not transit hectic-lab; both tunnels have independent reconnecting services.
|
||||||
- Shared proxy implementation: `nixos/module/generic/minecraft-public-relay.nix`.
|
- Shared proxy implementation: `nixos/module/generic/minecraft-public-relay.nix`.
|
||||||
Host settings remain in `minecraft-wow-proxy.nix` (hectic-lab) and
|
Host settings remain in `minecraft-wow-proxy.nix` (hectic-lab) and
|
||||||
`minecraft-wow.nix` (BFS). Only `/minecraft/` is added to the existing BFS
|
`minecraft-wow.nix` (BFS). A dedicated HTTPS virtual host serves `store.bfs.band`. The legacy
|
||||||
nginx virtual host; Element/Matrix routes remain intact.
|
`bfs.band/minecraft/` URLs remain available for already imported instances.
|
||||||
- Downloaded BFS ZIP seeds `bfs.band:25568` and uses the stable manifest
|
- Downloaded BFS ZIP seeds `wow.bfs.band` and uses the stable manifest
|
||||||
`https://bfs.band/minecraft/world-of-sosal/current/pack.toml`. It does not
|
`https://store.bfs.band/minecraft/world-of-sosal/current/pack.toml`. It does not
|
||||||
redirect installation metadata to hectic-lab. Upstream mod and Java/loader
|
redirect installation metadata to hectic-lab. Upstream mod and Java/loader
|
||||||
downloads still use their original providers (e.g. Modrinth, GitHub, Mojang).
|
downloads still use their original providers (e.g. Modrinth, GitHub, Mojang).
|
||||||
- Existing hectic-lab instances can be migrated without reinstalling mods:
|
- Existing hectic-lab instances can be migrated without reinstalling mods:
|
||||||
in Edit / Settings / Custom commands, replace only the manifest URL in
|
in Edit / Settings / Custom commands, replace only the manifest URL in
|
||||||
Pre-launch command with the BFS URL above. Change the multiplayer server
|
Pre-launch command with the BFS URL above. Change the multiplayer server
|
||||||
address to bfs.band:25568. New users should import the ZIP from BFS.
|
address to wow.bfs.band. New users should import the ZIP from BFS.
|
||||||
- `script/publish-prism-mirrors.py` builds host-specific ZIPs from one archive
|
- `script/publish-prism-mirrors.py` builds host-specific ZIPs from one archive
|
||||||
and publishes both mirrors. It checks that the running neuro server's cached
|
and publishes both mirrors. It checks that the running neuro server's cached
|
||||||
archive has the same SHA-256. Each host's switch is atomic; publication across
|
archive has the same SHA-256. Each host's switch is atomic; publication across
|
||||||
@@ -183,3 +183,49 @@ Clean installation through the BFS manifest passed: all 141 client mods and
|
|||||||
all overrides match the source archive. A second updater run performed no
|
all overrides match the source archive. A second updater run performed no
|
||||||
downloads and preserved options.txt. The public BFS login protocol reached
|
downloads and preserved options.txt. The public BFS login protocol reached
|
||||||
online authentication; the earlier full GUI login used hectic-lab.
|
online authentication; the earlier full GUI login used hectic-lab.
|
||||||
|
|
||||||
|
## BFS DNS and dedicated download site (2026-09-19)
|
||||||
|
|
||||||
|
Porkbun DNS, TTL 600:
|
||||||
|
|
||||||
|
| Type | Name | Value |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| A | store.bfs.band | 91.198.166.181 |
|
||||||
|
| A | wow.bfs.band | 91.198.166.181 |
|
||||||
|
| SRV | _minecraft._tcp.wow.bfs.band | 0 0 25568 wow.bfs.band |
|
||||||
|
|
||||||
|
Players enter `wow.bfs.band` without a port in Minecraft Java. In Porkbun,
|
||||||
|
SRV Priority is `0`, and Target is `0 25568 wow.bfs.band` (weight, port, host).
|
||||||
|
The root download URL https://store.bfs.band/ redirects to the WorldOfSosal page.
|
||||||
|
The NixOS virtual host obtains and renews its HTTPS certificate automatically.
|
||||||
|
The publication script now seeds this update URL and the port-free game address.
|
||||||
|
Existing BFS instances retain working legacy update URLs; switching their
|
||||||
|
pre-launch manifest to the new store host is optional. Root bfs.band remains
|
||||||
|
the existing Element entry point.
|
||||||
|
|
||||||
|
## RCON and authentication (2026-09-19)
|
||||||
|
|
||||||
|
The WoW server now has `online-mode=false`. Account authentication is disabled;
|
||||||
|
player names can be impersonated, and offline UUIDs differ from online UUIDs.
|
||||||
|
Existing inventory/permissions may require a separate UUID migration.
|
||||||
|
|
||||||
|
RCON listens on TCP 25575 on neuro; its port is not opened in the firewall or
|
||||||
|
forwarded through the public Minecraft relays. The server-specific automatic
|
||||||
|
firewall is disabled and only game port 25567 is explicitly permitted.
|
||||||
|
A random password is stored in SOPS as `minecraft/rcon-password`, injected into
|
||||||
|
server.properties at startup with mode 0600, and is absent from the Nix store.
|
||||||
|
|
||||||
|
Start a local-only SSH tunnel and leave it running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -NT -L 127.0.0.1:25575:127.0.0.1:25575 -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 neuro
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve the password in another terminal (do not paste it into logs):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh neuro cat /run/secrets/minecraft/rcon-password
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the RCON client with host `127.0.0.1`, port `25575`, and that password.
|
||||||
|
There is no RCON username. These changes apply to wowMineMap only.
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Project Zomboid backups
|
||||||
|
|
||||||
|
`hectic.services."project-zomboid".backup` creates local backups without stopping
|
||||||
|
or pausing the server. The default schedule is every 30 minutes. Each run:
|
||||||
|
|
||||||
|
1. sends the local RCON `save` command and waits for the configured save grace
|
||||||
|
period;
|
||||||
|
2. rsyncs `Zomboid/Saves/Multiplayer/<serverName>` and non-secret server
|
||||||
|
settings (`SandboxVars`, spawn-points, and spawn-regions) from
|
||||||
|
`Zomboid/Server` into a private staging tree;
|
||||||
|
3. waits five seconds and repeats the rsync to narrow the live-write window;
|
||||||
|
4. publishes a timestamped `tar.zst` archive; and
|
||||||
|
5. deletes local archives older than `backup.retentionDays`.
|
||||||
|
|
||||||
|
The service lock prevents overlapping runs. Missing save or server-config paths
|
||||||
|
skip the run through systemd `ConditionPathExists` checks.
|
||||||
|
|
||||||
|
## Consistency and secrets
|
||||||
|
|
||||||
|
This is a best-effort backup. It does not stop Project Zomboid and does not use
|
||||||
|
an atomic filesystem snapshot. The RCON save command flushes the world before
|
||||||
|
copying, and the second rsync narrows the remaining live-write window, but
|
||||||
|
neither makes the filesystem copy an atomic snapshot.
|
||||||
|
|
||||||
|
Archives do not include the generated server INI, `admin-password`,
|
||||||
|
host-generated password files, or the S3 credentials file. The server INI is
|
||||||
|
generated again during service startup; provision secret-backed values separately
|
||||||
|
after a restore.
|
||||||
|
|
||||||
|
## hectic-lab
|
||||||
|
|
||||||
|
hectic-lab runs the timer every 30 minutes and keeps local archives for 14 days:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/project-zomboid/backups/archive/
|
||||||
|
```
|
||||||
|
|
||||||
|
Check it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl list-timers project-zomboid-backup.timer
|
||||||
|
systemctl status project-zomboid-backup.service
|
||||||
|
journalctl -u project-zomboid-backup.service
|
||||||
|
```
|
||||||
|
|
||||||
|
RCON is enabled on localhost port `27015`; the firewall does not expose this
|
||||||
|
port. The password is generated at
|
||||||
|
`/var/lib/project-zomboid/rcon-password` with mode `0600`. The server also uses
|
||||||
|
`SaveWorldEveryMinutes=15` as a periodic persistence fallback.
|
||||||
|
|
||||||
|
## Optional S3 upload
|
||||||
|
|
||||||
|
S3 upload is disabled by default. Enabling it requires `bucket`, `endpoint`,
|
||||||
|
`region`, and an absolute runtime `credentialsFile` outside `/nix/store`. The
|
||||||
|
endpoint must use HTTPS. systemd reads the environment file without executing
|
||||||
|
it; this host keeps it owned by `project-zomboid` with mode `0400`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
AWS_ACCESS_KEY_ID=...
|
||||||
|
AWS_SECRET_ACCESS_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `backup.s3.prefix` to choose the object-key prefix and
|
||||||
|
`backup.s3.remoteRetentionDays` to prune old archives from that prefix. Remote
|
||||||
|
deletion runs only after a successful upload and only matches this server's
|
||||||
|
archive name prefix. Configure bucket lifecycle expiration/versioning too when
|
||||||
|
available; it remains the stronger recovery and cleanup control.
|
||||||
|
|
||||||
|
## Restore
|
||||||
|
|
||||||
|
Restoring must be done while the server is stopped so it cannot modify files
|
||||||
|
during extraction:
|
||||||
|
|
||||||
|
The versioned helper creates a fresh current-state backup, stops the timer and
|
||||||
|
server, validates archive paths, restores the save, and starts both services:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ./docs/project-zomboid-restore.sh \
|
||||||
|
/var/lib/project-zomboid/backups/archive/<archive>.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
It writes a rollback archive named
|
||||||
|
`project-zomboid-<serverName>-pre-restore-<timestamp>.tar.zst` before changing
|
||||||
|
the save.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl stop project-zomboid.service
|
||||||
|
tar --zstd --no-same-owner --no-same-permissions \
|
||||||
|
-xf /var/lib/project-zomboid/backups/archive/<archive>.tar.zst \
|
||||||
|
-C /var/lib/project-zomboid
|
||||||
|
chown -R project-zomboid:project-zomboid /var/lib/project-zomboid/Zomboid
|
||||||
|
systemctl start project-zomboid.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-provision password files and secret-backed INI values before starting.
|
||||||
|
Verify the restored save and server name before allowing players to reconnect.
|
||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SERVER_NAME=${SERVER_NAME:-servertest}
|
||||||
|
DATA_DIR=${DATA_DIR:-/var/lib/project-zomboid}
|
||||||
|
ARCHIVE=${1:-}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf '%s\n' "Usage: $0 /path/to/project-zomboid-${SERVER_NAME}-<timestamp>.tar.zst"
|
||||||
|
printf '%s\n' "Environment: SERVER_NAME, DATA_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
printf '%s\n' 'Run as root.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ARCHIVE" ]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -r "$ARCHIVE" ]; then
|
||||||
|
printf 'Backup archive is not readable: %s\n' "$ARCHIVE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCHIVE_DIR="$DATA_DIR/backups/archive"
|
||||||
|
SAVE_DIR="$DATA_DIR/Zomboid/Saves/Multiplayer/$SERVER_NAME"
|
||||||
|
SERVER_DIR="$DATA_DIR/Zomboid/Server"
|
||||||
|
TMP_LIST=$(mktemp)
|
||||||
|
ROLLBACK_ARCHIVE=''
|
||||||
|
SERVER_STOPPED=0
|
||||||
|
RESTORE_SUCCEEDED=0
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$TMP_LIST"
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
status=$?
|
||||||
|
if [ "$status" -ne 0 ] && [ "$SERVER_STOPPED" -eq 1 ] \
|
||||||
|
&& [ "$RESTORE_SUCCEEDED" -eq 0 ] && [ -n "$ROLLBACK_ARCHIVE" ]; then
|
||||||
|
set +e
|
||||||
|
rm -rf "$SAVE_DIR"
|
||||||
|
rm -f \
|
||||||
|
"$SERVER_DIR/${SERVER_NAME}_SandboxVars.lua" \
|
||||||
|
"$SERVER_DIR/${SERVER_NAME}_spawnpoints.lua" \
|
||||||
|
"$SERVER_DIR/${SERVER_NAME}_spawnregions.lua"
|
||||||
|
tar --zstd --no-same-owner --no-same-permissions \
|
||||||
|
-xpf "$ROLLBACK_ARCHIVE" -C "$DATA_DIR"
|
||||||
|
chown -R project-zomboid:project-zomboid "$SAVE_DIR" "$SERVER_DIR"
|
||||||
|
systemctl start project-zomboid.service
|
||||||
|
systemctl start project-zomboid-backup.timer
|
||||||
|
printf '%s\n' "Restore failed; current state restored from $ROLLBACK_ARCHIVE" >&2
|
||||||
|
fi
|
||||||
|
cleanup
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap on_exit EXIT
|
||||||
|
|
||||||
|
if ! tar --zstd -tf "$ARCHIVE" >"$TMP_LIST"; then
|
||||||
|
printf 'Archive integrity check failed: %s\n' "$ARCHIVE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r member; do
|
||||||
|
case "$member" in
|
||||||
|
Zomboid/*) ;;
|
||||||
|
*)
|
||||||
|
printf 'Unsafe archive member: %s\n' "$member" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case "$member" in
|
||||||
|
/*|*../*)
|
||||||
|
printf 'Path traversal member: %s\n' "$member" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <"$TMP_LIST"
|
||||||
|
|
||||||
|
if ! systemctl start project-zomboid-backup.service; then
|
||||||
|
printf '%s\n' 'Could not create fresh backup of current state.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_ARCHIVE=$(find "$ARCHIVE_DIR" -maxdepth 1 -type f \
|
||||||
|
-name "project-zomboid-$SERVER_NAME-*.tar.zst" \
|
||||||
|
-printf '%T@ %p\n' | sort -nr | awk 'NR == 1 {sub(/^[^ ]* /, ""); print}')
|
||||||
|
|
||||||
|
if [ -z "$CURRENT_ARCHIVE" ]; then
|
||||||
|
printf '%s\n' 'Fresh current-state backup was not found.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
ROLLBACK_ARCHIVE="$ARCHIVE_DIR/project-zomboid-$SERVER_NAME-pre-restore-$stamp.tar.zst"
|
||||||
|
cp --reflink=auto "$CURRENT_ARCHIVE" "$ROLLBACK_ARCHIVE" 2>/dev/null \
|
||||||
|
|| cp "$CURRENT_ARCHIVE" "$ROLLBACK_ARCHIVE"
|
||||||
|
chmod 0600 "$ROLLBACK_ARCHIVE"
|
||||||
|
chown project-zomboid:project-zomboid "$ROLLBACK_ARCHIVE"
|
||||||
|
|
||||||
|
systemctl stop project-zomboid-backup.timer
|
||||||
|
systemctl stop project-zomboid.service
|
||||||
|
SERVER_STOPPED=1
|
||||||
|
|
||||||
|
if [ "$(systemctl show project-zomboid --property=ActiveState --value)" != inactive ]; then
|
||||||
|
printf '%s\n' 'Project Zomboid did not stop; refusing to restore.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$SAVE_DIR"
|
||||||
|
rm -f \
|
||||||
|
"$SERVER_DIR/${SERVER_NAME}_SandboxVars.lua" \
|
||||||
|
"$SERVER_DIR/${SERVER_NAME}_spawnpoints.lua" \
|
||||||
|
"$SERVER_DIR/${SERVER_NAME}_spawnregions.lua"
|
||||||
|
|
||||||
|
tar --zstd --no-same-owner --no-same-permissions \
|
||||||
|
-xpf "$ARCHIVE" -C "$DATA_DIR"
|
||||||
|
chown -R project-zomboid:project-zomboid "$SAVE_DIR" "$SERVER_DIR"
|
||||||
|
|
||||||
|
systemctl start project-zomboid.service
|
||||||
|
started=0
|
||||||
|
for _ in $(seq 1 90); do
|
||||||
|
if [ "$(systemctl show project-zomboid --property=ActiveState --value)" = active ] \
|
||||||
|
&& [ "$(systemctl show project-zomboid --property=SubState --value)" = running ]; then
|
||||||
|
started=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$started" -ne 1 ]; then
|
||||||
|
printf 'Restore completed, but service did not become healthy. Rollback: %s\n' \
|
||||||
|
"$ROLLBACK_ARCHIVE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl start project-zomboid-backup.timer
|
||||||
|
RESTORE_SUCCEEDED=1
|
||||||
|
printf 'Restore completed.\n'
|
||||||
|
printf 'Rollback archive: %s\n' "$ROLLBACK_ARCHIVE"
|
||||||
Generated
+17
@@ -915,6 +915,22 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs-gitea": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1790323409,
|
||||||
|
"narHash": "sha256-VVTPf+Hyd5ebpjBMHmrLMSBIeW6ls48Bqtosj7CNKLA=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "e94cb152ed51bd6e24eb4a41f1460252beb52cd2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1779796641,
|
"lastModified": 1779796641,
|
||||||
@@ -1018,6 +1034,7 @@
|
|||||||
"nixos-mailserver": "nixos-mailserver",
|
"nixos-mailserver": "nixos-mailserver",
|
||||||
"nixos-wsl": "nixos-wsl",
|
"nixos-wsl": "nixos-wsl",
|
||||||
"nixpkgs": "nixpkgs_2",
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"nixpkgs-gitea": "nixpkgs-gitea",
|
||||||
"nixvim": "nixvim",
|
"nixvim": "nixvim",
|
||||||
"rust-overlay": "rust-overlay_2",
|
"rust-overlay": "rust-overlay_2",
|
||||||
"sops-nix": "sops-nix"
|
"sops-nix": "sops-nix"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||||
|
nixpkgs-gitea.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
rust-overlay = {
|
rust-overlay = {
|
||||||
url = "github:oxalica/rust-overlay";
|
url = "github:oxalica/rust-overlay";
|
||||||
inputs = {
|
inputs = {
|
||||||
|
|||||||
@@ -37,6 +37,162 @@
|
|||||||
) cfg.sandboxProperties;
|
) cfg.sandboxProperties;
|
||||||
zomboidDir = "${cfg.dataDir}/Zomboid";
|
zomboidDir = "${cfg.dataDir}/Zomboid";
|
||||||
adminPasswordFile = "${cfg.dataDir}/admin-password";
|
adminPasswordFile = "${cfg.dataDir}/admin-password";
|
||||||
|
rconPasswordFile = cfg.rcon.passwordFile;
|
||||||
|
backupCfg = cfg.backup;
|
||||||
|
s3CredentialsFile = if backupCfg.s3.credentialsFile == null then "" else backupCfg.s3.credentialsFile;
|
||||||
|
s3Bucket = if backupCfg.s3.bucket == null then "" else backupCfg.s3.bucket;
|
||||||
|
s3Endpoint = if backupCfg.s3.endpoint == null then "" else backupCfg.s3.endpoint;
|
||||||
|
s3Region = if backupCfg.s3.region == null then "" else backupCfg.s3.region;
|
||||||
|
saveDir = "${zomboidDir}/Saves/Multiplayer/${cfg.serverName}";
|
||||||
|
serverConfigDir = "${zomboidDir}/Server";
|
||||||
|
backupScript = pkgs.writeShellScript "project-zomboid-backup" ''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
staging_dir=${lib.escapeShellArg backupCfg.stagingDir}
|
||||||
|
archive_dir=${lib.escapeShellArg backupCfg.archiveDir}
|
||||||
|
lock_file="$archive_dir/.backup.lock"
|
||||||
|
|
||||||
|
${pkgs.coreutils}/bin/install -d -m 0700 \
|
||||||
|
"$staging_dir/Zomboid/Saves/Multiplayer/${cfg.serverName}" \
|
||||||
|
"$staging_dir/Zomboid/Server" \
|
||||||
|
"$archive_dir"
|
||||||
|
|
||||||
|
exec 9>"$lock_file"
|
||||||
|
if ! ${pkgs.util-linux}/bin/flock -n 9; then
|
||||||
|
${pkgs.coreutils}/bin/printf '%s\n' 'Project Zomboid backup already running; skipping.' >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
${lib.optionalString cfg.rcon.enable ''
|
||||||
|
rcon_password="$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg rconPasswordFile})"
|
||||||
|
if [ -z "$rcon_password" ]; then
|
||||||
|
${pkgs.coreutils}/bin/printf '%s\n' 'Project Zomboid RCON password file is empty.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
${pkgs.rcon}/bin/rcon \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--port ${toString cfg.rcon.port} \
|
||||||
|
--password "$rcon_password" \
|
||||||
|
save
|
||||||
|
${pkgs.coreutils}/bin/sleep ${toString backupCfg.saveWaitSeconds}
|
||||||
|
''}
|
||||||
|
|
||||||
|
sync_staging() {
|
||||||
|
${pkgs.rsync}/bin/rsync -a --delete \
|
||||||
|
${lib.escapeShellArg "${saveDir}/"} \
|
||||||
|
"$staging_dir/Zomboid/Saves/Multiplayer/${cfg.serverName}/"
|
||||||
|
${pkgs.rsync}/bin/rsync -a --delete --delete-excluded \
|
||||||
|
--include=${lib.escapeShellArg "/${cfg.serverName}_SandboxVars.lua"} \
|
||||||
|
--include=${lib.escapeShellArg "/${cfg.serverName}_spawnpoints.lua"} \
|
||||||
|
--include=${lib.escapeShellArg "/${cfg.serverName}_spawnregions.lua"} \
|
||||||
|
--exclude='*' \
|
||||||
|
${lib.escapeShellArg "${serverConfigDir}/"} \
|
||||||
|
"$staging_dir/Zomboid/Server/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Second pass narrows, but cannot eliminate, live-save inconsistency.
|
||||||
|
sync_staging
|
||||||
|
${pkgs.coreutils}/bin/sleep 5
|
||||||
|
sync_staging
|
||||||
|
|
||||||
|
timestamp="$(${pkgs.coreutils}/bin/date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
archive_name="project-zomboid-${cfg.serverName}-$timestamp.tar.zst"
|
||||||
|
archive_tmp="$archive_dir/.$archive_name.tmp"
|
||||||
|
archive="$archive_dir/$archive_name"
|
||||||
|
trap '${pkgs.coreutils}/bin/rm -f "$archive_tmp"' EXIT
|
||||||
|
${pkgs.gnutar}/bin/tar \
|
||||||
|
--use-compress-program=${lib.escapeShellArg "${pkgs.zstd}/bin/zstd -T0"} \
|
||||||
|
-C "$staging_dir" -cf "$archive_tmp" Zomboid
|
||||||
|
${pkgs.coreutils}/bin/chmod 0600 "$archive_tmp"
|
||||||
|
${pkgs.coreutils}/bin/mv "$archive_tmp" "$archive"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
${pkgs.findutils}/bin/find "$archive_dir" -maxdepth 1 -type f \
|
||||||
|
-name ${lib.escapeShellArg "project-zomboid-${cfg.serverName}-*.tar.zst"} \
|
||||||
|
-mmin +${toString (backupCfg.retentionDays * 1440)} -delete
|
||||||
|
|
||||||
|
${lib.optionalString backupCfg.s3.enable ''
|
||||||
|
if [ -z "''${AWS_ACCESS_KEY_ID:-}" ] || [ -z "''${AWS_SECRET_ACCESS_KEY:-}" ]; then
|
||||||
|
${pkgs.coreutils}/bin/printf '%s\n' \
|
||||||
|
'AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY missing from Project Zomboid S3 credentials file.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
s3_bucket=${lib.escapeShellArg s3Bucket}
|
||||||
|
s3_prefix=${lib.escapeShellArg backupCfg.s3.prefix}
|
||||||
|
s3_key="''${s3_prefix:+$s3_prefix/}$archive_name"
|
||||||
|
${pkgs.awscli2}/bin/aws s3 cp "$archive" \
|
||||||
|
"s3://$s3_bucket/$s3_key" \
|
||||||
|
--endpoint-url ${lib.escapeShellArg s3Endpoint} \
|
||||||
|
--region ${lib.escapeShellArg s3Region} \
|
||||||
|
--cli-connect-timeout 30 \
|
||||||
|
--cli-read-timeout 300 \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
remote_prefix="$s3_prefix"
|
||||||
|
if [ -n "$remote_prefix" ]; then
|
||||||
|
remote_prefix="$remote_prefix/"
|
||||||
|
fi
|
||||||
|
archive_prefix=${lib.escapeShellArg "project-zomboid-${cfg.serverName}-"}
|
||||||
|
remote_list="$staging_dir/.remote-objects.json"
|
||||||
|
remote_delete_dir="$staging_dir/.remote-delete"
|
||||||
|
${pkgs.awscli2}/bin/aws s3api list-objects-v2 \
|
||||||
|
--bucket "$s3_bucket" \
|
||||||
|
--prefix "$remote_prefix" \
|
||||||
|
--endpoint-url ${lib.escapeShellArg s3Endpoint} \
|
||||||
|
--region ${lib.escapeShellArg s3Region} \
|
||||||
|
--output json > "$remote_list"
|
||||||
|
${pkgs.python3}/bin/python3 - "$remote_list" "$remote_delete_dir" \
|
||||||
|
"$(( $(${pkgs.coreutils}/bin/date +%s) - ${toString (backupCfg.s3.remoteRetentionDays * 86400)} ))" \
|
||||||
|
"$remote_prefix$archive_prefix" <<'PY'
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
objects_path, delete_dir, cutoff, key_prefix = sys.argv[1:]
|
||||||
|
cutoff = int(cutoff)
|
||||||
|
archive_pattern = re.compile(
|
||||||
|
re.escape(key_prefix) + r"\d{8}T\d{6}Z\.tar\.zst\Z"
|
||||||
|
)
|
||||||
|
with open(objects_path, encoding="utf-8") as stream:
|
||||||
|
objects = json.load(stream).get("Contents", [])
|
||||||
|
|
||||||
|
old_keys = []
|
||||||
|
for item in objects:
|
||||||
|
key = item.get("Key", "")
|
||||||
|
if not archive_pattern.fullmatch(key):
|
||||||
|
continue
|
||||||
|
modified = datetime.datetime.fromisoformat(
|
||||||
|
item["LastModified"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if int(modified.timestamp()) < cutoff:
|
||||||
|
old_keys.append(key)
|
||||||
|
|
||||||
|
os.makedirs(delete_dir, exist_ok=True)
|
||||||
|
for batch_number in range(0, len(old_keys), 1000):
|
||||||
|
batch = old_keys[batch_number:batch_number + 1000]
|
||||||
|
manifest_path = os.path.join(
|
||||||
|
delete_dir, f"batch-{batch_number // 1000:04d}.json"
|
||||||
|
)
|
||||||
|
with open(manifest_path, "w", encoding="utf-8") as stream:
|
||||||
|
json.dump(
|
||||||
|
{"Objects": [{"Key": key} for key in batch], "Quiet": True},
|
||||||
|
stream,
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
for remote_manifest in "$remote_delete_dir"/*.json; do
|
||||||
|
[ -f "$remote_manifest" ] || continue
|
||||||
|
${pkgs.awscli2}/bin/aws s3api delete-objects \
|
||||||
|
--bucket "$s3_bucket" \
|
||||||
|
--delete "file://$remote_manifest" \
|
||||||
|
--endpoint-url ${lib.escapeShellArg s3Endpoint} \
|
||||||
|
--region ${lib.escapeShellArg s3Region} \
|
||||||
|
--only-show-errors
|
||||||
|
done
|
||||||
|
${pkgs.coreutils}/bin/rm -rf "$remote_list" "$remote_delete_dir"
|
||||||
|
''}
|
||||||
|
'';
|
||||||
startScript = pkgs.writeShellScript "project-zomboid-start" ''
|
startScript = pkgs.writeShellScript "project-zomboid-start" ''
|
||||||
admin_password=$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg adminPasswordFile})
|
admin_password=$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg adminPasswordFile})
|
||||||
exec ${pkgs.steam-run}/bin/steam-run \
|
exec ${pkgs.steam-run}/bin/steam-run \
|
||||||
@@ -131,9 +287,149 @@ in {
|
|||||||
default = true;
|
default = true;
|
||||||
description = "Open the Project Zomboid UDP ports in the firewall.";
|
description = "Open the Project Zomboid UDP ports in the firewall.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rcon = {
|
||||||
|
enable = lib.mkEnableOption "local RCON for Project Zomboid automation";
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 27015;
|
||||||
|
description = "RCON TCP port; not opened in the firewall by this module.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "${cfg.dataDir}/rcon-password";
|
||||||
|
description = "Runtime file containing the generated RCON password.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
backup = {
|
||||||
|
enable = lib.mkEnableOption "no-stop Project Zomboid backups";
|
||||||
|
|
||||||
|
onCalendar = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "*:0/30";
|
||||||
|
description = "systemd calendar expression controlling backup frequency.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stagingDir = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "${cfg.dataDir}/backups/staging";
|
||||||
|
description = "Local directory containing the two-pass rsync staging tree.";
|
||||||
|
};
|
||||||
|
|
||||||
|
archiveDir = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "${cfg.dataDir}/backups/archive";
|
||||||
|
description = "Local directory containing timestamped tar.zst archives.";
|
||||||
|
};
|
||||||
|
|
||||||
|
retentionDays = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 14;
|
||||||
|
description = "Delete local archives older than this many days.";
|
||||||
|
};
|
||||||
|
|
||||||
|
saveWaitSeconds = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 10;
|
||||||
|
description = "Seconds to wait after the RCON save command before rsync.";
|
||||||
|
};
|
||||||
|
|
||||||
|
s3 = {
|
||||||
|
enable = lib.mkEnableOption "uploading Project Zomboid backups to S3-compatible storage";
|
||||||
|
|
||||||
|
credentialsFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Runtime env file containing AWS_ACCESS_KEY_ID and
|
||||||
|
AWS_SECRET_ACCESS_KEY. Required when S3 upload is enabled.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
bucket = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "S3 bucket receiving backup archives.";
|
||||||
|
};
|
||||||
|
|
||||||
|
endpoint = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "S3-compatible endpoint URL.";
|
||||||
|
};
|
||||||
|
|
||||||
|
region = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "S3 region passed to awscli2.";
|
||||||
|
};
|
||||||
|
|
||||||
|
prefix = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "project-zomboid";
|
||||||
|
description = "Optional object key prefix within the S3 bucket.";
|
||||||
|
};
|
||||||
|
|
||||||
|
remoteRetentionDays = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 14;
|
||||||
|
description = "Delete uploaded archives older than this many days.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = !cfg.rcon.enable || (
|
||||||
|
lib.hasPrefix "/" cfg.rcon.passwordFile
|
||||||
|
&& !lib.hasPrefix "/nix/store/" cfg.rcon.passwordFile
|
||||||
|
);
|
||||||
|
message = "hectic.services.project-zomboid.rcon.passwordFile must be a runtime path outside /nix/store.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !backupCfg.s3.enable || backupCfg.enable;
|
||||||
|
message = "hectic.services.project-zomboid.backup must be enabled before S3 upload.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !backupCfg.s3.enable || backupCfg.s3.credentialsFile != null;
|
||||||
|
message = "hectic.services.project-zomboid.backup.s3.credentialsFile is required when S3 upload is enabled.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !backupCfg.s3.enable || backupCfg.s3.bucket != null;
|
||||||
|
message = "hectic.services.project-zomboid.backup.s3.bucket is required when S3 upload is enabled.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !backupCfg.s3.enable || backupCfg.s3.endpoint != null;
|
||||||
|
message = "hectic.services.project-zomboid.backup.s3.endpoint is required when S3 upload is enabled.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !backupCfg.s3.enable || backupCfg.s3.region != null;
|
||||||
|
message = "hectic.services.project-zomboid.backup.s3.region is required when S3 upload is enabled.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
!backupCfg.s3.enable
|
||||||
|
|| backupCfg.s3.credentialsFile == null
|
||||||
|
|| (
|
||||||
|
lib.hasPrefix "/" backupCfg.s3.credentialsFile
|
||||||
|
&& !lib.hasPrefix "/nix/store/" backupCfg.s3.credentialsFile
|
||||||
|
);
|
||||||
|
message = "hectic.services.project-zomboid.backup.s3.credentialsFile must be a runtime path outside /nix/store.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
!backupCfg.s3.enable
|
||||||
|
|| backupCfg.s3.endpoint == null
|
||||||
|
|| lib.hasPrefix "https://" backupCfg.s3.endpoint;
|
||||||
|
message = "hectic.services.project-zomboid.backup.s3.endpoint must use HTTPS.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
users.groups.project-zomboid = { };
|
users.groups.project-zomboid = { };
|
||||||
users.users.project-zomboid = {
|
users.users.project-zomboid = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
@@ -145,6 +441,11 @@ in {
|
|||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d ${cfg.dataDir} 0750 project-zomboid project-zomboid - -"
|
"d ${cfg.dataDir} 0750 project-zomboid project-zomboid - -"
|
||||||
"d ${cfg.installDir} 0750 project-zomboid project-zomboid - -"
|
"d ${cfg.installDir} 0750 project-zomboid project-zomboid - -"
|
||||||
|
] ++ lib.optionals backupCfg.enable [
|
||||||
|
"d ${cfg.dataDir}/backups 0700 project-zomboid project-zomboid - -"
|
||||||
|
"Z ${cfg.dataDir}/backups 0700 project-zomboid project-zomboid - -"
|
||||||
|
"d ${backupCfg.stagingDir} 0700 project-zomboid project-zomboid - -"
|
||||||
|
"d ${backupCfg.archiveDir} 0700 project-zomboid project-zomboid - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.project-zomboid = {
|
systemd.services.project-zomboid = {
|
||||||
@@ -160,6 +461,22 @@ in {
|
|||||||
umask 077
|
umask 077
|
||||||
${pkgs.openssl}/bin/openssl rand -base64 32 > ${lib.escapeShellArg adminPasswordFile}
|
${pkgs.openssl}/bin/openssl rand -base64 32 > ${lib.escapeShellArg adminPasswordFile}
|
||||||
fi
|
fi
|
||||||
|
${lib.optionalString cfg.rcon.enable ''
|
||||||
|
if [ ! -s ${lib.escapeShellArg rconPasswordFile} ]; then
|
||||||
|
umask 077
|
||||||
|
${pkgs.openssl}/bin/openssl rand -hex 32 > ${lib.escapeShellArg rconPasswordFile}
|
||||||
|
else
|
||||||
|
rcon_password=$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg rconPasswordFile})
|
||||||
|
case "$rcon_password" in
|
||||||
|
*[!0123456789abcdefABCDEF]*)
|
||||||
|
umask 077
|
||||||
|
${pkgs.openssl}/bin/openssl rand -hex 32 > ${lib.escapeShellArg rconPasswordFile}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
${pkgs.coreutils}/bin/chown project-zomboid:project-zomboid ${lib.escapeShellArg rconPasswordFile}
|
||||||
|
${pkgs.coreutils}/bin/chmod 0600 ${lib.escapeShellArg rconPasswordFile}
|
||||||
|
''}
|
||||||
${pkgs.steamcmd}/bin/steamcmd \
|
${pkgs.steamcmd}/bin/steamcmd \
|
||||||
+force_install_dir ${lib.escapeShellArg cfg.installDir} \
|
+force_install_dir ${lib.escapeShellArg cfg.installDir} \
|
||||||
+login anonymous \
|
+login anonymous \
|
||||||
@@ -176,6 +493,12 @@ in {
|
|||||||
) configLines}
|
) configLines}
|
||||||
${lib.optionalString (cfg.serverPropertiesFile != null)
|
${lib.optionalString (cfg.serverPropertiesFile != null)
|
||||||
"${pkgs.coreutils}/bin/cat ${lib.escapeShellArg cfg.serverPropertiesFile};"}
|
"${pkgs.coreutils}/bin/cat ${lib.escapeShellArg cfg.serverPropertiesFile};"}
|
||||||
|
${lib.optionalString cfg.rcon.enable ''
|
||||||
|
${pkgs.coreutils}/bin/printf '%s\n' ${lib.escapeShellArg "RCONPort=${toString cfg.rcon.port}"};
|
||||||
|
${pkgs.coreutils}/bin/printf '%s' 'RCONPassword=';
|
||||||
|
${pkgs.coreutils}/bin/cat ${lib.escapeShellArg rconPasswordFile};
|
||||||
|
${pkgs.coreutils}/bin/printf '\n';
|
||||||
|
''}
|
||||||
} > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}.ini"}
|
} > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}.ini"}
|
||||||
${lib.optionalString (cfg.sandboxProperties != { }) ''
|
${lib.optionalString (cfg.sandboxProperties != { }) ''
|
||||||
{
|
{
|
||||||
@@ -186,6 +509,10 @@ in {
|
|||||||
${pkgs.coreutils}/bin/printf '%s\n' '};';
|
${pkgs.coreutils}/bin/printf '%s\n' '};';
|
||||||
} > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}_SandboxVars.lua"}
|
} > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}_SandboxVars.lua"}
|
||||||
''}
|
''}
|
||||||
|
${lib.optionalString (cfg.sandboxProperties == { }) ''
|
||||||
|
${pkgs.coreutils}/bin/rm -f \
|
||||||
|
${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}_SandboxVars.lua"}
|
||||||
|
''}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
@@ -205,6 +532,34 @@ in {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.project-zomboid-backup = lib.mkIf backupCfg.enable {
|
||||||
|
description = "No-stop Project Zomboid backup";
|
||||||
|
after = [ "project-zomboid.service" ];
|
||||||
|
unitConfig.ConditionPathExists = [
|
||||||
|
saveDir
|
||||||
|
serverConfigDir
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "project-zomboid";
|
||||||
|
Group = "project-zomboid";
|
||||||
|
ExecStart = backupScript;
|
||||||
|
TimeoutStartSec = "30min";
|
||||||
|
UMask = "0077";
|
||||||
|
} // lib.optionalAttrs backupCfg.s3.enable {
|
||||||
|
EnvironmentFile = s3CredentialsFile;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.project-zomboid-backup = lib.mkIf backupCfg.enable {
|
||||||
|
description = "Run Project Zomboid backup";
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = backupCfg.onCalendar;
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [
|
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [
|
||||||
cfg.port
|
cfg.port
|
||||||
cfg.udpPort
|
cfg.udpPort
|
||||||
|
|||||||
@@ -6,6 +6,20 @@
|
|||||||
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay";
|
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay";
|
||||||
};
|
};
|
||||||
systemd.tmpfiles.rules = [ "d /var/www/store/minecraft/world-of-sosal 0755 root root -" ];
|
systemd.tmpfiles.rules = [ "d /var/www/store/minecraft/world-of-sosal 0755 root root -" ];
|
||||||
|
services.nginx.virtualHosts."store.bfs.band" = {
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
root = "/var/www/store";
|
||||||
|
locations."= /".return = "302 /minecraft/world-of-sosal/";
|
||||||
|
locations."= /minecraft".return = "302 /minecraft/world-of-sosal/";
|
||||||
|
locations."= /minecraft/".return = "302 /minecraft/world-of-sosal/";
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
autoindex off;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# Keep old pack URLs working for already imported Prism instances.
|
||||||
services.nginx.virtualHosts."bfs.band".locations = {
|
services.nginx.virtualHosts."bfs.band".locations = {
|
||||||
"= /minecraft".return = "302 /minecraft/world-of-sosal/";
|
"= /minecraft".return = "302 /minecraft/world-of-sosal/";
|
||||||
"= /minecraft/".return = "302 /minecraft/world-of-sosal/";
|
"= /minecraft/".return = "302 /minecraft/world-of-sosal/";
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ let
|
|||||||
UsePAM yes
|
UsePAM yes
|
||||||
AuthenticationMethods publickey
|
AuthenticationMethods publickey
|
||||||
AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u
|
AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u
|
||||||
LogLevel DEBUG3
|
LogLevel INFO
|
||||||
|
|
||||||
VersionAddendum none
|
VersionAddendum none
|
||||||
HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
|
HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
|
||||||
@@ -65,7 +65,8 @@ in
|
|||||||
Type = "simple";
|
Type = "simple";
|
||||||
StateDirectory = "experimental-sshd";
|
StateDirectory = "experimental-sshd";
|
||||||
RuntimeDirectory = "experimental-sshd";
|
RuntimeDirectory = "experimental-sshd";
|
||||||
ExecStart = "${pkgs.openssh}/bin/sshd -D -e -f /etc/ssh/experimental-sshd_config";
|
ExecStart = "${pkgs.openssh}/bin/sshd -D -f /etc/ssh/experimental-sshd_config";
|
||||||
|
StandardError = "journal";
|
||||||
};
|
};
|
||||||
preStart = ''
|
preStart = ''
|
||||||
${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config
|
${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ with builtins;
|
|||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
domain = "hectic-lab.com";
|
domain = "hectic-lab.com";
|
||||||
sshPort = 22;
|
giteaSshPort = 22223;
|
||||||
mailUserNames = [
|
mailUserNames = [
|
||||||
"security"
|
"security"
|
||||||
"founders"
|
"founders"
|
||||||
@@ -103,11 +103,25 @@ in {
|
|||||||
};
|
};
|
||||||
services."project-zomboid" = {
|
services."project-zomboid" = {
|
||||||
enable = true;
|
enable = true;
|
||||||
memory = "3g";
|
memory = "4g";
|
||||||
serverName = "servertest";
|
serverName = "servertest";
|
||||||
serverPropertiesFile = /var/lib/project-zomboid/server-password.ini;
|
serverPropertiesFile = /var/lib/project-zomboid/server-password.ini;
|
||||||
|
rcon.enable = true;
|
||||||
|
backup = {
|
||||||
|
enable = true;
|
||||||
|
onCalendar = "*:0/30";
|
||||||
|
retentionDays = 14;
|
||||||
|
s3 = {
|
||||||
|
enable = true;
|
||||||
|
bucket = "backup-hectic-lab";
|
||||||
|
endpoint = "https://hel1.your-objectstorage.com";
|
||||||
|
region = "hel1";
|
||||||
|
credentialsFile = "/var/lib/project-zomboid/s3-credentials";
|
||||||
|
};
|
||||||
|
};
|
||||||
serverProperties = {
|
serverProperties = {
|
||||||
Map = "vehicle_interior_arcadia75;Muldraugh, KY";
|
Map = "Muldraugh, KY";
|
||||||
|
SaveWorldEveryMinutes = 15;
|
||||||
DoLuaChecksum = false;
|
DoLuaChecksum = false;
|
||||||
Public = true;
|
Public = true;
|
||||||
AntiCheatSafety = 4;
|
AntiCheatSafety = 4;
|
||||||
@@ -127,54 +141,42 @@ in {
|
|||||||
AntiCheatNoClip = 4;
|
AntiCheatNoClip = 4;
|
||||||
AntiCheatServerCustomization = 4;
|
AntiCheatServerCustomization = 4;
|
||||||
};
|
};
|
||||||
|
workshopItems = [
|
||||||
|
"3676456221" # Lua Digital Watch Framework
|
||||||
|
"3600401184" # Realistic Temperature Mod
|
||||||
|
];
|
||||||
|
mods = [
|
||||||
|
"\\LuaDigitalWatchUI"
|
||||||
|
"\\RC_RealisticColdMod"
|
||||||
|
];
|
||||||
sandboxProperties = {
|
sandboxProperties = {
|
||||||
StartMonth = 12;
|
StartMonth = 12;
|
||||||
StartDay = 1;
|
StartDay = 1;
|
||||||
WaterShut = 3;
|
WaterShut = 3;
|
||||||
WaterShutModifier = 60;
|
WaterShutModifier = 150;
|
||||||
ElecShut = 3;
|
ElecShut = 3;
|
||||||
ElecShutModifier = 60;
|
ElecShutModifier = 150;
|
||||||
MinutesPerPage = 0.5;
|
MinutesPerPage = 0.5;
|
||||||
|
Zombies = 4;
|
||||||
|
ZombieConfig = {
|
||||||
|
PopulationMultiplier = 1.3;
|
||||||
|
PopulationStartMultiplier = 1.0;
|
||||||
|
PopulationPeakMultiplier = 1.0;
|
||||||
|
RespawnHours = 0.0;
|
||||||
|
RespawnUnseenHours = 0.0;
|
||||||
|
RespawnMultiplier = 0.0;
|
||||||
|
RedistributeHours = 0.0;
|
||||||
|
};
|
||||||
ZombieLore = {
|
ZombieLore = {
|
||||||
Transmission = 4;
|
Transmission = 4;
|
||||||
Mortality = 7;
|
Mortality = 7;
|
||||||
|
Speed = 2;
|
||||||
|
SprinterPercentage = 0;
|
||||||
|
Strength = 2;
|
||||||
|
Cognition = 2;
|
||||||
|
DoorOpeningPercentage = 10;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
workshopItems = [
|
|
||||||
"3773972040" # Arcadia RV Interiors B42.20
|
|
||||||
"2210760610" # Cryogenic Winter +Easy/Hard Modes
|
|
||||||
"3676456221" # Lua Digital Watch Framework
|
|
||||||
"3600401184" # Realistic Temperature Mod
|
|
||||||
"3387824513" # Material Weight Reducer
|
|
||||||
"3413150945" # More Damaged Objects
|
|
||||||
"3512708849" # Shotgun Trajectory
|
|
||||||
"3401576145" # Firearm Models: Redux
|
|
||||||
"3401134276" # Vanilla Gear Expanded
|
|
||||||
"2956146279" # Rain Cleans Blood
|
|
||||||
"3693258802" # Tactical Hold
|
|
||||||
"3394588830" # Simple Flashlight on Belt
|
|
||||||
"2684285534" # Spongie's Clothing
|
|
||||||
"2812326159" # Spongie's Open Jackets
|
|
||||||
];
|
|
||||||
mods = [
|
|
||||||
"\\ArcadiaRVInterior_B42_MP"
|
|
||||||
"\\ArcadiaRVInterior_B42_Vanilla"
|
|
||||||
"\\CryogenicWinter2NormalMode"
|
|
||||||
"\\LuaDigitalWatchUI"
|
|
||||||
"\\RC_RealisticColdMod"
|
|
||||||
"\\Material Weight Reducer"
|
|
||||||
"\\Ammunition Weight Reducer"
|
|
||||||
"\\MoreDamagedObjects"
|
|
||||||
"\\ShotgunTrajectory"
|
|
||||||
"\\FMR"
|
|
||||||
"\\VanillaGearExpanded"
|
|
||||||
"\\RainCleansBlood"
|
|
||||||
"\\TacHold Complete"
|
|
||||||
"\\LightOnBelt"
|
|
||||||
"\\SpnCloth"
|
|
||||||
"\\SpnOpenClothBase"
|
|
||||||
"\\SpnOpenCloth"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
services.p4d = {
|
services.p4d = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -265,12 +267,37 @@ in {
|
|||||||
"jwt-secret"
|
"jwt-secret"
|
||||||
"s3-access-key"
|
"s3-access-key"
|
||||||
"s3-secret-key"
|
"s3-secret-key"
|
||||||
]);
|
]) // {
|
||||||
|
"project-zomboid/s3-access-key" = {
|
||||||
|
key = "ente/s3-access-key";
|
||||||
|
owner = "project-zomboid";
|
||||||
|
group = "project-zomboid";
|
||||||
|
};
|
||||||
|
"project-zomboid/s3-secret-key" = {
|
||||||
|
key = "ente/s3-secret-key";
|
||||||
|
owner = "project-zomboid";
|
||||||
|
group = "project-zomboid";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.project-zomboid.preStart = lib.mkBefore ''
|
systemd.services.project-zomboid.preStart = lib.mkBefore ''
|
||||||
password_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password"}
|
password_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password"}
|
||||||
properties_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password.ini"}
|
properties_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password.ini"}
|
||||||
|
s3_credentials_file=${lib.escapeShellArg "/var/lib/project-zomboid/s3-credentials"}
|
||||||
|
s3_credentials_tmp="$(${pkgs.coreutils}/bin/mktemp "''${s3_credentials_file}.XXXXXX")"
|
||||||
|
trap '${pkgs.coreutils}/bin/rm -f "$s3_credentials_tmp"' EXIT
|
||||||
|
|
||||||
|
{
|
||||||
|
${pkgs.coreutils}/bin/printf 'AWS_ACCESS_KEY_ID='
|
||||||
|
${pkgs.coreutils}/bin/cat ${lib.escapeShellArg config.sops.secrets."project-zomboid/s3-access-key".path}
|
||||||
|
${pkgs.coreutils}/bin/printf '\n'
|
||||||
|
${pkgs.coreutils}/bin/printf 'AWS_SECRET_ACCESS_KEY='
|
||||||
|
${pkgs.coreutils}/bin/cat ${lib.escapeShellArg config.sops.secrets."project-zomboid/s3-secret-key".path}
|
||||||
|
${pkgs.coreutils}/bin/printf '\n'
|
||||||
|
} > "$s3_credentials_tmp"
|
||||||
|
${pkgs.coreutils}/bin/chmod 0400 "$s3_credentials_tmp"
|
||||||
|
${pkgs.coreutils}/bin/mv -f "$s3_credentials_tmp" "$s3_credentials_file"
|
||||||
|
|
||||||
if [ ! -s "$password_file" ] || ! ${pkgs.gnugrep}/bin/grep -Eq '^[0-9a-f]{48}$' "$password_file"; then
|
if [ ! -s "$password_file" ] || ! ${pkgs.gnugrep}/bin/grep -Eq '^[0-9a-f]{48}$' "$password_file"; then
|
||||||
umask 077
|
umask 077
|
||||||
@@ -315,7 +342,7 @@ in {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.openssh.ports = [ sshPort ];
|
services.openssh.ports = [ giteaSshPort ];
|
||||||
|
|
||||||
services.mailserver = {
|
services.mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -337,7 +364,7 @@ in {
|
|||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
sshPort # ssh
|
giteaSshPort # gitea ssh
|
||||||
80
|
80
|
||||||
443
|
443
|
||||||
3306 # mysql
|
3306 # mysql
|
||||||
@@ -507,6 +534,9 @@ in {
|
|||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
proxy_pass http://127.0.0.1:11011/;
|
proxy_pass http://127.0.0.1:11011/;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -516,15 +546,21 @@ in {
|
|||||||
gitea = {
|
gitea = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.hectic.gitea-heatmap;
|
package = pkgs.hectic.gitea-heatmap;
|
||||||
settings.service.DISABLE_REGISTRATION = false;
|
# Keep LFS storage limited to accounts provisioned by administrators.
|
||||||
|
settings.service.DISABLE_REGISTRATION = true;
|
||||||
|
settings.session.COOKIE_SECURE = true;
|
||||||
settings.actions.ENABLED = true;
|
settings.actions.ENABLED = true;
|
||||||
# Long CUDA builds must not hit Gitea's default three-hour task watchdog.
|
# Long CUDA builds must not hit Gitea's default three-hour task watchdog.
|
||||||
settings.actions.ENDLESS_TASK_TIMEOUT = "8h";
|
settings.actions.ENDLESS_TASK_TIMEOUT = "8h";
|
||||||
settings.server = {
|
settings.server = {
|
||||||
|
HTTP_ADDR = "127.0.0.1";
|
||||||
HTTP_PORT = 11011;
|
HTTP_PORT = 11011;
|
||||||
SSH_PORT = sshPort;
|
ROOT_URL = "https://gitea.${domain}/";
|
||||||
|
SSH_PORT = giteaSshPort;
|
||||||
SSH_DOMAIN = "hectic-lab.com";
|
SSH_DOMAIN = "hectic-lab.com";
|
||||||
};
|
};
|
||||||
|
lfs.enable = true;
|
||||||
|
settings.lfs.LFS_MAX_FILE_SIZE = 536870912;
|
||||||
database = {
|
database = {
|
||||||
createDatabase = true;
|
createDatabase = true;
|
||||||
type = "postgres";
|
type = "postgres";
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ in self.lib.nixpkgs-lib.nixosSystem {
|
|||||||
self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
|
self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
|
||||||
"minecraft-server"
|
"minecraft-server"
|
||||||
"neoforge"
|
"neoforge"
|
||||||
|
"steamcmd"
|
||||||
|
"steam-unwrapped"
|
||||||
|
|
||||||
"nvidia-x11"
|
"nvidia-x11"
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
serverName = "wowMineMap";
|
serverName = "wowMineMap";
|
||||||
remoteHost = "u664722.your-storagebox.de";
|
remoteHost = "u664722.your-storagebox.de";
|
||||||
remoteUser = "u664722";
|
remoteUser = "u664722";
|
||||||
remotePath = "minecraft/pack/WorldOfSosal.mrpack";
|
remotePath = "minecraft/pack/WorldOfSosal-v3.mrpack";
|
||||||
archiveName = "WorldOfSosal.mrpack";
|
archiveName = "WorldOfSosal.mrpack";
|
||||||
cacheDir = "/var/lib/minecraft-modpacks/worldOfSosal";
|
cacheDir = "/var/lib/minecraft-modpacks/worldOfSosal";
|
||||||
archiveSha256 = "f8c18acb9208e4592725632ae50dab4f9c308483b34fd43a6507c74fdbf8169f";
|
archiveSha256 = "f97cf251b14f40590e97e7b39e8a8ec43dacfce6da1b02357d15e0eee10d3ade";
|
||||||
expectedDependencies = {
|
expectedDependencies = {
|
||||||
minecraft = "1.21.1";
|
minecraft = "1.21.1";
|
||||||
neoforge = "21.1.250";
|
neoforge = "21.1.250";
|
||||||
|
|||||||
@@ -22,9 +22,28 @@
|
|||||||
hostPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA5EB5p/5Hp3hGW1oHok+PIOH9Pbn7cnUiGmUEBrCVjnAw+HrKyN8bYVV0dIGllswYXwkG/+bgiBlE6IVIBAq+JwVWu1Sss3KarHY3OvFJUXZoZyRRg/Gc/+LRCE7lyKpwWQ70dbelGRyyJFH36eNv6ySXoUYtGkwlU5IVaHPApOxe4LHPZa/qhSRbPo2hwoh0orCtgejRebNtW5nlx00DNFgsvn8Svz2cIYLxsPVzKgUxs8Zxsxgn+Q/UvR7uq4AbAhyBMLxv7DjJ1pc7PJocuTno2Rw9uMZi1gkjbnmiOh6TTXIEWbnroyIhwc8555uto9melEUmWNQ+C+PwAK+MPw==";
|
hostPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA5EB5p/5Hp3hGW1oHok+PIOH9Pbn7cnUiGmUEBrCVjnAw+HrKyN8bYVV0dIGllswYXwkG/+bgiBlE6IVIBAq+JwVWu1Sss3KarHY3OvFJUXZoZyRRg/Gc/+LRCE7lyKpwWQ70dbelGRyyJFH36eNv6ySXoUYtGkwlU5IVaHPApOxe4LHPZa/qhSRbPo2hwoh0orCtgejRebNtW5nlx00DNFgsvn8Svz2cIYLxsPVzKgUxs8Zxsxgn+Q/UvR7uq4AbAhyBMLxv7DjJ1pc7PJocuTno2Rw9uMZi1gkjbnmiOh6TTXIEWbnroyIhwc8555uto9melEUmWNQ+C+PwAK+MPw==";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sops.secrets."minecraft/rcon-password" = {
|
||||||
|
sopsFile = ../../../../sus/neuro-minecraft.yaml;
|
||||||
|
owner = "minecraft";
|
||||||
|
group = "minecraft";
|
||||||
|
mode = "0400";
|
||||||
|
restartUnits = [ "minecraft-server-wowMineMap.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# The module's automatic firewall would also expose RCON.
|
||||||
|
networking.firewall.allowedTCPPorts = [ 25567 ];
|
||||||
services.minecraft-servers.servers.wowMineMap = {
|
services.minecraft-servers.servers.wowMineMap = {
|
||||||
|
openFirewall = false;
|
||||||
|
extraStartPre = ''
|
||||||
|
chmod 600 server.properties
|
||||||
|
{
|
||||||
|
printf '\nrcon.password='
|
||||||
|
cat ${config.sops.secrets."minecraft/rcon-password".path}
|
||||||
|
printf '\n'
|
||||||
|
} >> server.properties
|
||||||
|
'';
|
||||||
enable = true;
|
enable = true;
|
||||||
jvmOpts = "-Xmx8G -Xms2G";
|
jvmOpts = "-Xmx24G -Xms2G";
|
||||||
# WorldOfSosal client and server use the same pinned NeoForge.
|
# WorldOfSosal client and server use the same pinned NeoForge.
|
||||||
package = pkgs.minecraftServers.neoforge-1_21_1.override (
|
package = pkgs.minecraftServers.neoforge-1_21_1.override (
|
||||||
builtins.fromJSON (builtins.readFile ./neoforge-21.1.250.json)
|
builtins.fromJSON (builtins.readFile ./neoforge-21.1.250.json)
|
||||||
@@ -33,7 +52,9 @@
|
|||||||
serverProperties = {
|
serverProperties = {
|
||||||
server-port = 25567;
|
server-port = 25567;
|
||||||
difficulty = "hard";
|
difficulty = "hard";
|
||||||
online-mode = true;
|
online-mode = false;
|
||||||
|
enable-rcon = true;
|
||||||
|
"rcon.port" = 25575;
|
||||||
view-distance = 12;
|
view-distance = 12;
|
||||||
simulation-distance = 8;
|
simulation-distance = 8;
|
||||||
motd = "WorldOfSosal — World of Warcraft";
|
motd = "WorldOfSosal — World of Warcraft";
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
enable = true;
|
enable = false;
|
||||||
virtualHosts."bfs.band" = let
|
virtualHosts."bfs.band" = let
|
||||||
site = pkgs.runCommand "bfs-band-site" {} ''
|
site = pkgs.runCommand "bfs-band-site" {} ''
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
@@ -224,6 +224,70 @@ in {
|
|||||||
archetype.dev.enable = true;
|
archetype.dev.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hectic.services."project-zomboid" = {
|
||||||
|
enable = true;
|
||||||
|
memory = "8g";
|
||||||
|
serverName = "servertest";
|
||||||
|
serverPropertiesFile = /var/lib/project-zomboid/server-password.ini;
|
||||||
|
serverProperties = {
|
||||||
|
Map = "Muldraugh, KY";
|
||||||
|
DoLuaChecksum = false;
|
||||||
|
Public = true;
|
||||||
|
AntiCheatSafety = 4;
|
||||||
|
AntiCheatMovement = 4;
|
||||||
|
AntiCheatSpeed = 4;
|
||||||
|
AntiCheatHit = 4;
|
||||||
|
AntiCheatPacket = 4;
|
||||||
|
AntiCheatPacketException = 4;
|
||||||
|
AntiCheatPermission = 4;
|
||||||
|
AntiCheatXP = 4;
|
||||||
|
AntiCheatFire = 4;
|
||||||
|
AntiCheatSafeHouse = 4;
|
||||||
|
AntiCheatRecipe = 4;
|
||||||
|
AntiCheatPlayer = 4;
|
||||||
|
AntiCheatChecksum = 4;
|
||||||
|
AntiCheatItem = 4;
|
||||||
|
AntiCheatNoClip = 4;
|
||||||
|
AntiCheatServerCustomization = 4;
|
||||||
|
};
|
||||||
|
workshopItems = [
|
||||||
|
"3676456221" # Lua Digital Watch Framework
|
||||||
|
"3600401184" # Realistic Temperature Mod
|
||||||
|
];
|
||||||
|
mods = [
|
||||||
|
"\\LuaDigitalWatchUI"
|
||||||
|
"\\RC_RealisticColdMod"
|
||||||
|
];
|
||||||
|
sandboxProperties = {
|
||||||
|
Zombies = 6;
|
||||||
|
ZombieConfig = {
|
||||||
|
PopulationMultiplier = 0.0;
|
||||||
|
PopulationStartMultiplier = 0.0;
|
||||||
|
PopulationPeakMultiplier = 0.0;
|
||||||
|
RespawnHours = 0.0;
|
||||||
|
RespawnUnseenHours = 0.0;
|
||||||
|
RespawnMultiplier = 0.0;
|
||||||
|
RedistributeHours = 0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.project-zomboid.preStart = lib.mkBefore ''
|
||||||
|
password_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password"}
|
||||||
|
properties_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password.ini"}
|
||||||
|
|
||||||
|
if [ ! -s "$password_file" ] || ! ${pkgs.gnugrep}/bin/grep -Eq '^[0-9a-f]{48}$' "$password_file"; then
|
||||||
|
umask 077
|
||||||
|
${pkgs.openssl}/bin/openssl rand -hex 24 > "$password_file"
|
||||||
|
fi
|
||||||
|
${pkgs.coreutils}/bin/chmod 0600 "$password_file"
|
||||||
|
|
||||||
|
properties_file_tmp="$( ${pkgs.coreutils}/bin/mktemp "$(dirname "$properties_file")/.server-password.ini.XXXXXX")"
|
||||||
|
${pkgs.coreutils}/bin/printf 'Password=%s\n' "$(<"$password_file")" > "$properties_file_tmp"
|
||||||
|
${pkgs.coreutils}/bin/chmod 0600 "$properties_file_tmp"
|
||||||
|
${pkgs.coreutils}/bin/mv "$properties_file_tmp" "$properties_file"
|
||||||
|
'';
|
||||||
|
|
||||||
sops = {
|
sops = {
|
||||||
gnupg.sshKeyPaths = [ ];
|
gnupg.sshKeyPaths = [ ];
|
||||||
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ in final: prev: (
|
|||||||
legacyPackages = self.legacyPackages.${prev.stdenv.hostPlatform.system};
|
legacyPackages = self.legacyPackages.${prev.stdenv.hostPlatform.system};
|
||||||
in {
|
in {
|
||||||
hectic = packages // legacyPackages;
|
hectic = packages // legacyPackages;
|
||||||
|
p4d = if final.stdenv.hostPlatform.system == "x86_64-linux" then prev.p4d.overrideAttrs (_: {
|
||||||
|
version = "2023.1/2797325";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "https://ftp.perforce.com/pub/perforce/r23.1/bin.linux26x86_64/helix-core-server.tgz";
|
||||||
|
hash = "sha256-O8znAlq2XjrixG0FA4cfkgcI9t/w9QMHV0spUjYKl48=";
|
||||||
|
};
|
||||||
|
}) else prev.p4d;
|
||||||
|
p4 = prev.p4.overrideAttrs (_: {
|
||||||
|
version = "2024.1/3006289";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "https://ftp.perforce.com/pub/perforce/r24.1/bin.tools/p4source.tgz";
|
||||||
|
hash = "sha256-z3I3cikbbSrmS7dUMMKi6edPnZk2BYAmdO+pfYRJUVQ=";
|
||||||
|
};
|
||||||
|
});
|
||||||
postgresql_17 = prev.postgresql_17 // {pkgs = prev.postgresql_17.pkgs // {
|
postgresql_17 = prev.postgresql_17 // {pkgs = prev.postgresql_17.pkgs // {
|
||||||
http = packages.pg-17-ext-http;
|
http = packages.pg-17-ext-http;
|
||||||
pg_smtp_client = packages.pg-17-ext-smtp-client;
|
pg_smtp_client = packages.pg-17-ext-smtp-client;
|
||||||
|
|||||||
+9
-2
@@ -1,4 +1,8 @@
|
|||||||
{ self, pkgs, inputs, ... }: let
|
{ self, pkgs, inputs, system, ... }: let
|
||||||
|
giteaPkgs = import inputs.nixpkgs-gitea {
|
||||||
|
inherit system;
|
||||||
|
config = pkgs.config;
|
||||||
|
};
|
||||||
rust = {
|
rust = {
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.pkgsBuildHost.rust-bin.stable."1.81.0".default
|
pkgs.pkgsBuildHost.rust-bin.stable."1.81.0".default
|
||||||
@@ -135,7 +139,9 @@ in {
|
|||||||
c-hectic = pkgs.callPackage ./c/hectic/default.nix {};
|
c-hectic = pkgs.callPackage ./c/hectic/default.nix {};
|
||||||
watch = pkgs.callPackage ./c/watch/default.nix {};
|
watch = pkgs.callPackage ./c/watch/default.nix {};
|
||||||
support-bot = pkgs.callPackage ./support-bot {};
|
support-bot = pkgs.callPackage ./support-bot {};
|
||||||
gitea-heatmap = pkgs.callPackage ./gitea {};
|
gitea-heatmap = giteaPkgs.callPackage ./gitea {
|
||||||
|
nixosTests = pkgs.nixosTests;
|
||||||
|
};
|
||||||
gitea-runner-nix-image = pkgs.callPackage ./gitea-runner-nix-image {};
|
gitea-runner-nix-image = pkgs.callPackage ./gitea-runner-nix-image {};
|
||||||
gitea-runner-controller = pkgs.callPackage ./gitea-runner-controller {};
|
gitea-runner-controller = pkgs.callPackage ./gitea-runner-controller {};
|
||||||
nix-derivation-hash = pkgs.callPackage ./nix-derivation-hash {};
|
nix-derivation-hash = pkgs.callPackage ./nix-derivation-hash {};
|
||||||
@@ -143,6 +149,7 @@ in {
|
|||||||
deploy = pkgs.callPackage ./deploy { inherit inputs; };
|
deploy = pkgs.callPackage ./deploy { inherit inputs; };
|
||||||
element-web = pkgs.callPackage ./element-web {};
|
element-web = pkgs.callPackage ./element-web {};
|
||||||
shellplot = pkgs.callPackage ./shellplot {};
|
shellplot = pkgs.callPackage ./shellplot {};
|
||||||
|
gitea-kanban-tui = pkgs.callPackage ./gitea-kanban-tui rust.commonArgs;
|
||||||
which-country-rs = pkgs.callPackage ./which-country-rs {};
|
which-country-rs = pkgs.callPackage ./which-country-rs {};
|
||||||
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
|
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
|
||||||
migrator = pkgs.callPackage ./migrator { inherit self; };
|
migrator = pkgs.callPackage ./migrator { inherit self; };
|
||||||
|
|||||||
Generated
+1453
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "gitea-kanban-tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Native-project Gitea Kanban terminal interface"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "=4.5.20", features = ["derive"] }
|
||||||
|
crossterm = "=0.28.1"
|
||||||
|
encoding_rs = "=0.8.35"
|
||||||
|
indexmap = "=2.6.0"
|
||||||
|
instability = "=0.3.2"
|
||||||
|
ratatui = "=0.29.0"
|
||||||
|
reqwest = { version = "=0.11.27", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||||
|
serde = { version = "=1.0.210", features = ["derive"] }
|
||||||
|
serde_json = "=1.0.128"
|
||||||
|
unicode-segmentation = "=1.12.0"
|
||||||
|
url = "=2.5.2"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# gitea-kanban-tui
|
||||||
|
|
||||||
|
Keyboard-driven Kanban board for native repository Projects in custom Gitea fork.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Required configuration can come from flags or environment variables:
|
||||||
|
|
||||||
|
| Flag | Environment | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `--url` | `GITEA_URL` | Gitea base URL |
|
||||||
|
| `--token-file` | `GITEA_TOKEN_FILE` | File containing API token |
|
||||||
|
| — | `GITEA_TOKEN` | API token fallback when no token file is configured |
|
||||||
|
| `--project` | `GITEA_PROJECT` | Exact native project name |
|
||||||
|
| `--project-id` | `GITEA_PROJECT_ID` | Native project ID instead of name |
|
||||||
|
| positional `OWNER` | `GITEA_OWNER` | Repository owner |
|
||||||
|
| positional `REPO` | `GITEA_REPO` | Repository name |
|
||||||
|
|
||||||
|
Native mode requires an enabled repository Projects unit, project read/write
|
||||||
|
repository permission, and a token with `read:issue`/`write:issue` scope. Issue
|
||||||
|
creation/editing follows Gitea issue permissions; deletion requires repository
|
||||||
|
admin permission in this fork. Token values are sent
|
||||||
|
only through Gitea's `Authorization` header and are never printed. Token-file
|
||||||
|
input takes precedence over environment variables. Remote URLs must use HTTPS; plain HTTP is
|
||||||
|
accepted only for loopback development.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export GITEA_URL=https://gitea.hectic-lab.com
|
||||||
|
export GITEA_TOKEN_FILE="$HOME/.config/gitea/token"
|
||||||
|
cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml -- \
|
||||||
|
--project Kanban yukkop util.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop .#ratatui
|
||||||
|
cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml -- \
|
||||||
|
--project-id 1 owner repo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keys
|
||||||
|
|
||||||
|
- Arrow keys or `h`/`j`/`k`/`l`: focus column/card
|
||||||
|
- `H`/`L`: move focused card left/right
|
||||||
|
- `r`: refresh project columns and issues
|
||||||
|
- `?`: toggle help
|
||||||
|
- `q`: quit
|
||||||
|
|
||||||
|
Native mode reads `/projects`, project columns, and each column's issues. Moves
|
||||||
|
use the issue's global API `id` and destination column `id`; optional sorting is
|
||||||
|
supported by the server API. Project names are exact, case-sensitive matches;
|
||||||
|
use `--project-id` when duplicate names exist. Empty native boards render
|
||||||
|
normally. `n` creates an issue assigned to the selected project, `e` edits the
|
||||||
|
focused issue title/body, and `d` deletes it after confirmation. In the editor,
|
||||||
|
`Enter` switches from title to body, `Tab` switches fields, `Ctrl-S` saves, and
|
||||||
|
`Esc` cancels. Closed projects, archived repositories, disabled Projects units,
|
||||||
|
unassigned issues, and cross-repository IDs are rejected by the server.
|
||||||
|
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop .#ratatui
|
||||||
|
cargo fmt --manifest-path package/gitea-kanban-tui/Cargo.toml -- --check
|
||||||
|
cargo clippy --manifest-path package/gitea-kanban-tui/Cargo.toml --all-targets -- -D warnings
|
||||||
|
cargo test --manifest-path package/gitea-kanban-tui/Cargo.toml
|
||||||
|
nix build .#gitea-kanban-tui
|
||||||
|
```
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
cargoToml,
|
||||||
|
lib,
|
||||||
|
nativeBuildInputs,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
cargo = cargoToml ./Cargo.toml;
|
||||||
|
in
|
||||||
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = cargo.package.name;
|
||||||
|
version = cargo.package.version;
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
inherit nativeBuildInputs;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
|
doCheck = true;
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = cargo.package.description;
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
mainProgram = "gitea-kanban-tui";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,538 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::blocking::{Client, Response};
|
||||||
|
use reqwest::redirect::Policy;
|
||||||
|
use reqwest::{StatusCode, Url};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::model::{
|
||||||
|
CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload, Project, ProjectColumn,
|
||||||
|
};
|
||||||
|
use crate::text::sanitize_terminal_text;
|
||||||
|
|
||||||
|
pub trait GiteaApi {
|
||||||
|
fn list_projects(&self) -> Result<Vec<Project>, ApiError>;
|
||||||
|
fn list_project_columns(&self, project_id: u64) -> Result<Vec<ProjectColumn>, ApiError>;
|
||||||
|
fn list_project_column_issues(
|
||||||
|
&self,
|
||||||
|
project_id: u64,
|
||||||
|
column_id: u64,
|
||||||
|
) -> Result<Vec<Issue>, ApiError>;
|
||||||
|
fn move_project_issue(
|
||||||
|
&self,
|
||||||
|
project_id: u64,
|
||||||
|
issue_id: u64,
|
||||||
|
payload: &MoveProjectIssuePayload,
|
||||||
|
) -> Result<(), ApiError>;
|
||||||
|
fn create_issue(&self, payload: &CreateIssuePayload) -> Result<Issue, ApiError>;
|
||||||
|
fn edit_issue(&self, issue_number: u64, payload: &EditIssuePayload) -> Result<Issue, ApiError>;
|
||||||
|
fn delete_issue(&self, issue_number: u64) -> Result<(), ApiError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GiteaClient {
|
||||||
|
client: Client,
|
||||||
|
api_base: Url,
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiError(String);
|
||||||
|
|
||||||
|
impl fmt::Display for ApiError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ApiError {}
|
||||||
|
|
||||||
|
pub fn resolve_project(
|
||||||
|
projects: &[Project],
|
||||||
|
project_name: Option<&str>,
|
||||||
|
project_id: Option<u64>,
|
||||||
|
) -> Result<Project, ApiError> {
|
||||||
|
if let Some(id) = project_id {
|
||||||
|
return projects
|
||||||
|
.iter()
|
||||||
|
.find(|project| project.id == id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| ApiError(format!("project ID {id} was not found in this repository")));
|
||||||
|
}
|
||||||
|
let name = project_name.ok_or_else(|| ApiError("project selector is missing".to_owned()))?;
|
||||||
|
let matches = projects
|
||||||
|
.iter()
|
||||||
|
.filter(|project| project.title == name)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
match matches.as_slice() {
|
||||||
|
[project] => Ok(project.clone()),
|
||||||
|
[] => Err(ApiError(format!(
|
||||||
|
"project named '{name}' was not found; names are matched exactly"
|
||||||
|
))),
|
||||||
|
_ => Err(ApiError(format!(
|
||||||
|
"multiple projects are named '{name}'; use --project-id"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaClient {
|
||||||
|
pub fn new(config: &Config) -> Result<Self, ApiError> {
|
||||||
|
let mut api_base = Url::parse(&config.base_url)
|
||||||
|
.map_err(|error| ApiError(format!("invalid Gitea URL: {error}")))?;
|
||||||
|
if !api_base.username().is_empty() || api_base.password().is_some() {
|
||||||
|
return Err(ApiError(
|
||||||
|
"Gitea URL must not contain embedded username or password".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
require_secure_transport(&api_base)?;
|
||||||
|
api_base.set_query(None);
|
||||||
|
api_base.set_fragment(None);
|
||||||
|
api_base
|
||||||
|
.path_segments_mut()
|
||||||
|
.map_err(|_| ApiError("Gitea URL cannot be used as an API base".to_owned()))?
|
||||||
|
.pop_if_empty()
|
||||||
|
.extend(["api", "v1", "repos", &config.owner, &config.repo]);
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(20))
|
||||||
|
.redirect(Policy::none())
|
||||||
|
.user_agent(concat!("gitea-kanban-tui/", env!("CARGO_PKG_VERSION")))
|
||||||
|
.build()
|
||||||
|
.map_err(|error| ApiError(format!("cannot create HTTP client: {error}")))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
api_base,
|
||||||
|
token: config.token.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint(&self, path: &str) -> Result<Url, ApiError> {
|
||||||
|
let mut url = self.api_base.clone();
|
||||||
|
url.path_segments_mut()
|
||||||
|
.map_err(|_| ApiError("Gitea URL cannot contain API paths".to_owned()))?
|
||||||
|
.extend(path.split('/').filter(|part| !part.is_empty()));
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode<T: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
response: Response,
|
||||||
|
operation: &str,
|
||||||
|
) -> Result<T, ApiError> {
|
||||||
|
let response = check_response(response, operation, &self.token)?;
|
||||||
|
response.json().map_err(|error| {
|
||||||
|
ApiError(format!(
|
||||||
|
"Gitea returned invalid JSON while {operation}: {error}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_all<T: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
operation: &str,
|
||||||
|
query: &[(&str, &str)],
|
||||||
|
) -> Result<Vec<T>, ApiError> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for page in 1..=10_000_u32 {
|
||||||
|
let page_value = page.to_string();
|
||||||
|
let mut page_query = query.to_vec();
|
||||||
|
page_query.extend([("limit", "100"), ("page", page_value.as_str())]);
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(self.endpoint(path)?)
|
||||||
|
.query(&page_query)
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.send()
|
||||||
|
.map_err(|error| {
|
||||||
|
ApiError(format!("cannot reach Gitea while {operation}: {error}"))
|
||||||
|
})?;
|
||||||
|
let page_items: Vec<T> = self.decode(response, operation)?;
|
||||||
|
if page_items.is_empty() {
|
||||||
|
return Ok(items);
|
||||||
|
}
|
||||||
|
items.extend(page_items);
|
||||||
|
}
|
||||||
|
Err(ApiError(format!(
|
||||||
|
"Gitea returned too many pages while {operation}; narrow repository data or check server pagination"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaApi for GiteaClient {
|
||||||
|
fn list_projects(&self) -> Result<Vec<Project>, ApiError> {
|
||||||
|
self.get_all(
|
||||||
|
"projects",
|
||||||
|
"listing repository projects",
|
||||||
|
&[("state", "all")],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_project_columns(&self, project_id: u64) -> Result<Vec<ProjectColumn>, ApiError> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(self.endpoint(&format!("projects/{project_id}/columns"))?)
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.send()
|
||||||
|
.map_err(|error| {
|
||||||
|
ApiError(format!(
|
||||||
|
"cannot reach Gitea while listing project columns: {error}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
self.decode(response, "listing project columns")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_project_column_issues(
|
||||||
|
&self,
|
||||||
|
project_id: u64,
|
||||||
|
column_id: u64,
|
||||||
|
) -> Result<Vec<Issue>, ApiError> {
|
||||||
|
self.get_all(
|
||||||
|
&format!("projects/{project_id}/columns/{column_id}/issues"),
|
||||||
|
"listing project issues",
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_project_issue(
|
||||||
|
&self,
|
||||||
|
project_id: u64,
|
||||||
|
issue_id: u64,
|
||||||
|
payload: &MoveProjectIssuePayload,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(self.endpoint(&format!("projects/{project_id}/issues/{issue_id}/move"))?)
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.json(payload)
|
||||||
|
.send()
|
||||||
|
.map_err(|error| {
|
||||||
|
ApiError(format!(
|
||||||
|
"cannot reach Gitea while moving project issue: {error}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
check_response(response, "moving project issue", &self.token)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_issue(&self, payload: &CreateIssuePayload) -> Result<Issue, ApiError> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(self.endpoint("issues")?)
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.json(payload)
|
||||||
|
.send()
|
||||||
|
.map_err(|error| {
|
||||||
|
ApiError(format!("cannot reach Gitea while creating issue: {error}"))
|
||||||
|
})?;
|
||||||
|
self.decode(response, "creating issue")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_issue(&self, issue_number: u64, payload: &EditIssuePayload) -> Result<Issue, ApiError> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.patch(self.endpoint(&format!("issues/{issue_number}"))?)
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.json(payload)
|
||||||
|
.send()
|
||||||
|
.map_err(|error| {
|
||||||
|
ApiError(format!(
|
||||||
|
"cannot reach Gitea while editing issue #{issue_number}: {error}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
self.decode(response, &format!("editing issue #{issue_number}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_issue(&self, issue_number: u64) -> Result<(), ApiError> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.delete(self.endpoint(&format!("issues/{issue_number}"))?)
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.send()
|
||||||
|
.map_err(|error| {
|
||||||
|
ApiError(format!(
|
||||||
|
"cannot reach Gitea while deleting issue #{issue_number}: {error}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
check_response(
|
||||||
|
response,
|
||||||
|
&format!("deleting issue #{issue_number}"),
|
||||||
|
&self.token,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_response(response: Response, operation: &str, token: &str) -> Result<Response, ApiError> {
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_success() {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = response
|
||||||
|
.text()
|
||||||
|
.ok()
|
||||||
|
.and_then(|body| serde_json::from_str::<serde_json::Value>(&body).ok())
|
||||||
|
.and_then(|value| value.get("message")?.as_str().map(sanitize_terminal_text))
|
||||||
|
.map(|message| message.replace(token, "[redacted token]"));
|
||||||
|
let detail = message
|
||||||
|
.map(|message| format!(": {message}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let guidance = match status {
|
||||||
|
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
||||||
|
" Check token validity and repository issue permissions."
|
||||||
|
}
|
||||||
|
StatusCode::NOT_FOUND | StatusCode::METHOD_NOT_ALLOWED => {
|
||||||
|
" Check repository owner/name and whether this Gitea version supports native project APIs."
|
||||||
|
}
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
Err(ApiError(format!(
|
||||||
|
"Gitea API failed while {operation} ({status}){detail}.{guidance}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_secure_transport(url: &Url) -> Result<(), ApiError> {
|
||||||
|
if url.scheme() == "https" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let loopback = url
|
||||||
|
.host_str()
|
||||||
|
.and_then(|host| {
|
||||||
|
host.trim_start_matches('[')
|
||||||
|
.trim_end_matches(']')
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.is_some_and(|address| address.is_loopback());
|
||||||
|
if url.scheme() == "http" && loopback {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(ApiError(
|
||||||
|
"Gitea URL must use HTTPS to protect the API token; plain HTTP is allowed only for loopback development"
|
||||||
|
.to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use std::sync::mpsc::{self, Receiver};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
fn config(base_url: &str) -> Config {
|
||||||
|
Config {
|
||||||
|
base_url: base_url.to_owned(),
|
||||||
|
token: "secret".to_owned(),
|
||||||
|
owner: "owner name".to_owned(),
|
||||||
|
repo: "repo/name".to_owned(),
|
||||||
|
project: Some("Kanban".to_owned()),
|
||||||
|
project_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mock_server(responses: Vec<&'static str>) -> (String, Receiver<String>) {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
|
||||||
|
let address = listener.local_addr().expect("mock address");
|
||||||
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
for response in responses {
|
||||||
|
let (mut stream, _) = listener.accept().expect("accept request");
|
||||||
|
let mut request = Vec::new();
|
||||||
|
let mut buffer = [0_u8; 4096];
|
||||||
|
loop {
|
||||||
|
let read = stream.read(&mut buffer).expect("read request");
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
request.extend_from_slice(&buffer[..read]);
|
||||||
|
let header_end = request
|
||||||
|
.windows(4)
|
||||||
|
.position(|window| window == b"\r\n\r\n")
|
||||||
|
.map(|position| position + 4);
|
||||||
|
if let Some(header_end) = header_end {
|
||||||
|
let headers = String::from_utf8_lossy(&request[..header_end]);
|
||||||
|
let content_length = headers
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| {
|
||||||
|
line.to_ascii_lowercase()
|
||||||
|
.strip_prefix("content-length: ")?
|
||||||
|
.parse::<usize>()
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
if request.len() >= header_end + content_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = sender.send(String::from_utf8(request).expect("UTF-8 request"));
|
||||||
|
stream
|
||||||
|
.write_all(response.as_bytes())
|
||||||
|
.expect("write response");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(format!("http://{address}"), receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_base_path_and_encodes_repository_segments() {
|
||||||
|
let client = GiteaClient::new(&config("https://gitea.example/subpath"))
|
||||||
|
.expect("client should build");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.endpoint("projects").expect("endpoint").as_str(),
|
||||||
|
"https://gitea.example/subpath/api/v1/repos/owner%20name/repo%2Fname/projects"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_remote_plain_http_but_allows_loopback() {
|
||||||
|
assert!(GiteaClient::new(&config("http://gitea.example")).is_err());
|
||||||
|
assert!(GiteaClient::new(&config("http://localhost:3000")).is_err());
|
||||||
|
assert!(GiteaClient::new(&config("http://127.0.0.1:3000")).is_ok());
|
||||||
|
assert!(GiteaClient::new(&config("http://[::1]:3000")).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_exact_project_name_and_rejects_ambiguity() {
|
||||||
|
let projects = vec![
|
||||||
|
Project {
|
||||||
|
id: 1,
|
||||||
|
title: "Kanban".to_owned(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
Project {
|
||||||
|
id: 2,
|
||||||
|
title: "kanban".to_owned(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
resolve_project(&projects, Some("Kanban"), None)
|
||||||
|
.expect("match")
|
||||||
|
.id,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert!(resolve_project(&projects, Some("Missing"), None).is_err());
|
||||||
|
assert_eq!(
|
||||||
|
resolve_project(&projects, None, Some(2)).expect("id").title,
|
||||||
|
"kanban"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_projects_with_pagination_and_reports_unsupported_api() {
|
||||||
|
let (base_url, requests) = mock_server(vec![
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n[{\"id\":4,\"title\":\"Kanban\",\"is_closed\":false}]",
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n[]",
|
||||||
|
]);
|
||||||
|
let client = GiteaClient::new(&config(&base_url)).expect("client");
|
||||||
|
let projects = client.list_projects().expect("projects");
|
||||||
|
assert_eq!(projects[0].title, "Kanban");
|
||||||
|
let first = requests.recv().expect("first request");
|
||||||
|
let second = requests.recv().expect("second request");
|
||||||
|
assert!(first.starts_with("GET /api/v1/repos/owner%20name/repo%2Fname/projects?"));
|
||||||
|
assert!(first.contains("state=all"));
|
||||||
|
assert!(first.contains("page=1"));
|
||||||
|
assert!(second.contains("page=2"));
|
||||||
|
assert!(first.contains("authorization: token secret"));
|
||||||
|
|
||||||
|
let (base_url, _) = mock_server(vec![
|
||||||
|
"HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"message\":\"projects unavailable secret\"}",
|
||||||
|
]);
|
||||||
|
let client = GiteaClient::new(&config(&base_url)).expect("client");
|
||||||
|
let error = client
|
||||||
|
.list_project_columns(4)
|
||||||
|
.expect_err("unsupported API must fail");
|
||||||
|
assert!(error.to_string().contains("404 Not Found"));
|
||||||
|
assert!(error.to_string().contains("projects unavailable"));
|
||||||
|
assert!(!error.to_string().contains("secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sends_global_issue_move_payload() {
|
||||||
|
let (base_url, requests) = mock_server(vec![
|
||||||
|
"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
|
||||||
|
]);
|
||||||
|
let client = GiteaClient::new(&config(&base_url)).expect("client");
|
||||||
|
client
|
||||||
|
.move_project_issue(
|
||||||
|
4,
|
||||||
|
99,
|
||||||
|
&MoveProjectIssuePayload {
|
||||||
|
column_id: 12,
|
||||||
|
sorting: Some(3),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("move");
|
||||||
|
let request = requests.recv().expect("request");
|
||||||
|
assert!(request.starts_with(
|
||||||
|
"POST /api/v1/repos/owner%20name/repo%2Fname/projects/4/issues/99/move HTTP/1.1"
|
||||||
|
));
|
||||||
|
assert!(request.ends_with("{\"column_id\":12,\"sorting\":3}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paginates_native_column_issues() {
|
||||||
|
let (base_url, requests) = mock_server(vec![
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n[{\"id\":99,\"number\":7,\"title\":\"Fix\"}]",
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n[]",
|
||||||
|
]);
|
||||||
|
let client = GiteaClient::new(&config(&base_url)).expect("client");
|
||||||
|
let issues = client
|
||||||
|
.list_project_column_issues(4, 12)
|
||||||
|
.expect("column issues");
|
||||||
|
assert_eq!(issues[0].id, 99);
|
||||||
|
assert!(requests.recv().expect("page one").contains("page=1"));
|
||||||
|
assert!(requests.recv().expect("page two").contains("page=2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_edits_and_deletes_issues() {
|
||||||
|
let (base_url, requests) = mock_server(vec![
|
||||||
|
"HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"id\":99,\"number\":7,\"title\":\"New\"}",
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"id\":99,\"number\":7,\"title\":\"Updated\"}",
|
||||||
|
"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
|
||||||
|
]);
|
||||||
|
let client = GiteaClient::new(&config(&base_url)).expect("client");
|
||||||
|
let created = client
|
||||||
|
.create_issue(&CreateIssuePayload {
|
||||||
|
title: "New".to_owned(),
|
||||||
|
body: "Details".to_owned(),
|
||||||
|
projects: vec![4],
|
||||||
|
})
|
||||||
|
.expect("create");
|
||||||
|
assert_eq!(created.number, 7);
|
||||||
|
let edited = client
|
||||||
|
.edit_issue(
|
||||||
|
7,
|
||||||
|
&EditIssuePayload {
|
||||||
|
title: "Updated".to_owned(),
|
||||||
|
body: "Changed".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
|
assert_eq!(edited.title, "Updated");
|
||||||
|
client.delete_issue(7).expect("delete");
|
||||||
|
|
||||||
|
assert!(requests
|
||||||
|
.recv()
|
||||||
|
.expect("create request")
|
||||||
|
.ends_with("{\"title\":\"New\",\"body\":\"Details\",\"projects\":[4]}"));
|
||||||
|
assert!(requests
|
||||||
|
.recv()
|
||||||
|
.expect("edit request")
|
||||||
|
.starts_with("PATCH /api/v1/repos/owner%20name/repo%2Fname/issues/7 HTTP/1.1"));
|
||||||
|
assert!(requests
|
||||||
|
.recv()
|
||||||
|
.expect("delete request")
|
||||||
|
.starts_with("DELETE /api/v1/repos/owner%20name/repo%2Fname/issues/7 HTTP/1.1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
use crate::model::{
|
||||||
|
ColumnSpec, EditIssuePayload, Issue, MoveProjectIssuePayload, Project, ProjectColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Column {
|
||||||
|
pub spec: ColumnSpec,
|
||||||
|
pub cards: Vec<Issue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub board_title: String,
|
||||||
|
pub columns: Vec<Column>,
|
||||||
|
pub focused_column: usize,
|
||||||
|
pub focused_cards: Vec<usize>,
|
||||||
|
pub status: String,
|
||||||
|
pub show_help: bool,
|
||||||
|
pub editor: Option<EditorState>,
|
||||||
|
project_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum EditorMode {
|
||||||
|
Create { project_id: u64 },
|
||||||
|
Edit { issue_number: u64 },
|
||||||
|
Delete { issue_number: u64, title: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum EditorField {
|
||||||
|
Title,
|
||||||
|
Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct EditorState {
|
||||||
|
pub mode: EditorMode,
|
||||||
|
pub field: EditorField,
|
||||||
|
pub title: String,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum IssueAction {
|
||||||
|
Create {
|
||||||
|
project_id: u64,
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
|
Edit {
|
||||||
|
issue_number: u64,
|
||||||
|
payload: EditIssuePayload,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
issue_number: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum MoveAction {
|
||||||
|
Project {
|
||||||
|
issue_id: u64,
|
||||||
|
project_id: u64,
|
||||||
|
payload: MoveProjectIssuePayload,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct MoveRequest {
|
||||||
|
pub source: usize,
|
||||||
|
pub target: usize,
|
||||||
|
pub action: MoveAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new_project(
|
||||||
|
project: Project,
|
||||||
|
project_columns: Vec<ProjectColumn>,
|
||||||
|
issues_by_column: Vec<Vec<Issue>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
if project_columns.is_empty() {
|
||||||
|
return Err(format!("project '{}' has no columns", project.title));
|
||||||
|
}
|
||||||
|
if project_columns.len() != issues_by_column.len() {
|
||||||
|
return Err("project columns and issue lists do not match".to_owned());
|
||||||
|
}
|
||||||
|
let columns = project_columns
|
||||||
|
.into_iter()
|
||||||
|
.zip(issues_by_column)
|
||||||
|
.map(|(column, cards)| Column {
|
||||||
|
spec: ColumnSpec {
|
||||||
|
id: column.id,
|
||||||
|
title: column.title,
|
||||||
|
},
|
||||||
|
cards,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let focused_column = columns
|
||||||
|
.iter()
|
||||||
|
.position(|column| !column.cards.is_empty())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let focused_cards = vec![0; columns.len()];
|
||||||
|
Ok(Self {
|
||||||
|
board_title: format!("{} (project {})", project.title, project.id),
|
||||||
|
columns,
|
||||||
|
focused_column,
|
||||||
|
focused_cards,
|
||||||
|
status: "Ready".to_owned(),
|
||||||
|
show_help: false,
|
||||||
|
editor: None,
|
||||||
|
project_id: project.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_left(&mut self) {
|
||||||
|
self.focused_column = self.focused_column.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_right(&mut self) {
|
||||||
|
self.focused_column = (self.focused_column + 1).min(self.columns.len() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_up(&mut self) {
|
||||||
|
let selected = &mut self.focused_cards[self.focused_column];
|
||||||
|
*selected = selected.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_down(&mut self) {
|
||||||
|
let column = &self.columns[self.focused_column];
|
||||||
|
if !column.cards.is_empty() {
|
||||||
|
let selected = &mut self.focused_cards[self.focused_column];
|
||||||
|
*selected = (*selected + 1).min(column.cards.len() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_card(&self) -> Option<&Issue> {
|
||||||
|
self.columns[self.focused_column]
|
||||||
|
.cards
|
||||||
|
.get(self.focused_cards[self.focused_column])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_move(&self, offset: isize) -> Option<MoveRequest> {
|
||||||
|
let target = self.focused_column.checked_add_signed(offset)?;
|
||||||
|
if target >= self.columns.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let issue = self.focused_card()?;
|
||||||
|
let action = MoveAction::Project {
|
||||||
|
issue_id: issue.id,
|
||||||
|
project_id: self.project_id,
|
||||||
|
payload: MoveProjectIssuePayload {
|
||||||
|
column_id: self.columns[target].spec.id,
|
||||||
|
sorting: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Some(MoveRequest {
|
||||||
|
source: self.focused_column,
|
||||||
|
target,
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_move(&mut self, request: MoveRequest) {
|
||||||
|
let selected = self.focused_cards[request.source];
|
||||||
|
let issue = self.columns[request.source].cards.remove(selected);
|
||||||
|
let issue_number = issue.number;
|
||||||
|
self.columns[request.target].cards.push(issue);
|
||||||
|
self.focused_cards[request.source] =
|
||||||
|
selected.min(self.columns[request.source].cards.len().saturating_sub(1));
|
||||||
|
self.focused_cards[request.target] = self.columns[request.target].cards.len() - 1;
|
||||||
|
self.focused_column = request.target;
|
||||||
|
self.status = format!("Moved issue #{issue_number}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_create(&mut self) {
|
||||||
|
self.editor = Some(EditorState {
|
||||||
|
mode: EditorMode::Create {
|
||||||
|
project_id: self.project_id,
|
||||||
|
},
|
||||||
|
field: EditorField::Title,
|
||||||
|
title: String::new(),
|
||||||
|
body: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_edit(&mut self) {
|
||||||
|
let Some(issue) = self.focused_card().cloned() else {
|
||||||
|
self.status = "No issue selected".to_owned();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.editor = Some(EditorState {
|
||||||
|
mode: EditorMode::Edit {
|
||||||
|
issue_number: issue.number,
|
||||||
|
},
|
||||||
|
field: EditorField::Title,
|
||||||
|
title: issue.title,
|
||||||
|
body: issue.body.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_delete(&mut self) {
|
||||||
|
let Some(issue) = self.focused_card() else {
|
||||||
|
self.status = "No issue selected".to_owned();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.editor = Some(EditorState {
|
||||||
|
mode: EditorMode::Delete {
|
||||||
|
issue_number: issue.number,
|
||||||
|
title: issue.title.clone(),
|
||||||
|
},
|
||||||
|
field: EditorField::Title,
|
||||||
|
title: String::new(),
|
||||||
|
body: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_editor_key(&mut self, key: KeyEvent) -> Option<IssueAction> {
|
||||||
|
let mut editor = self.editor.take()?;
|
||||||
|
if matches!(editor.mode, EditorMode::Delete { .. }) {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||||
|
if let EditorMode::Delete { issue_number, .. } = editor.mode {
|
||||||
|
return Some(IssueAction::Delete { issue_number });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||||
|
self.status = "Delete cancelled".to_owned();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.editor = Some(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.code == KeyCode::Esc {
|
||||||
|
self.status = "Edit cancelled".to_owned();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
|
||||||
|
return self.submit_editor(editor);
|
||||||
|
}
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Tab => {
|
||||||
|
editor.field = match editor.field {
|
||||||
|
EditorField::Title => EditorField::Body,
|
||||||
|
EditorField::Body => EditorField::Title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
KeyCode::Enter if editor.field == EditorField::Title => {
|
||||||
|
editor.field = EditorField::Body;
|
||||||
|
}
|
||||||
|
KeyCode::Enter => editor.body.push('\n'),
|
||||||
|
KeyCode::Backspace => match editor.field {
|
||||||
|
EditorField::Title => {
|
||||||
|
editor.title.pop();
|
||||||
|
}
|
||||||
|
EditorField::Body => {
|
||||||
|
editor.body.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KeyCode::Char(character) if !character.is_control() => match editor.field {
|
||||||
|
EditorField::Title => editor.title.push(character),
|
||||||
|
EditorField::Body => editor.body.push(character),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.editor = Some(editor);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_issue_action(&mut self, action: IssueAction) {
|
||||||
|
self.editor = Some(match action {
|
||||||
|
IssueAction::Create {
|
||||||
|
project_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
} => EditorState {
|
||||||
|
mode: EditorMode::Create { project_id },
|
||||||
|
field: EditorField::Body,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
IssueAction::Edit {
|
||||||
|
issue_number,
|
||||||
|
payload,
|
||||||
|
} => EditorState {
|
||||||
|
mode: EditorMode::Edit { issue_number },
|
||||||
|
field: EditorField::Body,
|
||||||
|
title: payload.title,
|
||||||
|
body: payload.body,
|
||||||
|
},
|
||||||
|
IssueAction::Delete { issue_number } => EditorState {
|
||||||
|
mode: EditorMode::Delete {
|
||||||
|
issue_number,
|
||||||
|
title: self
|
||||||
|
.focused_card()
|
||||||
|
.map(|issue| issue.title.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
},
|
||||||
|
field: EditorField::Title,
|
||||||
|
title: String::new(),
|
||||||
|
body: String::new(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_editor(&mut self, editor: EditorState) -> Option<IssueAction> {
|
||||||
|
if editor.title.trim().is_empty() {
|
||||||
|
self.status = "Title cannot be empty".to_owned();
|
||||||
|
self.editor = Some(editor);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match editor.mode {
|
||||||
|
EditorMode::Create { project_id } => Some(IssueAction::Create {
|
||||||
|
project_id,
|
||||||
|
title: editor.title,
|
||||||
|
body: editor.body,
|
||||||
|
}),
|
||||||
|
EditorMode::Edit { issue_number } => Some(IssueAction::Edit {
|
||||||
|
issue_number,
|
||||||
|
payload: EditIssuePayload {
|
||||||
|
title: editor.title,
|
||||||
|
body: editor.body,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
EditorMode::Delete { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn app() -> App {
|
||||||
|
App::new_project(
|
||||||
|
Project {
|
||||||
|
id: 8,
|
||||||
|
title: "Kanban".to_owned(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
vec![
|
||||||
|
ProjectColumn {
|
||||||
|
id: 10,
|
||||||
|
title: "Todo".to_owned(),
|
||||||
|
color: String::new(),
|
||||||
|
sorting: 0,
|
||||||
|
},
|
||||||
|
ProjectColumn {
|
||||||
|
id: 20,
|
||||||
|
title: "Done".to_owned(),
|
||||||
|
color: String::new(),
|
||||||
|
sorting: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
vec![
|
||||||
|
Issue {
|
||||||
|
id: 1,
|
||||||
|
number: 1,
|
||||||
|
title: "First".to_owned(),
|
||||||
|
body: None,
|
||||||
|
},
|
||||||
|
Issue {
|
||||||
|
id: 2,
|
||||||
|
number: 2,
|
||||||
|
title: "Second".to_owned(),
|
||||||
|
body: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("board builds")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn navigation_stays_within_board() {
|
||||||
|
let mut app = app();
|
||||||
|
app.focus_left();
|
||||||
|
app.focus_up();
|
||||||
|
assert_eq!(app.focused_column, 0);
|
||||||
|
assert_eq!(app.focused_cards[0], 0);
|
||||||
|
|
||||||
|
app.focus_down();
|
||||||
|
app.focus_down();
|
||||||
|
app.focus_right();
|
||||||
|
app.focus_right();
|
||||||
|
assert_eq!(app.focused_cards[0], 1);
|
||||||
|
assert_eq!(app.focused_column, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_move_past_board_edge() {
|
||||||
|
let app = app();
|
||||||
|
assert!(app.prepare_move(-1).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_board_returns_actionable_error() {
|
||||||
|
let result = App::new_project(
|
||||||
|
Project {
|
||||||
|
id: 1,
|
||||||
|
title: "Kanban".to_owned(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
vec![ProjectColumn {
|
||||||
|
id: 1,
|
||||||
|
title: "Todo".to_owned(),
|
||||||
|
color: String::new(),
|
||||||
|
sorting: 0,
|
||||||
|
}],
|
||||||
|
vec![Vec::new()],
|
||||||
|
);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_move_uses_global_issue_and_column_ids() {
|
||||||
|
let mut app = App::new_project(
|
||||||
|
Project {
|
||||||
|
id: 8,
|
||||||
|
title: "Kanban".to_owned(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
vec![
|
||||||
|
ProjectColumn {
|
||||||
|
id: 10,
|
||||||
|
title: "Todo".to_owned(),
|
||||||
|
color: String::new(),
|
||||||
|
sorting: 0,
|
||||||
|
},
|
||||||
|
ProjectColumn {
|
||||||
|
id: 20,
|
||||||
|
title: "Done".to_owned(),
|
||||||
|
color: String::new(),
|
||||||
|
sorting: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
vec![Issue {
|
||||||
|
id: 99,
|
||||||
|
number: 7,
|
||||||
|
title: "Fix".to_owned(),
|
||||||
|
body: None,
|
||||||
|
}],
|
||||||
|
Vec::new(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("board");
|
||||||
|
let request = app.prepare_move(1).expect("move");
|
||||||
|
assert!(matches!(
|
||||||
|
request.action,
|
||||||
|
MoveAction::Project { issue_id: 99, ref payload, .. } if payload.column_id == 20
|
||||||
|
));
|
||||||
|
app.apply_move(request);
|
||||||
|
assert_eq!(app.columns[1].cards[0].id, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_editor_creates_project_issue_action() {
|
||||||
|
let mut app = App::new_project(
|
||||||
|
Project {
|
||||||
|
id: 8,
|
||||||
|
title: "Kanban".to_owned(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
vec![ProjectColumn {
|
||||||
|
id: 10,
|
||||||
|
title: "Todo".to_owned(),
|
||||||
|
color: String::new(),
|
||||||
|
sorting: 0,
|
||||||
|
}],
|
||||||
|
vec![Vec::new()],
|
||||||
|
)
|
||||||
|
.expect("board");
|
||||||
|
app.begin_create();
|
||||||
|
for character in "New issue".chars() {
|
||||||
|
app.handle_editor_key(KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE));
|
||||||
|
}
|
||||||
|
app.handle_editor_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
|
for character in "Details".chars() {
|
||||||
|
app.handle_editor_key(KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE));
|
||||||
|
}
|
||||||
|
let action =
|
||||||
|
app.handle_editor_key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL));
|
||||||
|
assert!(matches!(
|
||||||
|
action,
|
||||||
|
Some(IssueAction::Create {
|
||||||
|
project_id: 8,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
}) if title == "New issue" && body == "Details"
|
||||||
|
));
|
||||||
|
assert!(app.editor.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "gitea-kanban-tui",
|
||||||
|
version,
|
||||||
|
about = "Browse and move Gitea issues using native projects"
|
||||||
|
)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Gitea base URL; falls back to GITEA_URL
|
||||||
|
#[arg(long)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
|
||||||
|
/// File containing API token; falls back to GITEA_TOKEN_FILE
|
||||||
|
#[arg(long, value_name = "PATH")]
|
||||||
|
pub token_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Exact native project name
|
||||||
|
#[arg(long)]
|
||||||
|
pub project: Option<String>,
|
||||||
|
|
||||||
|
/// Native project ID
|
||||||
|
#[arg(long)]
|
||||||
|
pub project_id: Option<u64>,
|
||||||
|
|
||||||
|
/// Repository owner; falls back to GITEA_OWNER
|
||||||
|
pub owner: Option<String>,
|
||||||
|
|
||||||
|
/// Repository name; falls back to GITEA_REPO
|
||||||
|
pub repo: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub base_url: String,
|
||||||
|
pub token: String,
|
||||||
|
pub owner: String,
|
||||||
|
pub repo: String,
|
||||||
|
pub project: Option<String>,
|
||||||
|
pub project_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct ConfigError(String);
|
||||||
|
|
||||||
|
impl fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load() -> Result<Self, ConfigError> {
|
||||||
|
Self::from_args_with(Args::parse(), |name| env::var(name).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_args_with<F>(args: Args, env_var: F) -> Result<Self, ConfigError>
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Option<String>,
|
||||||
|
{
|
||||||
|
let base_url = required(
|
||||||
|
args.url.or_else(|| env_var("GITEA_URL")),
|
||||||
|
"Gitea URL",
|
||||||
|
"--url or GITEA_URL",
|
||||||
|
)?;
|
||||||
|
let owner = required(
|
||||||
|
args.owner.or_else(|| env_var("GITEA_OWNER")),
|
||||||
|
"repository owner",
|
||||||
|
"OWNER argument or GITEA_OWNER",
|
||||||
|
)?;
|
||||||
|
let repo = required(
|
||||||
|
args.repo.or_else(|| env_var("GITEA_REPO")),
|
||||||
|
"repository name",
|
||||||
|
"REPO argument or GITEA_REPO",
|
||||||
|
)?;
|
||||||
|
let project = args
|
||||||
|
.project
|
||||||
|
.or_else(|| env_var("GITEA_PROJECT"))
|
||||||
|
.map(|value| value.trim().to_owned())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
let project_id = args
|
||||||
|
.project_id
|
||||||
|
.or_else(|| env_var("GITEA_PROJECT_ID").and_then(|value| value.parse::<u64>().ok()));
|
||||||
|
if project.is_some() == project_id.is_some() {
|
||||||
|
return Err(ConfigError(
|
||||||
|
"projects mode requires exactly one of --project/GITEA_PROJECT or --project-id/GITEA_PROJECT_ID"
|
||||||
|
.to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = if let Some(path) = args.token_file {
|
||||||
|
read_token_file(path)?
|
||||||
|
} else if let Some(path) = env_var("GITEA_TOKEN_FILE") {
|
||||||
|
read_token_file(PathBuf::from(path))?
|
||||||
|
} else if let Some(token) = env_var("GITEA_TOKEN") {
|
||||||
|
clean_token(token, "GITEA_TOKEN")?
|
||||||
|
} else {
|
||||||
|
return Err(ConfigError(
|
||||||
|
"missing Gitea token; use --token-file, GITEA_TOKEN, or GITEA_TOKEN_FILE"
|
||||||
|
.to_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
base_url: base_url.trim_end_matches('/').to_owned(),
|
||||||
|
token,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
project,
|
||||||
|
project_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required(value: Option<String>, name: &str, source: &str) -> Result<String, ConfigError> {
|
||||||
|
match value.map(|value| value.trim().to_owned()) {
|
||||||
|
Some(value) if !value.is_empty() => Ok(value),
|
||||||
|
_ => Err(ConfigError(format!("missing {name}; use {source}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_token(token: String, source: &str) -> Result<String, ConfigError> {
|
||||||
|
let token = token.trim().to_owned();
|
||||||
|
if token.is_empty() {
|
||||||
|
Err(ConfigError(format!("{source} contains an empty token")))
|
||||||
|
} else {
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_token_file(path: PathBuf) -> Result<String, ConfigError> {
|
||||||
|
let metadata = fs::symlink_metadata(&path).map_err(|error| {
|
||||||
|
ConfigError(format!(
|
||||||
|
"cannot inspect token file {}: {error}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(ConfigError(format!(
|
||||||
|
"token path {} is not a regular file",
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
if metadata.permissions().mode() & 0o077 != 0 {
|
||||||
|
return Err(ConfigError(format!(
|
||||||
|
"token file {} must not be group- or world-readable",
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let token = fs::read_to_string(&path).map_err(|error| {
|
||||||
|
ConfigError(format!(
|
||||||
|
"cannot read token file {}: {error}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
clean_token(token, &format!("token file {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn empty_env(_: &str) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_env(name: &str) -> Option<String> {
|
||||||
|
(name == "GITEA_TOKEN").then(|| "secret".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_explicit_configuration() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"gitea-kanban-tui",
|
||||||
|
"--url",
|
||||||
|
"https://gitea.example/",
|
||||||
|
"--project",
|
||||||
|
"Kanban",
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
])
|
||||||
|
.expect("arguments parse");
|
||||||
|
|
||||||
|
let config = Config::from_args_with(args, token_env).expect("config is valid");
|
||||||
|
assert_eq!(config.base_url, "https://gitea.example");
|
||||||
|
assert_eq!(config.owner, "owner");
|
||||||
|
assert_eq!(config.repo, "repo");
|
||||||
|
assert_eq!(config.project.as_deref(), Some("Kanban"));
|
||||||
|
assert_eq!(config.token, "secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_native_project_by_name() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"gitea-kanban-tui",
|
||||||
|
"--url",
|
||||||
|
"https://gitea.example",
|
||||||
|
"--project",
|
||||||
|
"Kanban",
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
])
|
||||||
|
.expect("arguments parse");
|
||||||
|
|
||||||
|
let config = Config::from_args_with(args, token_env).expect("config is valid");
|
||||||
|
assert_eq!(config.project.as_deref(), Some("Kanban"));
|
||||||
|
assert_eq!(config.project_id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requires_project_selector() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"gitea-kanban-tui",
|
||||||
|
"--url",
|
||||||
|
"https://gitea.example",
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
])
|
||||||
|
.expect("arguments parse");
|
||||||
|
|
||||||
|
let error =
|
||||||
|
Config::from_args_with(args, token_env).expect_err("project selector is required");
|
||||||
|
assert!(error.to_string().contains("--project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_project_selector_is_unambiguous() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"gitea-kanban-tui",
|
||||||
|
"--url",
|
||||||
|
"https://gitea.example",
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
])
|
||||||
|
.expect("arguments parse");
|
||||||
|
|
||||||
|
let error = Config::from_args_with(args, token_env).expect_err("selector is required");
|
||||||
|
assert!(error.to_string().contains("exactly one"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_environment() {
|
||||||
|
let args = Args::try_parse_from(["gitea-kanban-tui"]).expect("arguments parse");
|
||||||
|
let config = Config::from_args_with(args, |name| {
|
||||||
|
match name {
|
||||||
|
"GITEA_URL" => Some("https://gitea.example"),
|
||||||
|
"GITEA_TOKEN" => Some("secret"),
|
||||||
|
"GITEA_PROJECT" => Some("Kanban"),
|
||||||
|
"GITEA_OWNER" => Some("owner"),
|
||||||
|
"GITEA_REPO" => Some("repo"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
.map(str::to_owned)
|
||||||
|
})
|
||||||
|
.expect("config is valid");
|
||||||
|
|
||||||
|
assert_eq!(config.project.as_deref(), Some("Kanban"));
|
||||||
|
assert_eq!(config.owner, "owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_missing_token_without_exposing_values() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"gitea-kanban-tui",
|
||||||
|
"--url",
|
||||||
|
"https://gitea.example",
|
||||||
|
"--project",
|
||||||
|
"Kanban",
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
])
|
||||||
|
.expect("arguments parse");
|
||||||
|
let error = match Config::from_args_with(args, empty_env) {
|
||||||
|
Ok(_) => panic!("token should be required"),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("GITEA_TOKEN_FILE"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod app;
|
||||||
|
pub mod config;
|
||||||
|
pub mod model;
|
||||||
|
pub mod terminal;
|
||||||
|
pub mod text;
|
||||||
|
pub mod ui;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
|
|
||||||
|
use gitea_kanban_tui::api::{resolve_project, GiteaApi, GiteaClient};
|
||||||
|
use gitea_kanban_tui::app::{App, IssueAction, MoveAction};
|
||||||
|
use gitea_kanban_tui::config::Config;
|
||||||
|
use gitea_kanban_tui::model::CreateIssuePayload;
|
||||||
|
use gitea_kanban_tui::terminal::TerminalGuard;
|
||||||
|
use gitea_kanban_tui::text::sanitize_terminal_text;
|
||||||
|
use gitea_kanban_tui::ui;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(error) = run() {
|
||||||
|
eprintln!("error: {}", sanitize_terminal_text(&error.to_string()));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = Config::load()?;
|
||||||
|
let client = GiteaClient::new(&config)?;
|
||||||
|
let mut app = load_board(&client, &config)?;
|
||||||
|
let repository = format!("{}/{}", config.owner, config.repo);
|
||||||
|
let mut terminal = TerminalGuard::enter()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal
|
||||||
|
.terminal()
|
||||||
|
.draw(|frame| ui::draw(frame, &app, &repository))?;
|
||||||
|
if !event::poll(Duration::from_millis(250))? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Event::Key(key) = event::read()? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if key.kind != KeyEventKind::Press {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.editor.is_some() {
|
||||||
|
if let Some(action) = app.handle_editor_key(key) {
|
||||||
|
issue_action(&client, &config, &mut app, action);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => break,
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => app.focus_left(),
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => app.focus_right(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => app.focus_up(),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => app.focus_down(),
|
||||||
|
KeyCode::Char('H') => move_card(&client, &mut app, -1),
|
||||||
|
KeyCode::Char('L') => move_card(&client, &mut app, 1),
|
||||||
|
KeyCode::Char('n') => app.begin_create(),
|
||||||
|
KeyCode::Char('e') => app.begin_edit(),
|
||||||
|
KeyCode::Char('d') => app.begin_delete(),
|
||||||
|
KeyCode::Char('r') => match load_board(&client, &config) {
|
||||||
|
Ok(board) => app = board,
|
||||||
|
Err(error) => app.status = format!("Refresh failed: {error}"),
|
||||||
|
},
|
||||||
|
KeyCode::Char('?') => app.show_help = !app.show_help,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn issue_action(client: &impl GiteaApi, config: &Config, app: &mut App, action: IssueAction) {
|
||||||
|
let retry_action = action.clone();
|
||||||
|
let message = match action {
|
||||||
|
IssueAction::Create {
|
||||||
|
project_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
} => client
|
||||||
|
.create_issue(&CreateIssuePayload {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
projects: vec![project_id],
|
||||||
|
})
|
||||||
|
.map(|issue| format!("Created issue #{}", issue.number)),
|
||||||
|
IssueAction::Edit {
|
||||||
|
issue_number,
|
||||||
|
payload,
|
||||||
|
} => client
|
||||||
|
.edit_issue(issue_number, &payload)
|
||||||
|
.map(|_| format!("Updated issue #{issue_number}")),
|
||||||
|
IssueAction::Delete { issue_number } => client
|
||||||
|
.delete_issue(issue_number)
|
||||||
|
.map(|_| format!("Deleted issue #{issue_number}")),
|
||||||
|
};
|
||||||
|
match message {
|
||||||
|
Ok(message) => match load_board(client, config) {
|
||||||
|
Ok(mut board) => {
|
||||||
|
board.status = message;
|
||||||
|
*app = board;
|
||||||
|
}
|
||||||
|
Err(error) => app.status = format!("Saved, refresh failed: {error}"),
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
app.restore_issue_action(retry_action);
|
||||||
|
app.status = format!("Issue operation failed: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_board(client: &impl GiteaApi, config: &Config) -> Result<App, Box<dyn Error>> {
|
||||||
|
let projects = client.list_projects()?;
|
||||||
|
let project = resolve_project(&projects, config.project.as_deref(), config.project_id)?;
|
||||||
|
let columns = client.list_project_columns(project.id)?;
|
||||||
|
let issues_by_column = columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| client.list_project_column_issues(project.id, column.id))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
App::new_project(project, columns, issues_by_column).map_err(|error| error.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_card(client: &impl GiteaApi, app: &mut App, offset: isize) {
|
||||||
|
let Some(request) = app.prepare_move(offset) else {
|
||||||
|
app.status = "Cannot move card beyond board edge".to_owned();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let result = match &request.action {
|
||||||
|
MoveAction::Project {
|
||||||
|
issue_id,
|
||||||
|
project_id,
|
||||||
|
payload,
|
||||||
|
} => client.move_project_issue(*project_id, *issue_id, payload),
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(()) => app.apply_move(request),
|
||||||
|
Err(error) => app.status = format!("Move failed: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct Issue {
|
||||||
|
pub id: u64,
|
||||||
|
pub number: u64,
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct CreateIssuePayload {
|
||||||
|
pub title: String,
|
||||||
|
pub body: String,
|
||||||
|
pub projects: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct EditIssuePayload {
|
||||||
|
pub title: String,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ColumnSpec {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct Project {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ProjectColumn {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sorting: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct MoveProjectIssuePayload {
|
||||||
|
pub column_id: u64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sorting: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserializes_native_project_data_and_move_payload() {
|
||||||
|
let project: Project = serde_json::from_value(serde_json::json!({
|
||||||
|
"id": 4,
|
||||||
|
"title": "Kanban",
|
||||||
|
"is_closed": false
|
||||||
|
}))
|
||||||
|
.expect("project JSON");
|
||||||
|
let column: ProjectColumn = serde_json::from_value(serde_json::json!({
|
||||||
|
"id": 9,
|
||||||
|
"title": "Doing",
|
||||||
|
"sorting": 1
|
||||||
|
}))
|
||||||
|
.expect("column JSON");
|
||||||
|
let issue: Issue = serde_json::from_value(serde_json::json!({
|
||||||
|
"id": 70,
|
||||||
|
"number": 7,
|
||||||
|
"title": "Fix it"
|
||||||
|
}))
|
||||||
|
.expect("issue JSON");
|
||||||
|
let payload = MoveProjectIssuePayload {
|
||||||
|
column_id: column.id,
|
||||||
|
sorting: Some(3),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(project.title, "Kanban");
|
||||||
|
assert_eq!(issue.id, 70);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(payload).expect("move payload"),
|
||||||
|
serde_json::json!({"column_id": 9, "sorting": 3})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serializes_issue_create_and_edit_payloads() {
|
||||||
|
let create = CreateIssuePayload {
|
||||||
|
title: "New issue".to_owned(),
|
||||||
|
body: "Details".to_owned(),
|
||||||
|
projects: vec![4],
|
||||||
|
};
|
||||||
|
let edit = EditIssuePayload {
|
||||||
|
title: "Updated".to_owned(),
|
||||||
|
body: "Changed".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(create).expect("create payload"),
|
||||||
|
serde_json::json!({"title": "New issue", "body": "Details", "projects": [4]})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(edit).expect("edit payload"),
|
||||||
|
serde_json::json!({"title": "Updated", "body": "Changed"})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use std::io::{self, Stdout};
|
||||||
|
|
||||||
|
use crossterm::execute;
|
||||||
|
use crossterm::terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
};
|
||||||
|
use ratatui::backend::CrosstermBackend;
|
||||||
|
use ratatui::Terminal;
|
||||||
|
|
||||||
|
pub struct TerminalGuard {
|
||||||
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalGuard {
|
||||||
|
pub fn enter() -> io::Result<Self> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
if let Err(error) = execute!(stdout, EnterAlternateScreen) {
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
match Terminal::new(backend) {
|
||||||
|
Ok(terminal) => Ok(Self { terminal }),
|
||||||
|
Err(error) => {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
let _ = execute!(stdout, LeaveAlternateScreen);
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terminal(&mut self) -> &mut Terminal<CrosstermBackend<Stdout>> {
|
||||||
|
&mut self.terminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TerminalGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
|
||||||
|
let _ = self.terminal.show_cursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
pub fn sanitize_terminal_text(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.chars()
|
||||||
|
.map(|character| {
|
||||||
|
if character.is_control() {
|
||||||
|
' '
|
||||||
|
} else {
|
||||||
|
character
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_editor_text(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.chars()
|
||||||
|
.map(|character| {
|
||||||
|
if character == '\n' || !character.is_control() {
|
||||||
|
character
|
||||||
|
} else {
|
||||||
|
' '
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_terminal_control_characters() {
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_terminal_text("safe\u{1b}[31m\ntext"),
|
||||||
|
"safe [31m text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn editor_sanitizer_preserves_newlines() {
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_editor_text("first\nsecond\u{1b}[31m"),
|
||||||
|
"first\nsecond [31m"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
use crate::app::{App, Column, EditorField, EditorMode, EditorState};
|
||||||
|
use crate::text::{sanitize_editor_text, sanitize_terminal_text};
|
||||||
|
|
||||||
|
pub fn draw(frame: &mut Frame<'_>, app: &App, repository: &str) {
|
||||||
|
let areas = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(6),
|
||||||
|
Constraint::Length(4),
|
||||||
|
Constraint::Length(if app.show_help { 3 } else { 1 }),
|
||||||
|
])
|
||||||
|
.split(frame.area());
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(format!(
|
||||||
|
"Gitea Kanban — {} — {}",
|
||||||
|
sanitize_terminal_text(repository),
|
||||||
|
sanitize_terminal_text(&app.board_title)
|
||||||
|
))
|
||||||
|
.style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
areas[0],
|
||||||
|
);
|
||||||
|
draw_columns(frame, app, areas[1]);
|
||||||
|
draw_detail(frame, app, areas[2]);
|
||||||
|
|
||||||
|
let help = if app.show_help {
|
||||||
|
format!(
|
||||||
|
"{}\n←/h →/l: column ↑/k ↓/j: card H/L: move n: new e: edit d: delete r: refresh ?: help q: quit",
|
||||||
|
sanitize_terminal_text(&app.status)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} | ?: help q: quit",
|
||||||
|
sanitize_terminal_text(&app.status)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
frame.render_widget(Paragraph::new(help).wrap(Wrap { trim: true }), areas[3]);
|
||||||
|
if let Some(editor) = &app.editor {
|
||||||
|
draw_editor(frame, editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_editor(frame: &mut Frame<'_>, editor: &EditorState) {
|
||||||
|
let area = centered_rect(frame.area(), 80, 45);
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
let (title, content) = match &editor.mode {
|
||||||
|
EditorMode::Delete {
|
||||||
|
issue_number,
|
||||||
|
title,
|
||||||
|
} => (
|
||||||
|
"Delete issue".to_owned(),
|
||||||
|
format!(
|
||||||
|
"Delete issue #{issue_number} — {}?\n\n[y] confirm [n/Esc] cancel",
|
||||||
|
sanitize_terminal_text(title)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
EditorMode::Create { .. } => (
|
||||||
|
"New issue".to_owned(),
|
||||||
|
editor_content(editor, "Create issue"),
|
||||||
|
),
|
||||||
|
EditorMode::Edit { issue_number } => (
|
||||||
|
format!("Edit issue #{issue_number}"),
|
||||||
|
editor_content(editor, "Edit issue"),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(content)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" {title} "))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false }),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_content(editor: &EditorState, action: &str) -> String {
|
||||||
|
let title_marker = if editor.field == EditorField::Title {
|
||||||
|
"▶ "
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
let body_marker = if editor.field == EditorField::Body {
|
||||||
|
"▶ "
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{title_marker}Title: {}\n{body_marker}Body: {}\n\nTab: switch field Enter: title → body Ctrl-S: save Esc: cancel\n{action}",
|
||||||
|
sanitize_terminal_text(&editor.title),
|
||||||
|
sanitize_editor_text(&editor.body)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_rect(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
|
||||||
|
let vertical = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - height_percent) / 2),
|
||||||
|
Constraint::Percentage(height_percent),
|
||||||
|
Constraint::Percentage((100 - height_percent) / 2),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - width_percent) / 2),
|
||||||
|
Constraint::Percentage(width_percent),
|
||||||
|
Constraint::Percentage((100 - width_percent) / 2),
|
||||||
|
])
|
||||||
|
.split(vertical[1])[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_columns(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||||
|
let widths = vec![Constraint::Ratio(1, app.columns.len() as u32); app.columns.len()];
|
||||||
|
let areas = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(widths)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
for (index, column) in app.columns.iter().enumerate() {
|
||||||
|
draw_column(frame, app, column, index, areas[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_column(frame: &mut Frame<'_>, app: &App, column: &Column, index: usize, area: Rect) {
|
||||||
|
let focused = index == app.focused_column;
|
||||||
|
let border_style = if focused {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
let items: Vec<ListItem<'_>> = column
|
||||||
|
.cards
|
||||||
|
.iter()
|
||||||
|
.map(|issue| {
|
||||||
|
ListItem::new(Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("#{} ", issue.number),
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
),
|
||||||
|
Span::raw(sanitize_terminal_text(&issue.title)),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(
|
||||||
|
" {} ({}) ",
|
||||||
|
sanitize_terminal_text(&column.spec.title),
|
||||||
|
column.cards.len()
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(border_style),
|
||||||
|
)
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.bg(Color::Blue)
|
||||||
|
.fg(Color::White)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.highlight_symbol("▶ ");
|
||||||
|
let mut state = ListState::default();
|
||||||
|
if focused && !column.cards.is_empty() {
|
||||||
|
state.select(Some(app.focused_cards[index]));
|
||||||
|
}
|
||||||
|
frame.render_stateful_widget(list, area, &mut state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_detail(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||||
|
let text = app
|
||||||
|
.focused_card()
|
||||||
|
.map(|issue| {
|
||||||
|
let body = sanitize_terminal_text(
|
||||||
|
&issue
|
||||||
|
.body
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("No description")
|
||||||
|
.replace('\n', " "),
|
||||||
|
);
|
||||||
|
format!(
|
||||||
|
"#{} {}\n{}",
|
||||||
|
issue.number,
|
||||||
|
sanitize_terminal_text(&issue.title),
|
||||||
|
body
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "No card in focused column".to_owned());
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(text)
|
||||||
|
.block(Block::default().title(" Detail ").borders(Borders::ALL))
|
||||||
|
.wrap(Wrap { trim: true }),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
+15
-12
@@ -18,20 +18,26 @@
|
|||||||
|
|
||||||
let
|
let
|
||||||
pname = "gitea";
|
pname = "gitea";
|
||||||
version = "1.26.2";
|
version = "1.27.3";
|
||||||
src = ./source;
|
src = ./source;
|
||||||
pnpm = pnpm_10;
|
pnpm = pnpm_10;
|
||||||
|
pnpmPatches = [ ./pnpm-engine.patch ];
|
||||||
|
|
||||||
frontend = stdenv.mkDerivation {
|
frontend = stdenv.mkDerivation {
|
||||||
pname = "gitea-frontend";
|
pname = "gitea-frontend";
|
||||||
inherit src version;
|
inherit src version;
|
||||||
|
patches = pnpmPatches;
|
||||||
|
|
||||||
pnpmDeps = fetchPnpmDeps {
|
pnpmDeps = fetchPnpmDeps {
|
||||||
pname = "gitea-frontend";
|
pname = "gitea-frontend";
|
||||||
inherit version src;
|
inherit version src;
|
||||||
inherit pnpm;
|
inherit pnpm;
|
||||||
|
patches = pnpmPatches;
|
||||||
fetcherVersion = 3;
|
fetcherVersion = 3;
|
||||||
hash = "sha256-Qo0DLuZv+2GVLsBfCv/6CC9E/qhSE4HwV4StQL4HX4Y=";
|
prePnpmInstall = ''
|
||||||
|
pnpm config set engine-strict false
|
||||||
|
'';
|
||||||
|
hash = "sha256-H1sNMKRkoPlkheJFJVaof4bJ4gQHnbhosJaUqj9X8Gg=";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
@@ -40,6 +46,10 @@ let
|
|||||||
pnpm
|
pnpm
|
||||||
];
|
];
|
||||||
|
|
||||||
|
prePnpmInstall = ''
|
||||||
|
pnpm config set engine-strict false
|
||||||
|
'';
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
make frontend
|
make frontend
|
||||||
'';
|
'';
|
||||||
@@ -54,7 +64,8 @@ buildGo126Module rec {
|
|||||||
inherit pname version src;
|
inherit pname version src;
|
||||||
|
|
||||||
proxyVendor = true;
|
proxyVendor = true;
|
||||||
vendorHash = "sha256-7+M1n8RSgB3gZ/2na4RF9kYOf90H0bnsJZMDKpgAy64=";
|
deleteVendor = true;
|
||||||
|
vendorHash = "sha256-YRBMGWKIZgMxOXaXG2bIBj1XzkhSwiMyfRy+yQGw+Bo=";
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
"out"
|
"out"
|
||||||
@@ -63,17 +74,8 @@ buildGo126Module rec {
|
|||||||
|
|
||||||
patches = [ ./static-root-path.patch ];
|
patches = [ ./static-root-path.patch ];
|
||||||
|
|
||||||
overrideModAttrs = _: {
|
|
||||||
postPatch = ''
|
|
||||||
substituteInPlace go.mod \
|
|
||||||
--replace-fail "go 1.26.3" "go 1.26"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
postPatch = ''
|
postPatch = ''
|
||||||
substituteInPlace modules/setting/server.go --subst-var data
|
substituteInPlace modules/setting/server.go --subst-var data
|
||||||
substituteInPlace go.mod \
|
|
||||||
--replace-fail "go 1.26.3" "go 1.26"
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
subPackages = [ "." ];
|
subPackages = [ "." ];
|
||||||
@@ -93,6 +95,7 @@ buildGo126Module rec {
|
|||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
mv "$out/bin/gitea.dev" "$out/bin/gitea"
|
||||||
mkdir $data
|
mkdir $data
|
||||||
ln -s ${frontend}/public $data/public
|
ln -s ${frontend}/public $data/public
|
||||||
cp -R ./{templates,options} $data
|
cp -R ./{templates,options} $data
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/package.json b/package.json
|
||||||
|
--- a/package.json
|
||||||
|
+++ b/package.json
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
- "packageManager": "pnpm@11.9.0",
|
||||||
|
+ "packageManager": "pnpm@10.12.4",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22.18.0",
|
||||||
|
- "pnpm": ">= 11.0.0"
|
||||||
|
+ "pnpm": ">= 10.0.0"
|
||||||
|
},
|
||||||
@@ -37,10 +37,7 @@ groups:
|
|||||||
name: BUGFIXES
|
name: BUGFIXES
|
||||||
labels:
|
labels:
|
||||||
- type/bug
|
- type/bug
|
||||||
-
|
|
||||||
name: API
|
|
||||||
labels:
|
|
||||||
- modifies/api
|
|
||||||
-
|
-
|
||||||
name: TESTING
|
name: TESTING
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Gitea DevContainer",
|
"name": "Gitea DevContainer",
|
||||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
"image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
// override "local" from packaged version
|
// override "local" from packaged version
|
||||||
"GOTOOLCHAIN": "auto"
|
"GOTOOLCHAIN": "auto"
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ cpu.out
|
|||||||
*.log
|
*.log
|
||||||
|
|
||||||
/gitea
|
/gitea
|
||||||
/gitea-vet
|
|
||||||
/debug
|
/debug
|
||||||
/integrations.test
|
|
||||||
|
|
||||||
/bin
|
/bin
|
||||||
/dist
|
/dist
|
||||||
@@ -54,12 +52,6 @@ cpu.out
|
|||||||
/indexers
|
/indexers
|
||||||
/log
|
/log
|
||||||
/tests/integration/gitea-integration-*
|
/tests/integration/gitea-integration-*
|
||||||
/tests/integration/indexers-*
|
|
||||||
/tests/e2e/gitea-e2e-*
|
|
||||||
/tests/e2e/indexers-*
|
|
||||||
/tests/e2e/reports
|
|
||||||
/tests/e2e/test-artifacts
|
|
||||||
/tests/e2e/test-snapshots
|
|
||||||
/tests/*.ini
|
/tests/*.ini
|
||||||
/node_modules
|
/node_modules
|
||||||
/yarn.lock
|
/yarn.lock
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ indent_style = tab
|
|||||||
[templates/custom/*.tmpl]
|
[templates/custom/*.tmpl]
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
|
|
||||||
[templates/swagger/v1_json.tmpl]
|
[templates/swagger/*_json.tmpl]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
/assets/*.json linguist-generated
|
/assets/*.json linguist-generated
|
||||||
/public/assets/img/svg/*.svg linguist-generated
|
/public/assets/img/svg/*.svg linguist-generated
|
||||||
/templates/swagger/v1_json.tmpl linguist-generated
|
/templates/swagger/v1_json.tmpl linguist-generated
|
||||||
|
/templates/swagger/v1_openapi3_json.tmpl linguist-generated
|
||||||
/options/fileicon/** linguist-generated
|
/options/fileicon/** linguist-generated
|
||||||
/vendor/** -text -eol linguist-vendored
|
/vendor/** -text -eol linguist-vendored
|
||||||
/web_src/js/vendor/** -text -eol linguist-vendored
|
/web_src/js/vendor/** -text -eol linguist-vendored
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<!-- NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue -->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
1. Please speak English, this is the language all maintainers can speak and write.
|
|
||||||
2. Please ask questions or configuration/deploy problems on our Discord
|
|
||||||
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
|
|
||||||
3. Please take a moment to check that your issue doesn't already exist.
|
|
||||||
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
|
|
||||||
5. Please give all relevant information below for bug reports, because
|
|
||||||
incomplete details will be handled as an invalid report.
|
|
||||||
-->
|
|
||||||
|
|
||||||
- Gitea version (or commit ref):
|
|
||||||
- Git version:
|
|
||||||
- Operating system:
|
|
||||||
<!-- Please include information on whether you built gitea yourself, used one of our downloads or are using some other package -->
|
|
||||||
<!-- Please also tell us how you are running gitea, e.g. if it is being run from docker, a command-line, systemd etc. --->
|
|
||||||
<!-- If you are using a package or systemd tell us what distribution you are using -->
|
|
||||||
- Database (use `[x]`):
|
|
||||||
- [ ] PostgreSQL
|
|
||||||
- [ ] MySQL
|
|
||||||
- [ ] MSSQL
|
|
||||||
- [ ] SQLite
|
|
||||||
- Can you reproduce the bug at https://demo.gitea.com:
|
|
||||||
- [ ] Yes (provide example URL)
|
|
||||||
- [ ] No
|
|
||||||
- Log gist:
|
|
||||||
<!-- It really is important to provide pertinent logs -->
|
|
||||||
<!-- Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help -->
|
|
||||||
<!-- In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
<!-- If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please
|
|
||||||
disable the proxy/CDN fully and connect to gitea directly to confirm
|
|
||||||
the issue still persists without those services. -->
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
<!-- **If this issue involves the Web Interface, please include a screenshot** -->
|
|
||||||
+10
-73
@@ -1,91 +1,28 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Found something you weren't expecting? Report it here!
|
description: Something isn't working as expected.
|
||||||
labels: ["type/bug"]
|
labels: ["type/bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
|
- **Security issue?** Email security@gitea.io instead of opening a public issue.
|
||||||
- type: markdown
|
- **Need help** with setup or configuration? Ask on [Discord](https://discord.gg/Gitea) or the [forum](https://forum.gitea.com).
|
||||||
attributes:
|
- Search [existing issues](https://github.com/go-gitea/gitea/issues?q=is%3Aissue) first.
|
||||||
value: |
|
|
||||||
1. Please speak English, this is the language all maintainers can speak and write.
|
|
||||||
2. Please ask questions or configuration/deploy problems on our Discord
|
|
||||||
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
|
|
||||||
3. Make sure you are using the latest release and
|
|
||||||
take a moment to check that your issue hasn't been reported before.
|
|
||||||
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
|
|
||||||
5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support),
|
|
||||||
incomplete details will be handled as an invalid report.
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: |
|
|
||||||
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
|
|
||||||
If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services.
|
|
||||||
- type: input
|
- type: input
|
||||||
id: gitea-ver
|
id: gitea-ver
|
||||||
attributes:
|
attributes:
|
||||||
label: Gitea Version
|
label: Gitea Version
|
||||||
description: Gitea version (or commit reference) of your instance
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: textarea
|
||||||
id: can-reproduce
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Can you reproduce the bug on the Gitea demo site?
|
label: What happened?
|
||||||
description: |
|
description: What you did, what you expected to happen, and what happened instead. Include logs if relevant.
|
||||||
If so, please provide a URL in the Description field
|
|
||||||
URL of Gitea demo: https://demo.gitea.com
|
|
||||||
options:
|
|
||||||
- "Yes"
|
|
||||||
- "No"
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
It's really important to provide pertinent logs
|
|
||||||
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
|
|
||||||
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
|
|
||||||
- type: input
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Log Gist
|
|
||||||
description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshots
|
id: environment
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: If this issue involves the Web Interface, please provide one or more screenshots
|
|
||||||
- type: input
|
|
||||||
id: git-ver
|
|
||||||
attributes:
|
|
||||||
label: Git Version
|
|
||||||
description: The version of git running on the server
|
|
||||||
- type: input
|
|
||||||
id: os-ver
|
|
||||||
attributes:
|
|
||||||
label: Operating System
|
|
||||||
description: The operating system you are using to run Gitea
|
|
||||||
- type: textarea
|
|
||||||
id: run-info
|
|
||||||
attributes:
|
attributes:
|
||||||
label: How are you running Gitea?
|
label: How are you running Gitea?
|
||||||
description: |
|
description: Install method (binary, Docker, package), operating system, and database.
|
||||||
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
|
|
||||||
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
|
|
||||||
If you are using a package or systemd tell us what distribution you are using
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: database
|
|
||||||
attributes:
|
|
||||||
label: Database
|
|
||||||
description: What database system are you running?
|
|
||||||
options:
|
|
||||||
- PostgreSQL
|
|
||||||
- MySQL/MariaDB
|
|
||||||
- MSSQL
|
|
||||||
- SQLite
|
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here!
|
description: Suggest an idea for Gitea.
|
||||||
labels: ["type/proposal"]
|
labels: ["type/proposal"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
1. Please speak English, this is the language all maintainers can speak and write.
|
Search [existing issues](https://github.com/go-gitea/gitea/issues?q=is%3Aissue) first.
|
||||||
2. Please ask questions or configuration/deploy problems on our Discord
|
|
||||||
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
|
|
||||||
3. Please take a moment to check that your feature hasn't already been suggested.
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: Feature Description
|
label: What problem would this solve?
|
||||||
placeholder: |
|
|
||||||
I think it would be great if Gitea had...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshots
|
id: proposal
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots
|
label: What do you propose?
|
||||||
description: If you can, provide screenshots of an implementation on another site e.g. GitHub
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
name: Web Interface Bug Report
|
|
||||||
description: Something doesn't look quite as it should? Report it here!
|
|
||||||
labels: ["type/bug", "topic/ui"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
1. Please speak English, this is the language all maintainers can speak and write.
|
|
||||||
2. Please ask questions or configuration/deploy problems on our Discord
|
|
||||||
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
|
|
||||||
3. Please take a moment to check that your issue doesn't already exist.
|
|
||||||
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
|
|
||||||
5. Please give all relevant information below for bug reports, because
|
|
||||||
incomplete details will be handled as an invalid report.
|
|
||||||
6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript
|
|
||||||
error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us
|
|
||||||
DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help)
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: |
|
|
||||||
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
|
|
||||||
If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services.
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: Please provide at least 1 screenshot showing the issue.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: gitea-ver
|
|
||||||
attributes:
|
|
||||||
label: Gitea Version
|
|
||||||
description: Gitea version (or commit reference) your instance is running
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: can-reproduce
|
|
||||||
attributes:
|
|
||||||
label: Can you reproduce the bug on the Gitea demo site?
|
|
||||||
description: |
|
|
||||||
If so, please provide a URL in the Description field
|
|
||||||
URL of Gitea demo: https://demo.gitea.com
|
|
||||||
options:
|
|
||||||
- "Yes"
|
|
||||||
- "No"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: os-ver
|
|
||||||
attributes:
|
|
||||||
label: Operating System
|
|
||||||
description: The operating system you are using to access Gitea
|
|
||||||
- type: input
|
|
||||||
id: browser-ver
|
|
||||||
attributes:
|
|
||||||
label: Browser Version
|
|
||||||
description: The browser and version that you are using to access Gitea
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: free-disk-space
|
||||||
|
description: Free space on / before large cache restores
|
||||||
|
|
||||||
|
# Delete preinstalled toolchains which gitea doesn't use and show disk space usage
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
echo "free space before cleanup:"
|
||||||
|
df -h /
|
||||||
|
for dir in /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet; do
|
||||||
|
sudo rm -rf "$dir" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
echo "free space after cleanup:"
|
||||||
|
df -h /
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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 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.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
lint-cache:
|
||||||
|
description: Restore (and save in cache-seeder) ~/.cache/golangci-lint
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- 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' }}
|
||||||
|
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 }}
|
||||||
|
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
|
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
|
||||||
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/golangci-lint
|
||||||
|
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||||
|
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
|
||||||
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/golangci-lint
|
||||||
|
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||||
|
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: go-setup
|
||||||
|
description: Set up go and restore caches
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
cache:
|
||||||
|
description: Restore go caches
|
||||||
|
default: "true"
|
||||||
|
lint-cache:
|
||||||
|
description: Also restore the golangci-lint cache
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: ./.github/actions/free-disk-space
|
||||||
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- if: ${{ inputs.cache == 'true' }}
|
||||||
|
uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
lint-cache: ${{ inputs.lint-cache }}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: node-setup
|
||||||
|
description: Set up pnpm and node and restore caches
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
cache:
|
||||||
|
description: Cache pnpm downloads
|
||||||
|
default: "true"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
|
- if: ${{ inputs.cache == 'true' }}
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
- if: ${{ inputs.cache != 'true' }}
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: pgsql-shard
|
||||||
|
description: Run one pgsql integration test shard
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
shard:
|
||||||
|
description: Shard index
|
||||||
|
required: true
|
||||||
|
total-shards:
|
||||||
|
description: Total shard count
|
||||||
|
required: true
|
||||||
|
run-migration:
|
||||||
|
description: Also run migration tests
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Add hosts to /etc/hosts
|
||||||
|
shell: bash
|
||||||
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
|
||||||
|
- shell: bash
|
||||||
|
run: make deps-backend
|
||||||
|
- shell: bash
|
||||||
|
run: make backend
|
||||||
|
env:
|
||||||
|
TAGS: bindata
|
||||||
|
- name: run migration tests
|
||||||
|
if: ${{ inputs.run-migration == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: GITEA_TEST_DATABASE=pgsql make test-migration
|
||||||
|
- name: run tests
|
||||||
|
shell: bash
|
||||||
|
run: GITEA_TEST_DATABASE=pgsql make test-integration
|
||||||
|
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 gogit
|
||||||
|
TEST_LDAP: 1
|
||||||
|
TEST_SHARD: ${{ inputs.shard }}
|
||||||
|
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
labels: [modifies/dependencies]
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
cooldown:
|
|
||||||
default-days: 5
|
|
||||||
-77
@@ -1,80 +1,3 @@
|
|||||||
modifies/docs:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "**/*.md"
|
|
||||||
- "docs/**"
|
|
||||||
|
|
||||||
modifies/templates:
|
|
||||||
- changed-files:
|
|
||||||
- all-globs-to-any-file:
|
|
||||||
- "templates/**"
|
|
||||||
- "!templates/swagger/v1_json.tmpl"
|
|
||||||
|
|
||||||
modifies/api:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "routers/api/**"
|
|
||||||
- "templates/swagger/v1_json.tmpl"
|
|
||||||
|
|
||||||
modifies/cli:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "cmd/**"
|
|
||||||
|
|
||||||
modifies/translation:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "options/locale/*.ini"
|
|
||||||
|
|
||||||
modifies/migrations:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "models/migrations/**"
|
|
||||||
|
|
||||||
modifies/internal:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- ".air.toml"
|
|
||||||
- "Makefile"
|
|
||||||
- "Dockerfile"
|
|
||||||
- "Dockerfile.rootless"
|
|
||||||
- ".dockerignore"
|
|
||||||
- "docker/**"
|
|
||||||
- ".editorconfig"
|
|
||||||
- ".eslintrc.cjs"
|
|
||||||
- ".golangci.yml"
|
|
||||||
- ".markdownlint.yaml"
|
|
||||||
- ".spectral.yaml"
|
|
||||||
- "stylelint.config.*"
|
|
||||||
- ".yamllint.yaml"
|
|
||||||
- ".github/**"
|
|
||||||
- ".gitea/**"
|
|
||||||
- ".devcontainer/**"
|
|
||||||
- "build/**"
|
|
||||||
- "contrib/**"
|
|
||||||
|
|
||||||
modifies/dependencies:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "package.json"
|
|
||||||
- "pnpm-lock.yaml"
|
|
||||||
- "pyproject.toml"
|
|
||||||
- "uv.lock"
|
|
||||||
- "go.mod"
|
|
||||||
- "go.sum"
|
|
||||||
|
|
||||||
modifies/go:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "**/*.go"
|
|
||||||
|
|
||||||
modifies/frontend:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- "*.js"
|
|
||||||
- "*.ts"
|
|
||||||
- "web_src/**"
|
|
||||||
|
|
||||||
docs-update-needed:
|
docs-update-needed:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|||||||
+9
-10
@@ -1,10 +1,9 @@
|
|||||||
<!-- start tips -->
|
<!--
|
||||||
Please check the following:
|
Before submitting:
|
||||||
1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports.
|
- Target the `main` branch; release branches are for backports only.
|
||||||
2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md .
|
- Use a Conventional Commits title, e.g. `fix(repo): handle empty branch names`.
|
||||||
3. For documentations contribution, please go to https://gitea.com/gitea/docs
|
- Read the contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md
|
||||||
4. Describe what your pull request does and which issue you're targeting (if any).
|
- Documentation changes go to https://gitea.com/gitea/docs
|
||||||
5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
|
|
||||||
6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`.
|
Describe your change below and link any issue it fixes.
|
||||||
7. Delete all these tips before posting.
|
-->
|
||||||
<!-- end tips -->
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# 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:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "go.sum"
|
||||||
|
- ".golangci.yml"
|
||||||
|
- ".github/actions/go-cache/action.yml"
|
||||||
|
- ".github/actions/go-setup/action.yml"
|
||||||
|
- ".github/workflows/cache-seeder.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cache-seeder
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gobuild:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- 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 gogit" GOEXPERIMENT="" make test-integration-compile
|
||||||
|
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { tags: "bindata", target: "lint-backend" }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
|
- uses: ./.github/actions/go-setup
|
||||||
|
with:
|
||||||
|
lint-cache: "true"
|
||||||
|
- run: make deps-backend deps-tools
|
||||||
|
- run: make generate-go
|
||||||
|
env:
|
||||||
|
TAGS: ${{ matrix.tags }}
|
||||||
|
- run: make ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
TAGS: ${{ matrix.tags }}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: cron-flake-updater
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * 0' # runs weekly on Sunday at 00:00
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nix-flake-update:
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: DeterminateSystems/determinate-nix-action@v3
|
|
||||||
- uses: DeterminateSystems/update-flake-lock@main
|
|
||||||
with:
|
|
||||||
pr-title: "Update Nix flake"
|
|
||||||
pr-labels: |
|
|
||||||
dependencies
|
|
||||||
@@ -12,15 +12,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- run: make generate-gitignore
|
- run: make generate-gitignore
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
- name: push translations to repo
|
- name: push translations to repo
|
||||||
uses: appleboy/git-push-action@v1.2.0
|
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
|
||||||
with:
|
with:
|
||||||
author_email: "teabot@gitea.io"
|
author_email: "teabot@gitea.io"
|
||||||
author_name: GiteaBot
|
author_name: GiteaBot
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: cron-renovate
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "23 * * * *" # hourly at :23
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cron-renovate
|
||||||
|
|
||||||
|
env:
|
||||||
|
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cron-renovate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'go-gitea/gitea' # prevent running on forks
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- 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)$"]'
|
||||||
|
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
|
||||||
@@ -12,8 +12,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: crowdin/github-action@v2
|
- uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
||||||
with:
|
with:
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: update locales
|
- name: update locales
|
||||||
run: ./build/update-locales.sh
|
run: ./build/update-locales.sh
|
||||||
- name: push translations to repo
|
- name: push translations to repo
|
||||||
uses: appleboy/git-push-action@v1.2.0
|
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
|
||||||
with:
|
with:
|
||||||
author_email: "teabot@gitea.io"
|
author_email: "teabot@gitea.io"
|
||||||
author_name: GiteaBot
|
author_name: GiteaBot
|
||||||
|
|||||||
+43
-10
@@ -15,19 +15,26 @@ on:
|
|||||||
value: ${{ jobs.detect.outputs.templates }}
|
value: ${{ jobs.detect.outputs.templates }}
|
||||||
docker:
|
docker:
|
||||||
value: ${{ jobs.detect.outputs.docker }}
|
value: ${{ jobs.detect.outputs.docker }}
|
||||||
|
dockerfile:
|
||||||
|
value: ${{ jobs.detect.outputs.dockerfile }}
|
||||||
swagger:
|
swagger:
|
||||||
value: ${{ jobs.detect.outputs.swagger }}
|
value: ${{ jobs.detect.outputs.swagger }}
|
||||||
yaml:
|
yaml:
|
||||||
value: ${{ jobs.detect.outputs.yaml }}
|
value: ${{ jobs.detect.outputs.yaml }}
|
||||||
json:
|
json:
|
||||||
value: ${{ jobs.detect.outputs.json }}
|
value: ${{ jobs.detect.outputs.json }}
|
||||||
|
e2e:
|
||||||
|
value: ${{ jobs.detect.outputs.e2e }}
|
||||||
|
shell:
|
||||||
|
value: ${{ jobs.detect.outputs.shell }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect:
|
detect:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
outputs:
|
outputs:
|
||||||
backend: ${{ steps.changes.outputs.backend }}
|
backend: ${{ steps.changes.outputs.backend }}
|
||||||
frontend: ${{ steps.changes.outputs.frontend }}
|
frontend: ${{ steps.changes.outputs.frontend }}
|
||||||
@@ -35,12 +42,15 @@ jobs:
|
|||||||
actions: ${{ steps.changes.outputs.actions }}
|
actions: ${{ steps.changes.outputs.actions }}
|
||||||
templates: ${{ steps.changes.outputs.templates }}
|
templates: ${{ steps.changes.outputs.templates }}
|
||||||
docker: ${{ steps.changes.outputs.docker }}
|
docker: ${{ steps.changes.outputs.docker }}
|
||||||
|
dockerfile: ${{ steps.changes.outputs.dockerfile }}
|
||||||
swagger: ${{ steps.changes.outputs.swagger }}
|
swagger: ${{ steps.changes.outputs.swagger }}
|
||||||
yaml: ${{ steps.changes.outputs.yaml }}
|
yaml: ${{ steps.changes.outputs.yaml }}
|
||||||
json: ${{ steps.changes.outputs.json }}
|
json: ${{ steps.changes.outputs.json }}
|
||||||
|
e2e: ${{ steps.changes.outputs.e2e }}
|
||||||
|
shell: ${{ steps.changes.outputs.shell }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: dorny/paths-filter@v4
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
@@ -54,49 +64,61 @@ jobs:
|
|||||||
- ".golangci.yml"
|
- ".golangci.yml"
|
||||||
- ".editorconfig"
|
- ".editorconfig"
|
||||||
- "options/locale/locale_en-US.json"
|
- "options/locale/locale_en-US.json"
|
||||||
|
- "models/fixtures/**"
|
||||||
|
- "tests/*.ini.tmpl"
|
||||||
|
- "tests/gitea-repositories-meta/**"
|
||||||
|
- "tests/testdata/**"
|
||||||
|
- "tools/test-integration.sh"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
- "*.js"
|
|
||||||
- "*.ts"
|
- "*.ts"
|
||||||
- "web_src/**"
|
- "web_src/**"
|
||||||
- "tools/*.js"
|
- "tools/generate-svg.ts"
|
||||||
- "tools/*.ts"
|
- "tools/generate-svg-vscode-extensions.json"
|
||||||
|
- "tsconfig.json"
|
||||||
- "assets/emoji.json"
|
- "assets/emoji.json"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "pnpm-lock.yaml"
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
- ".eslintrc.cjs"
|
|
||||||
- ".npmrc"
|
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
- ".markdownlint.yaml"
|
- ".markdownlint.yaml"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "pnpm-lock.yaml"
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- ".github/workflows/*"
|
- ".github/workflows/*"
|
||||||
|
- ".github/actions/**"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
- "tools/lint-templates-*.js"
|
- "tools/lint-templates-*.ts"
|
||||||
- "templates/**/*.tmpl"
|
- "templates/**/*.tmpl"
|
||||||
- "pyproject.toml"
|
- "pyproject.toml"
|
||||||
- "uv.lock"
|
- "uv.lock"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
- ".github/workflows/pull-docker-dryrun.yml"
|
- ".github/workflows/pull-docker-dryrun.yml"
|
||||||
|
- ".github/actions/docker-dryrun/**"
|
||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
- "Dockerfile.rootless"
|
- "Dockerfile.rootless"
|
||||||
- "docker/**"
|
- "docker/**"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
|
|
||||||
|
dockerfile:
|
||||||
|
- "Dockerfile"
|
||||||
|
- "Dockerfile.rootless"
|
||||||
|
|
||||||
swagger:
|
swagger:
|
||||||
- "templates/swagger/v1_json.tmpl"
|
- "templates/swagger/v1_json.tmpl"
|
||||||
- "templates/swagger/v1_input.json"
|
- "templates/swagger/v1_input.json"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "pnpm-lock.yaml"
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
- ".spectral.yaml"
|
- ".spectral.yaml"
|
||||||
|
|
||||||
yaml:
|
yaml:
|
||||||
@@ -107,3 +129,14 @@ jobs:
|
|||||||
|
|
||||||
json:
|
json:
|
||||||
- "**/*.json"
|
- "**/*.json"
|
||||||
|
- "**/*.json5"
|
||||||
|
- "eslint.json.config.ts"
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
- "tests/e2e/**"
|
||||||
|
- "tools/test-e2e.sh"
|
||||||
|
- "playwright.config.ts"
|
||||||
|
|
||||||
|
shell:
|
||||||
|
- "**/*.sh"
|
||||||
|
- ".shellcheckrc"
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: giteabot backport
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
giteabot:
|
||||||
|
if: github.repository == 'go-gitea/gitea'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
|
gitea_fork: giteabot/gitea
|
||||||
|
checks: backport
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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, queue maintenance, and
|
||||||
|
# explicit bot actions triggered by relevant label changes.
|
||||||
|
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- labeled
|
||||||
|
- unlabeled
|
||||||
|
- closed
|
||||||
|
- review_requested
|
||||||
|
- review_request_removed
|
||||||
|
# Review events keep review-derived state such as lgtm labels and status checks
|
||||||
|
# in sync after approvals, edits, or dismissals.
|
||||||
|
pull_request_review:
|
||||||
|
types:
|
||||||
|
- 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
|
||||||
|
# behavior without waiting for another repository event.
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
checks:
|
||||||
|
description: Comma-separated list of non-backport checks to run
|
||||||
|
required: false
|
||||||
|
default: labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
statuses: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
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'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
# pull_request_review runs without repository secrets on fork PRs, so fall
|
||||||
|
# back to the workflow token for the non-backport checks handled here.
|
||||||
|
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
|
||||||
|
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||||
+37
-174
@@ -7,156 +7,63 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
files-changed:
|
files-changed:
|
||||||
uses: ./.github/workflows/files-changed.yml
|
uses: ./.github/workflows/files-changed.yml
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
lint-backend:
|
lint-backend:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
lint-cache: "true"
|
||||||
check-latest: true
|
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
|
- run: TAGS="bindata" make generate-go # lint-go also lints with "bindata" tags which requires "_bindata.go"
|
||||||
- run: make lint-backend
|
- run: make lint-backend
|
||||||
env:
|
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
|
||||||
|
|
||||||
lint-templates:
|
lint-on-demand:
|
||||||
if: needs.files-changed.outputs.templates == 'true'
|
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: astral-sh/setup-uv@v8.0.0
|
- uses: ./.github/actions/go-setup
|
||||||
- run: uv python install 3.14
|
|
||||||
- uses: pnpm/action-setup@v5
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
cache: "false"
|
||||||
cache: pnpm
|
- uses: ./.github/actions/node-setup
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-py
|
|
||||||
- run: make deps-frontend
|
|
||||||
- run: make lint-templates
|
|
||||||
|
|
||||||
lint-yaml:
|
|
||||||
if: needs.files-changed.outputs.yaml == 'true'
|
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: astral-sh/setup-uv@v8.0.0
|
|
||||||
- run: uv python install 3.14
|
|
||||||
- run: make deps-py
|
|
||||||
- run: make lint-yaml
|
|
||||||
|
|
||||||
lint-json:
|
|
||||||
if: needs.files-changed.outputs.json == 'true'
|
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v5
|
|
||||||
- uses: actions/setup-node@v5
|
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
cache: "false"
|
||||||
- run: make deps-frontend
|
|
||||||
- run: make lint-json
|
|
||||||
|
|
||||||
lint-swagger:
|
|
||||||
if: needs.files-changed.outputs.swagger == 'true'
|
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v5
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend
|
|
||||||
- run: make lint-swagger
|
|
||||||
|
|
||||||
lint-spell:
|
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
|
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- run: make lint-spell
|
- run: make lint-spell
|
||||||
|
|
||||||
lint-go-windows:
|
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
python-version: 3.14
|
||||||
check-latest: true
|
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
|
||||||
- run: make deps-backend deps-tools
|
run: make deps-py lint-templates lint-yaml
|
||||||
- run: make lint-go-windows lint-go-gitea-vet
|
|
||||||
env:
|
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
|
||||||
GOOS: windows
|
|
||||||
GOARCH: amd64
|
|
||||||
|
|
||||||
lint-go-gogit:
|
- if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true'
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
run: make deps-frontend lint-md lint-swagger lint-json
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
- if: needs.files-changed.outputs.actions == 'true'
|
||||||
permissions:
|
run: make lint-actions
|
||||||
contents: read
|
|
||||||
steps:
|
- if: needs.files-changed.outputs.shell == 'true'
|
||||||
- uses: actions/checkout@v6
|
run: make lint-shell
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- run: make deps-backend deps-tools
|
|
||||||
- run: make lint-go
|
|
||||||
env:
|
|
||||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
|
||||||
|
|
||||||
checks-backend:
|
checks-backend:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
||||||
|
|
||||||
@@ -164,16 +71,9 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: ./.github/actions/node-setup
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend
|
- run: make deps-frontend
|
||||||
- run: make lint-frontend
|
- run: make lint-frontend
|
||||||
- run: make checks-frontend
|
- run: make checks-frontend
|
||||||
@@ -184,20 +84,14 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
- run: make deps-backend generate-go
|
||||||
go-version-file: go.mod
|
# no frontend build here as backend should be able to build, even without any frontend files
|
||||||
check-latest: true
|
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
|
||||||
# no frontend build here as backend should be able to build
|
|
||||||
# even without any frontend files
|
|
||||||
- run: make deps-backend
|
|
||||||
- run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag
|
|
||||||
- name: build-backend-arm64
|
- name: build-backend-arm64
|
||||||
run: make backend # test cross compile
|
run: go build -o gitea_linux_arm64
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: arm64
|
GOARCH: arm64
|
||||||
@@ -209,38 +103,7 @@ jobs:
|
|||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
TAGS: bindata gogit
|
TAGS: bindata gogit
|
||||||
- name: build-backend-386
|
- name: build-backend-386
|
||||||
run: go build -o gitea_linux_386 # test if compatible with 32 bit
|
run: go build -o gitea_linux_386
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: 386
|
GOARCH: 386
|
||||||
|
|
||||||
docs:
|
|
||||||
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
|
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v5
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend
|
|
||||||
- run: make lint-md
|
|
||||||
|
|
||||||
actions:
|
|
||||||
if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
|
|
||||||
needs: files-changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- run: make lint-actions
|
|
||||||
|
|||||||
+100
-88
@@ -7,18 +7,18 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
uses: ./.github/workflows/files-changed.yml
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
test-pgsql:
|
jobs:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
files-changed:
|
||||||
|
uses: ./.github/workflows/files-changed.yml
|
||||||
|
|
||||||
|
test-pgsql-shard-1:
|
||||||
|
if: needs.files-changed.outputs.backend == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
timeout-minutes: 50
|
||||||
contents: read
|
|
||||||
services:
|
services:
|
||||||
pgsql:
|
pgsql:
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
@@ -28,79 +28,100 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
ldap:
|
ldap:
|
||||||
image: gitea/test-openldap:latest
|
image: gitea/test-openldap:latest@sha256:4ac633b01d684e6b2a458cc0c8530c92f9b3702f6e040ce5f365607df34fbda0
|
||||||
ports:
|
ports:
|
||||||
- "389:389"
|
- "389:389"
|
||||||
- "636:636"
|
- "636:636"
|
||||||
minio:
|
minio:
|
||||||
# as github actions doesn't support "entrypoint", we need to use a non-official image
|
# 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"
|
# that has a custom entrypoint set to "minio server /data"
|
||||||
image: bitnamilegacy/minio:2023.8.31
|
image: bitnamilegacy/minio:2025.7.23
|
||||||
env:
|
env:
|
||||||
MINIO_ROOT_USER: 123456
|
MINIO_ROOT_USER: 123456
|
||||||
MINIO_ROOT_PASSWORD: 12345678
|
MINIO_ROOT_PASSWORD: 12345678
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
|
- uses: ./.github/actions/pgsql-shard
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
shard: 1
|
||||||
check-latest: true
|
total-shards: 2
|
||||||
- name: Add hosts to /etc/hosts
|
run-migration: "true"
|
||||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
|
|
||||||
- run: make deps-backend
|
test-pgsql-shard-2:
|
||||||
- run: make backend
|
if: needs.files-changed.outputs.backend == 'true'
|
||||||
env:
|
needs: files-changed
|
||||||
TAGS: bindata
|
runs-on: ubuntu-latest
|
||||||
- name: run migration tests
|
|
||||||
run: make test-pgsql-migration
|
|
||||||
- name: run tests
|
|
||||||
run: make test-pgsql
|
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
|
services:
|
||||||
|
pgsql:
|
||||||
|
image: postgres:14
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit
|
POSTGRES_DB: test
|
||||||
RACE_ENABLED: true
|
POSTGRES_PASSWORD: postgres
|
||||||
TEST_TAGS: gogit
|
ports:
|
||||||
TEST_LDAP: 1
|
- "5432:5432"
|
||||||
|
ldap:
|
||||||
|
image: gitea/test-openldap:latest@sha256:4ac633b01d684e6b2a458cc0c8530c92f9b3702f6e040ce5f365607df34fbda0
|
||||||
|
ports:
|
||||||
|
- "389:389"
|
||||||
|
- "636:636"
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
MINIO_ROOT_USER: 123456
|
||||||
|
MINIO_ROOT_PASSWORD: 12345678
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
|
- uses: ./.github/actions/go-setup
|
||||||
|
- uses: ./.github/actions/pgsql-shard
|
||||||
|
with:
|
||||||
|
shard: 2
|
||||||
|
total-shards: 2
|
||||||
|
|
||||||
test-sqlite:
|
test-sqlite:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: GOEXPERIMENT='' make backend
|
- run: make backend
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
TAGS: bindata gogit
|
||||||
- name: run migration tests
|
GOEXPERIMENT:
|
||||||
run: make test-sqlite-migration
|
- run: GITEA_TEST_DATABASE=sqlite make test-migration
|
||||||
|
env:
|
||||||
|
TAGS: bindata gogit
|
||||||
- name: run tests
|
- name: run tests
|
||||||
run: GOEXPERIMENT='' make test-sqlite
|
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
||||||
RACE_ENABLED: true
|
GOTEST_FLAGS: -timeout=40m
|
||||||
TEST_TAGS: gogit sqlite sqlite_unlock_notify
|
TAGS: bindata gogit
|
||||||
|
GOEXPERIMENT:
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
services:
|
services:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: elasticsearch:7.5.0
|
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15
|
||||||
env:
|
env:
|
||||||
discovery.type: single-node
|
discovery.type: single-node
|
||||||
|
xpack.security.enabled: false
|
||||||
|
xpack.ml.enabled: false
|
||||||
|
ingest.geoip.downloader.enabled: false
|
||||||
|
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
|
||||||
ports:
|
ports:
|
||||||
- "9200:9200"
|
- "9200:9200"
|
||||||
meilisearch:
|
meilisearch:
|
||||||
@@ -110,7 +131,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- "7700:7700"
|
- "7700:7700"
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
|
||||||
options: >- # wait until redis has started
|
options: >- # wait until redis has started
|
||||||
--health-cmd "redis-cli ping"
|
--health-cmd "redis-cli ping"
|
||||||
--health-interval 5s
|
--health-interval 5s
|
||||||
@@ -119,51 +140,49 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
minio:
|
minio:
|
||||||
image: bitnamilegacy/minio:2021.3.17
|
image: bitnamilegacy/minio:2025.7.23
|
||||||
env:
|
env:
|
||||||
MINIO_ACCESS_KEY: 123456
|
MINIO_ROOT_USER: 123456
|
||||||
MINIO_SECRET_KEY: 12345678
|
MINIO_ROOT_PASSWORD: 12345678
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
||||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4
|
||||||
ports:
|
ports:
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- name: Add hosts to /etc/hosts
|
- 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'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: make backend
|
- run: make generate-go
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- name: unit-tests
|
- name: unit-tests
|
||||||
run: make unit-test-coverage test-check
|
run: make test-backend
|
||||||
env:
|
env:
|
||||||
|
GOTEST_FLAGS: -race -timeout=20m
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
RACE_ENABLED: true
|
|
||||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||||
- name: unit-tests-gogit
|
- name: unit-tests-gogit
|
||||||
run: GOEXPERIMENT='' make unit-test-coverage test-check
|
run: make test-backend
|
||||||
env:
|
env:
|
||||||
|
GOTEST_FLAGS: -race -timeout=20m
|
||||||
TAGS: bindata gogit
|
TAGS: bindata gogit
|
||||||
RACE_ENABLED: true
|
GOEXPERIMENT:
|
||||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||||
|
GITEA_TEST_CI_SKIP_EXTERNAL: true
|
||||||
|
- run: make test-check
|
||||||
|
|
||||||
test-mysql:
|
test-mysql:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
# the bitnami mysql image has more options than the official one, it's easier to customize
|
# the bitnami mysql image has more options than the official one, it's easier to customize
|
||||||
image: bitnamilegacy/mysql:8.0
|
image: bitnamilegacy/mysql:8.4
|
||||||
env:
|
env:
|
||||||
ALLOW_EMPTY_PASSWORD: true
|
ALLOW_EMPTY_PASSWORD: true
|
||||||
MYSQL_DATABASE: testgitea
|
MYSQL_DATABASE: testgitea
|
||||||
@@ -172,46 +191,42 @@ jobs:
|
|||||||
options: >-
|
options: >-
|
||||||
--mount type=tmpfs,destination=/bitnami/mysql/data
|
--mount type=tmpfs,destination=/bitnami/mysql/data
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: elasticsearch:7.5.0
|
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15
|
||||||
env:
|
env:
|
||||||
discovery.type: single-node
|
discovery.type: single-node
|
||||||
|
xpack.security.enabled: false
|
||||||
|
xpack.ml.enabled: false
|
||||||
|
ingest.geoip.downloader.enabled: false
|
||||||
|
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
|
||||||
ports:
|
ports:
|
||||||
- "9200:9200"
|
- "9200:9200"
|
||||||
smtpimap:
|
smtpimap:
|
||||||
image: tabascoterrier/docker-imap-devel:latest
|
image: tabascoterrier/docker-imap-devel:latest@sha256:3fb7cf50b47693e7b80f6f74abea2def4d7386016931d61359864de8a0aba551
|
||||||
ports:
|
ports:
|
||||||
- "25:25"
|
- "25:25"
|
||||||
- "143:143"
|
- "143:143"
|
||||||
- "587:587"
|
- "587:587"
|
||||||
- "993:993"
|
- "993:993"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- name: Add hosts to /etc/hosts
|
- 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'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: make backend
|
- run: make backend
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- name: run migration tests
|
- run: GITEA_TEST_DATABASE=mysql make test-migration
|
||||||
run: make test-mysql-migration
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
# run: make integration-test-coverage (at the moment, no coverage is really handled)
|
run: GITEA_TEST_DATABASE=mysql make test-integration
|
||||||
run: make test-mysql
|
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
RACE_ENABLED: true
|
|
||||||
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||||
|
|
||||||
test-mssql:
|
test-mssql:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
services:
|
services:
|
||||||
mssql:
|
mssql:
|
||||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||||
@@ -222,24 +237,21 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- "1433:1433"
|
- "1433:1433"
|
||||||
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
||||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4
|
||||||
ports:
|
ports:
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- name: Add hosts to /etc/hosts
|
- 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'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: make backend
|
- run: make backend
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- run: make test-mssql-migration
|
- run: GITEA_TEST_DATABASE=mssql make test-migration
|
||||||
- name: run tests
|
- name: run tests
|
||||||
run: make test-mssql
|
run: GITEA_TEST_DATABASE=mssql make test-integration
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
|
|||||||
+30
-23
@@ -7,34 +7,41 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
uses: ./.github/workflows/files-changed.yml
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
container:
|
jobs:
|
||||||
|
files-changed:
|
||||||
|
uses: ./.github/workflows/files-changed.yml
|
||||||
|
|
||||||
|
# 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'
|
if: needs.files-changed.outputs.docker == 'true'
|
||||||
needs: files-changed
|
needs: [files-changed]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: ./.github/actions/docker-dryrun
|
||||||
- uses: docker/setup-buildx-action@v4
|
|
||||||
- name: Build regular container image
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
with:
|
||||||
context: .
|
platform: linux/amd64
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
|
||||||
push: false
|
container-arm64:
|
||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||||
- name: Build rootless container image
|
needs: [files-changed]
|
||||||
uses: docker/build-push-action@v7
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
|
- uses: ./.github/actions/docker-dryrun
|
||||||
with:
|
with:
|
||||||
context: .
|
platform: linux/arm64
|
||||||
push: false
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
container-riscv64:
|
||||||
file: Dockerfile.rootless
|
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
needs: [files-changed]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
|
- uses: ./.github/actions/docker-dryrun
|
||||||
|
with:
|
||||||
|
platform: linux/riscv64
|
||||||
|
|||||||
+12
-18
@@ -7,37 +7,31 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
uses: ./.github/workflows/files-changed.yml
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
files-changed:
|
||||||
|
uses: ./.github/workflows/files-changed.yml
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.e2e == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: ./.github/actions/go-setup
|
||||||
with:
|
- uses: ./.github/actions/node-setup
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
- uses: pnpm/action-setup@v5
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
|
||||||
- run: make deps-frontend
|
- run: make deps-frontend
|
||||||
- run: make frontend
|
- run: make frontend
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: make gitea-e2e
|
- run: make backend
|
||||||
|
env:
|
||||||
|
TAGS: bindata
|
||||||
- run: make playwright
|
- run: make playwright
|
||||||
- run: make test-e2e
|
- run: make test-e2e
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
env:
|
env:
|
||||||
|
TAGS: bindata
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
GITEA_TEST_E2E_DEBUG: 1
|
GITEA_TEST_E2E_DEBUG: 1
|
||||||
|
|||||||
+30
-3
@@ -1,8 +1,10 @@
|
|||||||
name: labeler
|
name: labeler
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
# pull_request_target is required to label PRs from forks; jobs only use pinned
|
||||||
types: [opened, synchronize, reopened]
|
# actions or base-branch checkout, never PR-head code.
|
||||||
|
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||||
|
types: [opened, synchronize, reopened, edited, ready_for_review]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
@@ -15,6 +17,31 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v6
|
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|
||||||
|
pr-title:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
# Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here.
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
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:
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
- run: node ./tools/ci-tools.ts set-pr-labels
|
||||||
|
env:
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|||||||
@@ -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
|
||||||
+39
-27
@@ -13,17 +13,18 @@ jobs:
|
|||||||
runs-on: namespace-profile-gitea-release-binary
|
runs-on: namespace-profile-gitea-release-binary
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- 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 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -32,34 +33,43 @@ jobs:
|
|||||||
# xgo build
|
# xgo build
|
||||||
- run: make release
|
- run: make release
|
||||||
env:
|
env:
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||||
- name: import gpg key
|
- name: import gpg key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
- name: sign binaries
|
- name: sign binaries
|
||||||
|
env:
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
run: |
|
run: |
|
||||||
for f in dist/release/*; do
|
for f in dist/release/*; do
|
||||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
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
|
done
|
||||||
# clean branch name to get the folder name in S3
|
# clean branch name to get the folder name in the object storage
|
||||||
- name: Get cleaned branch name
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: upload binaries to cloudflare r2
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
env:
|
||||||
with:
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_DEFAULT_REGION: auto
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||||
- name: upload binaries to s3
|
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||||
|
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
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:
|
nightly-container:
|
||||||
runs-on: namespace-profile-gitea-release-docker
|
runs-on: namespace-profile-gitea-release-docker
|
||||||
@@ -67,18 +77,20 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- 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 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- name: Get cleaned branch name
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -88,7 +100,7 @@ jobs:
|
|||||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -102,18 +114,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular docker image
|
- name: build regular docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -123,7 +135,7 @@ jobs:
|
|||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
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
|
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
||||||
- name: build rootless docker image
|
- name: build rootless docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
+40
-29
@@ -14,17 +14,18 @@ jobs:
|
|||||||
runs-on: namespace-profile-gitea-release-binary
|
runs-on: namespace-profile-gitea-release-binary
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- 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 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -33,43 +34,53 @@ jobs:
|
|||||||
# xgo build
|
# xgo build
|
||||||
- run: make release
|
- run: make release
|
||||||
env:
|
env:
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||||
- name: import gpg key
|
- name: import gpg key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
- name: sign binaries
|
- name: sign binaries
|
||||||
|
env:
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
run: |
|
run: |
|
||||||
for f in dist/release/*; do
|
for f in dist/release/*; do
|
||||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
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
|
done
|
||||||
# clean branch name to get the folder name in S3
|
# clean branch name to get the folder name in the object storage
|
||||||
- name: Get cleaned branch name
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
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 "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: upload binaries to cloudflare r2
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
env:
|
||||||
with:
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_DEFAULT_REGION: auto
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||||
- name: upload binaries to s3
|
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||||
|
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
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
|
- name: Install GH CLI
|
||||||
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1
|
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||||
with:
|
with:
|
||||||
gh-cli-version: 2.39.1
|
gh-cli-version: 2.39.1
|
||||||
- name: create github release
|
- name: create github release
|
||||||
run: |
|
|
||||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
gh release create "$TAG" --title "$TAG" --draft --notes-from-tag dist/release/*
|
||||||
|
|
||||||
container:
|
container:
|
||||||
runs-on: namespace-profile-gitea-release-docker
|
runs-on: namespace-profile-gitea-release-docker
|
||||||
@@ -77,13 +88,13 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- 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 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -96,7 +107,7 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -112,18 +123,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular container image
|
- name: build regular container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -131,7 +142,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
annotations: ${{ steps.meta.outputs.annotations }}
|
||||||
- name: build rootless container image
|
- name: build rootless container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
+40
-29
@@ -17,17 +17,18 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- 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 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -36,43 +37,53 @@ jobs:
|
|||||||
# xgo build
|
# xgo build
|
||||||
- run: make release
|
- run: make release
|
||||||
env:
|
env:
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||||
- name: import gpg key
|
- name: import gpg key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
- name: sign binaries
|
- name: sign binaries
|
||||||
|
env:
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
run: |
|
run: |
|
||||||
for f in dist/release/*; do
|
for f in dist/release/*; do
|
||||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
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
|
done
|
||||||
# clean branch name to get the folder name in S3
|
# clean branch name to get the folder name in the object storage
|
||||||
- name: Get cleaned branch name
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
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 "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: upload binaries to cloudflare r2
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
env:
|
||||||
with:
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_DEFAULT_REGION: auto
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||||
- name: upload binaries to s3
|
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||||
|
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
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
|
- name: Install GH CLI
|
||||||
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1
|
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||||
with:
|
with:
|
||||||
gh-cli-version: 2.39.1
|
gh-cli-version: 2.39.1
|
||||||
- name: create github release
|
- name: create github release
|
||||||
run: |
|
|
||||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
gh release create "$TAG" --title "$TAG" --notes-from-tag dist/release/*
|
||||||
|
|
||||||
container:
|
container:
|
||||||
runs-on: namespace-profile-gitea-release-docker
|
runs-on: namespace-profile-gitea-release-docker
|
||||||
@@ -80,13 +91,13 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- 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 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
|
# 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
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -103,7 +114,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -124,18 +135,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular container image
|
- name: build regular container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -143,7 +154,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
annotations: ${{ steps.meta.outputs.annotations }}
|
||||||
- name: build rootless container image
|
- name: build rootless container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
@@ -55,10 +55,7 @@ cpu.out
|
|||||||
*.log.*.gz
|
*.log.*.gz
|
||||||
|
|
||||||
/gitea
|
/gitea
|
||||||
/gitea-e2e
|
|
||||||
/gitea-vet
|
|
||||||
/debug
|
/debug
|
||||||
/integrations.test
|
|
||||||
|
|
||||||
/bin
|
/bin
|
||||||
/dist
|
/dist
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ linters:
|
|||||||
desc: use os or io instead
|
desc: use os or io instead
|
||||||
- pkg: golang.org/x/exp
|
- pkg: golang.org/x/exp
|
||||||
desc: it's experimental and unreliable
|
desc: it's experimental and unreliable
|
||||||
- pkg: code.gitea.io/gitea/modules/git/internal
|
- pkg: gitea.dev/modules/git/internal
|
||||||
desc: do not use the internal package, use AddXxx function instead
|
desc: do not use the internal package, use AddXxx function instead
|
||||||
- pkg: gopkg.in/ini.v1
|
- pkg: gopkg.in/ini.v1
|
||||||
desc: do not use the ini package, use gitea's config system instead
|
desc: do not use the ini package, use gitea's config system instead
|
||||||
@@ -51,6 +51,14 @@ linters:
|
|||||||
desc: do not use the go-chi cache package, use gitea's cache system
|
desc: do not use the go-chi cache package, use gitea's cache system
|
||||||
- pkg: github.com/pkg/errors
|
- pkg: github.com/pkg/errors
|
||||||
desc: use builtin errors package instead
|
desc: use builtin errors package instead
|
||||||
|
migrations:
|
||||||
|
files:
|
||||||
|
- '**/models/migrations/**/*.go'
|
||||||
|
deny:
|
||||||
|
- 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 (API structures change over time)
|
||||||
nolintlint:
|
nolintlint:
|
||||||
allow-unused: false
|
allow-unused: false
|
||||||
require-explanation: true
|
require-explanation: true
|
||||||
@@ -99,6 +107,7 @@ linters:
|
|||||||
- -QF1008
|
- -QF1008
|
||||||
testifylint:
|
testifylint:
|
||||||
disable:
|
disable:
|
||||||
|
- empty
|
||||||
- go-require
|
- go-require
|
||||||
- require-error
|
- require-error
|
||||||
usetesting:
|
usetesting:
|
||||||
@@ -158,9 +167,16 @@ issues:
|
|||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gofmt
|
- gci
|
||||||
- gofumpt
|
- gofumpt
|
||||||
settings:
|
settings:
|
||||||
|
gci:
|
||||||
|
custom-order: true
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- prefix(gitea.dev)
|
||||||
|
- blank
|
||||||
|
- default
|
||||||
gofumpt:
|
gofumpt:
|
||||||
extra-rules: true
|
extra-rules: true
|
||||||
exclusions:
|
exclusions:
|
||||||
@@ -170,9 +186,6 @@ formatters:
|
|||||||
- .venv
|
- .venv
|
||||||
- public
|
- public
|
||||||
- web_src
|
- web_src
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
audit=false
|
|
||||||
fund=false
|
|
||||||
update-notifier=false
|
|
||||||
save-exact=true
|
|
||||||
auto-install-peers=true
|
|
||||||
dedupe-peer-dependents=false
|
|
||||||
enable-pre-post-scripts=true
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2154,SC2155,SC2164,SC2181,SC2207
|
||||||
@@ -4,6 +4,504 @@ This changelog goes through the changes that have been made in each release
|
|||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
|
## [1.27.3](https://github.com/go-gitea/gitea/releases/tag/v1.27.3) - 2026-08-29
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* fix(packages): restrict/limited/token-scope access (#39041, #39043, #39044, #39047, #39046) (#39058)
|
||||||
|
* fix(attachments): enforce owning repository path (#39048) (#39077)
|
||||||
|
* fix(markup): enforce same-repository issue access (#39045) (#39054)
|
||||||
|
* fix(actions): verify raw artifact signatures first (#39049) (#39053)
|
||||||
|
* fix(api): hide limited users from restricted viewers (#39004) (#39039)
|
||||||
|
* fix(repo): limit gitignore template selections (#39027) (#39040)
|
||||||
|
* fix(migrations): cancel GitLab version probes (#39023) (#39035)
|
||||||
|
* fix(packages): limit Swift package manifests (#39025) (#39032)
|
||||||
|
* fix(migrations): bound OneDev version responses (#39024) (#39033)
|
||||||
|
* fix(packages): limit Maven checksum uploads (#39028) (#39031)
|
||||||
|
* fix(packages): bound Alpine metadata entries (#39026) (#39029)
|
||||||
|
* fix(actions): enforce fork pull request trust boundaries (#39005) (#39018)
|
||||||
|
* fix(git): restrict hook permissions (#39008) (#39016)
|
||||||
|
* fix(api): enforce repository creation token authorization (#39007) (#39014)
|
||||||
|
* fix(api): enforce public-only scope for compare heads (#39006) (#39013)
|
||||||
|
* fix(repo): hide repositories of hidden owners (#39009) (#39012)
|
||||||
|
* fix: avoid enumerating every public repository in issue search (#38992) (#39000)
|
||||||
|
* refactor: private endpoints (#38964) (#38965)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* enhance: add permalinks to pull request reviews (#38849) (#39036)
|
||||||
|
* BUGFIXES
|
||||||
|
* fix: add missing query parameters on runner list page (#39163)
|
||||||
|
* fix(actions): keep step-level continue-on-error expressions unevaluated (#39141) (#39148)
|
||||||
|
* fix(packages): preserve SemVer prerelease identifiers in Swift Registry (#39156) (#39158)
|
||||||
|
* fix(repo): prevent MarkAsBrokenEmpty when repository is being migrated (#39091) (#39092)
|
||||||
|
* fix(asymkey): do not verify OpenPGP signatures with an SSH instance key (#39073) (#39086)
|
||||||
|
* fix(pull): keep the merged state in sync with git (#39062) (#39118)
|
||||||
|
* fix(pull): name the head repository in default compare links (#39075) (#39079)
|
||||||
|
* fix(git): parse co-author trailers that are not RFC 5322 addresses (#39076) (#39081)
|
||||||
|
* fix(actions): show "Complete job" logs when the last step is skipped (#38939) (#39003)
|
||||||
|
* fix(actions): Fix how jobs in matrixes are grouped (#38980) (#38998)
|
||||||
|
* fix: resolve YAML anchors and aliases in Actions workflows (#38984) (#38996)
|
||||||
|
* fix: honor environment variables during install (#38974) (#38976)
|
||||||
|
* fix: grant limited-org unit read access to authenticated non-members (#38871) (#38963)
|
||||||
|
* fix: allow anonymous theme switching when REQUIRE_SIGNIN_VIEW is set (#38956) (#38961)
|
||||||
|
* fix(actions): drop wrapper span around the action status icon (#38957) (#38959)
|
||||||
|
* fix(issues): sort scoped labels by exclusive order in dropdowns (#38893) (#38954)
|
||||||
|
* fix(indexer): correct bleve indexer token filters (#38853) (#38951)
|
||||||
|
* fix: make "login_name" field optional for API edit user (#38917) (#38945)
|
||||||
|
* fix(actions): reject non-mapping matrix include/exclude (#38933)
|
||||||
|
* fix(ui): respect FEED_PAGING_NUM on the dashboard feed (#38935) (#38936)
|
||||||
|
* MISC
|
||||||
|
* chore: repo compare link (#39088) (#39119)
|
||||||
|
* ci: remove AWS S3 uploads from release workflows (#38928) (#38929)
|
||||||
|
* chore: Pre-register a builtin OAuth2 application for the official Gitea mobile app (#38880) (#38922)
|
||||||
|
|
||||||
|
## [1.27.2](https://github.com/go-gitea/gitea/releases/tag/v1.27.2) - 2026-08-14
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Fix: update collaborator access mode and httpsign (#38894, #38862) (#38895)
|
||||||
|
* Refactor: external render (#38885) (#38898)
|
||||||
|
* Fix(actions): resolve pull_request_target reusable workflows at the base commit (#38886) (#38897)
|
||||||
|
* Refactor: markup render (#38864) (#38869)
|
||||||
|
* Fix(deps): update dependency mermaid to v11.16.1 (#38816)
|
||||||
|
* Fix(auth): set WebAuthn user verification per request (#38805) (#38810)
|
||||||
|
* Fix: render highlight language (#38793) (#38795)
|
||||||
|
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* enhance: add missing npm package metadata properties (#38826) (#38831)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* fix(actions): keep github.event.inputs as strings for workflow_dispatch (#38899) (#38908)
|
||||||
|
* fix(actions): let a rerun of selected jobs read the previous attempt's artifacts (#38857) (#38901)
|
||||||
|
* fix(lfs): accept successful transfer responses (#38866) (#38875)
|
||||||
|
* fix(packages): ignore nested Package.swift (#38788) (#38836)
|
||||||
|
* fix: drop newline-bearing member names in arch ParsePackage (#38102) (#38830)
|
||||||
|
* fix(storage): fix Azure Blob dump failing with file does not exist (#38814) (#38828)
|
||||||
|
* fix(migration): migration deletion returned json redirection (#38796) (#38825)
|
||||||
|
* fix(ui): change underlines to default browser style (#38819) (#38823)
|
||||||
|
* fix(actions): allow cancelling runs without running jobs (#35842) (#38812)
|
||||||
|
* fix(actions): evaluate each `${{ }}` part on its own (#38754) (#38797)
|
||||||
|
* fix(actions): write an action task report in one transaction (#38792) (#38794)
|
||||||
|
* fix: markup link (#38764) (#38765)
|
||||||
|
* fix: set a minio part size when the content size is unknown (#38753) (#38755)
|
||||||
|
* fix: bad path escape in subpath archive download (#38749) (#38750)
|
||||||
|
* fix: remove the pull merge box from UI when the refreshed page doesn't contain it (#38742) (#38744)
|
||||||
|
* fix(markdown): fix double strikethough on code (#38707) (#38729)
|
||||||
|
* fix(lfs): failed upload deletes a concurrent upload's meta object (#38693) (#38722)
|
||||||
|
* fix: correct full url when using sub-path (#38712) (#38716)
|
||||||
|
* fix: avoid markup render panic (#38698) (#38703)
|
||||||
|
* fix(ui): too many participants shown in commit avatar stacks (#38689) (#38700)
|
||||||
|
* fix: support HEAD requests on Alpine registry APKINDEX.tar.gz (#38686) (#38688)
|
||||||
|
* fix(migrations): use all configured GitHub tokens (#38841) (#38846)
|
||||||
|
|
||||||
|
## [1.27.1](https://github.com/go-gitea/gitea/releases/tag/v1.27.1) - 2026-07-27
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* fix(oauth2): enforce mandatory 2FA policy on OAuth2 authorize/grant endpoints (#38591) (#38606)
|
||||||
|
|
||||||
|
* API
|
||||||
|
* fix(api): align Swagger schemas for UserSettings and TopicListResponse (#38590) (#38592)
|
||||||
|
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* enhance: improve diff contrast in light and dark themes (#37477) (#38574)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* fix: skip OIDC end-session after password login for OAuth2 users (#38439) (#38666)
|
||||||
|
* fix: make Actions log parser support multiple line message encoding (#38659) (#38664)
|
||||||
|
* fix(actions): use base branch ref for pull_request_target context (#38636) (#38657)
|
||||||
|
* fix(actions): skip already-approved runs in `ApproveRuns` (#38653) (#38654)
|
||||||
|
* fix: orgmode render include path (#38642) (#38645)
|
||||||
|
* fix(actions): cancel tasks immediately when the runner stopped reporting (#38616) (#38644)
|
||||||
|
* fix(issues): fix label bulk-load key and reduce log noise in LoadLabel (#38632) (#38643)
|
||||||
|
* fix(actions): improve runner list status sorting, labels and task job links (#38586) (#38633)
|
||||||
|
* fix(actions): correctness and hardening fixes (#38518) (#38631)
|
||||||
|
* fix(repo): prevent double-write redirect collisions on dependency errors, fix ui (#38627) (#38628)
|
||||||
|
* fix: delete repo-scoped rows of seven more tables when deleting a repository (#38534) (#38618)
|
||||||
|
* fix(webhook): remove slack channel name check (#38608) (#38612)
|
||||||
|
* fix: download dropdown menu clipped on the branches page (#38604) (#38609)
|
||||||
|
* fix(project): prevent database mutations on invalid MoveIssues payload (#38600) (#38602)
|
||||||
|
* fix(actions): make SingleWorkflow.Marshal round-trip multi-line run blocks (stop silent job stranding) (#38520) (#38599)
|
||||||
|
* fix(file-tree): handle submodule links and missing view container (#38033) (#38589)
|
||||||
|
* fix(actions): fail unexpandable reusable workflow callers and decouple the job emitter's cross-run processing (#38565) (#38587)
|
||||||
|
* fix: keep serving valid ACME cert when renewal fails at startup (#38554) (#38583)
|
||||||
|
* fix: branch protection user list (#38570) (#38584)
|
||||||
|
* fix(pulls): respect diff.orderFile in diff file tree (#38566) (#38578)
|
||||||
|
* fix(issue): make issue action (issue list batch operation) elements have correct attributes (#38575) (#38580)
|
||||||
|
* fix(actions): support `matrix` when evaluating workflow `if` expression (#38474) (#38557)
|
||||||
|
* fix(actions): align status icon span for Safari rendering (#38558) (#38562)
|
||||||
|
* fix: revert git clone http redirection forbidden (#38530) (#38545)
|
||||||
|
* fix: clean up orphaned user-keyed tables in deleteUser (#38511) (#38514)
|
||||||
|
* fix(actions): coerce workflow_dispatch boolean inputs to native types (#38472) (#38521)
|
||||||
|
* fix: make the merge box button red if some checks fail (#38508) (#38516)
|
||||||
|
* fix(pull): sign the commit when updating a branch by merge (#38441) (#38499)
|
||||||
|
* fix: make commit message merge correctly (#38490) (#38502)
|
||||||
|
* fix(actions): explain why a blocked or waiting job has not started (#38476) (#38498)
|
||||||
|
* fix(actions): make `cancelled()` work in job `if` evaluation (#38495) (#38497)
|
||||||
|
* fix(actions): show retention info on hover for expired artifacts (#38477) (#38493)
|
||||||
|
* fix(actions): group reusable-workflow matrix legs in the workflow graph (#38475) (#38492)
|
||||||
|
* fix: full file highlighting for git diff with CR char (#38484) (#38491)
|
||||||
|
* fix(packages): serve noarch Alpine index for any requested architecture (#38479) (#38486)
|
||||||
|
* fix: 500 error when updating user visibility (#38480) (#38483)
|
||||||
|
* fix(actions): make job list item fully clickable (#38462) (#38471)
|
||||||
|
* fix: mail template for push event (#38467) (#38468)
|
||||||
|
* fix: make "test push webhook" always work (#38425) (#38455)
|
||||||
|
* fix(actions): prevent bulk actions from affecting all runners (#38453) (#38457)
|
||||||
|
* fix(org): align follow button and wrap description (#38448) (#38454)
|
||||||
|
* fix(actions): populate `github.event` for scheduled runs (#38446) (#38452)
|
||||||
|
|
||||||
|
* MISC
|
||||||
|
* refactor: git patch apply (#38637) (#38638)
|
||||||
|
|
||||||
|
## [1.27.0](https://github.com/go-gitea/gitea/releases/tag/v1.27.0) - 2026-07-13
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* Feat(actions)!: improve support for reusable workflows (#37478)
|
||||||
|
* Use Content-Security-Policy: script nonce (#37232)
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Fix: various security fixes (#38406) (#38426)
|
||||||
|
* Fix(security): harden access checks and migration validation (#38324) (#38400)
|
||||||
|
* Fix: enforce public-only token scope and harden push options / locale parsing (#38323) (#38399)
|
||||||
|
* Fix(pull): re-evaluate review official flag on target branch change (#38319) (#38402)
|
||||||
|
* Fix(api): stop leaking private repo metadata after access revocation (#38321) (#38390)
|
||||||
|
* Fix(lfs): require proof of possession for cross-repo objects (#38322) (#38389)
|
||||||
|
* Fix(mirror): disable HTTP redirects on pull mirror sync (#38320) (#38367)
|
||||||
|
* Fix: golang html template url escaping (#38363) (#38369)
|
||||||
|
* Fix(release): validate web attachment renames against allowed types (#38314) (#38328)
|
||||||
|
* Fix(release): gate draft release attachments on web download endpoints (#38318) (#38325)
|
||||||
|
* Fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 [security] (#37786)
|
||||||
|
* Fix(oauth): restrict introspection to the token's client (#38042)
|
||||||
|
* Fix(api): don't expose private org membership via public_members (#38145)
|
||||||
|
* Fix(actions): deny fork-PR cross-repo access via collaborative owner (#38214)
|
||||||
|
* Fix(migrations): prevent path traversal in repository restore (#38215)
|
||||||
|
|
||||||
|
* FEATURES
|
||||||
|
* Feat(actions): add workflow status badge modal (#38196)
|
||||||
|
* Feat(actions): support owner-level and global scoped workflows (#38154)
|
||||||
|
* Feat(api): support ref suffixes in compare (#38148)
|
||||||
|
* Feat(actions): implement `jobs.<job_id>.continue-on-error` (#38100)
|
||||||
|
* Feat(actions): show run status on browser tab favicon (#38071)
|
||||||
|
* Feat(api): add token introspection and self-deletion endpoint (#37995)
|
||||||
|
* Feat(api): add q parameter to list branches API for server-side filtering (#37982)
|
||||||
|
* Feat(repo): split repository creation limit into user and org scopes (#37872)
|
||||||
|
* Feat(actions): bulk delete, disable and enable runners in admin UI (#37869)
|
||||||
|
* Feat(actions): List workflows that were executed once but got removed from the default branch (#37835)
|
||||||
|
* Feat(org): add team visibility so org members can discover teams (#37680)
|
||||||
|
* Feat: add raw diff/patch endpoint for repository comparisons (#37632)
|
||||||
|
* Feat: Add avatar stacks (#37594)
|
||||||
|
* Feat(actions): add job summaries (GITHUB_STEP_SUMMARY) (#37500)
|
||||||
|
* Feat(web): Add Jupyter Notebook (.ipynb) Rendering Support (#37433)
|
||||||
|
* Support for Custom URI Schemes in OAuth2 Redirect URIs (#37356)
|
||||||
|
* Feat(orgs): Add search bar for organization members tab page (#37347)
|
||||||
|
* Feat(api): Add assignees APIs (#37330)
|
||||||
|
* Feat(api): Add GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs (#37196)
|
||||||
|
* Serve OpenAPI 3.0 spec at /openapi.v1.json (#37038)
|
||||||
|
* Add project column picker to issue and pull request sidebar (#37037)
|
||||||
|
* Allow multiple projects per issue and pull requests (#36784)
|
||||||
|
* Feat(ui): add "follow rename" to file commit history list (#34994)
|
||||||
|
* Feat(ssh): auto generate additional ssh keys (#33974)
|
||||||
|
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Enhance(actions): only create filtered-out workflow commit status for required contexts (#38371) (#38385)
|
||||||
|
* Enhance: allow builtin default git config options to be overridden (#38172)
|
||||||
|
* Enhance: allow MathML core elements (#38034)
|
||||||
|
* Enhance(markup): improve issue title rendering (#37908)
|
||||||
|
* Enhance(actions): set descriptive browser tab title on run view (#37870)
|
||||||
|
* Enhance: Migrate remaining gopkg.in/yaml.v3 usages to go.yaml.in/yaml/v4 (#37866)
|
||||||
|
* Enhance(actions): show workflow name from YAML instead of filename (#37833)
|
||||||
|
* Feat(actions): add before/after to PR synchronize event payload (#37827)
|
||||||
|
* Enhance(actions): add branch filters to run list (#37826)
|
||||||
|
* Enhance(actions): Make Summary UI more beautiful with more infos (#37824)
|
||||||
|
* Feat: add copy button to action step header, improve other copy buttons (#37744)
|
||||||
|
* Fix(icon): use repo-forked icon to display forks count (#37731)
|
||||||
|
* Feat(api): add sort and order query parameters to job list endpoints (#37672)
|
||||||
|
* Feat(api): add last_sync to repository API (#37566)
|
||||||
|
* Enhance: Adjust Workflow Graph styling (#37497)
|
||||||
|
* Improve code editor text selection and clean up lint enablement (#37474)
|
||||||
|
* Add mirror auth updates to repo edit API and settings (#37468)
|
||||||
|
* Replace `olivere/elastic` with REST API client, add OpenSearch support (#37411)
|
||||||
|
* Feat: Add default PR branch update style setting (#37410)
|
||||||
|
* Fix inconsistent disabled styling on logged-out repo header buttons (#37406)
|
||||||
|
* Allow fast-forward-only merge when signed commits are required (#37335)
|
||||||
|
* Enhance styling in actions page (#37323)
|
||||||
|
* Fix: improve actions status icons and texts (#37206)
|
||||||
|
* Make Markdown fenced code block work with more syntaxes (#37154)
|
||||||
|
* Fix: Sort action run jobs by JobID and Name with matrix examples (#37046)
|
||||||
|
* Add API endpoint to reply to pull request review comments (#36683)
|
||||||
|
|
||||||
|
* PERFORMANCE
|
||||||
|
* Perf(actions): debounce runner heartbeat writes and throttle task picks (#38281) (#38368)
|
||||||
|
* Perf(web): sort the action_run query by a repo-scoped index when possible (#38155)
|
||||||
|
* Perf: Various performance regression fixes (#38078)
|
||||||
|
* Perf: extend action `c_u` index to include `created_unix` for faster dashboard feeds (#38076)
|
||||||
|
* Batch-load related data in actions run, job, and task API endpoints (#37032)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix(util): reject invalid characters between time-estimate units (#38416) (#38423)
|
||||||
|
* Fix: represent a deleted assignee team as a Ghost team (#38413) (#38419)
|
||||||
|
* Fix(turnstile): route CAPTCHA verification through the configured proxy (#38412) (#38420)
|
||||||
|
* Fix: refresh pull request merge box when the commit status is pending (#38410) (#38411)
|
||||||
|
* Fix: actions task state concurrent update (#38405) (#38409)
|
||||||
|
* Fix(actions): keep workflow run trailing on one row with long branch names (#38382) (#38403)
|
||||||
|
* Fix(web): use locale-aware date formatting for contribution calendar tooltips (#38398) (#38401)
|
||||||
|
* Fix: co-author detection (#38392) (#38397)
|
||||||
|
* Fix: incorrect co-author detection on commit page (#38386) (#38387)
|
||||||
|
* Fix(ui): restore commits table column widths (#38379) (#38383)
|
||||||
|
* Fix: minio init check (#38355) (#38361)
|
||||||
|
* Fix: org project view assignee list (#38357) (#38360)
|
||||||
|
* Fix(actions): release claimed task if context is cancelled during `FetchTask` (#38343) (#38347)
|
||||||
|
* Fix(actions): make runner list pagination order deterministic (#38313) (#38327)
|
||||||
|
* Fix: Improve since/until when counting commits for X-Total-Count (#38243) (#38304)
|
||||||
|
* Fix(actions): prevent chevron overlap with log text when timestamps are enabled (#38227) (#38307)
|
||||||
|
* Fix(workflows): branch protection status checks fail when workflow uses on: paths filter (#38237) (#38302)
|
||||||
|
* Fix(oauth2): persist linkAccountData during auto-link 2FA flow (#38274) (#38295)
|
||||||
|
* Fix(actions): allow Actions bot to push to protected branches (#38284) (#38293)
|
||||||
|
* Fix(actions): include all aggregable run statuses in status filter (#38280) (#38287)
|
||||||
|
* Fix(archiver): use serializable repo-archive queue payload (#38273) (#38283)
|
||||||
|
* Fix: update npm dependencies, fix misc issues (#38257)
|
||||||
|
* Fix(api): respect since/until when counting commits for X-Total-Count (#38204)
|
||||||
|
* Fix: codemirror regressions (#38248)
|
||||||
|
* Fix(api): support HEAD requests on all API GET endpoints (#38245)
|
||||||
|
* Fix(actions): Cleanup workflow status badge code (#38241)
|
||||||
|
* Fix(web): Correctly align the "disabled" label on larger workflow names (#38240)
|
||||||
|
* Fix(actions): don't swallow HTML entities into linkified URLs (#38239)
|
||||||
|
* Fix(packages): accept npm "repository" and "bin" in string form (#38236)
|
||||||
|
* Fix(actions): fix 500 error when canceling a canceling task (#38223)
|
||||||
|
* Fix(deps): update module golang.org/x/image to v0.43.0 [security] (#38219)
|
||||||
|
* Fix(mssql): convert legacy DATETIME columns to DATETIME2 (#38216)
|
||||||
|
* Fix(api): deny private org member enumeration via /members (#38213)
|
||||||
|
* Fix(actions): ensure all waiting jobs get runners in large workflows (#38200)
|
||||||
|
* Fix(deps): update go dependencies (#38194)
|
||||||
|
* Fix(deps): update npm dependencies (#38193)
|
||||||
|
* Fix(cli): default must-change-password to false for bot users (#38175)
|
||||||
|
* Fix(actions): show run index in run view and fix summary graph height (#38165)
|
||||||
|
* Fix: csp (#38162)
|
||||||
|
* Fix(deps): update npm dependencies (#38123)
|
||||||
|
* Fix(mssql): expand legacy issue and comment long-text columns (#38120)
|
||||||
|
* Fix(packages): validate debian distribution and component names (#38116)
|
||||||
|
* Fix(packages): validate module version in goproxy ParsePackage (#38104)
|
||||||
|
* Fix(deps): update dependency esbuild to v0.28.1 [security] (#38097)
|
||||||
|
* Fix: git push hook post receive (#38089)
|
||||||
|
* Fix(ui): prevent commit status popup overflowing its row (#38081)
|
||||||
|
* Fix: validate gem name in rubygems parseMetadataFile (#38061)
|
||||||
|
* Fix: commit display name (#38057)
|
||||||
|
* Fix: csp regressions (#38047)
|
||||||
|
* Fix: api error message (#38031)
|
||||||
|
* Fix(deps): update npm dependencies (#38029)
|
||||||
|
* Fix: pgsql lint (#38022)
|
||||||
|
* Fix(indexer): fix assignee filters in issue search (#38021)
|
||||||
|
* Fix: various dropdown problems (#38020)
|
||||||
|
* Fix: refactor git error handling and make archive streaming handle non-existing commit id (#38007)
|
||||||
|
* Fix: raise git required version to 2.13 (#37996)
|
||||||
|
* Fix: remove "no-transfrom" from the cache-control header (#37985)
|
||||||
|
* Fix(deps): update module github.com/google/go-github/v87 to v88 (#37971)
|
||||||
|
* Fix: use committer time where ever possible as default (#37969)
|
||||||
|
* Fix(deps): update npm dependencies, remove nolyfill (#37968)
|
||||||
|
* Fix(deps): update go dependencies (#37967)
|
||||||
|
* Fix(pull): preserve squash message trailers and additional commit messages (#37954)
|
||||||
|
* Fix(deps): update module golang.org/x/image to v0.41.0 [security] (#37904)
|
||||||
|
* Fix: support ##[command] log prefix in action run UI (#37882)
|
||||||
|
* Fix(deps): update module github.com/google/go-github/v86 to v87 (#37845)
|
||||||
|
* Fix(deps): update npm dependencies (#37844)
|
||||||
|
* Fix(deps): update go dependencies (#37841)
|
||||||
|
* Fix(frontend): resolve Vite assets by manifest source path (#37836)
|
||||||
|
* Fix(locales): Replace hardcoded strings (#37788)
|
||||||
|
* Fix(packages): render markdown links relative to linked repo (#37676)
|
||||||
|
* Fix: persist mirror repository metadata (#37519)
|
||||||
|
* Fix cmd tests by mocking builtin paths (#37369)
|
||||||
|
* Add `form-fetch-action` to some forms, fix "fetch action" resp bug (#37305)
|
||||||
|
* Feat: execute post run cleanup when workflow is cancelled (#37275)
|
||||||
|
* Fix `relative-time` error and improve global error handler (#37241)
|
||||||
|
* Refactor flash message and remove SanitizeHTML template func (#37179)
|
||||||
|
|
||||||
|
* TESTING
|
||||||
|
* Test(e2e): fix race in pdf file render test (#38380) (#38381)
|
||||||
|
* Test: compare key file contents instead of `FileInfo` in `TestInitKeys` (#38330) (#38331)
|
||||||
|
* Test: speed up two tests (#37905)
|
||||||
|
* Test: Fix random failure test (#37887)
|
||||||
|
* Test: fix flaky `issue-comment` close test (#37880)
|
||||||
|
* Test: enable WAL for sqlite integration tests (#37861)
|
||||||
|
* Test: fix flaky `TestResourceIndex` and reduce its runtime (#37847)
|
||||||
|
* Test: run `TestAPIRepoMigrate` offline via a local clone source (#37817)
|
||||||
|
* Ci: shard tests and reduce redundant work (#37618)
|
||||||
|
* Test(e2e): run playwright via container (#37300)
|
||||||
|
* Remove external service dependencies in migration tests (#36866)
|
||||||
|
|
||||||
|
* BUILD
|
||||||
|
* Fix(actions): authenticate snapcraft before nightly remote build (#38252)
|
||||||
|
* Ci: cap Elasticsearch heap in db-tests (#37816)
|
||||||
|
* Build(snap): publish nightly version to snapcraft via actions (#37814)
|
||||||
|
* Ci: split pgsql shards into plain jobs, dedupe setup actions (#37802)
|
||||||
|
* Ci: narrow files-changed frontend filter (#37749)
|
||||||
|
* Ci: add `zizmor` to `lint-actions` (#37720)
|
||||||
|
* Chore: clean up "contrib" dir (#37690)
|
||||||
|
* Fix: snap build (main branch) (#37685)
|
||||||
|
* Ci: Also lint json5 files (#37659)
|
||||||
|
* Feat(editor): broaden language detection in web code editor (#37619)
|
||||||
|
* Build: update pnpm to v11 (#37591)
|
||||||
|
* Refactor(deps): migrate from `nektos/act` fork to `gitea/runner` (#37557)
|
||||||
|
* Refactor: lint bare `fill`/`stroke` colors, add vars for git graph color series (#37543)
|
||||||
|
* Update go js py dependencies (#37525)
|
||||||
|
* Ci: lint PR titles with commitlint (#37498)
|
||||||
|
* Chore: upgrade Go version in devcontainer image to 1.26 (#37374)
|
||||||
|
* Update GitHub Actions to latest major versions (#37313)
|
||||||
|
* Update go js dependencies (#37312)
|
||||||
|
* Fail vite build on rolldown warnings via NODE_ENV=test (#37270)
|
||||||
|
* Remove htmx (#37224)
|
||||||
|
* Replace custom Go formatter with `golangci-lint fmt` (#37194)
|
||||||
|
* Refactor htmx and fetch-action related code (#37186)
|
||||||
|
* Integrate renovate bot for all dependency updates (#37050)
|
||||||
|
* Build(sign): move to sigstore (#38250)
|
||||||
|
|
||||||
|
* DOCS
|
||||||
|
* Docs: update changelog for 1.26.3 & 1.26.4 (#38178)
|
||||||
|
* Docs: fix duplicated word in foreachref doc comment (#38161)
|
||||||
|
* Docs: Clarify criteria for becoming a merger (#38113)
|
||||||
|
* Docs: Publish TOC Election Result 2026 (#38111)
|
||||||
|
* Docs: mark openapi3 as autogenerated in attributes (#37963)
|
||||||
|
* Docs: add development setup guide (#37960)
|
||||||
|
|
||||||
|
* MISC
|
||||||
|
* Revert(sign): restore gpg (#38251)
|
||||||
|
* Refactor: replace legacy `delete-button` with `link-action` (#38143)
|
||||||
|
* Refactor(actions): read runner capabilities from proto field (#38068)
|
||||||
|
* Refactor(api): clarify APIError message usage and fix legacy lint error (#38012)
|
||||||
|
* Refactor: Use db.Get[] instead of db.GetEngine(ctx).Get(bean) to avoid zero value fetching wrong database record (#37977)
|
||||||
|
* Fix(deps): update go dependencies (#37851)
|
||||||
|
* Ci: Fix sync PR labels from the conventional-commit title (#37784) (#37825)
|
||||||
|
* Ci: tweak `files-changed`, add `free-disk-space` (#37819)
|
||||||
|
* Fix(deps): update module golang.org/x/crypto to v0.52.0 [security] (#37806)
|
||||||
|
* Test(e2e): add comment, release, star, PR and fork tests (#37800)
|
||||||
|
* Chore: simplify issue and pull request templates (#37799)
|
||||||
|
* Chore: Update giteabot to fix failure when backport (#37789)
|
||||||
|
* Fix(api): handle partial failures in push mirror synchronization gracefully (#37782)
|
||||||
|
* Fix(deps): update module gitlab.com/gitlab-org/api/client-go/v2 to v2.26.0 (#37771)
|
||||||
|
* Ci: split giteabot workflow (#37770)
|
||||||
|
* Fix(deps): update npm dependencies (#37768)
|
||||||
|
* Refactor(waitgroup): replace Add/Done goroutines with WaitGroup.Go (#37764)
|
||||||
|
* Fix(deps): update module google.golang.org/grpc to v1.81.1 (#37762)
|
||||||
|
* Ci: fix cache-related issues (#37761)
|
||||||
|
* Chore: fix tests (#37760)
|
||||||
|
* Fix(deps): update module github.com/google/go-github/v85 to v86 (#37754)
|
||||||
|
* Fix(deps): update npm dependencies (#37753)
|
||||||
|
* Fix(deps): update go dependencies (#37752)
|
||||||
|
* Chore(deps): update action dependencies (#37751)
|
||||||
|
* Fix(markup): wrap indented code blocks for the code-copy button (#37748)
|
||||||
|
* Chore(db): introduce db.Session and db.EngineMigration interfaces (#37746)
|
||||||
|
* Feat(web): also display PR counts in repo list (#37739)
|
||||||
|
* Refactor(glob): use strings.Builder for regexp compilation (#37730)
|
||||||
|
* Chore(doctor): remove four obsolete doctor check implementations (#37728)
|
||||||
|
* Refactor(org): simplify owner-team org repo creation logic (#37727)
|
||||||
|
* Refactor: move `workflowpattern` into `modules/actions` (#37717)
|
||||||
|
* Chore: clean up tests (#37715)
|
||||||
|
* Style: misc UI fixes (#37691)
|
||||||
|
* Ci: add shellcheck linter (#37682)
|
||||||
|
* Fix: catch and fix more lint problems (#37674)
|
||||||
|
* Fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test (#37662)
|
||||||
|
* Fix(deps): update npm dependencies (#37647)
|
||||||
|
* Ci(renovate): update Go import paths on major bumps (#37641)
|
||||||
|
* Fix(deps): update go dependencies (major) (#37639)
|
||||||
|
* Chore(deps): update action dependencies (major) (#37638)
|
||||||
|
* Fix(deps): update module code.gitea.io/sdk/gitea to v0.25.0 (#37637)
|
||||||
|
* Fix(deps): update npm dependencies (#37636)
|
||||||
|
* Refactor(log): replace log.Critical with log.Error (#37624)
|
||||||
|
* Build(deps): bump fast-uri from 3.1.0 to 3.1.2 (#37616)
|
||||||
|
* Feat(oauth): Support AWS Cognito OAuth2 provider (#37607)
|
||||||
|
* Chore(deps): update action dependencies (#37603)
|
||||||
|
* Ci: allow `chore` type in PR title lint (#37575)
|
||||||
|
* Refactor: only reset a database table when the table's data was changed (#37573)
|
||||||
|
* Ci: increase renovate frequency and fix RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS (#37565)
|
||||||
|
* Refactor: use modernc sqlite driver as default (#37562)
|
||||||
|
* Docs: fix 4 typos in CHANGELOG.md (#37549)
|
||||||
|
* Fix(deps): update go dependencies (#37541)
|
||||||
|
* Chore(deps): update action dependencies (#37540)
|
||||||
|
* Refactor pull request view (6) (#37522)
|
||||||
|
* Fix: redirect early CLI console logger to stderr (#37507)
|
||||||
|
* Refactor "flex-list" to "flex-divided-list" (#37505)
|
||||||
|
* Refactor compare diff/pull page (1) (#37481)
|
||||||
|
* Refactor pull request view (4) (#37451)
|
||||||
|
* Update 1.26.1 changelog in main (#37442)
|
||||||
|
* Refactor: use named `Permission` field in `Repository` struct instead of anonymous embedding (#37441)
|
||||||
|
* Refactor: serve site manifest via `/assets/site-manifest.json` endpoint (#37405)
|
||||||
|
* Remove IsValidExternalURL/IsAPIURL and use IsValidURL at call sites (#37364)
|
||||||
|
* Update `Block a user` form (#37359)
|
||||||
|
* Move review request functions to a standalone file (#37358)
|
||||||
|
* Feat(security): set X-Content-Type-Options: nosniff by default (#37354)
|
||||||
|
* Enable strict TypeScript, add `errorMessage` helper (#37292)
|
||||||
|
* Refactor frontend `tw-justify-between` layouts to `flex-left-right` (#37291)
|
||||||
|
* Update Nix flake (#37284)
|
||||||
|
* Fix Repository transferring page (#37277)
|
||||||
|
* Remove `SubmitEvent` polyfill (#37276)
|
||||||
|
* Remove dead code identified by `deadcode` tool (#37271)
|
||||||
|
* Upgrade go-git to v5.18.0 (#37268)
|
||||||
|
* Don't add useless labels which will bother changelog generation (#37267)
|
||||||
|
* Move heatmap to first-party code (#37262)
|
||||||
|
* Tests/integration: simplify code (#37249)
|
||||||
|
* Add pagination and search box to org teams list (#37245)
|
||||||
|
* Remove error returns from crypto random helpers and callers (#37240)
|
||||||
|
* Add `ExternalIDClaim` option for OAuth2 OIDC auth source (#37229)
|
||||||
|
* Refactor: simplify ParseCatFileTreeLine and catBatchParseTreeEntries (#37210)
|
||||||
|
* Refactor "htmx" to "fetch action" (#37208)
|
||||||
|
* Update go js py dependencies (#37204)
|
||||||
|
* Add comment for the design of "user activity time" (#37195)
|
||||||
|
* Remove outdated RunUser logic (#37180)
|
||||||
|
* Models/fixtures: add "DO NOT add more test data" comment to all yml fixture files (#37150)
|
||||||
|
* Update javascript dependencies (#37142)
|
||||||
|
* Update go dependencies (#37141)
|
||||||
|
* Frontport changelog of v1.26.0-rc0 (#37138)
|
||||||
|
* Introduce `ActionRunAttempt` to represent each execution of a run (#37119)
|
||||||
|
* Workflow Artifact Info Hover (#37100)
|
||||||
|
* Extend issue context popup beyond markdown content (#36908)
|
||||||
|
* Add bulk repository deletion for organizations (#36763)
|
||||||
|
* Feat: Add bypass allowlist for branch protection (#36514)
|
||||||
|
|
||||||
|
## [1.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009) (#38183)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* fix: walk git log context error handling (#38182) (#38185)
|
||||||
|
|
||||||
|
## [1.26.3](https://github.com/go-gitea/gitea/releases/tag/1.26.3) - 2026-06-18
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* fix(actions)!: require merged PR to bypass fork PR approval gate (#38010) (#38041)
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* fix(hostmatcher): patch incorrect private list (#38170) (#38173)
|
||||||
|
* fix: Various security fixes (#38103) (#38151)
|
||||||
|
* fix: Various sec fixes (#38108) (#38147)
|
||||||
|
* fix: allow git clone of private repos with anonymous code access (#38074) (#38146)
|
||||||
|
* fix(auth): ignore stale OIDC external login links to organizations (#37875) (#38141)
|
||||||
|
* fix(hostmatcher): block reserved IP ranges from external/private filters (#38039) (#38059)
|
||||||
|
* fix(lfs): require Code-unit access for cross-repo LFS object reuse (#38006) (#38050)
|
||||||
|
* fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) (#38015)
|
||||||
|
* fix: bound CODEOWNERS regex match time (#38011) (#38025)
|
||||||
|
* fix: bound debian ParseControlFile to a single control stanza (#38044) (#38055)
|
||||||
|
* fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829)
|
||||||
|
|
||||||
|
* API
|
||||||
|
* feat(api): add Link header in ListForks (#38052) (#38063)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* fix: Fix the panic when ssh remote lfs endpoint parsing failure (#38026) (#38158)
|
||||||
|
* fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115)
|
||||||
|
* fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) (#38096)
|
||||||
|
* fix: parse HEAD ref (#38119)
|
||||||
|
* fix: git cmd (#38084) (#38087)
|
||||||
|
* fix(releases): generate notes for initial tag (#37697) (#37986)
|
||||||
|
* fix(actions): return 404 when job log blob is missing (#38003) (#38004)
|
||||||
|
* fix(actions): exclude `workflow_call` from workflow trigger detection (#37894) (#37899)
|
||||||
|
* fix(actions): keep action run title clickable when commit subject is a URL (#37867) (#37898)
|
||||||
|
* fix(actions): reject workflow_dispatch for workflows without that trigger (#37660) (#37895)
|
||||||
|
* fix(actions): ack re-sent `UpdateLog` finalize idempotently (#37885) (#37892)
|
||||||
|
* fix: http content file render (#37850) (#37856)
|
||||||
|
* fix(issues): clear stale ReviewTypeRequest when submitting pending review (#37809) (#37815)
|
||||||
|
* fix: Fix issue target branch selection for non-collaborators (#36916) (#38164)
|
||||||
|
|
||||||
|
* BUILD
|
||||||
|
* fix(deps): update `@playwright/test` to 1.60.0 (#38144)
|
||||||
|
* ci: add `tools/ci-tools.ts` for the PR labeler workflow (#37831)
|
||||||
|
* fix(build): swagger css import (#37801) (#37803)
|
||||||
|
|
||||||
## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20
|
## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
@@ -1445,7 +1943,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
|||||||
* Fix mCaptcha bug (#33659) (#33661)
|
* Fix mCaptcha bug (#33659) (#33661)
|
||||||
* Git graph: don't show detached commits (#33645) (#33650)
|
* Git graph: don't show detached commits (#33645) (#33650)
|
||||||
* Use MatchPhraseQuery for bleve code search (#33628)
|
* Use MatchPhraseQuery for bleve code search (#33628)
|
||||||
* Adjust appearence of commit status webhook (#33778) #33789
|
* Adjust appearance of commit status webhook (#33778) #33789
|
||||||
* Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796
|
* Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796
|
||||||
|
|
||||||
## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
|
## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
|
||||||
@@ -2176,7 +2674,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
|||||||
* Optimize repo-list layout to enhance visual experience (#31272) (#31276)
|
* Optimize repo-list layout to enhance visual experience (#31272) (#31276)
|
||||||
* fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275)
|
* fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275)
|
||||||
* Fix Activity Page Contributors dropdown (#31264) (#31269)
|
* Fix Activity Page Contributors dropdown (#31264) (#31269)
|
||||||
* fix: allow actions artifacts storage migration to complete succesfully (#31251) (#31257)
|
* fix: allow actions artifacts storage migration to complete successfully (#31251) (#31257)
|
||||||
* Make blockquote attention recognize more syntaxes (#31240) (#31250)
|
* Make blockquote attention recognize more syntaxes (#31240) (#31250)
|
||||||
* Remove .segment from .project-column (#31204) (#31239)
|
* Remove .segment from .project-column (#31204) (#31239)
|
||||||
* Ignore FindRecentlyPushedNewBranches err (#31164) (#31171)
|
* Ignore FindRecentlyPushedNewBranches err (#31164) (#31171)
|
||||||
@@ -2360,7 +2858,7 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
* Performance optimization for git push and check permissions for push options (#30104) (#30354)
|
* Performance optimization for git push and check permissions for push options (#30104) (#30354)
|
||||||
* BUGFIXES
|
* BUGFIXES
|
||||||
* Fix close file in the Upload func (#30262) (#30269)
|
* Fix close file in the Upload func (#30262) (#30269)
|
||||||
* Fix inline math blocks can't be preceeded/followed by alphanumerical characters (#30175) (#30250)
|
* Fix inline math blocks can't be preceded/followed by alphanumerical characters (#30175) (#30250)
|
||||||
* Fix missing 0 prefix of GPG key id (#30245) (#30247)
|
* Fix missing 0 prefix of GPG key id (#30245) (#30247)
|
||||||
* Include encoding in signature payload (#30174) (#30181)
|
* Include encoding in signature payload (#30174) (#30181)
|
||||||
* Move from `max( id )` to `max( index )` for latest commit statuses (#30076) (#30155)
|
* Move from `max( id )` to `max( index )` for latest commit statuses (#30076) (#30155)
|
||||||
@@ -5652,7 +6150,7 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
* Fix navbar on project view (#17749)
|
* Fix navbar on project view (#17749)
|
||||||
* More pleasantly handle broken or missing git repositories (#17747)
|
* More pleasantly handle broken or missing git repositories (#17747)
|
||||||
* Use `*PushUpdateOptions` as receiver (#17724)
|
* Use `*PushUpdateOptions` as receiver (#17724)
|
||||||
* Remove unused `user` paramater (#17723)
|
* Remove unused `user` parameter (#17723)
|
||||||
* Better builtin avatar generator (#17707)
|
* Better builtin avatar generator (#17707)
|
||||||
* Cleanup and use global style on popups (#17674)
|
* Cleanup and use global style on popups (#17674)
|
||||||
* Move user/org deletion to services (#17673)
|
* Move user/org deletion to services (#17673)
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# Contribution Guidelines
|
# Contribution Guidelines
|
||||||
|
|
||||||
|
This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find.
|
||||||
|
|
||||||
|
| Topic | Document |
|
||||||
|
|:-----------------------|:-----------------------------------------------------------------|
|
||||||
|
| Setup and requirements | [docs/build-setup.md](docs/build-setup.md) |
|
||||||
|
| Development workflow | [docs/development.md](docs/development.md) |
|
||||||
|
| Build from source | [docs/build-source.md](docs/build-source.md) |
|
||||||
|
| Running the tests | [docs/testing.md](docs/testing.md) |
|
||||||
|
| Frontend guidelines | [docs/guidelines-frontend.md](docs/guidelines-frontend.md) |
|
||||||
|
| Backend guidelines | [docs/guidelines-backend.md](docs/guidelines-backend.md) |
|
||||||
|
| Refactoring | [docs/guidelines-refactoring.md](docs/guidelines-refactoring.md) |
|
||||||
|
| Community Governance | [docs/community-governance.md](docs/community-governance.md) |
|
||||||
|
| Release management | [docs/release-management.md](docs/release-management.md) |
|
||||||
|
|
||||||
<details><summary>Table of Contents</summary>
|
<details><summary>Table of Contents</summary>
|
||||||
|
|
||||||
- [Contribution Guidelines](#contribution-guidelines)
|
- [Contribution Guidelines](#contribution-guidelines)
|
||||||
@@ -11,10 +25,6 @@
|
|||||||
- [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
|
- [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
|
||||||
- [Issue locking](#issue-locking)
|
- [Issue locking](#issue-locking)
|
||||||
- [Building Gitea](#building-gitea)
|
- [Building Gitea](#building-gitea)
|
||||||
- [Dependencies](#dependencies)
|
|
||||||
- [Backend](#backend)
|
|
||||||
- [Frontend](#frontend)
|
|
||||||
- [Design guideline](#design-guideline)
|
|
||||||
- [Styleguide](#styleguide)
|
- [Styleguide](#styleguide)
|
||||||
- [Copyright](#copyright)
|
- [Copyright](#copyright)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
@@ -22,51 +32,23 @@
|
|||||||
- [Code review](#code-review)
|
- [Code review](#code-review)
|
||||||
- [Pull request format](#pull-request-format)
|
- [Pull request format](#pull-request-format)
|
||||||
- [PR title and summary](#pr-title-and-summary)
|
- [PR title and summary](#pr-title-and-summary)
|
||||||
- [Milestone](#milestone)
|
|
||||||
- [Labels](#labels)
|
|
||||||
- [Breaking PRs](#breaking-prs)
|
- [Breaking PRs](#breaking-prs)
|
||||||
- [What is a breaking PR?](#what-is-a-breaking-pr)
|
- [What is a breaking PR?](#what-is-a-breaking-pr)
|
||||||
- [How to handle breaking PRs?](#how-to-handle-breaking-prs)
|
- [How to handle breaking PRs?](#how-to-handle-breaking-prs)
|
||||||
- [Maintaining open PRs](#maintaining-open-prs)
|
- [Maintaining open PRs](#maintaining-open-prs)
|
||||||
- [Getting PRs merged](#getting-prs-merged)
|
- [Reviewing PRs](#reviewing-prs)
|
||||||
- [Final call](#final-call)
|
- [For PR authors](#for-pr-authors)
|
||||||
- [Commit messages](#commit-messages)
|
|
||||||
- [PR Co-authors](#pr-co-authors)
|
|
||||||
- [PRs targeting `main`](#prs-targeting-main)
|
|
||||||
- [Backport PRs](#backport-prs)
|
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- [API v1](#api-v1)
|
|
||||||
- [GitHub API compatibility](#github-api-compatibility)
|
|
||||||
- [Adding/Maintaining API routes](#addingmaintaining-api-routes)
|
|
||||||
- [When to use what HTTP method](#when-to-use-what-http-method)
|
|
||||||
- [Requirements for API routes](#requirements-for-api-routes)
|
|
||||||
- [Backports and Frontports](#backports-and-frontports)
|
|
||||||
- [What is backported?](#what-is-backported)
|
|
||||||
- [How to backport?](#how-to-backport)
|
|
||||||
- [Format of backport PRs](#format-of-backport-prs)
|
|
||||||
- [Frontports](#frontports)
|
|
||||||
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
|
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
|
||||||
- [Release Cycle](#release-cycle)
|
|
||||||
- [Maintainers](#maintainers)
|
|
||||||
- [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
|
|
||||||
- [TOC election process](#toc-election-process)
|
|
||||||
- [Current TOC members](#current-toc-members)
|
|
||||||
- [Previous TOC/owners members](#previous-tocowners-members)
|
|
||||||
- [Governance Compensation](#governance-compensation)
|
|
||||||
- [TOC \& Working groups](#toc--working-groups)
|
|
||||||
- [Roadmap](#roadmap)
|
|
||||||
- [Versions](#versions)
|
|
||||||
- [Releasing Gitea](#releasing-gitea)
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
This document explains how to contribute changes to the Gitea project. \
|
|
||||||
It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \
|
It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \
|
||||||
Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
|
Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
|
||||||
|
|
||||||
For configuring IDEs for Gitea development, see the [contributed IDE configurations](contrib/ide/).
|
For configuring IDEs for Gitea development, see the [IDE setup notes](docs/development.md#ide-configuration) and the [contributed configurations](contrib/development/).
|
||||||
|
|
||||||
## AI Contribution Policy
|
## AI Contribution Policy
|
||||||
|
|
||||||
@@ -129,35 +111,8 @@ If further discussion is needed, we encourage you to open a new issue instead an
|
|||||||
|
|
||||||
## Building Gitea
|
## Building Gitea
|
||||||
|
|
||||||
See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
|
See [docs/setup.md](docs/setup.md) for prerequisites and [docs/development.md](docs/development.md)
|
||||||
|
for building Gitea and the development workflow.
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \
|
|
||||||
You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules).
|
|
||||||
|
|
||||||
Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \
|
|
||||||
Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies.
|
|
||||||
|
|
||||||
The `go.mod`, `go.sum` update needs to be justified as part of the PR description,
|
|
||||||
and must be verified by the reviewers and/or merger to always reference
|
|
||||||
an existing upstream commit.
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
For the frontend, we use [npm](https://www.npmjs.com/).
|
|
||||||
|
|
||||||
The same restrictions apply for frontend dependencies as for backend dependencies, with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version.
|
|
||||||
|
|
||||||
## Design guideline
|
|
||||||
|
|
||||||
Depending on your change, please read the
|
|
||||||
|
|
||||||
- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend)
|
|
||||||
- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend)
|
|
||||||
- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring)
|
|
||||||
|
|
||||||
## Styleguide
|
## Styleguide
|
||||||
|
|
||||||
@@ -176,33 +131,7 @@ Afterwards, copyright should only be modified when the copyright author changes.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Before submitting a pull request, run all tests to make sure your changes don't cause a regression elsewhere.
|
Before submitting a pull request, run the linters (`make lint`, or the scoped `make lint-backend` / `make lint-frontend`) and the tests to make sure your changes don't cause a regression elsewhere. See [docs/testing.md](docs/testing.md) for how to run the unit, integration, end-to-end, and migration tests.
|
||||||
|
|
||||||
Here's how to run the test suite:
|
|
||||||
|
|
||||||
- code lint
|
|
||||||
|
|
||||||
| | |
|
|
||||||
| :-------------------- | :--------------------------------------------------------------------------- |
|
|
||||||
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|
|
||||||
|``make lint-frontend`` | lint frontend files |
|
|
||||||
|``make lint-backend`` | lint backend files |
|
|
||||||
|
|
||||||
- run tests (we suggest running them on Linux)
|
|
||||||
|
|
||||||
| Command | Action | |
|
|
||||||
| :------------------------------------------ | :------------------------------------------------------- | ------------------------------------------- |
|
|
||||||
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|
|
||||||
|``make test-sqlite[\#SpecificTestName]`` | run [integration](tests/integration) test(s) for SQLite | [More details](tests/integration/README.md) |
|
|
||||||
|``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | |
|
|
||||||
|
|
||||||
- E2E test environment variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
| :-------------------------------- | :---------------------------------------------------------- |
|
|
||||||
| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output |
|
|
||||||
| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` |
|
|
||||||
| ``GITEA_TEST_E2E_TIMEOUT_FACTOR`` | Timeout multiplier (default: 3 on CI, 1 locally) |
|
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
@@ -216,6 +145,8 @@ The tool `go run build/backport-locale.go` can be used to backport locales from
|
|||||||
|
|
||||||
## Code review
|
## Code review
|
||||||
|
|
||||||
|
How labels, milestones, and the merge queue work is documented in [docs/community-governance.md](docs/community-governance.md).
|
||||||
|
|
||||||
### Pull request format
|
### Pull request format
|
||||||
|
|
||||||
Please try to make your pull request easy to review for us. \
|
Please try to make your pull request easy to review for us. \
|
||||||
@@ -238,6 +169,38 @@ 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. \
|
Use the first comment as a summary of your PR. \
|
||||||
In the PR summary, you can describe exactly how you are fixing this problem.
|
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:
|
||||||
|
|
||||||
|
```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):`).
|
||||||
|
|
||||||
|
Use one of these types:
|
||||||
|
|
||||||
|
- `build`: Changes affecting the build system, packaging, or external dependencies
|
||||||
|
- `ci`: Changes to CI/CD configuration files and scripts
|
||||||
|
- `chore`: Maintenance changes that do not affect production code or should not appear in the changelog
|
||||||
|
- `docs`: Documentation-only changes
|
||||||
|
- `feat`: A larger user-facing feature, improvement, or new functionality
|
||||||
|
- `enhance`: Small or trivial user-facing improvements or UX polish (for example wording changes, color adjustments, spacing or padding tweaks, placeholders, small UI behavior improvements)
|
||||||
|
- `fix`: A bug fix, UX correction, or security-related dependency update
|
||||||
|
- `perf`: Performance improvements (speed, memory, scalability)
|
||||||
|
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
||||||
|
- `revert`: Reverts a previous change
|
||||||
|
- `style`: Formatting or style-only changes that do not affect code behavior (for example lint-driven edits)
|
||||||
|
- `test`: Adding or correcting tests
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fix(web): prevent avatar upload crash on empty file
|
||||||
|
feat(api): add pagination to repo hooks list
|
||||||
|
enhance(repo): improve diff toolbar spacing
|
||||||
|
ci(workflows): lint PR titles in CI
|
||||||
|
```
|
||||||
|
|
||||||
Keep this summary up-to-date as the PR evolves. \
|
Keep this summary up-to-date as the PR evolves. \
|
||||||
If your PR changes the UI, you must add **after** screenshots in the PR summary. \
|
If your PR changes the UI, you must add **after** screenshots in the PR summary. \
|
||||||
If you are not implementing a new feature, you should also post **before** screenshots for comparison.
|
If you are not implementing a new feature, you should also post **before** screenshots for comparison.
|
||||||
@@ -250,6 +213,10 @@ Another requirement for merging PRs is that the PR is labeled correctly.\
|
|||||||
However, this is not your job as a contributor, but the job of the person merging your PR.\
|
However, this is not your job as a contributor, but the job of the person merging your PR.\
|
||||||
If you think that your PR was labeled incorrectly, or notice that it was merged without labels, please let us know.
|
If you think that your PR was labeled incorrectly, or notice that it was merged without labels, please let us know.
|
||||||
|
|
||||||
|
For pull requests that use a valid Conventional Commits title, CI automatically applies a matching `type/…` label when the title prefix is `feat`, `enhance`, `fix`, `docs`, or `test` (for example `enhance(web): …` receives `type/enhancement`).\
|
||||||
|
That label is kept in sync with the PR title when the title is edited.\
|
||||||
|
Other title prefixes do not get an automatic `type/…` label; the merger still assigns the correct labels (including `type/…` when needed) for changelog and backport decisions.
|
||||||
|
|
||||||
If your PR closes some issues, you must note that in a way that both GitHub and Gitea understand, i.e. by appending a paragraph like
|
If your PR closes some issues, you must note that in a way that both GitHub and Gitea understand, i.e. by appending a paragraph like
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -260,29 +227,6 @@ Fixes/Closes/Resolves #<ISSUE_NR_Y>.
|
|||||||
to your summary. \
|
to your summary. \
|
||||||
Each issue that will be closed must stand on a separate line.
|
Each issue that will be closed must stand on a separate line.
|
||||||
|
|
||||||
### Milestone
|
|
||||||
|
|
||||||
A PR should only be assigned to a milestone if it will likely be merged into the given version. \
|
|
||||||
As a rule of thumb, assume that a PR will stay open for an additional month for every 100 added lines. \
|
|
||||||
PRs without a milestone may not be merged.
|
|
||||||
|
|
||||||
### Labels
|
|
||||||
|
|
||||||
Almost all labels used inside Gitea can be classified as one of the following:
|
|
||||||
|
|
||||||
- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI.
|
|
||||||
- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually.
|
|
||||||
- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one).
|
|
||||||
- `issue/…` / `pr/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `pr/need-2-approvals`
|
|
||||||
|
|
||||||
Every PR should be labeled correctly with every label that applies.
|
|
||||||
|
|
||||||
There are also some labels that will be managed automatically.\
|
|
||||||
In particular, these are
|
|
||||||
|
|
||||||
- the amount of pending required approvals
|
|
||||||
- has all `backport`s or needs a manual backport
|
|
||||||
|
|
||||||
### Breaking PRs
|
### Breaking PRs
|
||||||
|
|
||||||
#### What is a breaking PR?
|
#### What is a breaking PR?
|
||||||
@@ -311,165 +255,29 @@ Breaking PRs will not be merged as long as not both of these requirements are me
|
|||||||
|
|
||||||
### Maintaining open PRs
|
### Maintaining open PRs
|
||||||
|
|
||||||
The moment you create a non-draft PR or the moment you convert a draft PR to a non-draft PR is the moment code review starts for it. \
|
Code review starts when you open a non-draft PR or move a draft out of draft state. After that, do not rebase or squash your branch; it makes new changes harder to review.
|
||||||
Once that happens, do not rebase or squash your branch anymore as it makes it difficult to review the new changes. \
|
|
||||||
Merge the base branch into your branch only when you really need to, i.e. because of conflicting changes in the mean time. \
|
|
||||||
This reduces unnecessary CI runs. \
|
|
||||||
Don't worry about merge commits messing up your commit history as every PR will be squash merged. \
|
|
||||||
This means that all changes are joined into a single new commit whose message is as described below.
|
|
||||||
|
|
||||||
### Getting PRs merged
|
Merge the base branch into yours only when you need to, for example because of conflicting changes elsewhere. That limits unnecessary CI runs.
|
||||||
|
|
||||||
Changes to Gitea must be reviewed before they are accepted — no matter who
|
Every PR is squash-merged, so merge commits on your branch do not matter for final history. The squash produces a single commit; mergers follow the [commit message format](docs/community-governance.md#commit-messages) in the governance guide.
|
||||||
makes the change, even if they are an owner or a maintainer. \
|
|
||||||
The only exception are critical bugs that prevent Gitea from being compiled or started. \
|
|
||||||
Specifically, we require two approvals from maintainers for every PR. \
|
|
||||||
Once this criteria has been met, your PR receives the `lgtm/done` label. \
|
|
||||||
From this point on, your only responsibility is to fix merge conflicts or respond to/implement requests by maintainers. \
|
|
||||||
It is the responsibility of the maintainers from this point to get your PR merged.
|
|
||||||
|
|
||||||
If a PR has the `lgtm/done` label and there are no open discussions or merge conflicts anymore, any maintainer can add the `reviewed/wait-merge` label. \
|
### Reviewing PRs
|
||||||
This label means that the PR is part of the merge queue and will be merged as soon as possible. \
|
|
||||||
The merge queue will be cleared in the order of the list below:
|
|
||||||
|
|
||||||
<https://github.com/go-gitea/gitea/pulls?q=is%3Apr+label%3Areviewed%2Fwait-merge+sort%3Acreated-asc+is%3Aopen>
|
Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest.
|
||||||
|
|
||||||
Gitea uses it's own tool, the <https://github.com/GiteaBot/gitea-backporter> to automate parts of the review process. \
|
#### For PR authors
|
||||||
This tool does the things listed below automatically:
|
|
||||||
|
|
||||||
- create a backport PR if needed once the initial PR was merged
|
- **Response**: When answering reviewer questions, use real-world cases or examples and avoid speculation.
|
||||||
- remove the PR from the merge queue after the PR merged
|
- **Discussion**: A discussion is always welcome and should be used to clarify the changes and the intent of the PR.
|
||||||
- keep the oldest branch in the merge queue up to date with merges
|
- **Help**: If you need help with the PR or comments are unclear, ask for clarification.
|
||||||
|
|
||||||
### Final call
|
Guidance for reviewers, the merge queue, and the squash commit message format is in [docs/community-governance.md](docs/community-governance.md).
|
||||||
|
|
||||||
If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment.
|
|
||||||
|
|
||||||
After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously.
|
|
||||||
|
|
||||||
However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author).
|
|
||||||
|
|
||||||
### Commit messages
|
|
||||||
|
|
||||||
Mergers are able and required to rewrite the PR title and summary (the first comment of a PR) so that it can produce an easily understandable commit message if necessary. \
|
|
||||||
The final commit message should no longer contain any uncertainty such as `hopefully, <x> won't happen anymore`. Replace uncertainty with certainty.
|
|
||||||
|
|
||||||
#### PR Co-authors
|
|
||||||
|
|
||||||
A person counts as a PR co-author the moment they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. \
|
|
||||||
Mergers are required to remove such "false-positive" co-authors when writing the commit message. \
|
|
||||||
The true co-authors must remain in the commit message.
|
|
||||||
|
|
||||||
#### PRs targeting `main`
|
|
||||||
|
|
||||||
The commit message of PRs targeting `main` is always
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$PR_TITLE ($PR_INDEX)
|
|
||||||
|
|
||||||
$REWRITTEN_PR_SUMMARY
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Backport PRs
|
|
||||||
|
|
||||||
The commit message of backport PRs is always
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX)
|
|
||||||
|
|
||||||
$REWRITTEN_PR_SUMMARY
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs).
|
If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs).
|
||||||
**The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.**
|
**The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.**
|
||||||
|
|
||||||
## API v1
|
|
||||||
|
|
||||||
The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
|
|
||||||
|
|
||||||
### GitHub API compatibility
|
|
||||||
|
|
||||||
Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \
|
|
||||||
If Gitea provides functionality that GitHub does not, a new endpoint can be created. \
|
|
||||||
If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \
|
|
||||||
Updating an existing API should not remove existing fields unless there is a really good reason to do so. \
|
|
||||||
The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned).
|
|
||||||
|
|
||||||
### Adding/Maintaining API routes
|
|
||||||
|
|
||||||
All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \
|
|
||||||
All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \
|
|
||||||
and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \
|
|
||||||
They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \
|
|
||||||
All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \
|
|
||||||
and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \
|
|
||||||
They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279).
|
|
||||||
|
|
||||||
### When to use what HTTP method
|
|
||||||
|
|
||||||
In general, HTTP methods are chosen as follows:
|
|
||||||
|
|
||||||
- **GET** endpoints return the requested object(s) and status **OK (200)**
|
|
||||||
- **DELETE** endpoints return the status **No Content (204)** and no content either
|
|
||||||
- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object
|
|
||||||
- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either
|
|
||||||
- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)**
|
|
||||||
|
|
||||||
### Requirements for API routes
|
|
||||||
|
|
||||||
All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required).
|
|
||||||
|
|
||||||
Endpoints returning lists must
|
|
||||||
|
|
||||||
- support pagination (`page` & `limit` options in query)
|
|
||||||
- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
|
|
||||||
|
|
||||||
## Backports and Frontports
|
|
||||||
|
|
||||||
### What is backported?
|
|
||||||
|
|
||||||
We backport PRs given the following circumstances:
|
|
||||||
|
|
||||||
1. Feature freeze is active, but `<version>-rc0` has not been released yet. Here, we backport as much as possible. <!-- TODO: Is that our definition with the new backport bot? -->
|
|
||||||
2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. <!-- TODO: Is that our definition with the new backport bot? -->
|
|
||||||
3. We never backport new features.
|
|
||||||
4. We never backport breaking changes except when
|
|
||||||
1. The breaking change has no effect on the vast majority of users
|
|
||||||
2. The component triggering the breaking change is marked as experimental
|
|
||||||
|
|
||||||
### How to backport?
|
|
||||||
|
|
||||||
In the past, it was necessary to manually backport your PRs. \
|
|
||||||
Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR
|
|
||||||
|
|
||||||
- does not have the label `backport/manual`
|
|
||||||
- has the label `backport/<version>`
|
|
||||||
|
|
||||||
The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself.
|
|
||||||
|
|
||||||
### Format of backport PRs
|
|
||||||
|
|
||||||
The title of backport PRs should be
|
|
||||||
|
|
||||||
```
|
|
||||||
<original PR title> (#<original pr number>)
|
|
||||||
```
|
|
||||||
|
|
||||||
The first two lines of the summary of the backporting PR should be
|
|
||||||
|
|
||||||
```
|
|
||||||
Backport #<original pr number>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
with the rest of the summary and labels matching the original PR.
|
|
||||||
|
|
||||||
### Frontports
|
|
||||||
|
|
||||||
Frontports behave exactly as described above for backports.
|
|
||||||
|
|
||||||
## Developer Certificate of Origin (DCO)
|
## Developer Certificate of Origin (DCO)
|
||||||
|
|
||||||
We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \
|
We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \
|
||||||
@@ -483,148 +291,3 @@ Signed-off-by: Joe Smith <joe.smith@email.com>
|
|||||||
If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`.
|
If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`.
|
||||||
|
|
||||||
We assume in good faith that the information you provide is legally binding.
|
We assume in good faith that the information you provide is legally binding.
|
||||||
|
|
||||||
## Release Cycle
|
|
||||||
|
|
||||||
We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
|
|
||||||
The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
|
|
||||||
All the feature pull requests should be
|
|
||||||
merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
|
|
||||||
release branch is open for fixes backported from main branch. Release candidates
|
|
||||||
are made during this period for user testing to
|
|
||||||
obtain a final version that is maintained in this branch.
|
|
||||||
|
|
||||||
During a development cycle, we may also publish any necessary minor releases
|
|
||||||
for the previous version. For example, if the latest, published release is
|
|
||||||
v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are
|
|
||||||
still possible.
|
|
||||||
|
|
||||||
## Maintainers
|
|
||||||
|
|
||||||
To make sure every PR is checked, we have [maintainers](MAINTAINERS). \
|
|
||||||
Every PR **must** be reviewed by at least two maintainers (or owners) before it can get merged. \
|
|
||||||
For refactoring PRs after a week and documentation only PRs, the approval of only one maintainer is enough. \
|
|
||||||
A maintainer should be a contributor of Gitea and contributed at least
|
|
||||||
4 accepted PRs. A contributor should apply as a maintainer in the
|
|
||||||
[Discord](https://discord.gg/Gitea) `#develop` channel. The team maintainers may invite the contributor. A maintainer
|
|
||||||
should spend some time on code reviews. If a maintainer has no
|
|
||||||
time to do that, they should apply to leave the maintainers team
|
|
||||||
and we will give them the honor of being a member of the [advisors
|
|
||||||
team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if
|
|
||||||
an advisor has time to code review, we will gladly welcome them back
|
|
||||||
to the maintainers team. If a maintainer is inactive for more than 3
|
|
||||||
months and forgets to leave the maintainers team, the owners may move
|
|
||||||
him or her from the maintainers team to the advisors team.
|
|
||||||
For security reasons, Maintainers should use 2FA for their accounts and
|
|
||||||
if possible provide GPG signed commits.
|
|
||||||
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
|
|
||||||
https://help.github.com/articles/signing-commits-with-gpg/
|
|
||||||
|
|
||||||
Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
|
|
||||||
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
|
|
||||||
|
|
||||||
## Technical Oversight Committee (TOC)
|
|
||||||
|
|
||||||
At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
|
|
||||||
https://blog.gitea.com/quarterly-23q1/
|
|
||||||
|
|
||||||
### TOC election process
|
|
||||||
|
|
||||||
Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
|
|
||||||
A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
|
|
||||||
If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
|
|
||||||
|
|
||||||
The TOC is elected for one year, the TOC election happens yearly.
|
|
||||||
After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
|
|
||||||
If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
|
|
||||||
Refusals result in the person with the next highest vote getting the same choice.
|
|
||||||
As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
|
|
||||||
|
|
||||||
If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
|
|
||||||
|
|
||||||
### Current TOC members
|
|
||||||
|
|
||||||
- 2024-01-01 ~ 2024-12-31
|
|
||||||
- Company
|
|
||||||
- [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
|
|
||||||
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
|
|
||||||
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
|
|
||||||
- Community
|
|
||||||
- [6543](https://gitea.com/6543) <6543@obermui.de>
|
|
||||||
- [delvh](https://gitea.com/delvh) <dev.lh@web.de>
|
|
||||||
- [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
|
|
||||||
|
|
||||||
### Previous TOC/owners members
|
|
||||||
|
|
||||||
Here's the history of the owners and the time they served:
|
|
||||||
|
|
||||||
- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
|
||||||
- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
|
|
||||||
- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
|
|
||||||
- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
|
|
||||||
- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
|
||||||
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
|
||||||
- [6543](https://gitea.com/6543) - 2023
|
|
||||||
- [John Olheiser](https://gitea.com/jolheiser) - 2023
|
|
||||||
- [Jason Song](https://gitea.com/wolfogre) - 2023
|
|
||||||
|
|
||||||
## Governance Compensation
|
|
||||||
|
|
||||||
Each member of the community elected TOC will be granted $500 each month as compensation for their work.
|
|
||||||
|
|
||||||
Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release.
|
|
||||||
|
|
||||||
These funds will come from community sources like the OpenCollective rather than directly from the company.
|
|
||||||
Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties.
|
|
||||||
Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself.
|
|
||||||
|
|
||||||
## TOC & Working groups
|
|
||||||
|
|
||||||
With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places.
|
|
||||||
|
|
||||||
The currently proposed groups are:
|
|
||||||
|
|
||||||
- **Core Group**: maintain the primary Gitea repository
|
|
||||||
- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc.
|
|
||||||
- **Documentation Group**: maintain related documents and repositories
|
|
||||||
- **Translation Group**: coordinate with translators and maintain translations
|
|
||||||
- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders.
|
|
||||||
TOC members need to review the roadmap every year and work together on the direction of the project.
|
|
||||||
|
|
||||||
When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion.
|
|
||||||
|
|
||||||
You can visit our roadmap on the wiki.
|
|
||||||
|
|
||||||
## Versions
|
|
||||||
|
|
||||||
Gitea has the `main` branch as a tip branch and has version branches
|
|
||||||
such as `release/v1.19`. `release/v1.19` is a release branch and we will
|
|
||||||
tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept
|
|
||||||
pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag,
|
|
||||||
after bringing the bug fix also to the main branch.
|
|
||||||
|
|
||||||
Since the `main` branch is a tip version, if you wish to use Gitea
|
|
||||||
in production, please download the latest release tag version. All the
|
|
||||||
branches will be protected via GitHub, all the PRs to every branch must
|
|
||||||
be reviewed by two maintainers and must pass the automatic tests.
|
|
||||||
|
|
||||||
## Releasing Gitea
|
|
||||||
|
|
||||||
- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future.
|
|
||||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours.
|
|
||||||
- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
|
|
||||||
- Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`.
|
|
||||||
- When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin`
|
|
||||||
- If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged.
|
|
||||||
- Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
|
|
||||||
- And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
|
|
||||||
- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
|
|
||||||
- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
|
|
||||||
- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed:
|
|
||||||
- bump the version of https://dl.gitea.com/gitea/version.json
|
|
||||||
- merge the blog post PR
|
|
||||||
- announce the release in discord `#announcements`
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 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
|
RUN apk --no-cache add build-base git nodejs pnpm
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY package.json pnpm-lock.yaml .npmrc ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
|
||||||
COPY --exclude=.git/ . .
|
COPY --exclude=.git/ . .
|
||||||
RUN make frontend
|
RUN make frontend
|
||||||
|
|
||||||
# Build backend for each target platform
|
# Build backend for each target platform
|
||||||
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
|
||||||
|
|
||||||
ARG GITEA_VERSION
|
ARG GITEA_VERSION
|
||||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
ARG TAGS=""
|
||||||
ENV TAGS="bindata timetzdata $TAGS"
|
ENV TAGS="bindata timetzdata $TAGS"
|
||||||
ARG CGO_EXTRA_CFLAGS
|
ARG CGO_EXTRA_CFLAGS
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ RUN apk --no-cache add \
|
|||||||
build-base \
|
build-base \
|
||||||
git
|
git
|
||||||
|
|
||||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
WORKDIR ${GOPATH}/src/gitea.dev
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
# Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded.
|
# Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded.
|
||||||
@@ -42,9 +42,9 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
|||||||
/tmp/local/etc/s6/gitea/* \
|
/tmp/local/etc/s6/gitea/* \
|
||||||
/tmp/local/etc/s6/openssh/* \
|
/tmp/local/etc/s6/openssh/* \
|
||||||
/tmp/local/etc/s6/.s6-svscan/* \
|
/tmp/local/etc/s6/.s6-svscan/* \
|
||||||
/go/src/code.gitea.io/gitea/gitea
|
/go/src/gitea.dev/gitea
|
||||||
|
|
||||||
FROM docker.io/library/alpine:3.23 AS gitea
|
FROM docker.io/library/alpine:3.24 AS gitea
|
||||||
|
|
||||||
EXPOSE 22 3000
|
EXPOSE 22 3000
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ RUN addgroup \
|
|||||||
echo "git:*" | chpasswd -e
|
echo "git:*" | chpasswd -e
|
||||||
|
|
||||||
COPY --from=build-env /tmp/local /
|
COPY --from=build-env /tmp/local /
|
||||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
COPY --from=build-env /go/src/gitea.dev/gitea /app/gitea/gitea
|
||||||
|
|
||||||
ENV USER=git
|
ENV USER=git
|
||||||
ENV GITEA_CUSTOM=/data/gitea
|
ENV GITEA_CUSTOM=/data/gitea
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 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
|
RUN apk --no-cache add build-base git nodejs pnpm
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY package.json pnpm-lock.yaml .npmrc ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
|
||||||
COPY --exclude=.git/ . .
|
COPY --exclude=.git/ . .
|
||||||
RUN make frontend
|
RUN make frontend
|
||||||
|
|
||||||
# Build backend for each target platform
|
# Build backend for each target platform
|
||||||
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
|
||||||
|
|
||||||
ARG GITEA_VERSION
|
ARG GITEA_VERSION
|
||||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
ARG TAGS=""
|
||||||
ENV TAGS="bindata timetzdata $TAGS"
|
ENV TAGS="bindata timetzdata $TAGS"
|
||||||
ARG CGO_EXTRA_CFLAGS
|
ARG CGO_EXTRA_CFLAGS
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ RUN apk --no-cache add \
|
|||||||
build-base \
|
build-base \
|
||||||
git
|
git
|
||||||
|
|
||||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
WORKDIR ${GOPATH}/src/gitea.dev
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
# See the comments in Dockerfile
|
# See the comments in Dockerfile
|
||||||
@@ -37,9 +37,9 @@ COPY docker/rootless /tmp/local
|
|||||||
|
|
||||||
# Set permissions for builds that made under windows which strips the executable bit from file
|
# Set permissions for builds that made under windows which strips the executable bit from file
|
||||||
RUN chmod 755 /tmp/local/usr/local/bin/* \
|
RUN chmod 755 /tmp/local/usr/local/bin/* \
|
||||||
/go/src/code.gitea.io/gitea/gitea
|
/go/src/gitea.dev/gitea
|
||||||
|
|
||||||
FROM docker.io/library/alpine:3.23 AS gitea-rootless
|
FROM docker.io/library/alpine:3.24 AS gitea-rootless
|
||||||
|
|
||||||
EXPOSE 2222 3000
|
EXPOSE 2222 3000
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ RUN mkdir -p /var/lib/gitea /etc/gitea
|
|||||||
RUN chown git:git /var/lib/gitea /etc/gitea
|
RUN chown git:git /var/lib/gitea /etc/gitea
|
||||||
|
|
||||||
COPY --from=build-env /tmp/local /
|
COPY --from=build-env /tmp/local /
|
||||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
COPY --from=build-env --chown=root:root /go/src/gitea.dev/gitea /app/gitea/gitea
|
||||||
|
|
||||||
# git:git
|
# git:git
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
|
|||||||
+122
-278
@@ -7,33 +7,44 @@ export GOEXPERIMENT ?= jsonv2
|
|||||||
|
|
||||||
GO ?= go
|
GO ?= go
|
||||||
SHASUM ?= shasum -a 256
|
SHASUM ?= shasum -a 256
|
||||||
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
|
|
||||||
COMMA := ,
|
COMMA := ,
|
||||||
|
|
||||||
XGO_VERSION := go-1.25.x
|
XGO_VERSION := go-1.26.x
|
||||||
|
|
||||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
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
|
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go
|
||||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
|
||||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
|
||||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
|
||||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0
|
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go
|
||||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
|
||||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11
|
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
|
||||||
|
|
||||||
DOCKER_IMAGE ?= gitea/gitea
|
CONTAINER_RUNTIME ?= $(shell hash docker >/dev/null 2>&1 && echo docker || echo podman)
|
||||||
DOCKER_TAG ?= latest
|
|
||||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
|
||||||
|
|
||||||
|
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
|
||||||
ifeq ($(HAS_GO), yes)
|
ifeq ($(HAS_GO), yes)
|
||||||
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
|
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
|
||||||
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
|
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
MAKE_EVIDENCE_DIR := .make_evidence
|
||||||
|
|
||||||
|
# Use sqlite as default database if running tests, only do so for local tests, not in CI.
|
||||||
|
# CI should explicitly set the database to avoid unexpected results.
|
||||||
|
ifneq ($(findstring test-,$(MAKECMDGOALS)),)
|
||||||
|
ifeq ($(CI),)
|
||||||
|
GITEA_TEST_DATABASE ?= sqlite
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
TAGS ?=
|
||||||
|
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
||||||
|
|
||||||
CGO_ENABLED ?= 0
|
CGO_ENABLED ?= 0
|
||||||
ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
|
ifneq (,$(findstring sqlite_mattn,$(TAGS))$(findstring pam,$(TAGS)))
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -50,15 +61,16 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
|
|||||||
IS_WINDOWS := yes
|
IS_WINDOWS := yes
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# GOFLAGS and EXTRA_GOFLAGS are for the 'go build' command only
|
||||||
ifeq ($(IS_WINDOWS),yes)
|
ifeq ($(IS_WINDOWS),yes)
|
||||||
GOFLAGS := -v -buildmode=exe
|
GOFLAGS := -v -buildmode=exe
|
||||||
EXECUTABLE ?= gitea.exe
|
EXECUTABLE ?= gitea.exe
|
||||||
EXECUTABLE_E2E ?= gitea-e2e.exe
|
|
||||||
else
|
else
|
||||||
GOFLAGS := -v
|
GOFLAGS := -v
|
||||||
EXECUTABLE ?= gitea
|
EXECUTABLE ?= gitea
|
||||||
EXECUTABLE_E2E ?= gitea-e2e
|
|
||||||
endif
|
endif
|
||||||
|
EXTRA_GOFLAGS ?=
|
||||||
|
|
||||||
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
|
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
|
||||||
SED_INPLACE := sed -i
|
SED_INPLACE := sed -i
|
||||||
@@ -66,15 +78,8 @@ else
|
|||||||
SED_INPLACE := sed -i ''
|
SED_INPLACE := sed -i ''
|
||||||
endif
|
endif
|
||||||
|
|
||||||
EXTRA_GOFLAGS ?=
|
# GOTEST_FLAGS is for unit test and integration test
|
||||||
|
GOTEST_FLAGS ?= -timeout 40m
|
||||||
MAKE_EVIDENCE_DIR := .make_evidence
|
|
||||||
|
|
||||||
GOTESTFLAGS ?=
|
|
||||||
ifeq ($(RACE_ENABLED),true)
|
|
||||||
GOFLAGS += -race
|
|
||||||
GOTESTFLAGS += -race
|
|
||||||
endif
|
|
||||||
|
|
||||||
STORED_VERSION_FILE := VERSION
|
STORED_VERSION_FILE := VERSION
|
||||||
|
|
||||||
@@ -108,8 +113,8 @@ LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
|||||||
|
|
||||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
||||||
|
|
||||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
|
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 code.gitea.io/gitea/models/migrations/...)
|
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list gitea.dev/models/migrations/...)
|
||||||
|
|
||||||
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
|
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
|
||||||
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
|
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
|
||||||
@@ -122,17 +127,12 @@ BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* mo
|
|||||||
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
|
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
|
||||||
|
|
||||||
SVG_DEST_DIR := public/assets/img/svg
|
SVG_DEST_DIR := public/assets/img/svg
|
||||||
|
SVG_DEST_DIRS := $(SVG_DEST_DIR) options/fileicon
|
||||||
|
|
||||||
AIR_TMP_DIR := .air
|
AIR_TMP_DIR := .air
|
||||||
|
|
||||||
GO_LICENSE_FILE := assets/go-licenses.json
|
GO_LICENSE_FILE := assets/go-licenses.json
|
||||||
|
|
||||||
TAGS ?=
|
|
||||||
TAGS_SPLIT := $(subst $(COMMA), ,$(TAGS))
|
|
||||||
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
|
||||||
|
|
||||||
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
|
|
||||||
|
|
||||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
|
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
|
||||||
|
|
||||||
GO_DIRS := build cmd models modules routers services tests tools
|
GO_DIRS := build cmd models modules routers services tests tools
|
||||||
@@ -151,7 +151,8 @@ ESLINT_CONCURRENCY ?= 2
|
|||||||
|
|
||||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||||
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
|
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
|
||||||
SWAGGER_EXCLUDE := code.gitea.io/sdk
|
SWAGGER_EXCLUDE := gitea.dev/sdk
|
||||||
|
OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl
|
||||||
|
|
||||||
TEST_MYSQL_HOST ?= mysql:3306
|
TEST_MYSQL_HOST ?= mysql:3306
|
||||||
TEST_MYSQL_DBNAME ?= testgitea
|
TEST_MYSQL_DBNAME ?= testgitea
|
||||||
@@ -164,13 +165,19 @@ TEST_PGSQL_PASSWORD ?= postgres
|
|||||||
TEST_PGSQL_SCHEMA ?= gtestschema
|
TEST_PGSQL_SCHEMA ?= gtestschema
|
||||||
TEST_MINIO_ENDPOINT ?= minio:9000
|
TEST_MINIO_ENDPOINT ?= minio:9000
|
||||||
TEST_MSSQL_HOST ?= mssql:1433
|
TEST_MSSQL_HOST ?= mssql:1433
|
||||||
TEST_MSSQL_DBNAME ?= gitea
|
TEST_MSSQL_DBNAME ?= testgitea
|
||||||
TEST_MSSQL_USERNAME ?= sa
|
TEST_MSSQL_USERNAME ?= sa
|
||||||
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
|
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
|
||||||
|
|
||||||
# Include local Makefile
|
# Include local Makefile
|
||||||
# Makefile.local is listed in .gitignore
|
# Makefile.local is listed in .gitignore
|
||||||
sinclude Makefile.local
|
ifneq ("$(wildcard Makefile.local)","")
|
||||||
|
include Makefile.local
|
||||||
|
endif
|
||||||
|
|
||||||
|
$(foreach v, $(filter TEST_%, $(.VARIABLES)), $(eval MAKEFILE_VARS+=$v=$($v)))
|
||||||
|
$(foreach v, $(filter GITEA_TEST_%, $(.VARIABLES)), $(eval MAKEFILE_VARS+=$v=$($v)))
|
||||||
|
export MAKEFILE_VARS
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: build
|
all: build
|
||||||
@@ -179,15 +186,8 @@ all: build
|
|||||||
help: Makefile ## print Makefile help information.
|
help: Makefile ## print Makefile help information.
|
||||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST)
|
||||||
@printf " \033[36m%-46s\033[0m %s\n" "test-e2e" "test end to end using playwright"
|
@printf " \033[36m%-46s\033[0m %s\n" "test-e2e" "test end to end using playwright"
|
||||||
@printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test"
|
@printf " \033[36m%-46s\033[0m %s\n" "test-backend[#TestSpecificName]" "run unit test (sqlite only)"
|
||||||
@printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite"
|
@printf " \033[36m%-46s\033[0m %s\n" "test-integration[#TestSpecificName]" "run integration test for GITEA_TEST_DATABASE (sqlite, mysql, pgsql, mssql)"
|
||||||
|
|
||||||
.PHONY: git-check
|
|
||||||
git-check:
|
|
||||||
@if git lfs >/dev/null 2>&1 ; then : ; else \
|
|
||||||
echo "Gitea requires git with lfs support to run tests." ; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: clean-all
|
.PHONY: clean-all
|
||||||
clean-all: clean ## delete backend, frontend and integration files
|
clean-all: clean ## delete backend, frontend and integration files
|
||||||
@@ -195,18 +195,12 @@ clean-all: clean ## delete backend, frontend and integration files
|
|||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## delete backend and integration files
|
clean: ## delete backend and integration files
|
||||||
rm -rf $(EXECUTABLE) $(EXECUTABLE_E2E) $(DIST) $(BINDATA_DEST_WILDCARD) \
|
rm -f $(EXECUTABLE) test-*.test tests/*.ini
|
||||||
integrations*.test \
|
rm -rf $(DIST) $(BINDATA_DEST_WILDCARD) man tests/integration/gitea-integration-*
|
||||||
tests/integration/gitea-integration-* \
|
|
||||||
tests/integration/indexers-* \
|
|
||||||
tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
|
|
||||||
tests/e2e/gitea-e2e-*/ \
|
|
||||||
tests/e2e/indexers-*/ \
|
|
||||||
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
|
|
||||||
|
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: ## format the Go and template code
|
fmt: ## format the Go and template code
|
||||||
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}'
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) fmt
|
||||||
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
|
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
|
||||||
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
|
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
|
||||||
@# whitespace before it
|
@# whitespace before it
|
||||||
@@ -234,7 +228,7 @@ TAGS_PREREQ := $(TAGS_EVIDENCE)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: generate-swagger
|
.PHONY: generate-swagger
|
||||||
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
|
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
|
||||||
|
|
||||||
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
||||||
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
|
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
|
||||||
@@ -256,6 +250,21 @@ swagger-validate: ## check if the swagger spec is valid
|
|||||||
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
|
$(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
|
@$(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
|
||||||
|
|
||||||
|
$(OPENAPI3_SPEC): $(SWAGGER_SPEC) build/generate-openapi.go $(wildcard build/openapi3gen/*.go)
|
||||||
|
$(GO) run build/generate-openapi.go
|
||||||
|
|
||||||
|
.PHONY: openapi3-check
|
||||||
|
openapi3-check: generate-openapi3
|
||||||
|
@diff=$$(git diff --color=always '$(OPENAPI3_SPEC)'); \
|
||||||
|
if [ -n "$$diff" ]; then \
|
||||||
|
echo "Please run 'make generate-openapi3' and commit the result:"; \
|
||||||
|
printf "%s" "$${diff}"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
.PHONY: checks
|
.PHONY: checks
|
||||||
checks: checks-frontend checks-backend ## run various consistency checks
|
checks: checks-frontend checks-backend ## run various consistency checks
|
||||||
|
|
||||||
@@ -263,10 +272,10 @@ checks: checks-frontend checks-backend ## run various consistency checks
|
|||||||
checks-frontend: lockfile-check svg-check ## check frontend files
|
checks-frontend: lockfile-check svg-check ## check frontend files
|
||||||
|
|
||||||
.PHONY: checks-backend
|
.PHONY: checks-backend
|
||||||
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
|
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-frontend lint-backend lint-spell ## 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
|
.PHONY: lint-fix
|
||||||
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
|
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
|
||||||
@@ -278,10 +287,10 @@ lint-frontend: lint-js lint-css ## lint frontend files
|
|||||||
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
|
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
|
||||||
|
|
||||||
.PHONY: lint-backend
|
.PHONY: lint-backend
|
||||||
lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files
|
lint-backend: lint-go lint-editorconfig ## lint backend files
|
||||||
|
|
||||||
.PHONY: lint-backend-fix
|
.PHONY: lint-backend-fix
|
||||||
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
|
lint-backend-fix: lint-go-fix lint-editorconfig ## lint backend files and fix issues
|
||||||
|
|
||||||
.PHONY: lint-js
|
.PHONY: lint-js
|
||||||
lint-js: node_modules ## lint js and ts files
|
lint-js: node_modules ## lint js and ts files
|
||||||
@@ -323,23 +332,11 @@ lint-spell-fix: ## lint spelling and fix issues
|
|||||||
|
|
||||||
.PHONY: lint-go
|
.PHONY: lint-go
|
||||||
lint-go: ## lint go files
|
lint-go: ## lint go files
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
|
GO=$(GO) GOLANGCI_LINT_PACKAGE=$(GOLANGCI_LINT_PACKAGE) $(GO) run ./tools/lint-go-all.go
|
||||||
|
|
||||||
.PHONY: lint-go-fix
|
.PHONY: lint-go-fix
|
||||||
lint-go-fix: ## lint go files and fix issues
|
lint-go-fix: ## lint go files and fix issues
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
GO=$(GO) GOLANGCI_LINT_PACKAGE=$(GOLANGCI_LINT_PACKAGE) $(GO) run ./tools/lint-go-all.go --fix
|
||||||
|
|
||||||
# workaround step for the lint-go-windows CI task because 'go run' can not
|
|
||||||
# have distinct GOOS/GOARCH for its build and run steps
|
|
||||||
.PHONY: lint-go-windows
|
|
||||||
lint-go-windows:
|
|
||||||
@GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE)
|
|
||||||
golangci-lint run
|
|
||||||
|
|
||||||
.PHONY: lint-go-gitea-vet
|
|
||||||
lint-go-gitea-vet: ## lint go files with gitea-vet
|
|
||||||
@echo "Running gitea-vet..."
|
|
||||||
@$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./...
|
|
||||||
|
|
||||||
.PHONY: lint-editorconfig
|
.PHONY: lint-editorconfig
|
||||||
lint-editorconfig:
|
lint-editorconfig:
|
||||||
@@ -347,8 +344,13 @@ lint-editorconfig:
|
|||||||
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
|
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
|
||||||
|
|
||||||
.PHONY: lint-actions
|
.PHONY: lint-actions
|
||||||
lint-actions: ## lint action workflow files
|
lint-actions: .venv ## lint action workflow files
|
||||||
$(GO) run $(ACTIONLINT_PACKAGE)
|
@$(GO) run $(ACTIONLINT_PACKAGE)
|
||||||
|
@uv run --frozen zizmor --quiet --min-confidence=medium .github
|
||||||
|
|
||||||
|
.PHONY: lint-shell
|
||||||
|
lint-shell: ## lint shell scripts
|
||||||
|
@SHELLCHECK_IMAGE=$(SHELLCHECK_IMAGE) CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/lint-shell.sh $$(git ls-files '*.sh')
|
||||||
|
|
||||||
.PHONY: lint-templates
|
.PHONY: lint-templates
|
||||||
lint-templates: .venv node_modules ## lint template files
|
lint-templates: .venv node_modules ## lint template files
|
||||||
@@ -379,13 +381,10 @@ watch-frontend: node_modules ## start vite dev server for frontend
|
|||||||
watch-backend: ## watch backend files and continuously rebuild
|
watch-backend: ## watch backend files and continuously rebuild
|
||||||
GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
|
GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test: test-frontend test-backend ## test everything
|
|
||||||
|
|
||||||
.PHONY: test-backend
|
.PHONY: test-backend
|
||||||
test-backend: ## test backend files
|
test-backend: ## test backend files
|
||||||
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
|
@echo "Running go test with $(GOTEST_FLAGS) -tags '$(TAGS)'..."
|
||||||
@$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
|
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES)
|
||||||
|
|
||||||
.PHONY: test-frontend
|
.PHONY: test-frontend
|
||||||
test-frontend: node_modules ## test frontend files
|
test-frontend: node_modules ## test frontend files
|
||||||
@@ -403,10 +402,10 @@ test-check:
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: test\#%
|
.PHONY: test-backend\#%
|
||||||
test\#%:
|
test-backend\#%:
|
||||||
@echo "Running go test with -tags '$(TEST_TAGS)'..."
|
@echo "Running go test with -tags '$(TAGS)'..."
|
||||||
@$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
|
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
|
||||||
|
|
||||||
.PHONY: coverage
|
.PHONY: coverage
|
||||||
coverage:
|
coverage:
|
||||||
@@ -416,8 +415,8 @@ coverage:
|
|||||||
|
|
||||||
.PHONY: unit-test-coverage
|
.PHONY: unit-test-coverage
|
||||||
unit-test-coverage:
|
unit-test-coverage:
|
||||||
@echo "Running unit-test-coverage $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
|
@echo "Running unit-test-coverage $(GOTEST_FLAGS) -tags '$(TAGS)'..."
|
||||||
@$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
.PHONY: tidy
|
.PHONY: tidy
|
||||||
tidy: ## run go mod tidy
|
tidy: ## run go mod tidy
|
||||||
@@ -444,193 +443,45 @@ go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
|
|||||||
$(GO_LICENSE_FILE): go.mod go.sum
|
$(GO_LICENSE_FILE): go.mod go.sum
|
||||||
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
|
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
|
||||||
|
|
||||||
generate-ini-sqlite:
|
.PHONY: test-integration
|
||||||
sed -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-sqlite|g' \
|
test-integration:
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
@# Use a compiled binary: testlogger forwards gitea logs to t.Log, so `go test -v`
|
||||||
tests/sqlite.ini.tmpl > tests/sqlite.ini
|
@# would flood output per passing test. testcache can't help these tests anyway —
|
||||||
|
@# they mutate the work directory, so cache inputs change between runs.
|
||||||
|
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c gitea.dev/tests/integration -o ./test-integration-$(GITEA_TEST_DATABASE).test
|
||||||
|
./tools/test-integration.sh ./test-integration-$(GITEA_TEST_DATABASE).test
|
||||||
|
|
||||||
.PHONY: test-sqlite
|
.PHONY: test-integration-compile
|
||||||
test-sqlite: integrations.sqlite.test generate-ini-sqlite
|
test-integration-compile:
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test
|
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null gitea.dev/tests/integration
|
||||||
|
|
||||||
.PHONY: test-sqlite\#%
|
.PHONY: test-integration\#%
|
||||||
test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite
|
test-integration\#%:
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*)
|
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run $(subst .,/,$*) gitea.dev/tests/integration
|
||||||
|
|
||||||
.PHONY: test-sqlite-migration
|
.PHONY: test-migration
|
||||||
test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test
|
test-migration: migrations.integration.test migrations.individual.test
|
||||||
|
|
||||||
generate-ini-mysql:
|
.PHONY: migrations.integration.test
|
||||||
sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \
|
migrations.integration.test:
|
||||||
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
|
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' gitea.dev/tests/integration/migration-test
|
||||||
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
|
|
||||||
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
|
|
||||||
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mysql|g' \
|
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
|
||||||
tests/mysql.ini.tmpl > tests/mysql.ini
|
|
||||||
|
|
||||||
.PHONY: test-mysql
|
.PHONY: migrations.individual.test
|
||||||
test-mysql: integrations.mysql.test generate-ini-mysql
|
migrations.individual.test:
|
||||||
GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test
|
@# tests of multiple packages use the same database, don't run in parallel
|
||||||
|
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
||||||
|
|
||||||
.PHONY: test-mysql\#%
|
.PHONY: migrations.individual.test\#%
|
||||||
test-mysql\#%: integrations.mysql.test generate-ini-mysql
|
migrations.individual.test\#%:
|
||||||
GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*)
|
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' gitea.dev/models/migrations/$*
|
||||||
|
|
||||||
.PHONY: test-mysql-migration
|
|
||||||
test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test
|
|
||||||
|
|
||||||
generate-ini-pgsql:
|
|
||||||
sed -e 's|{{TEST_PGSQL_HOST}}|${TEST_PGSQL_HOST}|g' \
|
|
||||||
-e 's|{{TEST_PGSQL_DBNAME}}|${TEST_PGSQL_DBNAME}|g' \
|
|
||||||
-e 's|{{TEST_PGSQL_USERNAME}}|${TEST_PGSQL_USERNAME}|g' \
|
|
||||||
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
|
|
||||||
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
|
|
||||||
-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
|
|
||||||
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-pgsql|g' \
|
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
|
||||||
tests/pgsql.ini.tmpl > tests/pgsql.ini
|
|
||||||
|
|
||||||
.PHONY: test-pgsql
|
|
||||||
test-pgsql: integrations.pgsql.test generate-ini-pgsql
|
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test
|
|
||||||
|
|
||||||
.PHONY: test-pgsql\#%
|
|
||||||
test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql
|
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*)
|
|
||||||
|
|
||||||
.PHONY: test-pgsql-migration
|
|
||||||
test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test
|
|
||||||
|
|
||||||
generate-ini-mssql:
|
|
||||||
sed -e 's|{{TEST_MSSQL_HOST}}|${TEST_MSSQL_HOST}|g' \
|
|
||||||
-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
|
|
||||||
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
|
|
||||||
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
|
|
||||||
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mssql|g' \
|
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
|
||||||
tests/mssql.ini.tmpl > tests/mssql.ini
|
|
||||||
|
|
||||||
.PHONY: test-mssql
|
|
||||||
test-mssql: integrations.mssql.test generate-ini-mssql
|
|
||||||
GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test
|
|
||||||
|
|
||||||
.PHONY: test-mssql\#%
|
|
||||||
test-mssql\#%: integrations.mssql.test generate-ini-mssql
|
|
||||||
GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*)
|
|
||||||
|
|
||||||
.PHONY: test-mssql-migration
|
|
||||||
test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
|
|
||||||
|
|
||||||
.PHONY: playwright
|
.PHONY: playwright
|
||||||
playwright: deps-frontend
|
playwright: deps-frontend
|
||||||
@# on GitHub Actions VMs, playwright's system deps are pre-installed
|
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/test-e2e.sh install
|
||||||
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS)
|
|
||||||
|
|
||||||
.PHONY: test-e2e
|
.PHONY: test-e2e
|
||||||
test-e2e: playwright $(EXECUTABLE_E2E)
|
test-e2e: playwright frontend backend
|
||||||
@EXECUTABLE=$(EXECUTABLE_E2E) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS)
|
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS)
|
||||||
|
|
||||||
.PHONY: bench-sqlite
|
|
||||||
bench-sqlite: integrations.sqlite.test generate-ini-sqlite
|
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
|
|
||||||
|
|
||||||
.PHONY: bench-mysql
|
|
||||||
bench-mysql: integrations.mysql.test generate-ini-mysql
|
|
||||||
GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
|
|
||||||
|
|
||||||
.PHONY: bench-mssql
|
|
||||||
bench-mssql: integrations.mssql.test generate-ini-mssql
|
|
||||||
GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
|
|
||||||
|
|
||||||
.PHONY: bench-pgsql
|
|
||||||
bench-pgsql: integrations.pgsql.test generate-ini-pgsql
|
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
|
|
||||||
|
|
||||||
.PHONY: integration-test-coverage
|
|
||||||
integration-test-coverage: integrations.cover.test generate-ini-mysql
|
|
||||||
GITEA_TEST_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
|
|
||||||
|
|
||||||
.PHONY: integration-test-coverage-sqlite
|
|
||||||
integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite
|
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
|
|
||||||
|
|
||||||
integrations.mysql.test: git-check $(GO_SOURCES)
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test
|
|
||||||
|
|
||||||
integrations.pgsql.test: git-check $(GO_SOURCES)
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.pgsql.test
|
|
||||||
|
|
||||||
integrations.mssql.test: git-check $(GO_SOURCES)
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mssql.test
|
|
||||||
|
|
||||||
integrations.sqlite.test: git-check $(GO_SOURCES)
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
|
|
||||||
|
|
||||||
integrations.cover.test: git-check $(GO_SOURCES)
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.test
|
|
||||||
|
|
||||||
integrations.cover.sqlite.test: git-check $(GO_SOURCES)
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
|
|
||||||
|
|
||||||
.PHONY: migrations.mysql.test
|
|
||||||
migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test
|
|
||||||
GITEA_TEST_CONF=tests/mysql.ini ./migrations.mysql.test
|
|
||||||
|
|
||||||
.PHONY: migrations.pgsql.test
|
|
||||||
migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test
|
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini ./migrations.pgsql.test
|
|
||||||
|
|
||||||
.PHONY: migrations.mssql.test
|
|
||||||
migrations.mssql.test: $(GO_SOURCES) generate-ini-mssql
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test
|
|
||||||
GITEA_TEST_CONF=tests/mssql.ini ./migrations.mssql.test
|
|
||||||
|
|
||||||
.PHONY: migrations.sqlite.test
|
|
||||||
migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
|
||||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
|
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.mysql.test
|
|
||||||
migrations.individual.mysql.test: $(GO_SOURCES) generate-ini-mysql
|
|
||||||
GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.sqlite.test\#%
|
|
||||||
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.pgsql.test
|
|
||||||
migrations.individual.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
|
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.pgsql.test\#%
|
|
||||||
migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
|
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.mssql.test
|
|
||||||
migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql
|
|
||||||
GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.mssql.test\#%
|
|
||||||
migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
|
|
||||||
GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.sqlite.test
|
|
||||||
migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: migrations.individual.sqlite.test\#%
|
|
||||||
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
|
||||||
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
|
||||||
|
|
||||||
.PHONY: check
|
|
||||||
check: test
|
|
||||||
|
|
||||||
.PHONY: install $(TAGS_PREREQ)
|
|
||||||
install: $(wildcard *.go)
|
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
|
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: frontend backend ## build everything
|
build: frontend backend ## build everything
|
||||||
@@ -663,9 +514,6 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
|||||||
endif
|
endif
|
||||||
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||||
|
|
||||||
$(EXECUTABLE_E2E): $(GO_SOURCES) $(FRONTEND_DEST)
|
|
||||||
CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
|
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
|
||||||
|
|
||||||
@@ -730,7 +578,6 @@ deps-backend: ## install backend dependencies
|
|||||||
deps-tools: ## install tool dependencies
|
deps-tools: ## install tool dependencies
|
||||||
$(GO) install $(AIR_PACKAGE) & \
|
$(GO) install $(AIR_PACKAGE) & \
|
||||||
$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
|
$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
|
||||||
$(GO) install $(GOFUMPT_PACKAGE) & \
|
|
||||||
$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
|
$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
|
||||||
$(GO) install $(GXZ_PACKAGE) & \
|
$(GO) install $(GXZ_PACKAGE) & \
|
||||||
$(GO) install $(MISSPELL_PACKAGE) & \
|
$(GO) install $(MISSPELL_PACKAGE) & \
|
||||||
@@ -761,8 +608,6 @@ update-js: node_modules ## update js dependencies
|
|||||||
pnpm exec updates -u -f package.json
|
pnpm exec updates -u -f package.json
|
||||||
rm -rf node_modules pnpm-lock.yaml
|
rm -rf node_modules pnpm-lock.yaml
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm exec nolyfill install
|
|
||||||
pnpm install
|
|
||||||
@touch node_modules
|
@touch node_modules
|
||||||
|
|
||||||
.PHONY: update-py
|
.PHONY: update-py
|
||||||
@@ -789,10 +634,10 @@ svg: node_modules ## build svg files
|
|||||||
|
|
||||||
.PHONY: svg-check
|
.PHONY: svg-check
|
||||||
svg-check: svg
|
svg-check: svg
|
||||||
@git add $(SVG_DEST_DIR)
|
@git add $(SVG_DEST_DIRS)
|
||||||
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
|
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIRS)); \
|
||||||
if [ -n "$$diff" ]; then \
|
if [ -n "$$diff" ]; then \
|
||||||
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
|
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIRS)' and commit the result:"; \
|
||||||
printf "%s" "$${diff}"; \
|
printf "%s" "$${diff}"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@@ -816,6 +661,10 @@ generate-gitignore: ## update gitignore files
|
|||||||
generate-images: | node_modules ## generate images
|
generate-images: | node_modules ## generate images
|
||||||
cd tools && node generate-images.ts $(TAGS)
|
cd tools && node generate-images.ts $(TAGS)
|
||||||
|
|
||||||
|
.PHONY: generate-codemirror-languages
|
||||||
|
generate-codemirror-languages: | node_modules ## generate codemirror languages
|
||||||
|
node tools/generate-codemirror-languages.ts
|
||||||
|
|
||||||
.PHONY: generate-manpage
|
.PHONY: generate-manpage
|
||||||
generate-manpage: ## generate manpage
|
generate-manpage: ## generate manpage
|
||||||
@[ -f gitea ] || make backend
|
@[ -f gitea ] || make backend
|
||||||
@@ -824,11 +673,6 @@ generate-manpage: ## generate manpage
|
|||||||
@gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created
|
@gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created
|
||||||
@#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page
|
@#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page
|
||||||
|
|
||||||
.PHONY: docker
|
|
||||||
docker:
|
|
||||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
|
||||||
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
|
||||||
|
|
||||||
# Disable parallel execution because it would break some targets that don't
|
# Disable parallel execution because it would break some targets that don't
|
||||||
# specify exact dependencies like 'backend' which does currently not depend
|
# specify exact dependencies like 'backend' which does currently not depend
|
||||||
# on 'frontend' to enable Node.js-less builds from source tarballs.
|
# on 'frontend' to enable Node.js-less builds from source tarballs.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
[](https://goreportcard.com/report/gitea.dev "Go Report Card")
|
||||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
[](https://pkg.go.dev/gitea.dev "GoDoc")
|
||||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||||
@@ -14,21 +14,21 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
The goal of this project is to make the easiest, fastest, and most
|
The goal of Gitea is to make the easiest, fastest, and most painless way of
|
||||||
painless way of setting up a self-hosted Git service.
|
setting up a self-hosted all-in-one software development service,
|
||||||
|
including Git hosting, code management, code review, issue tracking, project kanban, wiki,
|
||||||
|
team collaboration, package registry and CI/CD which can reuse GitHub Actions.
|
||||||
|
|
||||||
As Gitea is written in Go, it works across **all** the platforms and
|
As Gitea is written in Go, it works across **all** the platforms and
|
||||||
architectures that are supported by Go, including Linux, macOS, and
|
architectures that are supported by Go, including Linux, macOS, FreeBSD/OpenBSD and Windows
|
||||||
Windows on x86, amd64, ARM and PowerPC architectures.
|
on x86, amd64, ARM, RISC-V 64 and PowerPC architectures.
|
||||||
This project has been
|
|
||||||
[forked](https://blog.gitea.com/welcome-to-gitea/) from
|
|
||||||
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
|
|
||||||
|
|
||||||
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
|
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
|
||||||
|
|
||||||
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
|
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
|
||||||
|
|
||||||
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
|
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com),
|
||||||
|
or use container (docker/podman/etc) to deploy on your own server with the [official image](https://hub.docker.com/r/gitea/gitea).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -40,31 +40,12 @@ If you have any suggestions or would like to contribute to it, you can visit the
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
From the root of the source tree, run:
|
See [docs/build-setup.md](docs/build-setup.md) for prerequisites
|
||||||
|
and [docs/development.md](docs/development.md) for setting up a local development environment, linting, and testing.
|
||||||
|
|
||||||
TAGS="bindata" make build
|
If you'd like to build from source or make a distribution package, see [docs/build-source.md](docs/build-source.md) for more information.
|
||||||
|
|
||||||
or if SQLite support is required:
|
After building, you can run `./gitea web` to start the server, or `./gitea help` to see all available commands.
|
||||||
|
|
||||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
|
||||||
|
|
||||||
The `build` target is split into two sub-targets:
|
|
||||||
|
|
||||||
- `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod).
|
|
||||||
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and [pnpm](https://pnpm.io/installation).
|
|
||||||
|
|
||||||
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
|
|
||||||
|
|
||||||
More info: https://docs.gitea.com/installation/install-from-source
|
|
||||||
|
|
||||||
## Using
|
|
||||||
|
|
||||||
After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
|
|
||||||
|
|
||||||
./gitea web
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -73,7 +54,8 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
|
> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
|
||||||
> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
|
> 2. New to the codebase? The [development guide](docs/development.md) walks through setting up a local environment and building from source.
|
||||||
|
> 3. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
|
||||||
|
|
||||||
## Translating
|
## Translating
|
||||||
|
|
||||||
@@ -130,14 +112,19 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
|||||||
|
|
||||||
Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
|
Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
|
||||||
|
|
||||||
**Why is this not hosted on a Gitea instance?**
|
**How do I configure Gitea?**
|
||||||
|
|
||||||
We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
|
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 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?**
|
**Where can I find the 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.
|
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))
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
This project is licensed under the MIT License.
|
||||||
@@ -147,7 +134,7 @@ for the full license text.
|
|||||||
## Further information
|
## Further information
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Looking for an overview of the interface? Check it out!</summary>
|
<summary>Looking for an overview of the interface? Check it out the screenshots!</summary>
|
||||||
|
|
||||||
### Login/Register Page
|
### Login/Register Page
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
[](https://goreportcard.com/report/gitea.dev "Go Report Card")
|
||||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
[](https://pkg.go.dev/gitea.dev "GoDoc")
|
||||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||||
@@ -38,10 +38,6 @@
|
|||||||
|
|
||||||
TAGS="bindata" make build
|
TAGS="bindata" make build
|
||||||
|
|
||||||
如果需要 SQLite 支持:
|
|
||||||
|
|
||||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
|
||||||
|
|
||||||
`build` 目标分为两个子目标:
|
`build` 目标分为两个子目标:
|
||||||
|
|
||||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
|
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
[](https://goreportcard.com/report/gitea.dev "Go Report Card")
|
||||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
[](https://pkg.go.dev/gitea.dev "GoDoc")
|
||||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||||
@@ -38,10 +38,6 @@
|
|||||||
|
|
||||||
TAGS="bindata" make build
|
TAGS="bindata" make build
|
||||||
|
|
||||||
如果需要 SQLite 支援:
|
|
||||||
|
|
||||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
|
||||||
|
|
||||||
`build` 目標分為兩個子目標:
|
`build` 目標分為兩個子目標:
|
||||||
|
|
||||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
|
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
|
||||||
|
|||||||
+1277
File diff suppressed because it is too large
Load Diff
+103
-83
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/assetfs"
|
"gitea.dev/modules/assetfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/json"
|
"gitea.dev/modules/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build ignore
|
//go:build ignore
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -15,7 +18,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
|||||||
|
|
||||||
// ignoredNames are LicenseEntry.Name values to exclude from the output.
|
// ignoredNames are LicenseEntry.Name values to exclude from the output.
|
||||||
var ignoredNames = map[string]bool{
|
var ignoredNames = map[string]bool{
|
||||||
"code.gitea.io/gitea": true,
|
"gitea.dev": true,
|
||||||
"code.gitea.io/gitea/options/license": true,
|
"gitea.dev/options/license": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludedExt = map[string]bool{
|
var excludedExt = map[string]bool{
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// generate-openapi converts Gitea's Swagger 2.0 spec into an OpenAPI 3.0 spec.
|
||||||
|
//
|
||||||
|
// Gitea generates a Swagger 2.0 spec from code annotations (make generate-swagger).
|
||||||
|
// This tool converts it to OAS3 so that SDK generators and tools that require
|
||||||
|
// OAS3 (e.g. progenitor for Rust) can consume it directly. The conversion also
|
||||||
|
// deduplicates inline enum definitions into named schema components, producing
|
||||||
|
// cleaner SDK output with proper enum types instead of anonymous strings.
|
||||||
|
//
|
||||||
|
// Run: go run build/generate-openapi.go
|
||||||
|
// Output: templates/swagger/v1_openapi3_json.tmpl
|
||||||
|
|
||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/build/openapi3gen"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
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 (
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("scanning swagger:enum annotations: %v", err)
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(astEnumMap))
|
||||||
|
for _, ns := range astEnumMap {
|
||||||
|
names = append(names, ns...)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", "))
|
||||||
|
|
||||||
|
data, err := os.ReadFile(swaggerSpecPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading swagger spec: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Generated %s\n", openapi3OutPath)
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package openapi3gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/json"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi2"
|
||||||
|
"github.com/getkin/kin-openapi/openapi2conv"
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// deprecated flags, and shared-enum extraction.
|
||||||
|
//
|
||||||
|
// astEnumMap is a value-set-key → Go-type-name(s) map (built by
|
||||||
|
// ScanSwaggerEnumTypes). When a value set is shared by multiple Go types,
|
||||||
|
// per-property disambiguation uses the x-go-enum-desc extension. If a shared
|
||||||
|
// enum in the spec has no matching entry, Convert returns an error — no
|
||||||
|
// fallback naming.
|
||||||
|
func Convert(swaggerJSON []byte, astEnumMap map[string][]string) (*openapi3.T, error) {
|
||||||
|
var swagger2 openapi2.T
|
||||||
|
if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing swagger 2.0: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oas3, err := openapi2conv.ToV3(&swagger2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting to openapi 3.0: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fixFileSchemas(oas3)
|
||||||
|
addURIFormats(oas3)
|
||||||
|
addDeprecatedFlags(oas3)
|
||||||
|
if err := extractSharedEnums(oas3, astEnumMap); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return oas3, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixFileSchemas(doc *openapi3.T) {
|
||||||
|
for _, pathItem := range doc.Paths.Map() {
|
||||||
|
for _, op := range []*openapi3.Operation{
|
||||||
|
pathItem.Get, pathItem.Post, pathItem.Put, pathItem.Patch,
|
||||||
|
pathItem.Delete, pathItem.Head, pathItem.Options, pathItem.Trace,
|
||||||
|
} {
|
||||||
|
if op == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, resp := range op.Responses.Map() {
|
||||||
|
if resp.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, mediaType := range resp.Value.Content {
|
||||||
|
fixSchema(mediaType.Schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if op.RequestBody != nil && op.RequestBody.Value != nil {
|
||||||
|
for _, mediaType := range op.RequestBody.Value.Content {
|
||||||
|
fixSchema(mediaType.Schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixSchema rewrites any "type: file" schemas to the OAS3 equivalent
|
||||||
|
// (type: string, format: binary), recursing into Properties, Items, and
|
||||||
|
// AllOf/OneOf/AnyOf/Not branches. $ref nodes are skipped so shared schemas
|
||||||
|
// are rewritten exactly once when visited through their declaration.
|
||||||
|
func fixSchema(ref *openapi3.SchemaRef) {
|
||||||
|
if ref == nil || ref.Value == nil || ref.Ref != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := ref.Value
|
||||||
|
if s.Type.Is("file") {
|
||||||
|
s.Type = &openapi3.Types{"string"}
|
||||||
|
s.Format = "binary"
|
||||||
|
}
|
||||||
|
for _, p := range s.Properties {
|
||||||
|
fixSchema(p)
|
||||||
|
}
|
||||||
|
fixSchema(s.Items)
|
||||||
|
for _, sub := range s.AllOf {
|
||||||
|
fixSchema(sub)
|
||||||
|
}
|
||||||
|
for _, sub := range s.OneOf {
|
||||||
|
fixSchema(sub)
|
||||||
|
}
|
||||||
|
for _, sub := range s.AnyOf {
|
||||||
|
fixSchema(sub)
|
||||||
|
}
|
||||||
|
fixSchema(s.Not)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addURIFormats sets format: uri on string properties whose names indicate
|
||||||
|
// they hold URLs. This information is lost in Swagger 2.0 but is valuable
|
||||||
|
// for code generators.
|
||||||
|
func addURIFormats(doc *openapi3.T) {
|
||||||
|
if doc.Components == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, schemaRef := range doc.Components.Schemas {
|
||||||
|
if schemaRef.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for propName, propRef := range schemaRef.Value.Properties {
|
||||||
|
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prop := propRef.Value
|
||||||
|
if !prop.Type.Is("string") || prop.Format != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isURLProperty(propName) {
|
||||||
|
prop.Format = "uri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isURLProperty(name string) bool {
|
||||||
|
if strings.HasSuffix(name, "_url") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "url", "html_url", "clone_url":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDeprecatedFlags sets deprecated: true on schema properties whose
|
||||||
|
// description starts with a "deprecated" marker (e.g. "Deprecated: true"
|
||||||
|
// or "deprecated (use X instead)"). Does not match negated phrases.
|
||||||
|
func addDeprecatedFlags(doc *openapi3.T) {
|
||||||
|
if doc.Components == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, schemaRef := range doc.Components.Schemas {
|
||||||
|
if schemaRef.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, propRef := range schemaRef.Value.Properties {
|
||||||
|
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rxDeprecated.MatchString(propRef.Value.Description) {
|
||||||
|
propRef.Value.Deprecated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type enumUsage struct {
|
||||||
|
schemaName string
|
||||||
|
propName string
|
||||||
|
propRef *openapi3.SchemaRef
|
||||||
|
inItems bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSharedEnums finds identical enum arrays used by multiple schema
|
||||||
|
// properties, creates a standalone named schema for each, and replaces
|
||||||
|
// the inline enums with $ref pointers.
|
||||||
|
//
|
||||||
|
// If the derived enum name collides with an existing component schema, or
|
||||||
|
// no // swagger:enum annotation matches the value set, generation aborts
|
||||||
|
// with an actionable error — there are no silent fallbacks.
|
||||||
|
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string][]string) error {
|
||||||
|
if doc.Components == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupKey struct {
|
||||||
|
valueSet string
|
||||||
|
typeName string
|
||||||
|
}
|
||||||
|
enumGroups := map[groupKey][]enumUsage{}
|
||||||
|
groupOrder := []groupKey{} // deterministic iteration
|
||||||
|
|
||||||
|
addUsage := func(key groupKey, u enumUsage) {
|
||||||
|
if _, seen := enumGroups[key]; !seen {
|
||||||
|
groupOrder = append(groupOrder, key)
|
||||||
|
}
|
||||||
|
enumGroups[key] = append(enumGroups[key], u)
|
||||||
|
}
|
||||||
|
|
||||||
|
for schemaName, schemaRef := range doc.Components.Schemas {
|
||||||
|
if schemaRef.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for propName, propRef := range schemaRef.Value.Properties {
|
||||||
|
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") {
|
||||||
|
key := groupKey{
|
||||||
|
valueSet: EnumKey(propRef.Value.Enum),
|
||||||
|
typeName: extractEnumTypeName(propRef.Value, astEnumMap),
|
||||||
|
}
|
||||||
|
addUsage(key, enumUsage{schemaName, propName, propRef, false})
|
||||||
|
}
|
||||||
|
if propRef.Value.Type.Is("array") && propRef.Value.Items != nil &&
|
||||||
|
propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" &&
|
||||||
|
len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") {
|
||||||
|
key := groupKey{
|
||||||
|
valueSet: EnumKey(propRef.Value.Items.Value.Enum),
|
||||||
|
typeName: extractEnumTypeName(propRef.Value.Items.Value, astEnumMap),
|
||||||
|
}
|
||||||
|
addUsage(key, enumUsage{schemaName, propName, propRef, true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range groupOrder {
|
||||||
|
usages := enumGroups[key]
|
||||||
|
if len(usages) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
enumName, err := deriveEnumName(key.valueSet, key.typeName, usages, astEnumMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, exists := doc.Components.Schemas[enumName]; exists {
|
||||||
|
return fmt.Errorf("enum name collision: %s already exists as a component schema", enumName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var enumValues []any
|
||||||
|
if usages[0].inItems {
|
||||||
|
enumValues = usages[0].propRef.Value.Items.Value.Enum
|
||||||
|
} else {
|
||||||
|
enumValues = usages[0].propRef.Value.Enum
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.Components.Schemas[enumName] = &openapi3.SchemaRef{
|
||||||
|
Value: &openapi3.Schema{
|
||||||
|
Type: &openapi3.Types{"string"},
|
||||||
|
Enum: enumValues,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := "#/components/schemas/" + enumName
|
||||||
|
|
||||||
|
for _, usage := range usages {
|
||||||
|
if usage.inItems {
|
||||||
|
usage.propRef.Value.Items = &openapi3.SchemaRef{Ref: ref}
|
||||||
|
} else {
|
||||||
|
old := usage.propRef.Value
|
||||||
|
if old.Description == "" && !old.Deprecated && old.Format == "" {
|
||||||
|
usage.propRef.Ref = ref
|
||||||
|
usage.propRef.Value = nil
|
||||||
|
} else {
|
||||||
|
usage.propRef.Value = &openapi3.Schema{
|
||||||
|
AllOf: openapi3.SchemaRefs{
|
||||||
|
{Ref: ref},
|
||||||
|
},
|
||||||
|
Description: old.Description,
|
||||||
|
Deprecated: old.Deprecated,
|
||||||
|
Format: old.Format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveEnumName looks up a shared enum's Go type name. If typeName is
|
||||||
|
// non-empty (because we recovered it from x-go-enum-desc), it is used
|
||||||
|
// directly. Otherwise the value-set must map to exactly one known type. On
|
||||||
|
// failure, returns an error identifying the offending properties.
|
||||||
|
func deriveEnumName(key, typeName string, usages []enumUsage, astEnumMap map[string][]string) (string, error) {
|
||||||
|
if typeName != "" {
|
||||||
|
return typeName, nil
|
||||||
|
}
|
||||||
|
names := astEnumMap[key]
|
||||||
|
if len(names) == 1 {
|
||||||
|
return names[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
props := map[string]bool{}
|
||||||
|
for _, u := range usages {
|
||||||
|
props[fmt.Sprintf("%s.%s", u.schemaName, u.propName)] = true
|
||||||
|
}
|
||||||
|
propList := make([]string, 0, len(props))
|
||||||
|
for p := range props {
|
||||||
|
propList = append(propList, p)
|
||||||
|
}
|
||||||
|
if len(names) > 1 {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"value-set %q is shared by multiple swagger:enum types %v and could not be disambiguated for properties: %v; "+
|
||||||
|
"ensure go-swagger emits x-go-enum-desc for those properties",
|
||||||
|
key, names, propList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"no swagger:enum annotation matches value-set %q used by %d properties: %v; "+
|
||||||
|
"fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus",
|
||||||
|
key, len(usages), propList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractEnumTypeName recovers the Go type name a schema's enum came from by
|
||||||
|
// parsing the property's x-go-enum-desc extension. go-swagger emits one line
|
||||||
|
// per value as "<value> <ConstName>[ <free text>]"; the type is the longest
|
||||||
|
// common prefix of the const names, narrowed to the candidate set in
|
||||||
|
// astEnumMap. Returns "" if extraction is inconclusive.
|
||||||
|
func extractEnumTypeName(s *openapi3.Schema, astEnumMap map[string][]string) string {
|
||||||
|
if s == nil || s.Extensions == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
raw, ok := s.Extensions["x-go-enum-desc"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
desc, ok := raw.(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
candidates := astEnumMap[EnumKey(s.Enum)]
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Collect the const names (second whitespace-separated field per line).
|
||||||
|
var consts []string
|
||||||
|
for line := range strings.SplitSeq(desc, "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
consts = append(consts, fields[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(consts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// A candidate matches when it is a prefix of every const name AND the
|
||||||
|
// first character after the prefix is an uppercase ASCII letter — this
|
||||||
|
// rejects e.g. "Alpha" matching "Alphabet" (suffix "bet" starts lower)
|
||||||
|
// while still accepting both "Alpha" and "AlphaPlus" against "AlphaPlusX"
|
||||||
|
// (both prefixes valid). The most specific (longest) wins; ties return
|
||||||
|
// "" so deriveEnumName surfaces the ambiguity rather than silently
|
||||||
|
// picking a winner.
|
||||||
|
ordered := append([]string(nil), candidates...)
|
||||||
|
sort.Slice(ordered, func(i, j int) bool { return len(ordered[i]) > len(ordered[j]) })
|
||||||
|
var matches []string
|
||||||
|
for _, name := range ordered {
|
||||||
|
ok := true
|
||||||
|
for _, c := range consts {
|
||||||
|
if !strings.HasPrefix(c, name) {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
suffix := c[len(name):]
|
||||||
|
// Empty suffix means the const name exactly equals the type name — valid exact match.
|
||||||
|
// A non-empty suffix must begin with an uppercase letter to reject incidental
|
||||||
|
// prefix matches (e.g. "Alpha" should not match "Alphabet").
|
||||||
|
if len(suffix) > 0 && (suffix[0] < 'A' || suffix[0] > 'Z') {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
matches = append(matches, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(matches) > 1 && len(matches[0]) == len(matches[1]) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return matches[0]
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package openapi3gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeriveEnumName_hit(t *testing.T) {
|
||||||
|
key := EnumKey([]any{"red", "green", "blue"})
|
||||||
|
astMap := map[string][]string{key: {"Color"}}
|
||||||
|
usages := []enumUsage{{schemaName: "Paint", propName: "color"}}
|
||||||
|
got, err := deriveEnumName(key, "", usages, astMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if got != "Color" {
|
||||||
|
t.Fatalf("got %q, want %q", got, "Color")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveEnumName_miss(t *testing.T) {
|
||||||
|
key := EnumKey([]any{"x", "y"})
|
||||||
|
usages := []enumUsage{{schemaName: "Thing", propName: "kind"}}
|
||||||
|
_, err := deriveEnumName(key, "", usages, map[string][]string{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected miss error, got nil")
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
if !strings.Contains(msg, "Thing.kind") {
|
||||||
|
t.Fatalf("error %q should list the missing usage", msg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg, "swagger:enum") {
|
||||||
|
t.Fatalf("error %q should hint at the fix", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSharedEnums_usesASTMap(t *testing.T) {
|
||||||
|
doc := &openapi3.T{
|
||||||
|
Components: &openapi3.Components{
|
||||||
|
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 {
|
||||||
|
t.Fatalf("extractSharedEnums: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := doc.Components.Schemas["Color"]; !ok {
|
||||||
|
t.Fatalf("expected Color schema to be extracted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixFileSchemas_recursesIntoNested(t *testing.T) {
|
||||||
|
fileType := func() *openapi3.SchemaRef {
|
||||||
|
return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"file"}}}
|
||||||
|
}
|
||||||
|
doc := &openapi3.T{
|
||||||
|
Paths: openapi3.NewPaths(),
|
||||||
|
}
|
||||||
|
doc.Paths.Set("/upload", &openapi3.PathItem{
|
||||||
|
Post: &openapi3.Operation{
|
||||||
|
RequestBody: &openapi3.RequestBodyRef{
|
||||||
|
Value: &openapi3.RequestBody{
|
||||||
|
Content: openapi3.Content{
|
||||||
|
"multipart/form-data": {
|
||||||
|
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{
|
||||||
|
Type: &openapi3.Types{"object"},
|
||||||
|
Properties: openapi3.Schemas{
|
||||||
|
"attachment": fileType(),
|
||||||
|
"items": {Value: &openapi3.Schema{
|
||||||
|
Type: &openapi3.Types{"array"},
|
||||||
|
Items: fileType(),
|
||||||
|
}},
|
||||||
|
"alt": {Value: &openapi3.Schema{
|
||||||
|
AllOf: openapi3.SchemaRefs{fileType()},
|
||||||
|
}},
|
||||||
|
"one": {Value: &openapi3.Schema{
|
||||||
|
OneOf: openapi3.SchemaRefs{fileType()},
|
||||||
|
}},
|
||||||
|
"any": {Value: &openapi3.Schema{
|
||||||
|
AnyOf: openapi3.SchemaRefs{fileType()},
|
||||||
|
}},
|
||||||
|
"not": {Value: &openapi3.Schema{
|
||||||
|
Not: fileType(),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Responses: openapi3.NewResponses(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fixFileSchemas(doc)
|
||||||
|
|
||||||
|
props := doc.Paths.Value("/upload").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties
|
||||||
|
if !props["attachment"].Value.Type.Is("string") || props["attachment"].Value.Format != "binary" {
|
||||||
|
t.Errorf("nested property not fixed: %+v", props["attachment"].Value)
|
||||||
|
}
|
||||||
|
if !props["items"].Value.Items.Value.Type.Is("string") || props["items"].Value.Items.Value.Format != "binary" {
|
||||||
|
t.Errorf("array items not fixed: %+v", props["items"].Value.Items.Value)
|
||||||
|
}
|
||||||
|
if !props["alt"].Value.AllOf[0].Value.Type.Is("string") || props["alt"].Value.AllOf[0].Value.Format != "binary" {
|
||||||
|
t.Errorf("allOf branch not fixed: %+v", props["alt"].Value.AllOf[0].Value)
|
||||||
|
}
|
||||||
|
if !props["one"].Value.OneOf[0].Value.Type.Is("string") || props["one"].Value.OneOf[0].Value.Format != "binary" {
|
||||||
|
t.Errorf("oneOf branch not fixed: %+v", props["one"].Value.OneOf[0].Value)
|
||||||
|
}
|
||||||
|
if !props["any"].Value.AnyOf[0].Value.Type.Is("string") || props["any"].Value.AnyOf[0].Value.Format != "binary" {
|
||||||
|
t.Errorf("anyOf branch not fixed: %+v", props["any"].Value.AnyOf[0].Value)
|
||||||
|
}
|
||||||
|
if !props["not"].Value.Not.Value.Type.Is("string") || props["not"].Value.Not.Value.Format != "binary" {
|
||||||
|
t.Errorf("not branch not fixed: %+v", props["not"].Value.Not.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractEnumTypeName_TeamVisibility(t *testing.T) {
|
||||||
|
enum := []any{"public", "limited", "private"}
|
||||||
|
key := EnumKey(enum)
|
||||||
|
astMap := map[string][]string{key: {"UserVisibility", "TeamVisibility"}}
|
||||||
|
schema := &openapi3.Schema{
|
||||||
|
Type: &openapi3.Types{"string"},
|
||||||
|
Enum: enum,
|
||||||
|
Extensions: map[string]any{
|
||||||
|
"x-go-enum-desc": "public TeamVisibilityPublic\nlimited TeamVisibilityLimited\nprivate TeamVisibilityPrivate",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if got := extractEnumTypeName(schema, astMap); got != "TeamVisibility" {
|
||||||
|
t.Fatalf("got %q, want %q", got, "TeamVisibility")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractEnumTypeName_ambiguousPrefixTie(t *testing.T) {
|
||||||
|
enum := []any{"one", "two"}
|
||||||
|
key := EnumKey(enum)
|
||||||
|
astMap := map[string][]string{key: {"AB", "AC"}}
|
||||||
|
schema := &openapi3.Schema{
|
||||||
|
Type: &openapi3.Types{"string"},
|
||||||
|
Enum: enum,
|
||||||
|
Extensions: map[string]any{
|
||||||
|
"x-go-enum-desc": "one ABOne\ntwo ACTwo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if got := extractEnumTypeName(schema, astMap); got != "" {
|
||||||
|
t.Fatalf("got %q, want empty string for ambiguous tie", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractEnumTypeName_rejectsIncidentalPrefix(t *testing.T) {
|
||||||
|
enum := []any{"a", "b"}
|
||||||
|
key := EnumKey(enum)
|
||||||
|
astMap := map[string][]string{key: {"Alpha", "Alphabet"}}
|
||||||
|
schema := &openapi3.Schema{
|
||||||
|
Type: &openapi3.Types{"string"},
|
||||||
|
Enum: enum,
|
||||||
|
Extensions: map[string]any{
|
||||||
|
"x-go-enum-desc": "a AlphabetA\nb AlphabetB",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if got := extractEnumTypeName(schema, astMap); got != "Alphabet" {
|
||||||
|
t.Fatalf("got %q, want %q", got, "Alphabet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
|
||||||
|
doc := &openapi3.T{
|
||||||
|
Components: &openapi3.Components{
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Package openapi3gen converts Gitea's Swagger 2.0 spec to an OpenAPI 3.0
|
||||||
|
// spec. It discovers Go enum type names by scanning swagger:enum annotations
|
||||||
|
// in the source tree, then names extracted shared-enum schemas accordingly.
|
||||||
|
package openapi3gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnumKey returns a canonical key for a set of enum values: values are
|
||||||
|
// stringified, sorted, and joined with "|". Used to match enum value sets
|
||||||
|
// across spec properties and scanned Go type declarations.
|
||||||
|
func EnumKey(values []any) string {
|
||||||
|
strs := make([]string, len(values))
|
||||||
|
for i, v := range values {
|
||||||
|
strs[i] = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
sort.Strings(strs)
|
||||||
|
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.
|
||||||
|
// distinct enum types that happen to share a value set such as
|
||||||
|
// {public, limited, private}); callers must disambiguate per-usage (typically
|
||||||
|
// by parsing the property's x-go-enum-desc extension to recover the const
|
||||||
|
// type prefix).
|
||||||
|
//
|
||||||
|
// Returns an error on parse failure or on an annotation for a type whose
|
||||||
|
// constants can't be extracted.
|
||||||
|
func ScanSwaggerEnumTypes(dirs []string) (map[string][]string, error) {
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
parsed := []*ast.File{}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(entry.Name(), "_test.go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, entry.Name())
|
||||||
|
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
parsed = append(parsed, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enumTypes := map[string]string{} // typeName → "" (presence marker)
|
||||||
|
enumValues := map[string][]any{} // typeName → values
|
||||||
|
|
||||||
|
// Pass 1: collect every // swagger:enum TypeName declaration.
|
||||||
|
for _, file := range parsed {
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
gd, ok := decl.(*ast.GenDecl)
|
||||||
|
if !ok || gd.Tok != token.TYPE {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := collectEnumType(gd, enumTypes); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", fset.Position(gd.Pos()).Filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: collect const values; now every annotated type is visible.
|
||||||
|
for _, file := range parsed {
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
gd, ok := decl.(*ast.GenDecl)
|
||||||
|
if !ok || gd.Tok != token.CONST {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
collectEnumValues(gd, enumTypes, enumValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string][]string{}
|
||||||
|
for typeName := range enumTypes {
|
||||||
|
values, ok := enumValues[typeName]
|
||||||
|
if !ok || len(values) == 0 {
|
||||||
|
return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName)
|
||||||
|
}
|
||||||
|
key := EnumKey(values)
|
||||||
|
result[key] = append(result[key], typeName)
|
||||||
|
}
|
||||||
|
for key, names := range result {
|
||||||
|
sort.Strings(names)
|
||||||
|
result[key] = names
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectEnumType scans a `type` GenDecl for // swagger:enum annotations,
|
||||||
|
// handling both the lone form (`// swagger:enum Foo\n type Foo string`)
|
||||||
|
// where the comment group is attached to the GenDecl, and the grouped form:
|
||||||
|
//
|
||||||
|
// type (
|
||||||
|
// // swagger:enum Foo
|
||||||
|
// Foo string
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// where the comment group is attached to each TypeSpec. Caveat: Go's parser
|
||||||
|
// only attaches a CommentGroup when it is immediately adjacent to the decl.
|
||||||
|
// A blank line (not a `//` continuation line) between the comment and the
|
||||||
|
// declaration drops the Doc, so annotations MUST sit directly above their
|
||||||
|
// type. All current annotated files obey this — the rule is noted here so
|
||||||
|
// a future edit that inserts a blank line fails fast rather than silently.
|
||||||
|
func collectEnumType(gd *ast.GenDecl, enumTypes map[string]string) error {
|
||||||
|
if err := registerEnumAnnotation(gd.Doc, gd.Specs, enumTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, spec := range gd.Specs {
|
||||||
|
ts, ok := spec.(*ast.TypeSpec)
|
||||||
|
if !ok || ts.Doc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := registerEnumAnnotation(ts.Doc, []ast.Spec{ts}, enumTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes map[string]string) error {
|
||||||
|
if doc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
annotated := matches[1]
|
||||||
|
for _, spec := range specs {
|
||||||
|
ts, ok := spec.(*ast.TypeSpec)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ts.Name.Name == annotated {
|
||||||
|
enumTypes[annotated] = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("swagger:enum %s: no type declaration with that name in the same decl group; check for a typo", annotated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectEnumValues(gd *ast.GenDecl, enumTypes map[string]string, enumValues map[string][]any) {
|
||||||
|
for _, spec := range gd.Specs {
|
||||||
|
vs, ok := spec.(*ast.ValueSpec)
|
||||||
|
if !ok || vs.Type == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ident, ok := vs.Type.(*ast.Ident)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, isEnum := enumTypes[ident.Name]; !isEnum {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, val := range vs.Values {
|
||||||
|
lit, ok := val.(*ast.BasicLit)
|
||||||
|
if !ok || lit.Kind != token.STRING {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
unquoted, err := strconv.Unquote(lit.Value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
enumValues[ident.Name] = append(enumValues[ident.Name], unquoted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package openapi3gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func single(got map[string][]string, key string) string {
|
||||||
|
v := got[key]
|
||||||
|
if len(v) != 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumKey_sortsAndJoins(t *testing.T) {
|
||||||
|
key := EnumKey([]any{"b", "a", "c"})
|
||||||
|
if key != "a|b|c" {
|
||||||
|
t.Fatalf("EnumKey = %q, want %q", key, "a|b|c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumKey_handlesNonStringValues(t *testing.T) {
|
||||||
|
key := EnumKey([]any{2, 1, 3})
|
||||||
|
if key != "1|2|3" {
|
||||||
|
t.Fatalf("EnumKey = %q, want %q", key, "1|2|3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanSwaggerEnumTypes_basic(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
src := `package fixture
|
||||||
|
|
||||||
|
// Color is a primary color.
|
||||||
|
// swagger:enum Color
|
||||||
|
type Color string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorRed Color = "red"
|
||||||
|
ColorGreen Color = "green"
|
||||||
|
ColorBlue Color = "blue"
|
||||||
|
)
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "color.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{"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
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateOpen StateType = "open"
|
||||||
|
)
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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
|
||||||
|
const (
|
||||||
|
AlphaX Alpha = "x"
|
||||||
|
AlphaY Alpha = "y"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swagger:enum Beta
|
||||||
|
type Beta string
|
||||||
|
const (
|
||||||
|
BetaX Beta = "x"
|
||||||
|
BetaY Beta = "y"
|
||||||
|
)
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
// Name ordering: `a_consts.go` < `b_type.go`, so readdir returns consts first.
|
||||||
|
// Old single-pass scanner would miss the values; two-pass must not.
|
||||||
|
constsSrc := `package fixture
|
||||||
|
|
||||||
|
const (
|
||||||
|
HueA Hue = "a"
|
||||||
|
HueB Hue = "b"
|
||||||
|
)
|
||||||
|
`
|
||||||
|
typeSrc := `package fixture
|
||||||
|
|
||||||
|
// swagger:enum Hue
|
||||||
|
type Hue string
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "a_consts.go"), []byte(constsSrc), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "b_type.go"), []byte(typeSrc), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||||
|
}
|
||||||
|
wantKey := EnumKey([]any{"a", "b"})
|
||||||
|
if single(got, wantKey) != "Hue" {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ ! -f ./build/test-env-check.sh ]; then
|
|
||||||
echo "${0} can only be executed in gitea source root directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
echo "check uid ..."
|
|
||||||
|
|
||||||
# the uid of gitea defined in "https://gitea.com/gitea/test-env" is 1000
|
|
||||||
gitea_uid=$(id -u gitea)
|
|
||||||
if [ "$gitea_uid" != "1000" ]; then
|
|
||||||
echo "The uid of linux user 'gitea' is expected to be 1000, but it is $gitea_uid"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cur_uid=$(id -u)
|
|
||||||
if [ "$cur_uid" != "0" -a "$cur_uid" != "$gitea_uid" ]; then
|
|
||||||
echo "The uid of current linux user is expected to be 0 or $gitea_uid, but it is $cur_uid"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ ! -f ./build/test-env-prepare.sh ]; then
|
|
||||||
echo "${0} can only be executed in gitea source root directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "change the owner of files to gitea ..."
|
|
||||||
chown -R gitea:gitea .
|
|
||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/private"
|
"gitea.dev/modules/private"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"gitea.dev/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"gitea.dev/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"gitea.dev/modules/log"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "gitea.dev/modules/repository"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "gitea.dev/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"gitea.dev/models/db"
|
||||||
auth_service "code.gitea.io/gitea/services/auth"
|
auth_service "gitea.dev/services/auth"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"gitea.dev/models/auth"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"gitea.dev/modules/util"
|
||||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
"gitea.dev/services/auth/source/ldap"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"gitea.dev/models/auth"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"gitea.dev/modules/test"
|
||||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
"gitea.dev/services/auth/source/ldap"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user