mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-12 02:45:09 -06:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a62dfffbe7 | ||
|
|
035dd58664 | ||
|
|
0d9ce64f76 | ||
|
|
4a77fbce28 | ||
|
|
2b37732d5f | ||
|
|
2695b47887 | ||
|
|
9c685dedbb | ||
|
|
e9b3917042 | ||
|
|
d7bc52beea | ||
|
|
ccd38f9a70 | ||
|
|
f47e3d930d | ||
|
|
9972bee41f | ||
|
|
375e6ea038 | ||
|
|
31c435454b | ||
|
|
a54324e2b7 | ||
|
|
d141dc729c | ||
|
|
27fed5a83a | ||
|
|
6cdd02bdad | ||
|
|
cc4ee6387b | ||
|
|
4780cffe08 | ||
|
|
483fb19b37 | ||
|
|
560535a97f | ||
|
|
62c61aa8ce | ||
|
|
337fa5a950 | ||
|
|
ad84c14d53 | ||
|
|
65ea4079ce | ||
|
|
1cf54fed70 | ||
|
|
1ae686696e | ||
|
|
e2f0358368 | ||
|
|
d88bbfd0db | ||
|
|
5e494f9cad | ||
|
|
9731ad7c3c | ||
|
|
148d528814 | ||
|
|
1af5277aba | ||
|
|
6d41731184 | ||
|
|
cfc9f4c685 | ||
|
|
895d848ff4 | ||
|
|
7199547218 | ||
|
|
7cb4201f8e | ||
|
|
5f017302bf | ||
|
|
59c619660c | ||
|
|
8599289459 | ||
|
|
da9f0a3726 | ||
|
|
08fd59959e | ||
|
|
d60215c2a2 | ||
|
|
bf594690db | ||
|
|
5cb7ec9304 | ||
|
|
6e86c4cde8 | ||
|
|
c32af046a2 | ||
|
|
a98468da30 | ||
|
|
b969123b7f | ||
|
|
c6627af968 | ||
|
|
de4b8277e9 | ||
|
|
acbc75e2bd | ||
|
|
c3325be816 | ||
|
|
d4250bafd9 | ||
|
|
6815d1646c | ||
|
|
8ba4d4ebec | ||
|
|
b2794f96b9 | ||
|
|
b6aa2b61c6 | ||
|
|
74ad781db9 | ||
|
|
ed493f0684 | ||
|
|
af7ba2edef | ||
|
|
8b4252e7f6 | ||
|
|
500a09e044 | ||
|
|
bfebc4b0e1 | ||
|
|
d9534e7bfa | ||
|
|
532828cc82 | ||
|
|
8cf988f0a6 | ||
|
|
04a26f40a0 | ||
|
|
fc4eac390a | ||
|
|
64d559a8e6 | ||
|
|
03372836ab | ||
|
|
1af1e06ac4 | ||
|
|
e07a5de53f | ||
|
|
d4d81af583 | ||
|
|
6cd2c647ec | ||
|
|
37c8604105 | ||
|
|
a9814e789c | ||
|
|
ab10e37acf | ||
|
|
f7bc6b89c1 | ||
|
|
fabc5e6fcb | ||
|
|
a016d678db | ||
|
|
b6e409badd | ||
|
|
2734504cfc | ||
|
|
eee7967b81 | ||
|
|
1df8f91691 | ||
|
|
4654e7eccd |
@@ -11,7 +11,7 @@ include_file = ["main.go"]
|
||||
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
|
||||
exclude_dir = [
|
||||
"models/fixtures",
|
||||
"modelmigration/fixtures",
|
||||
"models/migrations/fixtures",
|
||||
"modules/avatar/identicon/testdata",
|
||||
"modules/avatar/testdata",
|
||||
"modules/git/tests",
|
||||
|
||||
59
.changelog.yml
Normal file
59
.changelog.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
# The full repository name
|
||||
repo: go-gitea/gitea
|
||||
|
||||
# Service type (gitea or github)
|
||||
service: github
|
||||
|
||||
# Base URL for Gitea instance if using gitea service type (optional)
|
||||
# Default: https://gitea.com
|
||||
base-url:
|
||||
|
||||
# Changelog groups and which labeled PRs to add to each group
|
||||
groups:
|
||||
-
|
||||
name: BREAKING
|
||||
labels:
|
||||
- pr/breaking
|
||||
-
|
||||
name: SECURITY
|
||||
labels:
|
||||
- topic/security
|
||||
-
|
||||
name: FEATURES
|
||||
labels:
|
||||
- type/feature
|
||||
-
|
||||
name: ENHANCEMENTS
|
||||
labels:
|
||||
- type/enhancement
|
||||
-
|
||||
name: PERFORMANCE
|
||||
labels:
|
||||
- performance/memory
|
||||
- performance/speed
|
||||
- performance/bigrepo
|
||||
- performance/cpu
|
||||
-
|
||||
name: BUGFIXES
|
||||
labels:
|
||||
- type/bug
|
||||
|
||||
-
|
||||
name: TESTING
|
||||
labels:
|
||||
- type/testing
|
||||
-
|
||||
name: BUILD
|
||||
labels:
|
||||
- topic/build
|
||||
- topic/code-linting
|
||||
-
|
||||
name: DOCS
|
||||
labels:
|
||||
- type/docs
|
||||
-
|
||||
name: MISC
|
||||
default: true
|
||||
|
||||
# regex indicating which labels to skip for the changelog
|
||||
skip-labels: skip-changelog|backport\/.+
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Gitea DevContainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.27-trixie",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
|
||||
"containerEnv": {
|
||||
// override "local" from packaged version
|
||||
"GOTOOLCHAIN": "auto"
|
||||
|
||||
@@ -63,6 +63,7 @@ cpu.out
|
||||
/public/assets/fonts
|
||||
/public/assets/img/avatar
|
||||
/vendor
|
||||
/VERSION
|
||||
/.air
|
||||
/.go-licenses
|
||||
/Dockerfile
|
||||
|
||||
@@ -18,10 +18,13 @@ indent_style = tab
|
||||
[templates/custom/*.tmpl]
|
||||
insert_final_newline = false
|
||||
|
||||
[templates/swagger/*.generated.json]
|
||||
[templates/swagger/*_json.tmpl]
|
||||
indent_style = space
|
||||
insert_final_newline = false
|
||||
|
||||
[templates/user/auth/oidc_wellknown.tmpl]
|
||||
indent_style = space
|
||||
|
||||
[templates/shared/actions/runner_badge_*.tmpl]
|
||||
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
|
||||
charset = unset
|
||||
|
||||
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -2,12 +2,11 @@
|
||||
*.tmpl linguist-language=Handlebars
|
||||
*.pb.go linguist-generated
|
||||
/assets/*.json linguist-generated
|
||||
/public/assets/emoji.json linguist-generated
|
||||
/public/assets/img/svg/*.svg linguist-generated
|
||||
/templates/swagger/*.generated.json linguist-generated
|
||||
/templates/swagger/v1_json.tmpl linguist-generated
|
||||
/templates/swagger/v1_openapi3_json.tmpl linguist-generated
|
||||
/options/fileicon/** linguist-generated
|
||||
/services/migrations/_mock_data/** linguist-generated
|
||||
/tests/integration/_mock_data/** linguist-generated
|
||||
/vendor/** -text -eol linguist-vendored
|
||||
/web_src/js/vendor/** -text -eol linguist-vendored
|
||||
Dockerfile.* linguist-language=Dockerfile
|
||||
Makefile.* linguist-language=Makefile
|
||||
|
||||
59
.github/ISSUE_TEMPLATE/release.yaml
vendored
59
.github/ISSUE_TEMPLATE/release.yaml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Release
|
||||
description: Track a Gitea release (for release managers).
|
||||
title: "Release Gitea "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Follow the [release management guide](https://github.com/go-gitea/gitea/blob/main/docs/release-management.md).
|
||||
Replace the examples below and mark inapplicable tasks as such.
|
||||
CI signs the tag, generates release notes, and publishes binaries and containers. Track verification here; no manual changelog PR or release upload is needed.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
placeholder: "28.0.1"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: branch
|
||||
attributes:
|
||||
label: Release branch
|
||||
placeholder: "release/v28"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Release checklist
|
||||
description: Keep workflow runs, release URLs, and follow-up PRs alongside the relevant tasks.
|
||||
value: |
|
||||
### Preparation
|
||||
|
||||
- [ ] Resolve release blockers and confirm milestone issues and PRs are resolved or deferred.
|
||||
- [ ] Confirm required backports are merged and release branch CI passes.
|
||||
- [ ] For a new release line, create the release branch and tag its fork point on main with the next version's -dev tag.
|
||||
|
||||
### Release
|
||||
|
||||
- [ ] Run https://github.com/go-gitea/gitea/actions/workflows/release-create-tag.yml on the release branch with the selected version and obtain maintainer approval.
|
||||
- [ ] Confirm https://github.com/go-gitea/gitea/actions/workflows/release-tag-version.yml succeeds for the new tag (binaries and containers).
|
||||
- [ ] Verify the public GitHub release, generated notes, binary attachments, and signatures at https://github.com/go-gitea/gitea/releases.
|
||||
- [ ] Verify binaries and signatures at https://dl.gitea.com/gitea/ for this version.
|
||||
- [ ] Verify versioned regular and rootless images on Docker Hub and GHCR, and smoke-test the release.
|
||||
|
||||
### Follow-up
|
||||
|
||||
- [ ] Verify the automated https://dl.gitea.com/gitea/version.json update, where applicable to this release line.
|
||||
- [ ] Verify automated Helm chart and Terraform provider update PRs and follow up if needed.
|
||||
- [ ] Check Homebrew and Snap availability; record any outstanding packaging follow-up.
|
||||
- [ ] Confirm documentation reflects the release, where applicable.
|
||||
- [ ] Confirm and merge the release blog post, if planned: https://gitea.com/gitea/blog.
|
||||
- [ ] Announce the release in Discord #announcements.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Blockers and notes
|
||||
description: Link outstanding work or release-specific checks using full URLs. Do not include undisclosed security details.
|
||||
29
.github/actions/docker-dryrun/action.yml
vendored
Normal file
29
.github/actions/docker-dryrun/action.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: docker-dryrun
|
||||
description: Composite action that performs the container build steps for a single platform.
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: "The target platform: linux/amd64, linux/arm64, linux/riscv64."
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- name: Build regular image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: false
|
||||
file: Dockerfile
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: false
|
||||
file: Dockerfile.rootless
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
5
.github/actions/free-disk-space/action.yml
vendored
5
.github/actions/free-disk-space/action.yml
vendored
@@ -7,11 +7,6 @@ runs:
|
||||
steps:
|
||||
- shell: bash
|
||||
run: |
|
||||
avail=$(df --output=avail -BG / | tr -dc '0-9')
|
||||
if [ "$avail" -ge 50 ]; then
|
||||
echo "skipping cleanup, ${avail}G free"
|
||||
exit 0
|
||||
fi
|
||||
echo "free space before cleanup:"
|
||||
df -h /
|
||||
for dir in /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet; do
|
||||
|
||||
40
.github/actions/go-cache/action.yml
vendored
40
.github/actions/go-cache/action.yml
vendored
@@ -2,18 +2,12 @@ name: go-caches
|
||||
description: Restore the go module, build, and golangci-lint caches. Save only on the cache-seeder workflow.
|
||||
|
||||
# Only the cache-seeder workflow saves; rename requires updating cache-seeder.yml.
|
||||
# The lint job restores but does not save the gobuild and gomod caches, so every key
|
||||
# has a single writer and there is no contention on it.
|
||||
# The lint job restores but does not save the gobuild cache, so only one writer
|
||||
# (the gobuild job) populates it and there is no contention on the cache key.
|
||||
# Seeder restores by exact key only (no restore-keys) so each go.sum seeds a clean
|
||||
# cache and size stays bounded; do not add restore-keys here. PR runs keep them.
|
||||
# The go version is part of the gobuild and golint keys because a toolchain bump
|
||||
# invalidates every entry in them, so a cross-version fallback would restore
|
||||
# gigabytes that no build can use. The module cache is version-independent.
|
||||
|
||||
inputs:
|
||||
go-version:
|
||||
description: Go version the build caches belong to
|
||||
required: true
|
||||
lint-cache:
|
||||
description: Restore (and save in cache-seeder) ~/.cache/golangci-lint
|
||||
default: "false"
|
||||
@@ -21,36 +15,36 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- if: ${{ github.workflow == 'cache-seeder' }}
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- if: ${{ github.workflow != 'cache-seeder' }}
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}-
|
||||
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
|
||||
- if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }}
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: gobuild-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum') }}
|
||||
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: gobuild-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum') }}
|
||||
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-
|
||||
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
|
||||
- if: ${{ inputs.lint-cache == 'true' && github.workflow == 'cache-seeder' }}
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/golangci-lint
|
||||
key: golint-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/golangci-lint
|
||||
key: golint-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}-go${{ inputs.go-version }}-
|
||||
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
4
.github/actions/go-setup/action.yml
vendored
4
.github/actions/go-setup/action.yml
vendored
@@ -13,8 +13,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: ./.github/actions/free-disk-space
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
id: setup-go
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -22,5 +21,4 @@ runs:
|
||||
- if: ${{ inputs.cache == 'true' }}
|
||||
uses: ./.github/actions/go-cache
|
||||
with:
|
||||
go-version: ${{ steps.setup-go.outputs.go-version }}
|
||||
lint-cache: ${{ inputs.lint-cache }}
|
||||
|
||||
28
.github/actions/node-setup/action.yml
vendored
28
.github/actions/node-setup/action.yml
vendored
@@ -1,30 +1,22 @@
|
||||
name: node-setup
|
||||
description: Set up pnpm and node and restore the pnpm store cache
|
||||
description: Set up pnpm and node and restore caches
|
||||
|
||||
inputs:
|
||||
cache:
|
||||
description: Restore the pnpm store cache
|
||||
description: Cache pnpm downloads
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
|
||||
with:
|
||||
node-version: 26
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- if: ${{ inputs.cache == 'true' }}
|
||||
id: store
|
||||
shell: bash
|
||||
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
- if: ${{ inputs.cache == 'true' && github.workflow == 'cache-seeder' }}
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
path: ${{ steps.store.outputs.path }}
|
||||
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- if: ${{ inputs.cache == 'true' && github.workflow != 'cache-seeder' }}
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- if: ${{ inputs.cache != 'true' }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
path: ${{ steps.store.outputs.path }}
|
||||
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-${{ runner.arch }}-
|
||||
node-version: 24
|
||||
|
||||
2
.github/actions/pgsql-shard/action.yml
vendored
2
.github/actions/pgsql-shard/action.yml
vendored
@@ -34,7 +34,7 @@ runs:
|
||||
env:
|
||||
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
|
||||
GOTEST_FLAGS: -race -timeout=40m
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
TEST_LDAP: 1
|
||||
TEST_SHARD: ${{ inputs.shard }}
|
||||
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
|
||||
|
||||
10
.github/actions/python-setup/action.yml
vendored
10
.github/actions/python-setup/action.yml
vendored
@@ -1,10 +0,0 @@
|
||||
name: python-setup
|
||||
description: Set up uv and python and restore the uv cache
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
||||
with:
|
||||
python-version: 3.15 # renovate: datasource=python-version depName=python versioning=python
|
||||
save-cache: ${{ github.workflow == 'cache-seeder' }}
|
||||
48
.github/workflows/cache-prune.yml
vendored
48
.github/workflows/cache-prune.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: cache-prune
|
||||
|
||||
# Keeps the repository's total cache size below a fixed limit, so that GitHub never
|
||||
# rejects a save for being over the allowance.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "37 */6 * * *" # every six hours at :37
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: cache-prune
|
||||
|
||||
jobs:
|
||||
prune:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
permissions:
|
||||
actions: write # to delete caches
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
# Keep the newest generation per key, restores never reach the older ones.
|
||||
- name: delete superseded caches
|
||||
run: |
|
||||
gh cache list --limit 1000 --json id,key,ref,createdAt |
|
||||
jq -r 'group_by([.ref, (.key | sub("(-go[0-9.]+)?-[0-9a-f]{40,64}(-[0-9]+-[0-9]+)?$"; ""))])[]
|
||||
| sort_by(.createdAt)[:-1][] | "\(.id) \(.key)"' |
|
||||
while read -r id key; do
|
||||
echo "deleting $key"
|
||||
gh cache delete "$id" || true
|
||||
done
|
||||
|
||||
# Deletes least recently used first, the order GitHub itself evicts in.
|
||||
- name: delete caches over the size limit
|
||||
run: |
|
||||
caches=$(gh cache list --limit 1000 --sort last_accessed_at --order asc --json id,key,sizeInBytes)
|
||||
size=$(jq '[.[].sizeInBytes] | add // 0' <<< "$caches")
|
||||
echo "cache usage: $((size / 1000000)) MB"
|
||||
while [ "$size" -gt 6000000000 ] && read -r id bytes key; do
|
||||
echo "deleting $key"
|
||||
gh cache delete "$id" || true
|
||||
size=$((size - bytes))
|
||||
done <<< "$(jq -r '.[] | "\(.id) \(.sizeInBytes) \(.key)"' <<< "$caches")"
|
||||
54
.github/workflows/cache-seeder.yml
vendored
54
.github/workflows/cache-seeder.yml
vendored
@@ -1,25 +1,21 @@
|
||||
# Populates main's cache scope so PR runs warm-start from it. Caches are ref-scoped:
|
||||
# PR runs read their own scope then fall back to the base branch, and only this
|
||||
# workflow saves, so push-to-main is the only chance to populate the fallback scope.
|
||||
# Populates main's cache scope so PR runs warm-start from it. Saves the go
|
||||
# module, go build (incl. test compile), and golangci-lint caches.
|
||||
#
|
||||
# Caches are ref-scoped: PR runs read their own scope then fall back to the
|
||||
# base branch. Per .github/actions/go-cache/action.yml, PRs are restore-only,
|
||||
# so push-to-main is the only opportunity to populate the fallback scope.
|
||||
|
||||
name: cache-seeder
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "go.mod" # a toolchain bump invalidates the build caches
|
||||
- "go.sum"
|
||||
- ".golangci.yml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- ".github/actions/go-cache/action.yml"
|
||||
- ".github/actions/go-setup/action.yml"
|
||||
- ".github/actions/node-setup/action.yml"
|
||||
- ".github/actions/python-setup/action.yml"
|
||||
- ".github/workflows/cache-seeder.yml"
|
||||
|
||||
concurrency:
|
||||
@@ -33,19 +29,27 @@ jobs:
|
||||
gobuild:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend deps-tools
|
||||
- run: TAGS="bindata" make backend
|
||||
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
|
||||
- name: warm test compile cache (bindata)
|
||||
env:
|
||||
TAGS: bindata
|
||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||
run: make test-backend
|
||||
- name: warm test compile cache (bindata gogit)
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||
run: make test-backend
|
||||
- name: warm integration compile cache
|
||||
run: |
|
||||
TAGS="bindata" make test-integration-compile
|
||||
TAGS="bindata" GOTEST_FLAGS="-race" make test-integration-compile
|
||||
TAGS="bindata gogit" GOEXPERIMENT="" make test-integration-compile
|
||||
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -55,7 +59,7 @@ jobs:
|
||||
include:
|
||||
- { tags: "bindata", target: "lint-backend" }
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
with:
|
||||
lint-cache: "true"
|
||||
@@ -63,28 +67,6 @@ jobs:
|
||||
- run: make generate-go
|
||||
env:
|
||||
TAGS: ${{ matrix.tags }}
|
||||
- run: make "$TARGET"
|
||||
- run: make ${{ matrix.target }}
|
||||
env:
|
||||
TAGS: ${{ matrix.tags }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/node-setup
|
||||
- run: make deps-frontend
|
||||
|
||||
python:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/python-setup
|
||||
- run: make deps-py
|
||||
|
||||
# reclaims the caches this run superseded, so the next save still fits in the allowance
|
||||
prune:
|
||||
needs: [gobuild, lint, frontend, python]
|
||||
permissions:
|
||||
actions: write
|
||||
uses: ./.github/workflows/cache-prune.yml
|
||||
|
||||
7
.github/workflows/cron-licenses.yml
vendored
7
.github/workflows/cron-licenses.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
# - cron: "7 0 * * 1" # every Monday at 00:07 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
cron-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -14,12 +12,11 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- run: make generate-gitignore
|
||||
timeout-minutes: 40
|
||||
- name: push translations to repo
|
||||
|
||||
9
.github/workflows/cron-renovate.yml
vendored
9
.github/workflows/cron-renovate.yml
vendored
@@ -9,7 +9,7 @@ concurrency:
|
||||
group: cron-renovate
|
||||
|
||||
env:
|
||||
RENOVATE_VERSION: 44.210.0 # renovate: datasource=npm depName=renovate
|
||||
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -20,14 +20,13 @@ jobs:
|
||||
if: github.repository == 'go-gitea/gitea' # prevent running on forks
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: renovatebot/github-action@1c0cf989c02653fba0efe7ca5772338860ca71dc # v46.3.12
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16
|
||||
with:
|
||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||
configurationFile: renovate.json5
|
||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||
env:
|
||||
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
|
||||
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg|generate-codemirror-languages)$"]'
|
||||
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]'
|
||||
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
|
||||
RENOVATE_DOCKER_MAX_PAGES: 10 # https://github.com/renovatebot/renovate/issues/46925
|
||||
|
||||
6
.github/workflows/cron-translations.yml
vendored
6
.github/workflows/cron-translations.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
- cron: "7 0 * * *" # every day at 00:07 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
crowdin-pull:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -14,8 +12,8 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: crowdin/github-action@5b9790df740c85af5412a3a3a91b24988d5d324f # v3.5.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
25
.github/workflows/files-changed.yml
vendored
25
.github/workflows/files-changed.yml
vendored
@@ -15,6 +15,8 @@ on:
|
||||
value: ${{ jobs.detect.outputs.templates }}
|
||||
docker:
|
||||
value: ${{ jobs.detect.outputs.docker }}
|
||||
dockerfile:
|
||||
value: ${{ jobs.detect.outputs.dockerfile }}
|
||||
swagger:
|
||||
value: ${{ jobs.detect.outputs.swagger }}
|
||||
yaml:
|
||||
@@ -40,21 +42,22 @@ jobs:
|
||||
actions: ${{ steps.changes.outputs.actions }}
|
||||
templates: ${{ steps.changes.outputs.templates }}
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
dockerfile: ${{ steps.changes.outputs.dockerfile }}
|
||||
swagger: ${{ steps.changes.outputs.swagger }}
|
||||
yaml: ${{ steps.changes.outputs.yaml }}
|
||||
json: ${{ steps.changes.outputs.json }}
|
||||
e2e: ${{ steps.changes.outputs.e2e }}
|
||||
shell: ${{ steps.changes.outputs.shell }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- "**/*.go"
|
||||
- "templates/**/*.tmpl"
|
||||
- "public/assets/emoji.json"
|
||||
- "assets/emoji.json"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- "Makefile"
|
||||
@@ -70,11 +73,10 @@ jobs:
|
||||
frontend:
|
||||
- "*.ts"
|
||||
- "web_src/**"
|
||||
- "tools/**/*.ts"
|
||||
- "tools/**/*.json"
|
||||
- "tools/playwright.sh"
|
||||
- "tools/generate-svg.ts"
|
||||
- "tools/generate-svg-vscode-extensions.json"
|
||||
- "tsconfig.json"
|
||||
- "public/assets/emoji.json"
|
||||
- "assets/emoji.json"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
@@ -100,13 +102,19 @@ jobs:
|
||||
|
||||
docker:
|
||||
- ".github/workflows/pull-docker-dryrun.yml"
|
||||
- ".github/actions/docker-dryrun/**"
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
- "docker/**"
|
||||
- "Makefile"
|
||||
|
||||
dockerfile:
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
|
||||
swagger:
|
||||
- "templates/swagger/*.json"
|
||||
- "templates/swagger/v1_json.tmpl"
|
||||
- "templates/swagger/v1_input.json"
|
||||
- "Makefile"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
@@ -127,7 +135,6 @@ jobs:
|
||||
e2e:
|
||||
- "tests/e2e/**"
|
||||
- "tools/test-e2e.sh"
|
||||
- "tools/playwright.sh"
|
||||
- "playwright.config.ts"
|
||||
|
||||
shell:
|
||||
|
||||
7
.github/workflows/giteabot-backport.yml
vendored
7
.github/workflows/giteabot-backport.yml
vendored
@@ -4,14 +4,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/v*
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
@@ -20,8 +19,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
|
||||
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
gitea_fork: giteabot/gitea
|
||||
checks: backport,backport_conflict_free
|
||||
checks: backport
|
||||
|
||||
19
.github/workflows/giteabot-review.yml
vendored
19
.github/workflows/giteabot-review.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: giteabot-review
|
||||
|
||||
# Relays PR reviews to giteabot.yml through its workflow_run trigger, because review
|
||||
# runs on fork PRs get no secrets and a read-only token. The job itself does nothing.
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
- edited
|
||||
- dismissed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
relay:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
48
.github/workflows/giteabot.yml
vendored
48
.github/workflows/giteabot.yml
vendored
@@ -1,11 +1,16 @@
|
||||
name: giteabot
|
||||
|
||||
on:
|
||||
# When main advances, rerun merge queue maintenance so the oldest
|
||||
# reviewed/wait-merge PR can be updated against the new base promptly.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# pull_request_target gives this workflow access to GITEABOT_TOKEN on PRs from
|
||||
# forks, which the bot needs to write labels, statuses and comments. Safe here
|
||||
# because the job only runs a pinned action and never checks out PR HEAD.
|
||||
# These PR lifecycle events drive label maintenance, lgtm, and explicit bot
|
||||
# actions triggered by relevant label changes.
|
||||
# These PR lifecycle events drive label maintenance, queue maintenance, and
|
||||
# explicit bot actions triggered by relevant label changes.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types:
|
||||
- opened
|
||||
@@ -15,13 +20,15 @@ on:
|
||||
- closed
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
# Reviews arrive through giteabot-review because fork PR review runs get no secrets
|
||||
workflow_run:
|
||||
workflows:
|
||||
- giteabot-review
|
||||
# Review events keep review-derived state such as lgtm labels and status checks
|
||||
# in sync after approvals, edits, or dismissals.
|
||||
pull_request_review:
|
||||
types:
|
||||
- requested
|
||||
# Daily housekeeping like locks, stale feedback issues, last calls and milestones
|
||||
- submitted
|
||||
- edited
|
||||
- dismissed
|
||||
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
||||
# other housekeeping, even though main pushes now trigger it promptly.
|
||||
schedule:
|
||||
- cron: "15 3 * * *"
|
||||
# Allow maintainers to rerun selected checks manually when debugging bot
|
||||
@@ -31,26 +38,27 @@ on:
|
||||
checks:
|
||||
description: Comma-separated list of non-backport checks to run
|
||||
required: false
|
||||
default: backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
||||
default: labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ format('{0}-{1}', github.workflow, github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && format('review-{0}', github.event.workflow_run.head_sha) || 'maintenance') }}
|
||||
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
giteabot:
|
||||
if: github.repository == 'go-gitea/gitea' && (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request_review')
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@15b18fdedd98a80d0968d0bd9db3a9428814b808 # v1.5.1
|
||||
# pull_request_review runs without repository secrets on fork PRs, so fall
|
||||
# back to the workflow token for the non-backport checks handled here.
|
||||
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
checks: ${{ github.event.inputs.checks || 'backport_branches,backport_conflict_free,labels,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
|
||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
|
||||
16
.github/workflows/merge-group.yml
vendored
16
.github/workflows/merge-group.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: merge-group
|
||||
|
||||
# Reports the required giteabot/lgtm check on merge queue commits. giteabot only sets it
|
||||
# on PR heads, which already passed it before entering the queue.
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
lgtm:
|
||||
name: giteabot/lgtm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
34
.github/workflows/pull-compliance.yml
vendored
34
.github/workflows/pull-compliance.yml
vendored
@@ -2,7 +2,6 @@ name: compliance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@@ -20,19 +19,19 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
with:
|
||||
lint-cache: "true"
|
||||
- run: make deps-backend deps-tools
|
||||
- run: TAGS="bindata" make generate-go lint-backend # lint-go can lint with "bindata" tags
|
||||
- run: TAGS="bindata" make generate-go # lint-go also lints with "bindata" tags which requires "_bindata.go"
|
||||
- run: make lint-backend
|
||||
|
||||
lint-on-demand:
|
||||
if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true' || needs.files-changed.outputs.shell == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
with:
|
||||
cache: "false"
|
||||
@@ -40,8 +39,12 @@ jobs:
|
||||
with:
|
||||
cache: "false"
|
||||
|
||||
- run: make lint-spell
|
||||
|
||||
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
uses: ./.github/actions/python-setup
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
python-version: 3.14
|
||||
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
|
||||
run: make deps-py lint-templates lint-yaml
|
||||
|
||||
@@ -59,7 +62,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
||||
@@ -69,12 +72,11 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/node-setup
|
||||
- run: make deps-frontend
|
||||
- run: make lint-frontend
|
||||
- run: make checks-frontend
|
||||
- run: make playwright
|
||||
- run: make test-frontend
|
||||
- run: make frontend
|
||||
|
||||
@@ -83,27 +85,25 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend generate-go
|
||||
# no frontend build here as backend should be able to build, even without any frontend files
|
||||
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
|
||||
- name: build-backend-arm64
|
||||
run: go build -ldflags '-s -w' -o gitea-linux-arm64
|
||||
run: go build -o gitea_linux_arm64
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-windows
|
||||
run: go build -ldflags '-s -w' -o gitea-windows
|
||||
run: go build -o gitea_windows
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-386
|
||||
run: go build -ldflags '-s -w' -o gitea-linux-386
|
||||
run: go build -o gitea_linux_386
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: 386
|
||||
- name: binary-sizes
|
||||
run: ls -l gitea-*
|
||||
|
||||
60
.github/workflows/pull-db-tests.yml
vendored
60
.github/workflows/pull-db-tests.yml
vendored
@@ -2,7 +2,6 @@ name: db-tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@@ -22,12 +21,10 @@ jobs:
|
||||
timeout-minutes: 50
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:14@sha256:156f0b253fd61366d5fc2107ad45955027d5612f695a8436ce20167f3fa79bff
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
ldap:
|
||||
@@ -38,14 +35,14 @@ jobs:
|
||||
minio:
|
||||
# as github actions doesn't support "entrypoint", we need to use a non-official image
|
||||
# that has a custom entrypoint set to "minio server /data"
|
||||
image: bitnamilegacy/minio:2025.7.23@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7
|
||||
image: bitnamilegacy/minio:2025.7.23
|
||||
env:
|
||||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
ports:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- uses: ./.github/actions/pgsql-shard
|
||||
with:
|
||||
@@ -60,12 +57,10 @@ jobs:
|
||||
timeout-minutes: 50
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:14@sha256:156f0b253fd61366d5fc2107ad45955027d5612f695a8436ce20167f3fa79bff
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
ldap:
|
||||
@@ -76,14 +71,14 @@ jobs:
|
||||
minio:
|
||||
# as github actions doesn't support "entrypoint", we need to use a non-official image
|
||||
# that has a custom entrypoint set to "minio server /data"
|
||||
image: bitnamilegacy/minio:2025.7.23@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7
|
||||
image: bitnamilegacy/minio:2025.7.23
|
||||
env:
|
||||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
ports:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- uses: ./.github/actions/pgsql-shard
|
||||
with:
|
||||
@@ -95,22 +90,24 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
- run: GITEA_TEST_DATABASE=sqlite make test-migration
|
||||
env:
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
||||
GOTEST_FLAGS: -timeout=40m
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
|
||||
test-unit:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
@@ -118,7 +115,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
elasticsearch:
|
||||
image: elasticsearch:8.19.23@sha256:ef5cafd961d7e273f13fed8def82e63c5ae0491106b5eecb5a69aa12d64f089b
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
@@ -128,13 +125,13 @@ jobs:
|
||||
ports:
|
||||
- "9200:9200"
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1@sha256:88f2a08eb9532cc54a60d8fd014a23bf5cfda33d0c01ecbbaca9b4b463e17466
|
||||
image: getmeili/meilisearch:v1
|
||||
env:
|
||||
MEILI_ENV: development # disable auth
|
||||
ports:
|
||||
- "7700:7700"
|
||||
redis:
|
||||
image: redis:8@sha256:298e5b3bc566bade82f46ad5511777a4a07a294097ce16ada2f6a42be5239df5
|
||||
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
|
||||
options: >- # wait until redis has started
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 5s
|
||||
@@ -143,18 +140,18 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
minio:
|
||||
image: bitnamilegacy/minio:2025.7.23@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7
|
||||
image: bitnamilegacy/minio:2025.7.23
|
||||
env:
|
||||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
ports:
|
||||
- "9000:9000"
|
||||
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:f9791d0a0613e93057c06ae31f7ac0c0222f0530590e67e020b0986c66a36fa6
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
|
||||
@@ -168,6 +165,14 @@ jobs:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
- name: unit-tests-gogit
|
||||
run: make test-backend
|
||||
env:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
GITEA_TEST_CI_SKIP_EXTERNAL: true
|
||||
- run: make test-check
|
||||
|
||||
test-mysql:
|
||||
@@ -177,7 +182,7 @@ jobs:
|
||||
services:
|
||||
mysql:
|
||||
# the bitnami mysql image has more options than the official one, it's easier to customize
|
||||
image: bitnamilegacy/mysql:8.4@sha256:7089d796fc9b4629a628bd445e4afabe607351ee665444c3197bdeaed812ea65
|
||||
image: bitnamilegacy/mysql:8.4
|
||||
env:
|
||||
ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: testgitea
|
||||
@@ -186,7 +191,7 @@ jobs:
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/bitnami/mysql/data
|
||||
elasticsearch:
|
||||
image: elasticsearch:8.19.23@sha256:ef5cafd961d7e273f13fed8def82e63c5ae0491106b5eecb5a69aa12d64f089b
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
@@ -203,7 +208,7 @@ jobs:
|
||||
- "587:587"
|
||||
- "993:993"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
|
||||
@@ -216,6 +221,7 @@ jobs:
|
||||
run: GITEA_TEST_DATABASE=mysql make test-integration
|
||||
env:
|
||||
TAGS: bindata
|
||||
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||
|
||||
test-mssql:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
@@ -223,7 +229,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest@sha256:07d7dedecdbfcf6776035dc5eefb5bfd9aa1804b2f6b99f7dedb397d8a599069
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
env:
|
||||
ACCEPT_EULA: Y
|
||||
MSSQL_PID: Standard
|
||||
@@ -231,11 +237,11 @@ jobs:
|
||||
ports:
|
||||
- "1433:1433"
|
||||
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:f9791d0a0613e93057c06ae31f7ac0c0222f0530590e67e020b0986c66a36fa6
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
|
||||
|
||||
40
.github/workflows/pull-docker-dryrun.yml
vendored
40
.github/workflows/pull-docker-dryrun.yml
vendored
@@ -14,26 +14,34 @@ jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
container:
|
||||
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
|
||||
# Run amd64 when any docker-related files change, which is fast (4 minutes).
|
||||
container-amd64:
|
||||
if: needs.files-changed.outputs.docker == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- name: Build regular image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
platform: linux/amd64
|
||||
|
||||
container-arm64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
platform: linux/arm64
|
||||
|
||||
container-riscv64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
file: Dockerfile.rootless
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
platform: linux/riscv64
|
||||
|
||||
3
.github/workflows/pull-e2e-tests.yml
vendored
3
.github/workflows/pull-e2e-tests.yml
vendored
@@ -2,7 +2,6 @@ name: e2e-tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@@ -20,7 +19,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: ./.github/actions/go-setup
|
||||
- uses: ./.github/actions/node-setup
|
||||
- run: make deps-frontend
|
||||
|
||||
11
.github/workflows/pull-labeler.yml
vendored
11
.github/workflows/pull-labeler.yml
vendored
@@ -10,8 +10,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,7 +17,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
@@ -32,12 +30,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
# Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
node-version: 24
|
||||
# Labels are only synced after the title lints, so an invalid title never reaches the label diff.
|
||||
- run: node ./tools/ci-tools.ts lint-pr-title
|
||||
env:
|
||||
@@ -45,6 +43,5 @@ jobs:
|
||||
- run: node ./tools/ci-tools.ts set-pr-labels
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_TITLE_BEFORE: ${{ github.event.changes.title.from }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
34
.github/workflows/release-create-tag.yml
vendored
34
.github/workflows/release-create-tag.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: release-create-tag
|
||||
run-name: Release v${{ inputs.version }} from ${{ github.ref_name }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to release, for example 28.0.1
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release-signing
|
||||
deployment: false
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
- uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_committer_email: teabot@gitea.io
|
||||
- env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
[[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
git tag -s -m "v$VERSION" "v$VERSION"
|
||||
git push origin tag "v$VERSION"
|
||||
45
.github/workflows/release-nightly-snapcraft.yml
vendored
Normal file
45
.github/workflows/release-nightly-snapcraft.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: release-nightly-snapcraft
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- name: Install snapcraft
|
||||
run: sudo snap install snapcraft --classic
|
||||
|
||||
- name: Authenticate snapcraft
|
||||
shell: bash
|
||||
run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS")
|
||||
|
||||
- name: Remote build
|
||||
run: |
|
||||
snapcraft remote-build \
|
||||
--launchpad-accept-public-upload \
|
||||
--build-for=amd64,arm64,armhf
|
||||
|
||||
- name: List built snaps
|
||||
run: find . -maxdepth 1 -type f -name '*.snap' -print
|
||||
|
||||
- name: Upload and release snapcraft nightly build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
for snap in ./*.snap; do
|
||||
echo "Uploading $snap to edge"
|
||||
snapcraft upload --release="latest/edge" "$snap"
|
||||
done
|
||||
146
.github/workflows/release-nightly.yml
vendored
146
.github/workflows/release-nightly.yml
vendored
@@ -1,66 +1,39 @@
|
||||
name: release-nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 */12 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, release/v*]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
branches:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
outputs:
|
||||
branches: ${{ steps.changed.outputs.branches }}
|
||||
steps:
|
||||
- id: changed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
since=$(gh run list --workflow release-nightly.yml --status success --limit 1 --json createdAt -q '.[0].createdAt')
|
||||
branches=$(gh api --paginate "repos/$GH_REPO/branches" -q '.[].name | select(. == "main" or startswith("release/v"))' |
|
||||
while read -r branch; do
|
||||
if [ -z "$since" ] || [ -n "$(gh api "repos/$GH_REPO/commits?sha=$branch&since=$since&per_page=1" -q '.[].sha')" ]; then
|
||||
echo "$branch"
|
||||
fi
|
||||
done | jq -Rnc '[inputs]')
|
||||
echo "branches=$branches" >> "$GITHUB_OUTPUT"
|
||||
|
||||
nightly-binary:
|
||||
needs: branches
|
||||
if: needs.branches.outputs.branches != '[]'
|
||||
strategy:
|
||||
matrix:
|
||||
branch: ${{ fromJSON(needs.branches.outputs.branches) }}
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/node-setup
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend deps-backend
|
||||
- run: make release GITHUB_REF_NAME="$BRANCH"
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
BRANCH: ${{ matrix.branch }}
|
||||
TAGS: bindata
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
- name: import gpg key
|
||||
@@ -78,20 +51,34 @@ jobs:
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in the object storage
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
REF: ${{ matrix.branch }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
|
||||
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
@@ -99,37 +86,27 @@ jobs:
|
||||
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
|
||||
|
||||
nightly-container:
|
||||
needs: branches
|
||||
if: needs.branches.outputs.branches != '[]'
|
||||
strategy:
|
||||
matrix:
|
||||
branch: ${{ fromJSON(needs.branches.outputs.branches) }}
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
REF: ${{ matrix.branch }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
id: meta
|
||||
with:
|
||||
context: git
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
@@ -137,10 +114,9 @@ jobs:
|
||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
context: git
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
@@ -152,18 +128,18 @@ jobs:
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular docker image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
@@ -173,7 +149,7 @@ jobs:
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
@@ -183,47 +159,3 @@ jobs:
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless,mode=max
|
||||
|
||||
nightly-snap:
|
||||
needs: branches
|
||||
if: contains(fromJSON(needs.branches.outputs.branches), 'main')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-24.04, ubuntu-24.04-arm]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Build snap
|
||||
id: build
|
||||
env:
|
||||
SNAPCRAFT_IMAGE_INFO: '{"build_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
|
||||
run: |
|
||||
# snapcraft installs build-snaps and apt packages system-wide, which needs root
|
||||
sudo snap install --classic snapcraft
|
||||
sudo SNAPCRAFT_BUILD_INFO=1 SNAPCRAFT_IMAGE_INFO="$SNAPCRAFT_IMAGE_INFO" snapcraft pack --destructive-mode
|
||||
echo "snap=$(ls ./*.snap)" >> "$GITHUB_OUTPUT"
|
||||
# retry snapcraft uploads which can be flaky
|
||||
- name: Publish snap
|
||||
run: |
|
||||
# part-gitea-pull.sh decides what is built, see the comments there:
|
||||
# * grade devel (main or a prerelease tag) goes to latest/edge
|
||||
# * grade stable (a new release tag) goes to latest/stable, and to latest/candidate which marks it as released
|
||||
channel=latest/edge
|
||||
if unsquashfs -cat "$SNAP" meta/snap.yaml | grep -qx 'grade: stable'; then
|
||||
channel=latest/stable,latest/candidate
|
||||
fi
|
||||
for attempt in 1 2 3 4 5; do
|
||||
snapcraft upload "$SNAP" --release "$channel" && exit 0
|
||||
echo "::warning::snap upload attempt $attempt failed, retrying in 15s"
|
||||
sleep 15
|
||||
done
|
||||
exit 1
|
||||
env:
|
||||
SNAP: ${{ steps.build.outputs.snap }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||
|
||||
166
.github/workflows/release-tag-rc.yml
vendored
Normal file
166
.github/workflows/release-tag-rc.yml
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
name: release-tag-rc
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v1*-rc*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
- name: sign binaries
|
||||
env:
|
||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
run: |
|
||||
for f in dist/release/*; do
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
|
||||
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$TAG" --title "$TAG" --draft --notes-from-tag dist/release/*
|
||||
|
||||
container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
75
.github/workflows/release-tag-version.yml
vendored
75
.github/workflows/release-tag-version.yml
vendored
@@ -3,15 +3,14 @@ name: release-tag-version
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]*"
|
||||
- "!v[0-9]*-*"
|
||||
- "v1.*"
|
||||
- "!v1*-rc*"
|
||||
- "!v1*-dev"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
@@ -20,18 +19,25 @@ jobs:
|
||||
packages: write # to publish to ghcr.io
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/node-setup
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
- name: import gpg key
|
||||
@@ -49,7 +55,7 @@ jobs:
|
||||
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
|
||||
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in the object storage
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
env:
|
||||
@@ -58,35 +64,40 @@ jobs:
|
||||
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress
|
||||
# configure-aws-credentials exports AWS_REGION job-wide and it wins over AWS_DEFAULT_REGION, so pin it here
|
||||
- name: upload binaries to cloudflare r2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_REGION: auto
|
||||
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}
|
||||
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
|
||||
BRANCH: ${{ steps.clean_name.outputs.branch }}
|
||||
run: |
|
||||
aws s3 sync dist/release "s3://$CLOUDFLARE_R2_BUCKET/gitea/$BRANCH" --endpoint-url "https://$CLOUDFLARE_R2_ACCOUNT_ID.r2.cloudflarestorage.com" --no-progress
|
||||
- id: range
|
||||
run: |
|
||||
previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one
|
||||
echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT"
|
||||
- name: skip reverts and reverted commits in changelog
|
||||
env:
|
||||
RANGE: ${{ steps.range.outputs.range }}
|
||||
run: ./tools/generate-cliffignore.sh "$RANGE" | sort -u > .cliffignore
|
||||
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||
with:
|
||||
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
env:
|
||||
GH_CLI_VERSION: 2.103.3 # renovate: datasource=github-releases depName=cli/cli extractVersion=^v(?<version>.+)$
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.tar.gz" | tar xz --strip-components=2 "gh_${GH_CLI_VERSION}_linux_amd64/bin/gh"
|
||||
./gh release create "$TAG" --title "$TAG" --notes-file git-cliff/CHANGELOG.md dist/release/*
|
||||
gh release create "$TAG" --title "$TAG" --notes-from-tag dist/release/*
|
||||
|
||||
container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
@@ -94,15 +105,13 @@ jobs:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
@@ -119,7 +128,7 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
@@ -140,18 +149,18 @@ jobs:
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
@@ -159,7 +168,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
|
||||
9
.github/zizmor.yml
vendored
9
.github/zizmor.yml
vendored
@@ -1,9 +0,0 @@
|
||||
rules:
|
||||
anonymous-definition:
|
||||
disable: true
|
||||
undocumented-permissions:
|
||||
disable: true
|
||||
concurrency-limits:
|
||||
disable: true
|
||||
self-repository:
|
||||
disable: true # actionlint rejects "$/" uses, https://github.com/rhysd/actionlint/issues/711
|
||||
@@ -10,9 +10,7 @@ linters:
|
||||
- depguard
|
||||
- dupl
|
||||
- errcheck
|
||||
- exhaustruct_v5
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- gocheckcompilerdirectives
|
||||
- gocritic
|
||||
- govet
|
||||
@@ -53,23 +51,14 @@ linters:
|
||||
desc: do not use the go-chi cache package, use gitea's cache system
|
||||
- pkg: github.com/pkg/errors
|
||||
desc: use builtin errors package instead
|
||||
- pkg: gitea.com/go-chi/binding
|
||||
desc: use our wrapper
|
||||
migrations:
|
||||
files:
|
||||
- '**/modelmigration/**/*.go'
|
||||
- '**/models/migrations/**/*.go'
|
||||
deny:
|
||||
- pkg: gitea.dev/models
|
||||
desc: "migrations must not depend on the models package. HINT: MIGRATION-STRUCT-FROZEN"
|
||||
- pkg: gitea.dev/models$
|
||||
desc: migrations must not depend on the models package
|
||||
- pkg: gitea.dev/modules/structs
|
||||
desc: "migrations must not depend on modules/structs. HINT: MIGRATION-STRUCT-FROZEN"
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pattern: '^(fmt\.Print(|f|ln)|print|println)$' # default
|
||||
- pattern: '^exec\.CommandContext$' # use our wrapper for graceful termination
|
||||
modernize:
|
||||
disable:
|
||||
- embedlit
|
||||
desc: migrations must not depend on modules/structs (API structures change over time)
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
require-explanation: true
|
||||
@@ -98,11 +87,6 @@ linters:
|
||||
- name: increment-decrement
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: package-naming
|
||||
arguments:
|
||||
- convention-name-check-regex: '^[a-z][a-z0-9_]*$' # underscores are used by migration packages
|
||||
skip-default-bad-name-check: true
|
||||
skip-collision-with-common-std: true
|
||||
- name: redefines-builtin-id
|
||||
- name: superfluous-else
|
||||
- name: time-naming
|
||||
@@ -112,7 +96,7 @@ linters:
|
||||
arguments:
|
||||
- [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
|
||||
- [] # DenyList
|
||||
- - skip-initialism-name-checks: true
|
||||
- - skip-package-name-checks: true # supress errors from underscore in migration packages
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
@@ -134,8 +118,6 @@ linters:
|
||||
enable:
|
||||
- nilness
|
||||
- unusedwrite
|
||||
exhaustruct_v5:
|
||||
explicit-mode: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
@@ -147,13 +129,16 @@ linters:
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- forbidigo
|
||||
- staticcheck
|
||||
- unparam
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
path: modelmigration/v
|
||||
- errcheck
|
||||
path: models/migrations/v
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd
|
||||
- linters:
|
||||
- dupl
|
||||
text: (?i)webhook
|
||||
@@ -170,10 +155,13 @@ linters:
|
||||
- gocritic
|
||||
text: '(?i)exitAfterDefer:'
|
||||
paths:
|
||||
- ^node_modules/
|
||||
- ^\.venv/
|
||||
- ^public/
|
||||
- ^web_src/
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
@@ -190,15 +178,14 @@ formatters:
|
||||
- blank
|
||||
- default
|
||||
gofumpt:
|
||||
extra:
|
||||
group-params: true
|
||||
extra-rules: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- ^node_modules/
|
||||
- ^\.venv/
|
||||
- ^public/
|
||||
- ^web_src/
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
|
||||
run:
|
||||
timeout: 10m
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
# SC2153: false-alert "Possible misspelling: TAGS may not be assigned. Did you mean tags?". We already use strict mode.
|
||||
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2153,SC2154,SC2155,SC2164,SC2181,SC2207
|
||||
disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2154,SC2155,SC2164,SC2181,SC2207
|
||||
|
||||
@@ -4,4 +4,9 @@ rules:
|
||||
info-contact: off
|
||||
oas2-api-host: off
|
||||
oas2-parameter-description: off
|
||||
oas2-schema: off
|
||||
oas2-valid-schema-example: off
|
||||
openapi-tags: off
|
||||
operation-description: off
|
||||
operation-singular-tag: off
|
||||
operation-tag-defined: off
|
||||
|
||||
46
AGENTS.md
46
AGENTS.md
@@ -1,26 +1,20 @@
|
||||
- Never assume, verify before claiming
|
||||
- List development targets with `make help`
|
||||
- Before working on code, read the `docs` guides for every area touched: `guidelines-backend.md` for Go, `guidelines-frontend.md` for TS, CSS and Vue, both for templates, `testing.md` for tests, `development.md` for config options and migrations, `guidelines-refactoring.md` for refactors
|
||||
- Before designing a feature or behavior, check prior art in GitHub and GitLab
|
||||
- Don't add code for theoretical edge cases, only for ones that can realistically occur. Ask humans to make decisions if the edge case matters, comment the edge case for why it can be ignored if it doesn't matter
|
||||
- Write docs concisely and user-facing, skip implementation details
|
||||
- PR descriptions: minimal, only what and why, no task or file listings, no headings. Aim for less than 1000 characters. References go at the end of the description like `Fixes: <url>`.
|
||||
- PR descriptions and update comments: Include screenshots for UI changes, before and after side by side. Take them at 4x scale in dark theme, cropped to the changes with some padding, and embed them at half CSS size
|
||||
- Reference issues and PRs by full URL, not by number
|
||||
- Use Conventional Commits for commit messages and PR titles, plus Gitea's `enhance` type for user-facing enhancements
|
||||
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages and PR descriptions, never `Co-Authored-By` or `Signed-off-by`
|
||||
- Attribute agent authorship on one trailing line in issue and PR comments, never as a PR description section
|
||||
- Never rewrite git history unless asked, update PRs with new commits and normal push
|
||||
- Comments: write almost none, short and preferably same-line, explaining why for a future reader. Never narrate code, the change or the prompt. Preserve existing ones that still apply. If you need to write a paragraph-long comment, rethink your implementation, it is likely too complicated
|
||||
- Add the current year to copyright headers of new `.go` files
|
||||
- In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically
|
||||
- In TS, use `!` instead of `?.`/`??` when a value always exists
|
||||
- In Go, prefer to use modern language features wherever possible
|
||||
- In Go, function-name prefixes in errors like `fmt.Errorf("Foo: %w", err)` must always name the function they are in
|
||||
- Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10`
|
||||
- Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important`
|
||||
- Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates`
|
||||
- Fix the cause rather than disabling a linter or weakening a test. Where unavoidable, use the narrowest scope with a trailing comment giving the reason
|
||||
- Run single tests with `go test -run '^TestName$' ./modulepath/` for Go, `pnpm exec vitest <path-filter>` for TS and `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e` for e2e
|
||||
- Write the fewest, fastest tests covering the behavior, extending an existing one where possible. Prefer unit tests where logic is testable in isolation
|
||||
- Aim for sub-2s per integration test and sub-4s per e2e test. Wait on a deterministic condition rather than `sleep`, and prefer semantic locators in e2e tests
|
||||
- Use `make help` to find available development targets
|
||||
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
|
||||
- Run `make lint-js` to lint `.ts` files
|
||||
- Run `make tidy` after any `go.mod` changes
|
||||
- Run single go tests with `go test -run '^TestName$' ./modulepath/`
|
||||
- Run single js test files with `pnpm exec vitest <path-filter>`
|
||||
- Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e`
|
||||
- Add the current year into the copyright header of new `.go` files
|
||||
- Ensure no trailing whitespace in edited files
|
||||
- Use Conventional Commits for commit messages and PR titles, e.g. `type(scope): subject`; `!` before the colon if breaking. Use `test` type for test-only changes.
|
||||
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
|
||||
- Preserve existing code comments, do not remove or rewrite comments that are still relevant
|
||||
- Keep comments short, prefer same-line, explain why, never narrate code
|
||||
- Prefer unit tests over integration tests when logic is testable in isolation
|
||||
- Aim for sub-2s local runtime for integration and e2e tests
|
||||
- In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist
|
||||
- For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important`
|
||||
- Include authorship attribution in issue and pull request comments
|
||||
- Always add `Assisted-By` trailers to commit messages in format `Assisted-by: AGENT_NAME:MODEL_VERSION`
|
||||
- Never add `Co-Authored-By` `Signed-off-by` trailer to commit messages. Sign off must be done by a human.
|
||||
|
||||
5223
CHANGELOG-archived.md
Normal file
5223
CHANGELOG-archived.md
Normal file
File diff suppressed because it is too large
Load Diff
6274
CHANGELOG.md
Normal file
6274
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -58,8 +58,7 @@ Contributions made with the assistance of AI tools are welcome, but contributors
|
||||
2. Manually test the changes and add appropriate automated tests where feasible.
|
||||
3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review.
|
||||
4. Disclose AI-assisted content clearly.
|
||||
5. Do not rely solely on AI to generate replies or engage in discussions.
|
||||
Ensure your responses are concise, accurate, and thoughtful, avoiding generic or low-quality AI-generated content.
|
||||
5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model.
|
||||
6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit.
|
||||
|
||||
Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves.
|
||||
@@ -112,7 +111,7 @@ If further discussion is needed, we encourage you to open a new issue instead an
|
||||
|
||||
## Building Gitea
|
||||
|
||||
See [docs/build-setup.md](docs/build-setup.md) for prerequisites and [docs/development.md](docs/development.md)
|
||||
See [docs/setup.md](docs/setup.md) for prerequisites and [docs/development.md](docs/development.md)
|
||||
for building Gitea and the development workflow.
|
||||
|
||||
## Styleguide
|
||||
@@ -170,13 +169,13 @@ In the PR title, describe the problem you are fixing, not how you are fixing it.
|
||||
Use the first comment as a summary of your PR. \
|
||||
In the PR summary, you can describe exactly how you are fixing this problem.
|
||||
|
||||
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message and release notes entry:
|
||||
PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message:
|
||||
|
||||
```text
|
||||
type(scope)!: subject
|
||||
```
|
||||
|
||||
The scope in parentheses is optional. A `!` immediately before the colon marks a [breaking change](https://www.conventionalcommits.org/en/v1.0.0/#summary): either `type!:` or `type(scope)!:` (not `type!(scope):`). Do not mark `build`, `chore`, `ci`, `docs`, `style`, or `test` as breaking.
|
||||
The scope in parentheses is optional. A `!` immediately before the colon marks a [breaking change](https://www.conventionalcommits.org/en/v1.0.0/#summary): either `type!:` or `type(scope)!:` (not `type!(scope):`).
|
||||
|
||||
Use one of these types:
|
||||
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS frontend-build
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
|
||||
RUN apk --no-cache add build-base git nodejs pnpm
|
||||
WORKDIR /src
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
|
||||
COPY --exclude=.git/ . .
|
||||
RUN make frontend
|
||||
|
||||
# Cross-compile backend on the native platform to avoid slow QEMU emulation
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS=""
|
||||
@@ -28,15 +28,11 @@ RUN go mod download
|
||||
# ".git" directory is mounted separately later only for version data extraction.
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
|
||||
make backend
|
||||
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
@@ -52,7 +48,7 @@ FROM docker.io/library/alpine:3.24 AS gitea
|
||||
|
||||
EXPOSE 22 3000
|
||||
|
||||
RUN apk --no-cache upgrade && apk --no-cache add \
|
||||
RUN apk --no-cache add \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS frontend-build
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
|
||||
RUN apk --no-cache add build-base git nodejs pnpm
|
||||
WORKDIR /src
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
|
||||
COPY --exclude=.git/ . .
|
||||
RUN make frontend
|
||||
|
||||
# Cross-compile backend on the native platform
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS=""
|
||||
@@ -27,15 +27,11 @@ RUN go mod download
|
||||
# See the comments in Dockerfile
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
|
||||
make backend
|
||||
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
@@ -47,7 +43,7 @@ FROM docker.io/library/alpine:3.24 AS gitea-rootless
|
||||
|
||||
EXPOSE 2222 3000
|
||||
|
||||
RUN apk --no-cache upgrade && apk --no-cache add \
|
||||
RUN apk --no-cache add \
|
||||
bash \
|
||||
ca-certificates \
|
||||
dumb-init \
|
||||
|
||||
@@ -41,6 +41,7 @@ Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
|
||||
Leon Hofmeister <dev.lh@web.de> (@delvh)
|
||||
Wim <wim@42.be> (@42wim)
|
||||
Jason Song <i@wolfogre.com> (@wolfogre)
|
||||
Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
|
||||
Yu Tian <zettat123@gmail.com> (@Zettat123)
|
||||
Dong Ge <gedong_1994@163.com> (@sillyguodong)
|
||||
Xinyi Gong <hestergong@gmail.com> (@HesterG)
|
||||
@@ -64,6 +65,3 @@ Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
|
||||
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
|
||||
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
|
||||
Nicolas <bircni@icloud.com> (@bircni)
|
||||
Minjie Fang <wingsallen@gmail.com> (@wingsallen)
|
||||
Shudhanshu Singh <sudhanshuwriterblc@gmail.com> (@SudhanshuMatrix)
|
||||
Shivansh Garg <shivanshgarg587@gmail.com> (@SHIVANSHGARG07)
|
||||
224
Makefile
224
Makefile
@@ -1,23 +1,29 @@
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
HASH := \#
|
||||
|
||||
# By default use go's 1.25 experimental json v2 library when building
|
||||
# TODO: remove when no longer experimental
|
||||
export GOEXPERIMENT ?= jsonv2
|
||||
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
COMMA := ,
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.7 # renovate: datasource=go
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.9.0 # renovate: datasource=go
|
||||
XGO_VERSION := go-1.26.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
|
||||
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
|
||||
|
||||
CONTAINER_RUNTIME ?= $(shell hash docker >/dev/null 2>&1 && echo docker || echo podman)
|
||||
|
||||
PLAYWRIGHT_BROWSERS ?= chromium firefox webkit
|
||||
PLAYWRIGHT_FLAGS ?=
|
||||
|
||||
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
|
||||
ifeq ($(HAS_GO), yes)
|
||||
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
|
||||
@@ -36,13 +42,18 @@ endif
|
||||
|
||||
TAGS ?=
|
||||
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
||||
CGO_TAGS := sqlite_mattn pam
|
||||
|
||||
CGO_ENABLED ?= 0
|
||||
ifneq ($(strip $(filter $(CGO_TAGS),$(TAGS))),)
|
||||
ifneq (,$(findstring sqlite_mattn,$(TAGS))$(findstring pam,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
STATIC ?=
|
||||
EXTLDFLAGS ?=
|
||||
ifneq ($(STATIC),)
|
||||
EXTLDFLAGS = -extldflags "-static"
|
||||
endif
|
||||
|
||||
ifeq ($(GOOS),windows)
|
||||
IS_WINDOWS := yes
|
||||
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
|
||||
@@ -51,13 +62,14 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
|
||||
endif
|
||||
endif
|
||||
|
||||
# GOFLAGS and EXTRA_GOFLAGS are for the 'go build' command only
|
||||
ifeq ($(IS_WINDOWS),yes)
|
||||
GOFLAGS := -v -buildmode=exe
|
||||
EXECUTABLE ?= gitea.exe
|
||||
else
|
||||
GOFLAGS := -v
|
||||
EXECUTABLE ?= gitea
|
||||
endif
|
||||
|
||||
# EXTRA_GOFLAGS is for the 'go build' command only
|
||||
EXTRA_GOFLAGS ?=
|
||||
|
||||
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
|
||||
@@ -68,27 +80,20 @@ endif
|
||||
|
||||
# GOTEST_FLAGS is for unit test and integration test
|
||||
GOTEST_FLAGS ?= -timeout 40m
|
||||
# disable race detection for the packages that slow down the tests
|
||||
GOTEST_FLAGS := -gcflags=modernc.org/sqlite/lib=-race=false $(GOTEST_FLAGS)
|
||||
|
||||
STORED_VERSION_FILE := VERSION
|
||||
|
||||
GITHUB_REF_TYPE ?= branch
|
||||
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# VERSION: the name for the build and filenames, e.g.: "1.2.3" for tag "v1.2.3", "main-nightly" for branch "main"
|
||||
# GITEA_VERSION: the Gitea's internal version for display, e.g. "1.28.0+dev-356-ge47d0b66ea"
|
||||
ifeq ($(GITHUB_REF_TYPE),tag)
|
||||
# convert tag "v1.2.3" to "1.2.3"
|
||||
ifneq ($(GITHUB_REF_TYPE),branch)
|
||||
VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
|
||||
GITEA_VERSION ?= $(VERSION)
|
||||
else ifeq ($(GITHUB_REF_TYPE),branch)
|
||||
else
|
||||
ifneq ($(GITHUB_REF_NAME),)
|
||||
# convert branch "release/v1.2" to "1.2-nightly"
|
||||
VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
|
||||
else
|
||||
# no branch name info, use git ref name "HEAD" instead
|
||||
VERSION ?= HEAD
|
||||
VERSION ?= main
|
||||
endif
|
||||
|
||||
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
|
||||
@@ -97,15 +102,19 @@ else ifeq ($(GITHUB_REF_TYPE),branch)
|
||||
else
|
||||
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
|
||||
endif
|
||||
else
|
||||
$(error unsupported ref type $(GITHUB_REF_TYPE))
|
||||
endif
|
||||
|
||||
# if version = "main" then update version to "nightly"
|
||||
ifeq ($(VERSION),main)
|
||||
VERSION := main-nightly
|
||||
endif
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
RELEASE_ENV = GO="$(GO)" TAGS="$(TAGS)" LDFLAGS="$(LDFLAGS)" DIST="$(DIST)" VERSION="$(VERSION)"
|
||||
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list gitea.dev/modelmigration/...) gitea.dev/tests/integration/migration-test gitea.dev/tests gitea.dev/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
|
||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list gitea.dev/modelmigration/...)
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
||||
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list gitea.dev/models/migrations/...) gitea.dev/tests/integration/migration-test gitea.dev/tests gitea.dev/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
|
||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list gitea.dev/models/migrations/...)
|
||||
|
||||
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
|
||||
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
|
||||
@@ -126,10 +135,12 @@ GO_LICENSE_FILE := assets/go-licenses.json
|
||||
|
||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
|
||||
|
||||
GO_DIRS := build cmd modelmigration models modules routers services tests tools
|
||||
GO_DIRS := build cmd models modules routers services tests tools
|
||||
WEB_DIRS := web_src/js web_src/css
|
||||
|
||||
ESLINT_FILES := web_src/js tools *.ts tests/e2e
|
||||
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
|
||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
|
||||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json
|
||||
|
||||
GO_SOURCES := $(wildcard *.go)
|
||||
@@ -137,12 +148,11 @@ GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
|
||||
GO_SOURCES += $(GENERATED_GO_DEST)
|
||||
|
||||
ESLINT_CONCURRENCY ?= 2
|
||||
ESLINT_ARGS := --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY)
|
||||
|
||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
|
||||
SWAGGER_EXCLUDE := gitea.dev/sdk
|
||||
SWAGGER_SPEC_INPUT := templates/swagger/v1-input.json
|
||||
SWAGGER_SPEC := templates/swagger/v1-swagger.generated.json
|
||||
OPENAPI3_SPEC := templates/swagger/v1-openapi3.generated.json
|
||||
OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl
|
||||
|
||||
TEST_MYSQL_HOST ?= mysql:3306
|
||||
TEST_MYSQL_DBNAME ?= testgitea
|
||||
@@ -201,7 +211,7 @@ fmt: ## format the Go and template code
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check: fmt
|
||||
@diff=$$(git diff --color=always $(GO_SOURCES) templates); \
|
||||
@diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make fmt' and commit the result:"; \
|
||||
printf "%s" "$${diff}"; \
|
||||
@@ -221,9 +231,7 @@ endif
|
||||
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
|
||||
|
||||
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
||||
@output="$$($(GO) run $(SWAGGER_PACKAGE) generate spec --enable-allof-compounding --skip-enum-desc --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' 2>&1)" || { printf '%s\n' "$$output" >&2; exit 1; }; \
|
||||
warnings="$$(printf '%s\n' "$$output" | grep -v '^go: ')"; \
|
||||
if [ -n "$$warnings" ]; then printf '%s\n' "$$warnings" >&2; exit 1; fi
|
||||
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
|
||||
|
||||
.PHONY: swagger-check
|
||||
swagger-check: generate-swagger
|
||||
@@ -236,10 +244,11 @@ swagger-check: generate-swagger
|
||||
|
||||
.PHONY: swagger-validate
|
||||
swagger-validate: ## check if the swagger spec is valid
|
||||
@# ensure no warnings
|
||||
@output="$$($(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' 2>&1)"; status=$$?; \
|
||||
printf '%s\n' "$$output" | grep -v '^go: '; \
|
||||
case "$$output" in *WARNING:*) exit 1;; esac; exit $$status
|
||||
@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
|
||||
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
|
||||
@# FIXME: there are some warnings
|
||||
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
|
||||
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
|
||||
|
||||
.PHONY: generate-openapi3
|
||||
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
|
||||
@@ -266,10 +275,10 @@ checks-frontend: lockfile-check svg-check ## check frontend files
|
||||
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend lint-templates lint-swagger lint-md lint-actions lint-json lint-yaml lint-shell ## lint everything
|
||||
lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml lint-shell ## lint everything
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: lint-frontend-fix lint-backend-fix ## lint everything and fix issues
|
||||
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
|
||||
|
||||
.PHONY: lint-frontend
|
||||
lint-frontend: lint-js lint-css ## lint frontend files
|
||||
@@ -285,12 +294,12 @@ lint-backend-fix: lint-go-fix lint-editorconfig ## lint backend files and fix is
|
||||
|
||||
.PHONY: lint-js
|
||||
lint-js: node_modules ## lint js and ts files
|
||||
pnpm exec eslint $(ESLINT_ARGS) $(ESLINT_FILES)
|
||||
pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES)
|
||||
pnpm exec vue-tsc
|
||||
|
||||
.PHONY: lint-js-fix
|
||||
lint-js-fix: node_modules ## lint js and ts files and fix issues
|
||||
pnpm exec eslint $(ESLINT_ARGS) $(ESLINT_FILES) --fix
|
||||
pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) --fix
|
||||
pnpm exec vue-tsc
|
||||
|
||||
.PHONY: lint-css
|
||||
@@ -303,7 +312,7 @@ lint-css-fix: node_modules ## lint css files and fix issues
|
||||
|
||||
.PHONY: lint-swagger
|
||||
lint-swagger: node_modules ## lint swagger files
|
||||
pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) $(OPENAPI3_SPEC)
|
||||
pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC)
|
||||
|
||||
.PHONY: lint-md
|
||||
lint-md: node_modules ## lint markdown files
|
||||
@@ -313,6 +322,14 @@ lint-md: node_modules ## lint markdown files
|
||||
lint-md-fix: node_modules ## lint markdown files and fix issues
|
||||
pnpm exec markdownlint --fix *.md
|
||||
|
||||
.PHONY: lint-spell
|
||||
lint-spell: ## lint spelling
|
||||
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error
|
||||
|
||||
.PHONY: lint-spell-fix
|
||||
lint-spell-fix: ## lint spelling and fix issues
|
||||
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -w
|
||||
|
||||
.PHONY: lint-go
|
||||
lint-go: ## lint go files
|
||||
GO=$(GO) GOLANGCI_LINT_PACKAGE=$(GOLANGCI_LINT_PACKAGE) $(GO) run ./tools/lint-go-all.go
|
||||
@@ -329,7 +346,7 @@ lint-editorconfig:
|
||||
.PHONY: lint-actions
|
||||
lint-actions: .venv ## lint action workflow files
|
||||
@$(GO) run $(ACTIONLINT_PACKAGE)
|
||||
@uv run --frozen zizmor --quiet --persona=pedantic --min-confidence=medium .github
|
||||
@uv run --frozen zizmor --quiet --min-confidence=medium .github
|
||||
|
||||
.PHONY: lint-shell
|
||||
lint-shell: ## lint shell scripts
|
||||
@@ -346,11 +363,11 @@ lint-yaml: .venv ## lint yaml files
|
||||
|
||||
.PHONY: lint-json
|
||||
lint-json: node_modules ## lint json files
|
||||
pnpm exec eslint -c eslint.json.config.ts $(ESLINT_ARGS)
|
||||
pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY)
|
||||
|
||||
.PHONY: lint-json-fix
|
||||
lint-json-fix: node_modules ## lint and fix json files
|
||||
pnpm exec eslint -c eslint.json.config.ts $(ESLINT_ARGS) --fix
|
||||
pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) --fix
|
||||
|
||||
.PHONY: watch
|
||||
watch: ## watch everything and continuously rebuild
|
||||
@@ -370,7 +387,7 @@ test-backend: ## test backend files
|
||||
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES)
|
||||
|
||||
.PHONY: test-frontend
|
||||
test-frontend: playwright ## test frontend files
|
||||
test-frontend: node_modules ## test frontend files
|
||||
pnpm exec vitest
|
||||
|
||||
.PHONY: test-check
|
||||
@@ -385,11 +402,10 @@ test-check:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
TEST_BACKEND_GOALS := $(filter test-backend$(HASH)%,$(MAKECMDGOALS))
|
||||
.PHONY: $(TEST_BACKEND_GOALS)
|
||||
$(TEST_BACKEND_GOALS): test-backend$(HASH)%:
|
||||
.PHONY: test-backend\#%
|
||||
test-backend\#%:
|
||||
@echo "Running go test with -tags '$(TAGS)'..."
|
||||
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run '$*' $(GO_TEST_PACKAGES)
|
||||
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
@@ -405,12 +421,7 @@ unit-test-coverage:
|
||||
.PHONY: tidy
|
||||
tidy: ## run go mod tidy
|
||||
$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
|
||||
$(eval GO_TOOLCHAIN := $(shell grep -Eo '^toolchain\s+go[0-9.]+' go.mod | cut -d' ' -f2))
|
||||
$(GO) mod tidy -compat=$(MIN_GO_VERSION)
|
||||
@# workaround https://github.com/golang/go/issues/75331: restore toolchain if tidy dropped it
|
||||
@if [ -n "$(GO_TOOLCHAIN)" ] && ! grep -qE '^toolchain\s' go.mod; then \
|
||||
$(GO) mod edit -toolchain=$(GO_TOOLCHAIN); \
|
||||
fi
|
||||
@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
|
||||
|
||||
vendor: go.mod go.sum
|
||||
@@ -433,7 +444,7 @@ $(GO_LICENSE_FILE): go.mod go.sum
|
||||
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
|
||||
|
||||
.PHONY: test-integration
|
||||
test-integration: $(EXECUTABLE)
|
||||
test-integration:
|
||||
@# Use a compiled binary: testlogger forwards gitea logs to t.Log, so `go test -v`
|
||||
@# would flood output per passing test. testcache can't help these tests anyway —
|
||||
@# they mutate the work directory, so cache inputs change between runs.
|
||||
@@ -444,10 +455,9 @@ test-integration: $(EXECUTABLE)
|
||||
test-integration-compile:
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c -o /dev/null gitea.dev/tests/integration
|
||||
|
||||
TEST_INTEGRATION_GOALS := $(filter test-integration$(HASH)%,$(MAKECMDGOALS))
|
||||
.PHONY: $(TEST_INTEGRATION_GOALS)
|
||||
$(TEST_INTEGRATION_GOALS): test-integration$(HASH)%: $(EXECUTABLE)
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run '$*' gitea.dev/tests/integration
|
||||
.PHONY: test-integration\#%
|
||||
test-integration\#%:
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run $(subst .,/,$*) gitea.dev/tests/integration
|
||||
|
||||
.PHONY: test-migration
|
||||
test-migration: migrations.integration.test migrations.individual.test
|
||||
@@ -463,15 +473,15 @@ migrations.individual.test:
|
||||
|
||||
.PHONY: migrations.individual.test\#%
|
||||
migrations.individual.test\#%:
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' gitea.dev/modelmigration/$*
|
||||
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' gitea.dev/models/migrations/$*
|
||||
|
||||
.PHONY: playwright
|
||||
playwright: deps-frontend
|
||||
@./tools/playwright.sh $(PLAYWRIGHT_FLAGS) $(PLAYWRIGHT_BROWSERS)
|
||||
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/test-e2e.sh install
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: playwright frontend backend
|
||||
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS)
|
||||
@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS)
|
||||
|
||||
.PHONY: build
|
||||
build: frontend backend ## build everything
|
||||
@@ -496,38 +506,38 @@ generate-go: $(TAGS_PREREQ)
|
||||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||
|
||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build -v $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
|
||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||
$(error pam support set via TAGS does not support static builds)
|
||||
endif
|
||||
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||
|
||||
.PHONY: release
|
||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
|
||||
|
||||
$(DIST_DIRS):
|
||||
mkdir -p $(DIST_DIRS)
|
||||
|
||||
# Release builds always use Go's native cross compilation. To cross-compile with CGO,
|
||||
# use "build" target with proper TAGS/LDFLAGS/CGO_CFLAGS to make "$(EXECUTABLE)" target run the "go build" command.
|
||||
.PHONY: release
|
||||
release: frontend release-binaries release-copy release-compress vendor release-sources release-check
|
||||
|
||||
.PHONY: release-binaries
|
||||
release-binaries: | $(DIST_DIRS)
|
||||
@$(RELEASE_ENV) ./tools/build-release.sh
|
||||
|
||||
.PHONY: release-windows
|
||||
release-windows: | $(DIST_DIRS)
|
||||
@$(RELEASE_ENV) ./tools/build-release.sh windows
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
||||
ifeq (,$(findstring gogit,$(TAGS)))
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
|
||||
endif
|
||||
|
||||
.PHONY: release-linux
|
||||
release-linux: | $(DIST_DIRS)
|
||||
@$(RELEASE_ENV) ./tools/build-release.sh linux
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
|
||||
|
||||
.PHONY: release-darwin
|
||||
release-darwin: | $(DIST_DIRS)
|
||||
@$(RELEASE_ENV) ./tools/build-release.sh darwin
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
|
||||
|
||||
.PHONY: release-freebsd
|
||||
release-freebsd: | $(DIST_DIRS)
|
||||
@$(RELEASE_ENV) ./tools/build-release.sh freebsd
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
|
||||
|
||||
.PHONY: release-copy
|
||||
release-copy: | $(DIST_DIRS)
|
||||
@@ -543,11 +553,11 @@ release-compress: | $(DIST_DIRS)
|
||||
|
||||
.PHONY: release-sources
|
||||
release-sources: | $(DIST_DIRS)
|
||||
echo $(GITEA_VERSION) > $(STORED_VERSION_FILE)
|
||||
echo $(VERSION) > $(STORED_VERSION_FILE)
|
||||
# bsdtar needs a ^ to prevent matching subdirectories
|
||||
$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
|
||||
# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
|
||||
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '|^./|gitea-src-$(VERSION)/|'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
|
||||
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
|
||||
tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
|
||||
rm -f $(STORED_VERSION_FILE)
|
||||
|
||||
@@ -565,14 +575,16 @@ deps-backend: ## install backend dependencies
|
||||
$(GO) mod download
|
||||
|
||||
.PHONY: deps-tools
|
||||
deps-tools: ## fetch and compile tool dependencies
|
||||
$(GO) run -exec true $(AIR_PACKAGE) & \
|
||||
$(GO) run -exec true $(EDITORCONFIG_CHECKER_PACKAGE) & \
|
||||
$(GO) run -exec true $(GOLANGCI_LINT_PACKAGE) & \
|
||||
$(GO) run -exec true $(GXZ_PACKAGE) & \
|
||||
$(GO) run -exec true $(SWAGGER_PACKAGE) & \
|
||||
$(GO) run -exec true $(GOVULNCHECK_PACKAGE) & \
|
||||
$(GO) run -exec true $(ACTIONLINT_PACKAGE) & \
|
||||
deps-tools: ## install tool dependencies
|
||||
$(GO) install $(AIR_PACKAGE) & \
|
||||
$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
|
||||
$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
|
||||
$(GO) install $(GXZ_PACKAGE) & \
|
||||
$(GO) install $(MISSPELL_PACKAGE) & \
|
||||
$(GO) install $(SWAGGER_PACKAGE) & \
|
||||
$(GO) install $(XGO_PACKAGE) & \
|
||||
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
||||
wait
|
||||
|
||||
node_modules: pnpm-lock.yaml
|
||||
@@ -583,6 +595,28 @@ node_modules: pnpm-lock.yaml
|
||||
uv sync
|
||||
@touch .venv
|
||||
|
||||
.PHONY: update
|
||||
update: update-go update-js update-py ## update dependencies
|
||||
|
||||
.PHONY: update-go
|
||||
update-go: ## update go dependencies
|
||||
$(GO) get -u ./...
|
||||
$(MAKE) tidy
|
||||
|
||||
.PHONY: update-js
|
||||
update-js: node_modules ## update js dependencies
|
||||
pnpm exec updates -u -f package.json
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
pnpm install
|
||||
@touch node_modules
|
||||
|
||||
.PHONY: update-py
|
||||
update-py: node_modules ## update py dependencies
|
||||
pnpm exec updates -u -f pyproject.toml
|
||||
rm -rf .venv uv.lock
|
||||
uv sync
|
||||
@touch .venv
|
||||
|
||||
.PHONY: vite
|
||||
vite: $(FRONTEND_DEST) ## build vite files
|
||||
|
||||
@@ -623,10 +657,6 @@ lockfile-check:
|
||||
generate-gitignore: ## update gitignore files
|
||||
$(GO) run build/generate-gitignores.go
|
||||
|
||||
.PHONY: generate-emoji
|
||||
generate-emoji: ## update emoji data from Unicode
|
||||
$(GO) run build/generate-emoji.go
|
||||
|
||||
.PHONY: generate-images
|
||||
generate-images: | node_modules ## generate images
|
||||
cd tools && node generate-images.ts $(TAGS)
|
||||
|
||||
11
README.md
11
README.md
@@ -2,6 +2,7 @@
|
||||
|
||||
[](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://goreportcard.com/report/gitea.dev "Go Report Card")
|
||||
[](https://pkg.go.dev/gitea.dev "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
@@ -62,13 +63,13 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language, ask one of the managers in the Crowdin project to add a new language there.
|
||||
|
||||
You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord.
|
||||
You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty, but we hope to fill it as questions pop up.
|
||||
|
||||
Get more information from [the translation section of our contributing guide](CONTRIBUTING.md#translation).
|
||||
Get more information from [documentation](https://docs.gitea.com/contributing/localization).
|
||||
|
||||
## Official and Third-Party Projects
|
||||
|
||||
We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [gt](https://gitea.com/gitea/cli) and an [action runner](https://gitea.com/gitea/runner) for Gitea Action.
|
||||
We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action.
|
||||
|
||||
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more.
|
||||
|
||||
@@ -115,12 +116,12 @@ Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea"
|
||||
|
||||
For dynamic config options, you can change it on your admin panel's configuration section.
|
||||
|
||||
For static config options, you can edit your `app.ini` file and restart the instance.
|
||||
For static config options, you can edit your `app.ini` file and resart the instance.
|
||||
See [app.example.ini](https://github.com/go-gitea/gitea/blob/main/custom/conf/app.example.ini) or [configuration documentation](https://docs.gitea.com/administration/config-cheat-sheet) for more details.
|
||||
|
||||
**Where can I find the security patches?**
|
||||
|
||||
Check the [release notes](https://github.com/go-gitea/gitea/releases) and [security advisories](https://github.com/go-gitea/gitea/security/advisories) for security patches.
|
||||
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
|
||||
|
||||
(more FAQs are listed in [FAQ documentation](https://docs.gitea.com/help/faq))
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[](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://goreportcard.com/report/gitea.dev "Go Report Card")
|
||||
[](https://pkg.go.dev/gitea.dev "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
@@ -72,11 +73,11 @@
|
||||
|
||||
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
|
||||
|
||||
更多信息请参阅 [文件](https://docs.gitea.com/zh-cn/contributing/localization)。
|
||||
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
|
||||
|
||||
## 官方和第三方项目
|
||||
|
||||
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [gt](https://gitea.com/gitea/cli) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/runner)。
|
||||
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
|
||||
|
||||
@@ -125,7 +126,7 @@ Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea
|
||||
|
||||
**在哪里可以找到安全补丁?**
|
||||
|
||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 中,搜索关键词 `SECURITY` 以找到安全补丁。
|
||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[](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://goreportcard.com/report/gitea.dev "Go Report Card")
|
||||
[](https://pkg.go.dev/gitea.dev "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
@@ -72,11 +73,11 @@
|
||||
|
||||
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
|
||||
|
||||
更多信息請參閱 [文件](https://docs.gitea.com/zh-tw/contributing/localization)。
|
||||
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
|
||||
|
||||
## 官方和第三方項目
|
||||
|
||||
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [gt](https://gitea.com/gitea/cli) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/runner)。
|
||||
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
|
||||
|
||||
@@ -125,7 +126,7 @@ Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea
|
||||
|
||||
**在哪裡可以找到安全補丁?**
|
||||
|
||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
|
||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
|
||||
|
||||
## 許可證
|
||||
|
||||
|
||||
83
SECURITY.md
83
SECURITY.md
@@ -6,46 +6,20 @@ If you discover a security issue, please bring it to their attention right away!
|
||||
|
||||
Previous vulnerabilities are listed at https://about.gitea.com/security.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes follow our [support policy](docs/release-management.md#end-of-life-eol): only the most recent major release and the `main` branch receive them.
|
||||
|
||||
Reports must be filed against a supported version. Before reporting, verify that the issue reproduces on a supported version; any report that only affects an unsupported version will be closed as out of scope.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **DO NOT** file a public issue. Instead, report the vulnerability privately by opening a [GitHub security advisory](https://github.com/go-gitea/gitea/security/advisories/new), or optionally by sending your report to `security@gitea.io`. Former is preferred if given a choice.
|
||||
If reporting via GitHub, open an advisory per found issue. You can group the found issues if they relate to a single area (same feature, same fault present in multiple routes) but do not group unrelated reports into one as it makes it much harder to assess and track for duplicates.
|
||||
Do not use LLM to write the reports. It's meant for people to read so take your time and use your own words to write it. If you have used LLM to find the bug, be transparent about it and verify that it is not hallucination before submitting the report.
|
||||
|
||||
If your report turns out to be a duplicate, you will be credited in the original report.
|
||||
|
||||
### Report requirements
|
||||
|
||||
When filing the report please document it. Reports must include:
|
||||
|
||||
- Version or a range of versions it was successfully tested against. If reproducible against the `main` branch, a commit hash is also acceptable. Ambiguous versions like `latest` are not acceptable.
|
||||
- Settings used, especially when they diverge from the defaults
|
||||
- Reproduction steps from freshly installed instance. If the fault is only reproducible through upgrade from older version, reproduction should state that.
|
||||
|
||||
Take your time to format it properly as it'll be much easier to read. Prefer a concise writing style.
|
||||
When submitting an advisory, it should have the `gitea.dev` go package as package `name.cd`
|
||||
|
||||
### Triage process
|
||||
|
||||
Github does not have any search or tagging in GHSA so in most cases your report will leave triage state when we have a fix for it in works for next release.
|
||||
This helps us keep track of items fixed and pending release and separate them from things that we either haven't looked at yet or have not fixed.
|
||||
Please **DO NOT** file a public issue, instead send your report privately to `security@gitea.io`.
|
||||
|
||||
## Protecting Security Information
|
||||
|
||||
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
|
||||
|
||||
The PGP key is valid until July 23, 2027.
|
||||
The PGP key is valid until July 4, 2026.
|
||||
|
||||
```
|
||||
Key ID: 6FCD2D5B
|
||||
Key Type: RSA
|
||||
Expires: 7/23/2027
|
||||
Expires: 7/4/2026
|
||||
Key Size: 4096/4096
|
||||
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
|
||||
```
|
||||
@@ -68,18 +42,18 @@ lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
|
||||
AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
|
||||
tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
|
||||
BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
|
||||
LVsFAmpi3IkFCQmOqAsACgkQqv0jgW/NLVsgyxAAuTyj4+PImOFr7ZkEJcseWUy+
|
||||
gWQrJDagj+Ks2SKGneVroJ63q5Vngx44b7JNabauKel4IJJCScLdwA7k0DKFHPOM
|
||||
rUS6aavW5+961CMQxJ13vWz7qxLxhI4FXle255xFSXgOo9T1W7+wtm/wtze8+sws
|
||||
8XKAVBZGuVI1/KzVpE6bWxpIIYK79DC07NWGDAZzMbuO92TFHBRblXsicPWExBjC
|
||||
/oFmT70YMcA0wOwU/YKYMg2eK2XYUr5+uYX6FnP7iUhDj1vZmwkF33Cj9v9D3Hv9
|
||||
NzAquj97uLht4wL2sqsIgZd4Vc37uipPDFaddzTb/lon7lPbJPMLObcd4HjSTkIo
|
||||
nEMM0wdOQZU7mIIgRM9o3mr/gyVigl3uARmUKaP9PhBaOGTjkni0nrv90hSHBHeu
|
||||
ayM76vl2tzAB2KpDxHIHbI2SEdytV28l/rxWrsbk2kG7s7QB6VKiThTatYevh1kg
|
||||
6Ujgl2xNSntG6t6PWjQ65OMSq5Iiwe7RfOZkNmPRGP/rbzMbgWXB3iCR2gVQvsxA
|
||||
ctYdDZdUYAeQ5g6xSKJBloygDQdLzk9m0hqNUyl6gxhRNRw69hqp9E4mxFF3L1Gi
|
||||
Z2LspLX1ZZWMh0bp0AniWzZuzLuNI0KYhlok6fnshjidJ1qYM/lbz5eJTYWxoJZW
|
||||
rczn8gB3+MZbULc/iiO5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
|
||||
LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
|
||||
f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
|
||||
cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
|
||||
t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
|
||||
HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
|
||||
I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
|
||||
LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
|
||||
RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
|
||||
HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
|
||||
+ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
|
||||
ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
|
||||
Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
|
||||
T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
|
||||
GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
|
||||
XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
|
||||
@@ -91,20 +65,21 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
|
||||
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
|
||||
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
|
||||
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
|
||||
WwUCamLcngUJCY6oIAAKCRCq/SOBb80tWxilD/41xF1pwD2lGqjWLmlXcd0Ok5xm
|
||||
67TSGiKiFZUgI3ftkRVlqmeJqjVmRjpc2dLfCuVBzlaP0Oyv5Jn+2CPqgmWIk5tw
|
||||
pqlxqmsXiIZ9Uv2wWItxrXIwQ6DHc1qcIOu7feTnt1HVhxp2GqlXl3tA/+MTTDmn
|
||||
4rqrHffI7ps9eCbK/SfSuoQk3+x/x4GPqWDbsNw1E9dDjPPhsKfdwzZ6dFR2Yhzu
|
||||
2U0GDc2aeBA0Gjy6VFBj1ksmGxQCL3TqtGEHOonoPpTIjmDd35Xq6BSu119onOQS
|
||||
THZ7+3JQRpizBVrIklKf33UOikZy9WHuffFddP46m8qC2Alh1aB7xcD8K3P9gzd5
|
||||
OJO9Zr8JR+rvi8eOsCHvJqGhe0JMMf9bgXKjQ1tMTDTe6r0Jnw7HbkDghjpj9xG7
|
||||
GYmDB0CL4kP5nAZ1OPfIasIu3Llhcy6mS52ECw0X6PNwUINwBn7F6EKAiciw8/OS
|
||||
CXoGJDI5alwNhh+3DL4oBH1aq1lnmxmhFNpwpRDvtbRGmZeeASTeC4LMAtqP85Er
|
||||
8W9ulIgQ4Ub8QuoxvE5hzWEB1QUkJgozI8vO4DoGlrsz7P8cPzrOT7WqUfUPTYE6
|
||||
oZEfEYDSj1IZHnPYPS+yLBJMJ/Lc1h8OCetmbJZx4QWaX3Fgar7Zks8FfJQOTN6i
|
||||
HnOlrsaMDYNyO76uBQ==
|
||||
=4kVJ
|
||||
WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
|
||||
f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
|
||||
vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
|
||||
zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
|
||||
NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
|
||||
WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
|
||||
bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
|
||||
U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
|
||||
RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
|
||||
kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
|
||||
sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
|
||||
9M2VbqL9C51z/wyHLg==
|
||||
=SfZA
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
```
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.
|
||||
|
||||
19
assets/codemirror-languages.json
generated
19
assets/codemirror-languages.json
generated
@@ -436,7 +436,6 @@
|
||||
"jsonl",
|
||||
"mcmeta",
|
||||
"sarif",
|
||||
"slnlaunch",
|
||||
"tact",
|
||||
"tfstate",
|
||||
"topojson",
|
||||
@@ -453,8 +452,6 @@
|
||||
".htmlhintrc",
|
||||
".imgbotconfig",
|
||||
".nycrc",
|
||||
".releaserc",
|
||||
".secrets.baseline",
|
||||
".tern-config",
|
||||
".tern-project",
|
||||
".watchmanconfig",
|
||||
@@ -694,17 +691,10 @@
|
||||
"extensions": [
|
||||
"ini",
|
||||
"cnf",
|
||||
"container",
|
||||
"dof",
|
||||
"lektorproject",
|
||||
"mount",
|
||||
"network",
|
||||
"prefs",
|
||||
"properties",
|
||||
"service",
|
||||
"socket",
|
||||
"target",
|
||||
"timer",
|
||||
"url",
|
||||
"conf"
|
||||
],
|
||||
@@ -712,7 +702,6 @@
|
||||
".buckconfig",
|
||||
".coveragerc",
|
||||
".flake8",
|
||||
".gitreview",
|
||||
".pylintrc",
|
||||
"HOSTS",
|
||||
"buildozer.spec",
|
||||
@@ -787,12 +776,9 @@
|
||||
"extensions": [
|
||||
"r",
|
||||
"rd",
|
||||
"rhistory",
|
||||
"rsx"
|
||||
],
|
||||
"filenames": [
|
||||
".Rapp.history",
|
||||
".Rhistory",
|
||||
".Rprofile",
|
||||
"expr-dist"
|
||||
]
|
||||
@@ -912,7 +898,6 @@
|
||||
"bats",
|
||||
"command",
|
||||
"ksh",
|
||||
"pacscript",
|
||||
"sbatch",
|
||||
"slurm",
|
||||
"tmux",
|
||||
@@ -1176,7 +1161,6 @@
|
||||
"kml",
|
||||
"launch",
|
||||
"mdpolicy",
|
||||
"meta4",
|
||||
"mjml",
|
||||
"mod",
|
||||
"mojo",
|
||||
@@ -1197,7 +1181,6 @@
|
||||
"pt",
|
||||
"pubxml",
|
||||
"qhelp",
|
||||
"rbxmx",
|
||||
"rdf",
|
||||
"res",
|
||||
"resx",
|
||||
@@ -1284,9 +1267,7 @@
|
||||
".clang-format",
|
||||
".clang-tidy",
|
||||
".clangd",
|
||||
".eslintrc",
|
||||
".gemrc",
|
||||
".releaserc",
|
||||
"CITATION.cff",
|
||||
"glide.lock",
|
||||
"pixi.lock",
|
||||
|
||||
11483
assets/emoji.json
generated
Normal file
11483
assets/emoji.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
307
assets/go-licenses.json
generated
307
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
21
assets/misspellings.csv
Normal file
21
assets/misspellings.csv
Normal file
@@ -0,0 +1,21 @@
|
||||
acounts,accounts
|
||||
canidate,candidate
|
||||
comfirm,confirm
|
||||
converage,coverage
|
||||
currrently,currently
|
||||
delimeter,delimiter
|
||||
differrent,different
|
||||
exclusing,excluding
|
||||
finshed,finished
|
||||
formated,formatted
|
||||
inderect,indirect
|
||||
insuficient,insufficient
|
||||
likly,likely
|
||||
mergable,mergeable
|
||||
overrided,overridden
|
||||
priortized,prioritized
|
||||
registeration,registration
|
||||
reuqest,request
|
||||
reviwer,reviewer
|
||||
superceded,superseded
|
||||
underlaying,underlying
|
||||
|
@@ -7,129 +7,213 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
)
|
||||
|
||||
const (
|
||||
emojiTestURL = "https://www.unicode.org/Public/17.0.0/emoji/emoji-test.txt"
|
||||
jsonFile = "public/assets/emoji.json"
|
||||
gemojiURL = "https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json"
|
||||
maxUnicodeVersion = 16
|
||||
)
|
||||
|
||||
type emoji struct {
|
||||
Emoji string `json:"emoji"`
|
||||
Aliases []string `json:"aliases"`
|
||||
description string
|
||||
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
|
||||
|
||||
// Gemoji is a set of emoji data.
|
||||
type Gemoji []Emoji
|
||||
|
||||
// Emoji represents a single emoji and associated data.
|
||||
type Emoji struct {
|
||||
Emoji string `json:"emoji"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Aliases []string `json:"aliases"`
|
||||
UnicodeVersion string `json:"unicode_version,omitempty"`
|
||||
SkinTones bool `json:"skin_tones,omitempty"`
|
||||
}
|
||||
|
||||
var slugRe = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`[^a-z0-9]+`)
|
||||
})
|
||||
// Don't include some fields in JSON
|
||||
func (e Emoji) MarshalJSON() ([]byte, error) {
|
||||
type emoji Emoji
|
||||
x := emoji(e)
|
||||
x.UnicodeVersion = ""
|
||||
x.Description = ""
|
||||
x.SkinTones = false
|
||||
return json.Marshal(x)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := generate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
func generate() error {
|
||||
// the existing file is also the alias source, so existing aliases stay stable
|
||||
existing, err := os.ReadFile(jsonFile)
|
||||
// generate data
|
||||
buf, err := generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var existingEmojis []*emoji
|
||||
if err := json.Unmarshal(existing, &existingEmojis); err != nil {
|
||||
return err
|
||||
}
|
||||
existingAliases := make(map[string][]string, len(existingEmojis))
|
||||
for _, e := range existingEmojis {
|
||||
existingAliases[e.Emoji] = e.Aliases
|
||||
log.Fatalf("generate err: %v", err)
|
||||
}
|
||||
|
||||
emojis, err := fetchEmojis(existingAliases)
|
||||
// write
|
||||
err = os.WriteFile(*flagOut, buf, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatalf("WriteFile err: %v", err)
|
||||
}
|
||||
|
||||
lines := make([]string, len(emojis))
|
||||
for i, e := range emojis {
|
||||
line, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines[i] = string(line)
|
||||
}
|
||||
return os.WriteFile(jsonFile, []byte("[\n"+strings.Join(lines, ",\n")+"\n]\n"), 0o644)
|
||||
}
|
||||
|
||||
func isSkinTone(r rune) bool {
|
||||
return r >= 0x1f3fb && r <= 0x1f3ff
|
||||
}
|
||||
var replacer = strings.NewReplacer(
|
||||
"main.Gemoji", "Gemoji",
|
||||
"main.Emoji", "\n",
|
||||
"}}", "},\n}",
|
||||
", Description:", ", ",
|
||||
", Aliases:", ", ",
|
||||
", UnicodeVersion:", ", ",
|
||||
", SkinTones:", ", ",
|
||||
)
|
||||
|
||||
func fetchEmojis(existingAliases map[string][]string) ([]*emoji, error) {
|
||||
res, err := http.Get(emojiTestURL)
|
||||
var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
|
||||
|
||||
func generate() ([]byte, error) {
|
||||
// load gemoji data
|
||||
res, err := http.Get(gemojiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("fetching %s: %s", emojiTestURL, res.Status)
|
||||
}
|
||||
|
||||
var emojis []*emoji
|
||||
scanner := bufio.NewScanner(res.Body)
|
||||
for scanner.Scan() {
|
||||
// e.g. "1F44D ; fully-qualified # 👍 E0.6 thumbs up"
|
||||
_, rest, _ := strings.Cut(scanner.Text(), ";")
|
||||
status, comment, _ := strings.Cut(rest, "#")
|
||||
if strings.TrimSpace(status) != "fully-qualified" {
|
||||
continue
|
||||
}
|
||||
fields := strings.SplitN(strings.TrimSpace(comment), " ", 3)
|
||||
if strings.ContainsFunc(fields[0], isSkinTone) {
|
||||
continue
|
||||
}
|
||||
emojis = append(emojis, &emoji{Emoji: fields[0], description: fields[2]})
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
// read all
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
aliasOwners := map[string]string{}
|
||||
for _, e := range emojis {
|
||||
e.Aliases = existingAliases[e.Emoji]
|
||||
if e.Aliases == nil {
|
||||
e.Aliases = []string{strings.Trim(slugRe().ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
|
||||
}
|
||||
delete(existingAliases, e.Emoji)
|
||||
for _, alias := range e.Aliases {
|
||||
if owner, ok := aliasOwners[alias]; ok {
|
||||
errs = append(errs, fmt.Errorf("alias %q used by both %q and %q", alias, owner, e.Emoji))
|
||||
}
|
||||
aliasOwners[alias] = e.Emoji
|
||||
}
|
||||
}
|
||||
for code, aliases := range existingAliases {
|
||||
errs = append(errs, fmt.Errorf("emoji %q with aliases %v is missing from Unicode data", code, aliases))
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
// unmarshal
|
||||
var data Gemoji
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slices.SortFunc(emojis, func(a, b *emoji) int {
|
||||
return strings.Compare(a.Aliases[0], b.Aliases[0])
|
||||
skinTones := make(map[string]string)
|
||||
|
||||
skinTones["\U0001f3fb"] = "Light Skin Tone"
|
||||
skinTones["\U0001f3fc"] = "Medium-Light Skin Tone"
|
||||
skinTones["\U0001f3fd"] = "Medium Skin Tone"
|
||||
skinTones["\U0001f3fe"] = "Medium-Dark Skin Tone"
|
||||
skinTones["\U0001f3ff"] = "Dark Skin Tone"
|
||||
|
||||
var tmp Gemoji
|
||||
|
||||
// filter out emoji that require greater than max unicode version
|
||||
for i := range data {
|
||||
val, _ := strconv.ParseFloat(data[i].UnicodeVersion, 64)
|
||||
if int(val) <= maxUnicodeVersion {
|
||||
tmp = append(tmp, data[i])
|
||||
}
|
||||
}
|
||||
data = tmp
|
||||
|
||||
sort.Slice(data, func(i, j int) bool {
|
||||
return data[i].Aliases[0] < data[j].Aliases[0]
|
||||
})
|
||||
return emojis, nil
|
||||
|
||||
aliasMap := make(map[string]int, len(data))
|
||||
|
||||
for i, e := range data {
|
||||
if e.Emoji == "" || len(e.Aliases) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, a := range e.Aliases {
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
aliasMap[a] = i
|
||||
}
|
||||
}
|
||||
|
||||
// gitea customizations
|
||||
i, ok := aliasMap["tada"]
|
||||
if ok {
|
||||
data[i].Aliases = append(data[i].Aliases, "hooray")
|
||||
}
|
||||
i, ok = aliasMap["laughing"]
|
||||
if ok {
|
||||
data[i].Aliases = append(data[i].Aliases, "laugh")
|
||||
}
|
||||
|
||||
// write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet)
|
||||
file, _ := json.MarshalIndent(data, "", " ")
|
||||
_ = os.WriteFile("assets/emoji.json", append(file, '\n'), 0o644)
|
||||
|
||||
// Add skin tones to emoji that support it
|
||||
var (
|
||||
s []string
|
||||
newEmoji string
|
||||
newDescription string
|
||||
newData Emoji
|
||||
)
|
||||
|
||||
for i := range data {
|
||||
if data[i].SkinTones {
|
||||
for k, v := range skinTones {
|
||||
s = strings.Split(data[i].Emoji, "")
|
||||
|
||||
if utf8.RuneCountInString(data[i].Emoji) == 1 {
|
||||
s = append(s, k)
|
||||
} else {
|
||||
// insert into slice after first element because all emoji that support skin tones
|
||||
// have that modifier placed at this spot
|
||||
s = append(s, "")
|
||||
copy(s[2:], s[1:])
|
||||
s[1] = k
|
||||
}
|
||||
|
||||
newEmoji = strings.Join(s, "")
|
||||
newDescription = data[i].Description + ": " + v
|
||||
newAlias := data[i].Aliases[0] + "_" + strings.ReplaceAll(v, " ", "_")
|
||||
|
||||
newData = Emoji{newEmoji, newDescription, []string{newAlias}, "12.0", false}
|
||||
data = append(data, newData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(data, func(i, j int) bool {
|
||||
return data[i].Aliases[0] < data[j].Aliases[0]
|
||||
})
|
||||
|
||||
// add header
|
||||
str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data))
|
||||
|
||||
// change the format of the unicode string
|
||||
str = emojiRE.ReplaceAllStringFunc(str, func(s string) string {
|
||||
var err error
|
||||
s, err = strconv.Unquote(s[len("{Emoji:"):])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return "{" + strconv.QuoteToASCII(s)
|
||||
})
|
||||
|
||||
// format
|
||||
return format.Source([]byte(str))
|
||||
}
|
||||
|
||||
const hdr = `
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
package emoji
|
||||
|
||||
// Code generated by build/generate-emoji.go. DO NOT EDIT.
|
||||
// Sourced from %s
|
||||
var GemojiData = %#v
|
||||
`
|
||||
|
||||
@@ -15,24 +15,17 @@ import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
licenseRe *regexp.Regexp
|
||||
primaryLicenseRe *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
// regexp is based on go-license, excluding README and NOTICE
|
||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
||||
// also defined in vite.config.ts
|
||||
ret.licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||
// When a directory has both primary and variant files (e.g. LICENSE and
|
||||
// LICENSE.docs), only the primary files are kept.
|
||||
ret.primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
||||
return ret
|
||||
})
|
||||
// regexp is based on go-license, excluding README and NOTICE
|
||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
||||
// also defined in vite.config.ts
|
||||
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||
|
||||
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||
// When a directory has both primary and variant files (e.g. LICENSE and
|
||||
// LICENSE.docs), only the primary files are kept.
|
||||
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
||||
|
||||
// ignoredNames are LicenseEntry.Name values to exclude from the output.
|
||||
var ignoredNames = map[string]bool{
|
||||
@@ -151,7 +144,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !globalVars().licenseRe.MatchString(name) {
|
||||
if !licenseRe.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
if excludedExt[strings.ToLower(filepath.Ext(name))] {
|
||||
@@ -188,7 +181,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
|
||||
var primary []LicenseEntry
|
||||
for _, e := range entries {
|
||||
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
|
||||
if globalVars().primaryLicenseRe.MatchString(fileName) {
|
||||
if primaryLicenseRe.MatchString(fileName) {
|
||||
primary = append(primary, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// cleaner SDK output with proper enum types instead of anonymous strings.
|
||||
//
|
||||
// Run: go run build/generate-openapi.go
|
||||
// Output: templates/swagger/v1-openapi3.generated.json
|
||||
// Output: templates/swagger/v1_openapi3_json.tmpl
|
||||
|
||||
//go:build ignore
|
||||
|
||||
@@ -21,21 +21,35 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/build/openapi3gen"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
const (
|
||||
swaggerSpecPath = "templates/swagger/v1-swagger.generated.json"
|
||||
openapi3OutPath = "templates/swagger/v1-openapi3.generated.json"
|
||||
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
|
||||
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
|
||||
|
||||
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
|
||||
appVerVar = "{{.SwaggerAppVer}}"
|
||||
|
||||
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
|
||||
appVerPlaceholder = "0.0.0-gitea-placeholder"
|
||||
)
|
||||
|
||||
var enumScanDirs = []string{
|
||||
"modules/structs",
|
||||
"modules/commitstatus",
|
||||
}
|
||||
var (
|
||||
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
|
||||
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
|
||||
|
||||
enumScanDirs = []string{
|
||||
"modules/structs",
|
||||
"modules/commitstatus",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs)
|
||||
@@ -54,17 +68,28 @@ func main() {
|
||||
log.Fatalf("reading swagger spec: %v", err)
|
||||
}
|
||||
|
||||
oas3, err := openapi3gen.Convert(data, astEnumMap)
|
||||
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
|
||||
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
|
||||
|
||||
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
|
||||
if err != nil {
|
||||
log.Fatalf("converting to openapi 3.0: %v", err)
|
||||
}
|
||||
|
||||
oas3.Servers = openapi3.Servers{
|
||||
{URL: appSubUrlPlaceholder + "/api/v1"},
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(oas3, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshaling openapi 3.0: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(openapi3OutPath, out, 0o644); err != nil {
|
||||
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
|
||||
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
|
||||
result = strings.TrimSpace(result)
|
||||
|
||||
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
|
||||
log.Fatalf("writing openapi 3.0 spec: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
|
||||
@@ -17,19 +16,11 @@ import (
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
rxDeprecated *regexp.Regexp
|
||||
rxSwaggerEnum *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
// rxDeprecated matches "deprecated" as a word at the start of a description
|
||||
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
|
||||
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
|
||||
// like "not deprecated" or "previously deprecated, now supported".
|
||||
ret.rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
|
||||
ret.rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
|
||||
return ret
|
||||
})
|
||||
// rxDeprecated matches "deprecated" as a word at the start of a description
|
||||
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
|
||||
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
|
||||
// like "not deprecated" or "previously deprecated, now supported".
|
||||
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
|
||||
|
||||
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
|
||||
// Gitea-specific post-processing: file-schema fixups, URI formats,
|
||||
@@ -167,7 +158,7 @@ func addDeprecatedFlags(doc *openapi3.T) {
|
||||
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||
continue
|
||||
}
|
||||
if globalVars().rxDeprecated.MatchString(propRef.Value.Description) {
|
||||
if rxDeprecated.MatchString(propRef.Value.Description) {
|
||||
propRef.Value.Deprecated = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,19 +42,27 @@ func TestDeriveEnumName_miss(t *testing.T) {
|
||||
func TestExtractSharedEnums_usesASTMap(t *testing.T) {
|
||||
doc := &openapi3.T{
|
||||
Components: &openapi3.Components{
|
||||
Schemas: openapi3.Schemas{},
|
||||
},
|
||||
}
|
||||
for _, name := range []string{"A", "B"} {
|
||||
doc.Components.Schemas[name] = &openapi3.SchemaRef{Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green", "blue"},
|
||||
Schemas: openapi3.Schemas{
|
||||
"A": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green", "blue"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"B": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green", "blue"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}}
|
||||
},
|
||||
}
|
||||
astMap := map[string][]string{EnumKey([]any{"red", "green", "blue"}): {"Color"}}
|
||||
if err := extractSharedEnums(doc, astMap); err != nil {
|
||||
@@ -182,19 +190,27 @@ func TestExtractEnumTypeName_rejectsIncidentalPrefix(t *testing.T) {
|
||||
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
|
||||
doc := &openapi3.T{
|
||||
Components: &openapi3.Components{
|
||||
Schemas: openapi3.Schemas{},
|
||||
},
|
||||
}
|
||||
for _, name := range []string{"A", "B"} {
|
||||
doc.Components.Schemas[name] = &openapi3.SchemaRef{Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green"},
|
||||
Schemas: openapi3.Schemas{
|
||||
"A": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"B": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}}
|
||||
},
|
||||
}
|
||||
if err := extractSharedEnums(doc, map[string][]string{}); err == nil {
|
||||
t.Fatal("expected miss error")
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -30,6 +31,8 @@ func EnumKey(values []any) string {
|
||||
return strings.Join(strs, "|")
|
||||
}
|
||||
|
||||
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
|
||||
|
||||
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
|
||||
// a canonical value-set key (see EnumKey) to the Go type names declared with
|
||||
// // swagger:enum TypeName. Multiple type names per key are allowed (e.g.
|
||||
@@ -143,7 +146,7 @@ func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes m
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
matches := globalVars().rxSwaggerEnum.FindStringSubmatch(doc.Text())
|
||||
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
|
||||
if len(matches) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,17 +33,9 @@ func TestEnumKey_handlesNonStringValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes(t *testing.T) {
|
||||
scan := func(t *testing.T, filename, src string) (map[string][]string, error) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, filename), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ScanSwaggerEnumTypes([]string{dir})
|
||||
}
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
got, err := scan(t, "color.go", `package fixture
|
||||
func TestScanSwaggerEnumTypes_basic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// Color is a primary color.
|
||||
// swagger:enum Color
|
||||
@@ -54,18 +46,24 @@ const (
|
||||
ColorGreen Color = "green"
|
||||
ColorBlue Color = "blue"
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"red", "green", "blue"})
|
||||
if single(got, wantKey) != "Color" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
|
||||
}
|
||||
})
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "color.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("orphanAnnotation", func(t *testing.T) {
|
||||
_, err := scan(t, "typo.go", `package fixture
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"red", "green", "blue"})
|
||||
if single(got, wantKey) != "Color" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_orphanAnnotation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// swagger:enum Sttype
|
||||
type StateType string
|
||||
@@ -73,17 +71,23 @@ type StateType string
|
||||
const (
|
||||
StateOpen StateType = "open"
|
||||
)
|
||||
`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for annotation referencing a non-matching type name")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Sttype") {
|
||||
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
|
||||
}
|
||||
})
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("collision", func(t *testing.T) {
|
||||
got, err := scan(t, "dup.go", `package fixture
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for annotation referencing a non-matching type name")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Sttype") {
|
||||
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_collision(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// swagger:enum Alpha
|
||||
type Alpha string
|
||||
@@ -98,90 +102,52 @@ const (
|
||||
BetaX Beta = "x"
|
||||
BetaY Beta = "y"
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
key := EnumKey([]any{"x", "y"})
|
||||
names := got[key]
|
||||
if !slices.Equal(names, []string{"Alpha", "Beta"}) {
|
||||
t.Fatalf("map[%q] = %v, want [Alpha Beta]", key, names)
|
||||
}
|
||||
})
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("parseFailure", func(t *testing.T) {
|
||||
_, err := scan(t, "bad.go", "package fixture\nfunc Foo() {")
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error, got nil")
|
||||
}
|
||||
})
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
key := EnumKey([]any{"x", "y"})
|
||||
names := got[key]
|
||||
if !slices.Equal(names, []string{"Alpha", "Beta"}) {
|
||||
t.Fatalf("map[%q] = %v, want [Alpha Beta]", key, names)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("annotationWithoutConsts", func(t *testing.T) {
|
||||
_, err := scan(t, "lonely.go", `package fixture
|
||||
func TestScanSwaggerEnumTypes_parseFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "bad.go"), []byte("package fixture\nfunc Foo() {"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_annotationWithoutConsts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// swagger:enum Lonely
|
||||
type Lonely string
|
||||
`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for annotation without consts")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Lonely") {
|
||||
t.Fatalf("error %q should mention Lonely", err.Error())
|
||||
}
|
||||
})
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("constsBeforeType", func(t *testing.T) {
|
||||
got, err := scan(t, "shade.go", `package fixture
|
||||
|
||||
const (
|
||||
ShadeDark Shade = "dark"
|
||||
ShadeLight Shade = "light"
|
||||
)
|
||||
|
||||
// swagger:enum Shade
|
||||
type Shade string
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"dark", "light"})
|
||||
if single(got, wantKey) != "Shade" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("groupedTypeDecl", func(t *testing.T) {
|
||||
got, err := scan(t, "grouped.go", `package fixture
|
||||
|
||||
type (
|
||||
// swagger:enum Color
|
||||
Color string
|
||||
// swagger:enum Shade
|
||||
Shade string
|
||||
)
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
ColorBlue Color = "blue"
|
||||
)
|
||||
|
||||
const (
|
||||
ShadeDark Shade = "dark"
|
||||
ShadeLight Shade = "light"
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
colorKey := EnumKey([]any{"red", "blue"})
|
||||
shadeKey := EnumKey([]any{"dark", "light"})
|
||||
if single(got, colorKey) != "Color" {
|
||||
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
|
||||
}
|
||||
if single(got, shadeKey) != "Shade" {
|
||||
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
|
||||
}
|
||||
})
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for annotation without consts")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Lonely") {
|
||||
t.Fatalf("error %q should mention Lonely", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_constsAndTypeInDifferentFiles(t *testing.T) {
|
||||
@@ -216,3 +182,68 @@ type Hue string
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_constsBeforeType(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
const (
|
||||
ShadeDark Shade = "dark"
|
||||
ShadeLight Shade = "light"
|
||||
)
|
||||
|
||||
// swagger:enum Shade
|
||||
type Shade string
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "shade.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"dark", "light"})
|
||||
if single(got, wantKey) != "Shade" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_groupedTypeDecl(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
type (
|
||||
// swagger:enum Color
|
||||
Color string
|
||||
// swagger:enum Shade
|
||||
Shade string
|
||||
)
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
ColorBlue Color = "blue"
|
||||
)
|
||||
|
||||
const (
|
||||
ShadeDark Shade = "dark"
|
||||
ShadeLight Shade = "light"
|
||||
)
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "grouped.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
colorKey := EnumKey([]any{"red", "blue"})
|
||||
shadeKey := EnumKey([]any{"dark", "light"})
|
||||
if single(got, colorKey) != "Color" {
|
||||
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
|
||||
}
|
||||
if single(got, shadeKey) != "Shade" {
|
||||
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
|
||||
}
|
||||
}
|
||||
|
||||
10
cliff.toml
10
cliff.toml
@@ -1,10 +0,0 @@
|
||||
[git]
|
||||
commit_parsers = [
|
||||
{ message = "^(chore|ci)(\\([\\w/.-]+\\))?!?: ", skip = true },
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^enhance", group = "Enhancements" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = ".*", group = "Miscellaneous" },
|
||||
]
|
||||
@@ -5,6 +5,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -48,6 +49,6 @@ func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
|
||||
if extra.HasError() {
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
cprintln(c, respText.Text)
|
||||
_, _ = fmt.Printf("%s\n", respText.Text)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
|
||||
@@ -54,9 +55,6 @@ func newAuthCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "auth",
|
||||
Usage: "Modify external auth providers",
|
||||
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
|
||||
return cliAuditContext(ctx), nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
microcmdAuthAddOauth(),
|
||||
microcmdAuthUpdateOauth(),
|
||||
@@ -130,8 +128,8 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
|
||||
}
|
||||
log.Trace("Processing next %d repos of %d", len(repos), count)
|
||||
for _, repo := range repos {
|
||||
log.Trace("Synchronizing repo %s", repo.FullName())
|
||||
gitRepo, err := git.OpenRepository(ctx, repo)
|
||||
log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath())
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Warn("OpenRepository: %v", err)
|
||||
continue
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/modules/util"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/auth/source/ldap"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -49,11 +48,11 @@ func commonLdapCLIFlags() []cli.Flag {
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "Comma-separated LDAP hosts or ldap://, ldaps:// or ldap+starttls:// URLs, tried in order.",
|
||||
Usage: "The address where the LDAP server can be reached.",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "The port to use when connecting to hosts given without a URL.",
|
||||
Usage: "The port to use when connecting to the LDAP server.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-search-base",
|
||||
@@ -222,8 +221,8 @@ func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
|
||||
func newAuthService() *authService {
|
||||
return &authService{
|
||||
initDB: initDB,
|
||||
createAuthSource: auth_service.CreateSource,
|
||||
updateAuthSource: auth_service.UpdateSource,
|
||||
createAuthSource: auth.CreateSource,
|
||||
updateAuthSource: auth.UpdateSource,
|
||||
getAuthSourceByID: auth.GetSourceByID,
|
||||
}
|
||||
}
|
||||
@@ -355,10 +354,13 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// getAuthSourceOfType gets the login source by id.
|
||||
// It returns an error if the id does not match any source or if the source is not of the expected type.
|
||||
func (a *authService) getAuthSourceOfType(ctx context.Context, id int64, authType auth.Type) (*auth.Source, error) {
|
||||
authSource, err := a.getAuthSourceByID(ctx, id)
|
||||
// getAuthSource gets the login source by its id defined in the command line flags.
|
||||
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
|
||||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
|
||||
if err := argsSet(c, "id"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -370,15 +372,6 @@ func (a *authService) getAuthSourceOfType(ctx context.Context, id int64, authTyp
|
||||
return authSource, nil
|
||||
}
|
||||
|
||||
// getAuthSource gets the login source by its id defined in the command line flags.
|
||||
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
|
||||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
|
||||
if err := argsSet(c, "id"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.getAuthSourceOfType(ctx, c.Int64("id"), authType)
|
||||
}
|
||||
|
||||
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
|
||||
func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
|
||||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
|
||||
@@ -388,17 +381,16 @@ func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapConfig := &ldap.Source{
|
||||
Enabled: true, // always true
|
||||
}
|
||||
authSource := &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
IsActive: true, // active by default
|
||||
Cfg: ldapConfig,
|
||||
Cfg: &ldap.Source{
|
||||
Enabled: true, // always true
|
||||
},
|
||||
}
|
||||
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, ldapConfig); err != nil {
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -415,10 +407,9 @@ func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ldapConfig := auth.MustSourceCfg[*ldap.Source](authSource)
|
||||
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, ldapConfig); err != nil {
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -435,17 +426,16 @@ func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) err
|
||||
return err
|
||||
}
|
||||
|
||||
ldapConfig := &ldap.Source{
|
||||
Enabled: true, // always true
|
||||
}
|
||||
authSource := &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
IsActive: true, // active by default
|
||||
Cfg: ldapConfig,
|
||||
Cfg: &ldap.Source{
|
||||
Enabled: true, // always true
|
||||
},
|
||||
}
|
||||
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, ldapConfig); err != nil {
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -462,10 +452,9 @@ func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ldapConfig := auth.MustSourceCfg[*ldap.Source](authSource)
|
||||
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, ldapConfig); err != nil {
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -489,20 +489,13 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
|
||||
// Test cases
|
||||
type testCase struct {
|
||||
cases := []struct {
|
||||
args []string
|
||||
id int64
|
||||
existingAuthSource *auth.Source
|
||||
authSource *auth.Source
|
||||
errMsg string
|
||||
}
|
||||
withConfig := func(config *ldap.Source, args ...string) testCase {
|
||||
return testCase{
|
||||
args: append([]string{"ldap-test", "--id", "1"}, args...),
|
||||
authSource: &auth.Source{Type: auth.LDAP, Cfg: config},
|
||||
}
|
||||
}
|
||||
cases := []testCase{
|
||||
}{
|
||||
// case 0
|
||||
{
|
||||
args: []string{
|
||||
@@ -581,7 +574,16 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// case 1
|
||||
withConfig(&ldap.Source{}),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{},
|
||||
},
|
||||
},
|
||||
// case 2
|
||||
{
|
||||
args: []string{
|
||||
@@ -616,44 +618,215 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// case 4
|
||||
withConfig(&ldap.Source{SecurityProtocol: ldap.SecurityProtocol(1)}, "--security-protocol", "LDAPS"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--security-protocol", "LDAPS",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
SecurityProtocol: ldap.SecurityProtocol(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 5
|
||||
withConfig(&ldap.Source{SkipVerify: true}, "--skip-tls-verify"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--skip-tls-verify",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
SkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 6
|
||||
withConfig(&ldap.Source{Host: "ldap-server"}, "--host", "ldap-server"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--host", "ldap-server",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
Host: "ldap-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 7
|
||||
withConfig(&ldap.Source{Port: 389}, "--port", "389"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--port", "389",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
Port: 389,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 8
|
||||
withConfig(
|
||||
&ldap.Source{UserBase: "ou=Users,dc=domain,dc=org"},
|
||||
"--user-search-base", "ou=Users,dc=domain,dc=org",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--user-search-base", "ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
UserBase: "ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 9
|
||||
withConfig(
|
||||
&ldap.Source{Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)"},
|
||||
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 10
|
||||
withConfig(
|
||||
&ldap.Source{AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)"},
|
||||
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 11
|
||||
withConfig(&ldap.Source{AttributeUsername: "uid"}, "--username-attribute", "uid"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--username-attribute", "uid",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeUsername: "uid",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 12
|
||||
withConfig(&ldap.Source{AttributeName: "givenName"}, "--firstname-attribute", "givenName"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--firstname-attribute", "givenName",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeName: "givenName",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 13
|
||||
withConfig(&ldap.Source{AttributeSurname: "sn"}, "--surname-attribute", "sn"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--surname-attribute", "sn",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeSurname: "sn",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 14
|
||||
withConfig(&ldap.Source{AttributeMail: "mail"}, "--email-attribute", "mail"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--email-attribute", "mail",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeMail: "mail",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 15
|
||||
withConfig(&ldap.Source{AttributesInBind: true}, "--attributes-in-bind"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--attributes-in-bind",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributesInBind: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 16
|
||||
withConfig(&ldap.Source{AttributeSSHPublicKey: "publickey"}, "--public-ssh-key-attribute", "publickey"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--public-ssh-key-attribute", "publickey",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeSSHPublicKey: "publickey",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 17
|
||||
withConfig(&ldap.Source{BindDN: "cn=readonly,dc=domain,dc=org"}, "--bind-dn", "cn=readonly,dc=domain,dc=org"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--bind-dn", "cn=readonly,dc=domain,dc=org",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
BindDN: "cn=readonly,dc=domain,dc=org",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 18
|
||||
withConfig(&ldap.Source{BindPassword: "secret"}, "--bind-password", "secret"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--bind-password", "secret",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
BindPassword: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 19
|
||||
{
|
||||
args: []string{
|
||||
@@ -668,7 +841,19 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// case 20
|
||||
withConfig(&ldap.Source{SearchPageSize: 12}, "--page-size", "12"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--page-size", "12",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.LDAP,
|
||||
Cfg: &ldap.Source{
|
||||
SearchPageSize: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 21
|
||||
{
|
||||
args: []string{
|
||||
@@ -779,20 +964,13 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
|
||||
// Test cases
|
||||
type testCase struct {
|
||||
cases := []struct {
|
||||
args []string
|
||||
id int64
|
||||
existingAuthSource *auth.Source
|
||||
authSource *auth.Source
|
||||
errMsg string
|
||||
}
|
||||
withConfig := func(config *ldap.Source, args ...string) testCase {
|
||||
return testCase{
|
||||
args: append([]string{"ldap-test", "--id", "1"}, args...),
|
||||
authSource: &auth.Source{Type: auth.DLDAP, Cfg: config},
|
||||
}
|
||||
}
|
||||
cases := []testCase{
|
||||
}{
|
||||
// case 0
|
||||
{
|
||||
args: []string{
|
||||
@@ -842,7 +1020,16 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// case 1
|
||||
withConfig(&ldap.Source{}),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{},
|
||||
},
|
||||
},
|
||||
// case 2
|
||||
{
|
||||
args: []string{
|
||||
@@ -877,43 +1064,187 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// case 4
|
||||
withConfig(&ldap.Source{SecurityProtocol: ldap.SecurityProtocol(2)}, "--security-protocol", "starttls"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--security-protocol", "starttls",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
SecurityProtocol: ldap.SecurityProtocol(2),
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 5
|
||||
withConfig(&ldap.Source{SkipVerify: true}, "--skip-tls-verify"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--skip-tls-verify",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
SkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 6
|
||||
withConfig(&ldap.Source{Host: "ldap-server"}, "--host", "ldap-server"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--host", "ldap-server",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
Host: "ldap-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 7
|
||||
withConfig(&ldap.Source{Port: 987}, "--port", "987"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--port", "987",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
Port: 987,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 8
|
||||
withConfig(
|
||||
&ldap.Source{UserBase: "ou=Users,dc=domain,dc=org"},
|
||||
"--user-search-base", "ou=Users,dc=domain,dc=org",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--user-search-base", "ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
UserBase: "ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 9
|
||||
withConfig(
|
||||
&ldap.Source{Filter: "(&(objectClass=posixAccount)(cn=%s))"},
|
||||
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
Filter: "(&(objectClass=posixAccount)(cn=%s))",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 10
|
||||
withConfig(
|
||||
&ldap.Source{AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)"},
|
||||
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 11
|
||||
withConfig(&ldap.Source{AttributeUsername: "uid"}, "--username-attribute", "uid"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--username-attribute", "uid",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeUsername: "uid",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 12
|
||||
withConfig(&ldap.Source{AttributeName: "givenName"}, "--firstname-attribute", "givenName"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--firstname-attribute", "givenName",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeName: "givenName",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 13
|
||||
withConfig(&ldap.Source{AttributeSurname: "sn"}, "--surname-attribute", "sn"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--surname-attribute", "sn",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeSurname: "sn",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 14
|
||||
withConfig(&ldap.Source{AttributeMail: "mail"}, "--email-attribute", "mail"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--email-attribute", "mail",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeMail: "mail",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 15
|
||||
withConfig(&ldap.Source{AttributeSSHPublicKey: "publickey"}, "--public-ssh-key-attribute", "publickey"),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--public-ssh-key-attribute", "publickey",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
AttributeSSHPublicKey: "publickey",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 16
|
||||
withConfig(
|
||||
&ldap.Source{UserDN: "cn=%s,ou=Users,dc=domain,dc=org"},
|
||||
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
|
||||
),
|
||||
{
|
||||
args: []string{
|
||||
"ldap-test",
|
||||
"--id", "1",
|
||||
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.DLDAP,
|
||||
Cfg: &ldap.Source{
|
||||
UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 17
|
||||
{
|
||||
args: []string{
|
||||
|
||||
@@ -221,11 +221,12 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := a.getAuthSourceOfType(ctx, c.Int64("id"), auth_model.OAuth2)
|
||||
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oAuth2Config := auth_model.MustSourceCfg[*oauth2.Source](source)
|
||||
|
||||
oAuth2Config := source.Cfg.(*oauth2.Source)
|
||||
|
||||
if c.IsSet("name") {
|
||||
source.Name = c.String("name")
|
||||
|
||||
@@ -16,11 +16,10 @@ import (
|
||||
|
||||
func TestAddOauth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
config *oauth2.Source
|
||||
twoFactorPolicy string
|
||||
errMsg string
|
||||
name string
|
||||
args []string
|
||||
source *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
@@ -30,11 +29,17 @@ func TestAddOauth(t *testing.T) {
|
||||
"--key", "some_key",
|
||||
"--secret", "some_secret",
|
||||
},
|
||||
config: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
Provider: "github",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
Provider: "github",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -46,12 +51,18 @@ func TestAddOauth(t *testing.T) {
|
||||
"--secret", "some_secret",
|
||||
"--auto-discover-url", "https://example.com",
|
||||
},
|
||||
config: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
Provider: "openidConnect",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com",
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
Provider: "openidConnect",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -80,30 +91,35 @@ func TestAddOauth(t *testing.T) {
|
||||
"--ssh-public-key-claim-name", "attr_ssh_pub_key",
|
||||
"--full-name-claim-name", "attr_full_name",
|
||||
},
|
||||
config: &oauth2.Source{
|
||||
Provider: "gitlab",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: "https://example.com/token",
|
||||
AuthURL: "https://example.com/auth",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "some_tenant",
|
||||
source: &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "gitlab",
|
||||
ClientID: "some_key",
|
||||
ClientSecret: "some_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: "https://example.com/token",
|
||||
AuthURL: "https://example.com/auth",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "some_tenant",
|
||||
},
|
||||
IconURL: "https://example.com/icon",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
RequiredClaimName: "claim_name",
|
||||
RequiredClaimValue: "claim_value",
|
||||
GroupClaimName: "group_name",
|
||||
AdminGroup: "admin",
|
||||
RestrictedGroup: "restricted",
|
||||
GroupTeamMap: `{"group1": [1,2]}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
SSHPublicKeyClaimName: "attr_ssh_pub_key",
|
||||
FullNameClaimName: "attr_full_name",
|
||||
},
|
||||
IconURL: "https://example.com/icon",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
RequiredClaimName: "claim_name",
|
||||
RequiredClaimValue: "claim_value",
|
||||
GroupClaimName: "group_name",
|
||||
AdminGroup: "admin",
|
||||
RestrictedGroup: "restricted",
|
||||
GroupTeamMap: `{"group1": [1,2]}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
SSHPublicKeyClaimName: "attr_ssh_pub_key",
|
||||
FullNameClaimName: "attr_full_name",
|
||||
TwoFactorPolicy: "skip",
|
||||
},
|
||||
twoFactorPolicy: "skip",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -134,13 +150,7 @@ func TestAddOauth(t *testing.T) {
|
||||
assert.EqualError(t, err, tc.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "test",
|
||||
IsActive: true,
|
||||
Cfg: tc.config,
|
||||
TwoFactorPolicy: tc.twoFactorPolicy,
|
||||
}, createdSource)
|
||||
assert.Equal(t, tc.source, createdSource)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -148,10 +158,12 @@ func TestAddOauth(t *testing.T) {
|
||||
|
||||
func TestUpdateOauth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
authSource *auth_model.Source
|
||||
errMsg string
|
||||
name string
|
||||
args []string
|
||||
id int64
|
||||
existingAuthSource *auth_model.Source
|
||||
authSource *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing id",
|
||||
@@ -162,6 +174,19 @@ func TestUpdateOauth(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
id: 1,
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "github",
|
||||
ClientID: "old_key",
|
||||
ClientSecret: "old_secret",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
@@ -185,6 +210,37 @@ func TestUpdateOauth(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "valid config with options",
|
||||
id: 1,
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "gitlab",
|
||||
ClientID: "old_key",
|
||||
ClientSecret: "old_secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
TokenURL: "https://old.example.com/token",
|
||||
AuthURL: "https://old.example.com/auth",
|
||||
ProfileURL: "https://old.example.com/profile",
|
||||
EmailURL: "https://old.example.com/email",
|
||||
Tenant: "old_tenant",
|
||||
},
|
||||
IconURL: "https://old.example.com/icon",
|
||||
Scopes: []string{"old_scope1", "old_scope2"},
|
||||
RequiredClaimName: "old_claim_name",
|
||||
RequiredClaimValue: "old_claim_value",
|
||||
GroupClaimName: "old_group_name",
|
||||
AdminGroup: "old_admin",
|
||||
RestrictedGroup: "old_restricted",
|
||||
GroupTeamMap: `{"old_group1": [1,2]}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
SSHPublicKeyClaimName: "old_ssh_pub_key",
|
||||
FullNameClaimName: "old_full_name",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
|
||||
@@ -175,11 +175,12 @@ func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := a.getAuthSourceOfType(ctx, c.Int64("id"), auth_model.SMTP)
|
||||
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
smtpConfig := auth_model.MustSourceCfg[*smtp.Source](source)
|
||||
|
||||
smtpConfig := source.Cfg.(*smtp.Source)
|
||||
|
||||
if err := parseSMTPConfig(c, smtpConfig); err != nil {
|
||||
return err
|
||||
|
||||
@@ -132,10 +132,11 @@ func TestAddSMTP(t *testing.T) {
|
||||
|
||||
func TestUpdateSMTP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
authSource *auth_model.Source
|
||||
errMsg string
|
||||
name string
|
||||
args []string
|
||||
existingAuthSource *auth_model.Source
|
||||
authSource *auth_model.Source
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing id",
|
||||
@@ -148,6 +149,17 @@ func TestUpdateSMTP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
Host: "old host",
|
||||
Port: 26,
|
||||
},
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
@@ -168,6 +180,20 @@ func TestUpdateSMTP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "valid config with options",
|
||||
existingAuthSource: &auth_model.Source{
|
||||
ID: 1,
|
||||
Type: auth_model.SMTP,
|
||||
Name: "old name",
|
||||
IsActive: true,
|
||||
Cfg: &smtp.Source{
|
||||
Auth: "PLAIN",
|
||||
Host: "old host",
|
||||
Port: 26,
|
||||
HeloHostname: "old.example.com",
|
||||
AllowedDomains: "old.example.com",
|
||||
},
|
||||
TwoFactorPolicy: "",
|
||||
},
|
||||
args: []string{
|
||||
"--id", "1",
|
||||
"--name", "test",
|
||||
|
||||
@@ -4,34 +4,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// parseUserTypeFlag parses "--user-type", keeping the lowercase "individual" and "bot" the flag shipped with
|
||||
func parseUserTypeFlag(s string) (user_model.UserType, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "user", "individual":
|
||||
return user_model.UserTypeIndividual, nil
|
||||
case "bot":
|
||||
return user_model.UserTypeBot, nil
|
||||
default:
|
||||
return 0, util.NewInvalidArgumentErrorf("invalid user type %q (expected User, Bot)", s)
|
||||
}
|
||||
}
|
||||
|
||||
func newUserCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
|
||||
return cliAuditContext(ctx), nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
microcmdUserCreate(),
|
||||
newUserListCommand(),
|
||||
@@ -39,8 +18,6 @@ func newUserCommand() *cli.Command {
|
||||
microcmdUserDelete(),
|
||||
newUserGenerateAccessTokenCommand(),
|
||||
microcmdUserMustChangePassword(),
|
||||
microcmdUserDisableTwoFactor(),
|
||||
microcmdUserChangeType(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,6 @@ func runChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
}
|
||||
|
||||
cprintf(c, "%s's password has been successfully updated!\n", user.Name)
|
||||
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
user_service "gitea.dev/services/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func microcmdUserChangeType() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "change-type",
|
||||
Usage: "Convert an account between the user and bot types",
|
||||
Action: runChangeUserType,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "The user to convert",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-type",
|
||||
Usage: "New user type: User or Bot",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runChangeUserType(ctx context.Context, c *cli.Command) error {
|
||||
targetType, err := parseUserTypeFlag(c.String("user-type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !setting.IsInTesting {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, c.String("username"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{UserType: optional.Some(targetType)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cprintf(c, "%s's type has been successfully changed to %s!\n", user.Name, targetType.DisplayName())
|
||||
return nil
|
||||
}
|
||||
@@ -44,8 +44,8 @@ func microcmdUserCreate() *cli.Command {
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "user-type",
|
||||
Usage: "Set user's type: User or Bot",
|
||||
Value: "User",
|
||||
Usage: "Set user's type: individual or bot",
|
||||
Value: "individual",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
@@ -105,16 +105,20 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
|
||||
setting.LoadSettings()
|
||||
|
||||
userType, err := parseUserTypeFlag(c.String("user-type"))
|
||||
if err != nil {
|
||||
return err
|
||||
userTypes := map[string]user_model.UserType{
|
||||
"individual": user_model.UserTypeIndividual,
|
||||
"bot": user_model.UserTypeBot,
|
||||
}
|
||||
userType, ok := userTypes[c.String("user-type")]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
|
||||
}
|
||||
if userType != user_model.UserTypeIndividual {
|
||||
// Some other commands like "change-password" also only support regular user accounts.
|
||||
// Some other commands like "change-password" also only support individual users.
|
||||
// It needs to clarify the "password" behavior for bot users in the future.
|
||||
// At the moment, we do not allow setting password for bot users.
|
||||
if c.IsSet("password") || c.IsSet("random-password") {
|
||||
return errors.New("password can only be set for user accounts")
|
||||
return errors.New("password can only be set for individual users")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +152,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
// codeql[disable-next-line=go/clear-text-logging]
|
||||
cprintf(c, "generated random password is '%s'\n", password)
|
||||
fmt.Printf("generated random password is '%s'\n", password)
|
||||
} else if userType == user_model.UserTypeIndividual {
|
||||
return errors.New("must set either password or random-password flag")
|
||||
}
|
||||
@@ -158,7 +162,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
mustChangePassword := userType == user_model.UserTypeIndividual
|
||||
if c.IsSet("must-change-password") {
|
||||
if userType != user_model.UserTypeIndividual {
|
||||
return errors.New("must-change-password flag can only be set for user accounts")
|
||||
return errors.New("must-change-password flag can only be set for individual users")
|
||||
}
|
||||
// if the flag is set, use the value provided by the user
|
||||
mustChangePassword = c.Bool("must-change-password")
|
||||
@@ -224,7 +228,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
cprintf(c, "New user '%s' has been successfully created!\n", username)
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
@@ -232,7 +236,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
cprintf(c, "Access token was successfully created... %s\n", t.Token)
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,21 +56,14 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
t.Run("UserType", func(t *testing.T) {
|
||||
reset()
|
||||
assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
|
||||
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for user accounts")
|
||||
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for user accounts")
|
||||
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--admin"), "bot user can not be a site administrator")
|
||||
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
|
||||
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
|
||||
|
||||
assert.NoError(t, createUser("u", "--user-type", "bot"))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
|
||||
assert.Equal(t, user_model.UserTypeBot, u.Type)
|
||||
assert.Empty(t, u.Passwd)
|
||||
assert.False(t, u.MustChangePassword, "bot users should not be forced to change password")
|
||||
|
||||
changeType := func(userType string) error {
|
||||
return microcmdUserChangeType().Run(t.Context(), []string{"change-type", "--username", "u", "--user-type", userType})
|
||||
}
|
||||
assert.NoError(t, changeType("User"))
|
||||
assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"}).IsIndividual())
|
||||
})
|
||||
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
|
||||
@@ -24,29 +24,42 @@ func TestAdminUserDelete(t *testing.T) {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
|
||||
}()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"delete user by id", []string{"delete-test", "--id", ""}},
|
||||
{"delete user by username", []string{"delete-test", "--username", "testuser"}},
|
||||
{"delete user by email", []string{"delete-test", "--email", "testuser@gitea.local"}},
|
||||
{"delete user by all 3 attributes", []string{"delete", "--id", "", "--username", "testuser", "--email", "testuser@gitea.local"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.args[1] == "--id" {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
tc.args[2] = strconv.FormatInt(user.ID, 10)
|
||||
}
|
||||
err = microcmdUserDelete().Run(ctx, tc.args)
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
setupTestUser := func(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("delete user by id", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
t.Run("delete user by username", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
t.Run("delete user by email", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
t.Run("delete user by all 3 attributes", func(t *testing.T) {
|
||||
setupTestUser(t)
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminUserDeleteFailure(t *testing.T) {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func microcmdUserDisableTwoFactor() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "disable-2fa",
|
||||
Usage: "Disable two-factor authentication for a user",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username of the user to disable 2FA for",
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of the user to disable 2FA for",
|
||||
},
|
||||
},
|
||||
Action: runDisableTwoFactor,
|
||||
}
|
||||
}
|
||||
|
||||
func runDisableTwoFactor(ctx context.Context, c *cli.Command) error {
|
||||
if !c.IsSet("id") && !c.IsSet("username") {
|
||||
return errors.New("either --id or --username must be provided")
|
||||
}
|
||||
|
||||
if !setting.IsInTesting {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var user *user_model.User
|
||||
var err error
|
||||
if c.IsSet("id") {
|
||||
user, err = user_model.GetUserByID(ctx, c.Int64("id"))
|
||||
} else {
|
||||
user, err = user_model.GetUserByName(ctx, c.String("username"))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// When both selectors are given, make sure they refer to the same user.
|
||||
if c.IsSet("id") && c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
|
||||
return fmt.Errorf("the user with id %d is %q, which does not match the provided username %q", user.ID, user.Name, c.String("username"))
|
||||
}
|
||||
|
||||
totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cprintf(c, "Disabled 2FA for user %q (removed %d TOTP and %d WebAuthn credential(s))\n", user.Name, totp, webAuthn)
|
||||
return nil
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDisableTwoFactorCommand(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}, &auth_model.TwoFactor{}, &auth_model.WebAuthnCredential{}))
|
||||
}()
|
||||
|
||||
t.Run("disable TOTP and WebAuthn", func(t *testing.T) {
|
||||
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "tfuser", "--email", "tfuser@gitea.local", "--random-password"}))
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "tfuser"})
|
||||
|
||||
// Enroll TOTP.
|
||||
tf := &auth_model.TwoFactor{UID: user.ID}
|
||||
require.NoError(t, tf.SetSecret("test-secret"))
|
||||
_, err := tf.GenerateScratchToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
|
||||
|
||||
// Register a WebAuthn credential.
|
||||
_, err = auth_model.CreateCredential(ctx, user.ID, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")})
|
||||
require.NoError(t, err)
|
||||
|
||||
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, has)
|
||||
|
||||
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "tfuser"}))
|
||||
|
||||
// Both factors must be gone afterwards.
|
||||
has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
})
|
||||
|
||||
t.Run("disable by id", func(t *testing.T) {
|
||||
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "iduser", "--email", "iduser@gitea.local", "--random-password"}))
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "iduser"})
|
||||
|
||||
tf := &auth_model.TwoFactor{UID: user.ID}
|
||||
require.NoError(t, tf.SetSecret("test-secret"))
|
||||
require.NoError(t, auth_model.NewTwoFactor(ctx, tf))
|
||||
|
||||
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", strconv.FormatInt(user.ID, 10)}))
|
||||
|
||||
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
})
|
||||
|
||||
t.Run("no enrollment is a no-op", func(t *testing.T) {
|
||||
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "plainuser", "--email", "plainuser@gitea.local", "--random-password"}))
|
||||
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "plainuser"}))
|
||||
})
|
||||
|
||||
t.Run("id and username must match when both given", func(t *testing.T) {
|
||||
require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "matchuser", "--email", "matchuser@gitea.local", "--random-password"}))
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "matchuser"})
|
||||
id := strconv.FormatInt(user.ID, 10)
|
||||
|
||||
// Matching id + username is accepted.
|
||||
require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", id, "--username", "matchuser"}))
|
||||
|
||||
// Mismatched id + username is rejected.
|
||||
cmd := microcmdUserDisableTwoFactor()
|
||||
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
|
||||
err := cmd.Run(ctx, []string{"disable-2fa", "--id", id, "--username", "someotheruser"})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "does not match the provided username")
|
||||
})
|
||||
|
||||
t.Run("failure cases", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "user does not exist",
|
||||
args: []string{"disable-2fa", "--username", "nonexistentuser"},
|
||||
expectedErr: "user does not exist",
|
||||
},
|
||||
{
|
||||
name: "neither id nor username",
|
||||
args: []string{"disable-2fa"},
|
||||
expectedErr: "either --id or --username must be provided",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := microcmdUserDisableTwoFactor()
|
||||
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
|
||||
err := cmd.Run(ctx, tc.args)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectedErr)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -88,9 +88,9 @@ func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
if c.Bool("raw") {
|
||||
cprintln(c, t.Token)
|
||||
fmt.Printf("%s\n", t.Token)
|
||||
} else {
|
||||
cprintf(c, "Access token was successfully created: %s\n", t.Token)
|
||||
fmt.Printf("Access token was successfully created: %s\n", t.Token)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -33,7 +33,7 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
users, err := user_model.GetAllUsersForAdminListing(ctx)
|
||||
users, err := user_model.GetAllUsers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -41,17 +41,17 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
|
||||
|
||||
if c.IsSet("admin") {
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tType\n")
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
|
||||
for _, u := range users {
|
||||
if u.IsAdmin {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%s\n", u.ID, u.Name, u.Email, u.IsActive, u.Type.DisplayName())
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\tType\n")
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
|
||||
for _, u := range users {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\t%s\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID], u.Type.DisplayName())
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -58,6 +59,6 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
// codeql[disable-next-line=go/clear-text-logging]
|
||||
cprintf(c, "Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
|
||||
fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,28 +18,61 @@ func TestMustChangePassword(t *testing.T) {
|
||||
defer func() {
|
||||
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
|
||||
}()
|
||||
for _, name := range []string{"testuser", "testuserexclude"} {
|
||||
require.NoError(t, microcmdUserCreate().Run(t.Context(), []string{"create", "--username", name, "--email", name + "@gitea.local", "--random-password"}))
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
assertUser, assertExcludedUser assert.BoolAssertionFunc
|
||||
}{
|
||||
// Reset password change flag
|
||||
{[]string{"change-test", "--all", "--unset"}, assert.False, assert.False},
|
||||
// Make all users change password
|
||||
{[]string{"change-test", "--all"}, assert.True, assert.True},
|
||||
// Reset password change flag but exclude all tested users
|
||||
{[]string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"}, assert.True, assert.True},
|
||||
// Reset password change flag by listing multiple users
|
||||
{[]string{"change-test", "--unset", "testuser", "testuserexclude"}, assert.False, assert.False},
|
||||
// Exclude a user from all user
|
||||
{[]string{"change-test", "--all", "--exclude", "testuserexclude"}, assert.True, assert.False},
|
||||
// Unset a flag for single user
|
||||
{[]string{"change-test", "--unset", "testuser"}, assert.False, assert.False},
|
||||
} {
|
||||
require.NoError(t, microcmdUserMustChangePassword().Run(t.Context(), tc.args))
|
||||
tc.assertUser(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}).MustChangePassword)
|
||||
tc.assertExcludedUser(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}).MustChangePassword)
|
||||
}
|
||||
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
|
||||
require.NoError(t, err)
|
||||
// Reset password change flag
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, testUser.MustChangePassword)
|
||||
testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Make all users change password
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.True(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Reset password change flag but exclude all tested users
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.True(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Reset password change flag by listing multiple users
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Exclude a user from all user
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.True(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
|
||||
// Unset a flag for single user
|
||||
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
|
||||
assert.False(t, testUser.MustChangePassword)
|
||||
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
|
||||
assert.False(t, testUserExclude.MustChangePassword)
|
||||
}
|
||||
|
||||
17
cmd/audit.go
17
cmd/audit.go
@@ -1,17 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
func cliAuditContext(ctx context.Context) context.Context {
|
||||
ctx = audit.WithOrigin(ctx, audit_model.OriginCLI)
|
||||
return audit.WithDoer(ctx, user_model.NewCliUser())
|
||||
}
|
||||
@@ -76,82 +76,110 @@ func TestCliCmd(t *testing.T) {
|
||||
cli.SubcommandHelpTemplate = "(subcommand help template)"
|
||||
|
||||
cases := []struct {
|
||||
env map[string]string
|
||||
cmds []string
|
||||
exp string
|
||||
env map[string]string
|
||||
cmd string
|
||||
exp string
|
||||
}{
|
||||
// help commands
|
||||
{
|
||||
cmds: []string{"./gitea -h", "./gitea help"},
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
cmd: "./gitea -h",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
{
|
||||
cmds: []string{
|
||||
"./gitea -c /dev/null -h",
|
||||
"./gitea -c /dev/null help",
|
||||
"./gitea help -c /dev/null",
|
||||
"./gitea -c /dev/null test-cmd -h",
|
||||
"./gitea test-cmd -c /dev/null -h",
|
||||
"./gitea test-cmd -h -c /dev/null",
|
||||
"./gitea -c /dev/null test-cmd help",
|
||||
"./gitea test-cmd -c /dev/null help",
|
||||
"./gitea test-cmd help -c /dev/null",
|
||||
},
|
||||
cmd: "./gitea help",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -h -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
// parse paths
|
||||
{
|
||||
cmds: []string{"./gitea test-cmd"},
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
|
||||
cmd: "./gitea test-cmd",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
|
||||
},
|
||||
{
|
||||
cmds: []string{"./gitea -c /tmp/app.ini test-cmd"},
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
cmd: "./gitea -c /tmp/app.ini test-cmd",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
},
|
||||
{
|
||||
cmds: []string{"./gitea test-cmd -c /tmp/app.ini"},
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
cmd: "./gitea test-cmd -c /tmp/app.ini",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmds: []string{"./gitea test-cmd"},
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd",
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmds: []string{"./gitea test-cmd --work-path /tmp/other"},
|
||||
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd --work-path /tmp/other",
|
||||
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmds: []string{"./gitea test-cmd --config /tmp/app-other.ini"},
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
for _, command := range c.cmds {
|
||||
t.Run(command, func(t *testing.T) {
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
for k, v := range c.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
args := strings.Split(command, " ") // for test only, "split" is good enough
|
||||
r, err := runTestApp(app, args...)
|
||||
assert.NoError(t, err, command)
|
||||
assert.NotEmpty(t, c.exp, command)
|
||||
if !assert.Contains(t, r.Stdout, c.exp, command) {
|
||||
t.Log("Full output:\n" + r.Stdout)
|
||||
t.Log("Expected:\n" + c.exp)
|
||||
}
|
||||
t.Run(c.cmd, func(t *testing.T) {
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
for k, v := range c.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
||||
r, err := runTestApp(app, args...)
|
||||
assert.NoError(t, err, c.cmd)
|
||||
assert.NotEmpty(t, c.exp, c.cmd)
|
||||
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
|
||||
t.Log("Full output:\n" + r.Stdout)
|
||||
t.Log("Expected:\n" + c.exp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"gitea.dev/modelmigration"
|
||||
"gitea.dev/modelmigration/base"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/migrations"
|
||||
migrate_base "gitea.dev/models/migrations/base"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -114,8 +114,8 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
setting.Database.LogSQL = debug
|
||||
if err := db.InitEngine(ctx); err != nil {
|
||||
cprintln(cmd, err)
|
||||
cprintln(cmd, "Check if you are using the right config file. You can use a --config directive to specify one.")
|
||||
fmt.Println(err)
|
||||
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,10 +129,10 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recreateTables := base.RecreateTables(beans...)
|
||||
recreateTables := migrate_base.RecreateTables(beans...)
|
||||
|
||||
return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x db.EngineMigration) error {
|
||||
if err := modelmigration.EnsureUpToDate(ctx, x); err != nil {
|
||||
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
|
||||
return err
|
||||
}
|
||||
return recreateTables(x)
|
||||
|
||||
@@ -5,6 +5,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
@@ -39,15 +40,15 @@ func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
|
||||
log.Fatal("Failed to convert database & table: %v", err)
|
||||
return err
|
||||
}
|
||||
cprintln(cmd, "Converted successfully, please confirm your database's character set is now utf8mb4")
|
||||
fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4")
|
||||
case setting.Database.Type.IsMSSQL():
|
||||
if err := db.ConvertVarcharToNVarchar(); err != nil {
|
||||
log.Fatal("Failed to convert database from varchar to nvarchar: %v", err)
|
||||
return err
|
||||
}
|
||||
cprintln(cmd, "Converted successfully, please confirm your database's all columns character is NVARCHAR now")
|
||||
fmt.Println("Converted successfully, please confirm your database's all columns character is NVARCHAR now")
|
||||
default:
|
||||
cprintln(cmd, "This command can only be used with a MySQL or MSSQL database")
|
||||
fmt.Println("This command can only be used with a MySQL or MSSQL database")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
14
cmd/dump.go
14
cmd/dump.go
@@ -13,11 +13,13 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/dump"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -90,7 +92,7 @@ func newDumpCommand() *cli.Command {
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: `Dump output format, default to the --file extension if supported, else "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -196,7 +198,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
defer func() {
|
||||
_ = dbDump.Close()
|
||||
if err := os.Remove(dbDump.Name()); err != nil {
|
||||
if err := util.Remove(dbDump.Name()); err != nil {
|
||||
log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
|
||||
}
|
||||
}()
|
||||
@@ -247,8 +249,12 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
log.Info("Packing data directory...%s", setting.AppDataPath)
|
||||
|
||||
var excludes []string
|
||||
if setting.SessionConfig.Provider == "file" {
|
||||
excludes = append(excludes, setting.SessionConfig.ProviderConfig)
|
||||
if setting.SessionConfig.OriginalProvider == "file" {
|
||||
var opts session.Options
|
||||
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
excludes = append(excludes, opts.ProviderConfig)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/gitproxy"
|
||||
"gitea.dev/services/migrations"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -33,7 +32,7 @@ func newDumpRepositoryCommand() *cli.Command {
|
||||
&cli.StringFlag{
|
||||
Name: "git_service",
|
||||
Value: "",
|
||||
Usage: "Git service, git, github, gitea, forgejo, gitlab. If clone_addr could be recognized, this could be ignored.",
|
||||
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
@@ -95,9 +94,6 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := git.InitSimple(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gitproxy.Run(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("AppPath: %s", setting.AppPath)
|
||||
log.Info("AppWorkPath: %s", setting.AppWorkPath)
|
||||
@@ -117,8 +113,6 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
serviceStr = "gitlab"
|
||||
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://gitea.com/") {
|
||||
serviceStr = "gitea"
|
||||
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://codeberg.org/") {
|
||||
serviceStr = "forgejo"
|
||||
}
|
||||
if serviceStr == "" {
|
||||
return errors.New("git_service missed or clone_addr cannot be recognized")
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"gitea.dev/modules/public"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -150,7 +151,7 @@ func runListDo(c *cli.Command) error {
|
||||
}
|
||||
|
||||
for _, a := range matchedAssetFiles {
|
||||
cprintln(c, a.path)
|
||||
fmt.Println(a.path)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -194,7 +195,7 @@ func runExtractDo(c *cli.Command) error {
|
||||
destdir = c.String("destination")
|
||||
} else if c.Bool("custom") {
|
||||
destdir = setting.CustomPath
|
||||
cprintln(c, "Using app.ini at", setting.CustomConf)
|
||||
fmt.Println("Using app.ini at", setting.CustomConf)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(destdir)
|
||||
@@ -213,13 +214,13 @@ func runExtractDo(c *cli.Command) error {
|
||||
return fmt.Errorf("destination %q is not a directory", destdir)
|
||||
}
|
||||
|
||||
cprintf(c, "Extracting to %s:\n", destdir)
|
||||
fmt.Printf("Extracting to %s:\n", destdir)
|
||||
|
||||
overwrite := c.Bool("overwrite")
|
||||
rename := c.Bool("rename")
|
||||
|
||||
for _, a := range matchedAssetFiles {
|
||||
if err := extractAsset(c, destdir, a, overwrite, rename); err != nil {
|
||||
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
|
||||
// Non-fatal error
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err)
|
||||
}
|
||||
@@ -228,7 +229,7 @@ func runExtractDo(c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractAsset(c *cli.Command, d string, a assetFile, overwrite, rename bool) error {
|
||||
func extractAsset(d string, a assetFile, overwrite, rename bool) error {
|
||||
dest := filepath.Join(d, filepath.FromSlash(a.path))
|
||||
dir := filepath.Dir(dest)
|
||||
|
||||
@@ -249,23 +250,29 @@ func extractAsset(c *cli.Command, d string, a assetFile, overwrite, rename bool)
|
||||
return fmt.Errorf("%s: %w", dest, err)
|
||||
}
|
||||
} else if !overwrite && !rename {
|
||||
cprintf(c, "%s already exists; skipped.\n", dest)
|
||||
fmt.Printf("%s already exists; skipped.\n", dest)
|
||||
return nil
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
|
||||
} else if rename {
|
||||
if err := os.Rename(dest, dest+".bak"); err != nil {
|
||||
if err := util.Rename(dest, dest+".bak"); err != nil {
|
||||
return fmt.Errorf("error creating backup for %s: %w", dest, err)
|
||||
}
|
||||
// Attempt to respect file permissions mask (even if user:group will be set anew)
|
||||
perms = fi.Mode()
|
||||
}
|
||||
|
||||
if err := os.WriteFile(dest, data, perms); err != nil {
|
||||
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", dest, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.Write(data); err != nil {
|
||||
return fmt.Errorf("%s: %w", dest, err)
|
||||
}
|
||||
|
||||
cprintln(c, dest)
|
||||
fmt.Println(dest)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -300,7 +307,7 @@ func compileCollectPatterns(args []string) (_ []glob.Glob, err error) {
|
||||
pat := make([]glob.Glob, len(args))
|
||||
for i := range args {
|
||||
if pat[i], err = glob.Compile(args[i], '/'); err != nil {
|
||||
return nil, fmt.Errorf("invalid glob pattern %q: %w", args[i], err)
|
||||
return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err)
|
||||
}
|
||||
}
|
||||
return pat, nil
|
||||
|
||||
@@ -106,10 +106,10 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cprintf(c, "%s", internalToken)
|
||||
fmt.Printf("%s", internalToken)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
cprintf(c, "\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -117,10 +117,10 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
|
||||
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
|
||||
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
|
||||
cprintf(c, "%s", jwtSecretBase64)
|
||||
fmt.Printf("%s", jwtSecretBase64)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
cprintf(c, "\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -133,10 +133,10 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
// codeql[disable-next-line=go/clear-text-logging]
|
||||
cprintf(c, "%s", secretKey)
|
||||
fmt.Printf("%s", secretKey)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
cprintf(c, "\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -168,14 +168,14 @@ func runGenerateKeyPair(_ context.Context, c *cli.Command) error {
|
||||
// Check if file exists to prevent overwriting
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
if !confirm(c.Reader, c.Writer, "%s already exists.\nOverwrite (y/n)? ", file) {
|
||||
cprintln(c, "Aborting")
|
||||
fmt.Println("Aborting")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
bits := c.Int("bits")
|
||||
err := ssh.GenKeyPair(file, generate.SSHKeyType(keyType), bits)
|
||||
if err == nil {
|
||||
cprintf(c, "Your SSH key has been saved in %s\n", file)
|
||||
fmt.Printf("Your SSH key has been saved in %s\n", file)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package cmd provides subcommands to the gitea binary - such as "web" or
|
||||
// "admin".
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -20,8 +22,8 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// argsSet checks that all the required arguments are set.
|
||||
// args is a list of arguments that must be set in the command context.
|
||||
// argsSet checks that all the required arguments are set. args is a list of
|
||||
// arguments that must be set in the passed Context.
|
||||
func argsSet(c *cli.Command, args ...string) error {
|
||||
for _, a := range args {
|
||||
if !c.IsSet(a) {
|
||||
@@ -139,11 +141,3 @@ func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
func cprintf(c *cli.Command, format string, args ...any) {
|
||||
_, _ = fmt.Fprintf(c.Writer, format, args...)
|
||||
}
|
||||
|
||||
func cprintln(c *cli.Command, args ...any) {
|
||||
_, _ = fmt.Fprintln(c.Writer, args...)
|
||||
}
|
||||
|
||||
111
cmd/hook.go
111
cmd/hook.go
@@ -169,6 +169,10 @@ func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, re
|
||||
}
|
||||
|
||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
@@ -182,24 +186,23 @@ Gitea or set your environment appropriately.`, "")
|
||||
|
||||
// the environment is set by serv command
|
||||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||||
ownerName := os.Getenv(repo_module.EnvRepoUsername)
|
||||
repoName := os.Getenv(repo_module.EnvRepoName)
|
||||
username := os.Getenv(repo_module.EnvRepoUsername)
|
||||
reponame := os.Getenv(repo_module.EnvRepoName)
|
||||
userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||||
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
|
||||
deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
|
||||
actionsTaskID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionsTaskID), 10, 64)
|
||||
|
||||
hookOptions := private.HookOptions{
|
||||
IsWiki: isWiki,
|
||||
|
||||
UserID: userID,
|
||||
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
|
||||
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
||||
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
||||
GitPushOptions: pushOptions(),
|
||||
|
||||
PullRequestID: prID,
|
||||
|
||||
UserID: userID,
|
||||
UserName: os.Getenv(repo_module.EnvPusherName),
|
||||
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
|
||||
PullRequestID: prID,
|
||||
DeployKeyID: deployKeyID,
|
||||
ActionsTaskID: actionsTaskID,
|
||||
IsWiki: isWiki,
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
@@ -209,6 +212,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
refFullNames := make([]git.RefName, hookBatchSize)
|
||||
count := 0
|
||||
total := 0
|
||||
lastline := 0
|
||||
|
||||
out := io.Discard
|
||||
if setting.Git.VerbosePush {
|
||||
@@ -221,6 +225,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
}
|
||||
}
|
||||
|
||||
supportProcReceive := git.DefaultFeatures().SupportProcReceive
|
||||
|
||||
for scanner.Scan() {
|
||||
// TODO: support news feeds for wiki
|
||||
if isWiki {
|
||||
@@ -233,23 +239,37 @@ Gitea or set your environment appropriately.`, "")
|
||||
}
|
||||
|
||||
total++
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
count++
|
||||
fmt.Fprintf(out, "*")
|
||||
lastline++
|
||||
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
// If the ref is a branch or tag, check if it's protected
|
||||
// if supportProcReceive all ref should be checked because
|
||||
// permission check was delayed
|
||||
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
count++
|
||||
fmt.Fprintf(out, "*")
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||||
}
|
||||
count = 0
|
||||
lastline = 0
|
||||
}
|
||||
count = 0
|
||||
} else {
|
||||
fmt.Fprintf(out, ".")
|
||||
}
|
||||
if lastline >= hookBatchSize {
|
||||
fmt.Fprintf(out, "\n")
|
||||
lastline = 0
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -263,10 +283,12 @@ Gitea or set your environment appropriately.`, "")
|
||||
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
|
||||
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
|
||||
extra := private.HookPreReceive(ctx, username, reponame, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
|
||||
}
|
||||
} else if lastline > 0 {
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Checked %d references in total\n", total)
|
||||
@@ -276,6 +298,10 @@ Gitea or set your environment appropriately.`, "")
|
||||
// runHookUpdate avoid to do heavy operations on update hook because it will be
|
||||
// invoked for every ref update which does not like pre-receive and post-receive
|
||||
func runHookUpdate(_ context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update is empty and is kept only for backwards compatibility
|
||||
if len(os.Args) < 3 {
|
||||
return nil
|
||||
@@ -296,6 +322,11 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error {
|
||||
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
|
||||
}
|
||||
|
||||
// Now if we're an internal don't do anything else
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
||||
return fail(ctx, `Rejecting changes as Gitea environment not set.
|
||||
@@ -322,20 +353,18 @@ Gitea or set your environment appropriately.`, "")
|
||||
repoName := os.Getenv(repo_module.EnvRepoName)
|
||||
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||||
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
|
||||
pusherName := os.Getenv(repo_module.EnvPusherName)
|
||||
|
||||
hookOptions := private.HookOptions{
|
||||
IsWiki: isWiki,
|
||||
|
||||
UserName: pusherName,
|
||||
UserID: pusherID,
|
||||
GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
|
||||
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
||||
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
||||
GitPushOptions: pushOptions(),
|
||||
|
||||
PullRequestID: prID,
|
||||
|
||||
UserID: pusherID,
|
||||
UserName: os.Getenv(repo_module.EnvPusherName),
|
||||
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
|
||||
PullRequestID: prID,
|
||||
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
|
||||
IsWiki: isWiki,
|
||||
}
|
||||
|
||||
oldCommitIDs := make([]string, 0, hookBatchSize)
|
||||
@@ -443,11 +472,16 @@ Gitea or set your environment appropriately.`, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||||
repoName := os.Getenv(repo_module.EnvRepoName)
|
||||
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||||
pusherName := os.Getenv(repo_module.EnvPusherName)
|
||||
|
||||
// 1. Version and features negotiation.
|
||||
// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
|
||||
@@ -519,13 +553,10 @@ Gitea or set your environment appropriately.`, "")
|
||||
// S: ... ...
|
||||
// S: flush-pkt
|
||||
hookOptions := private.HookOptions{
|
||||
IsWiki: isWiki,
|
||||
|
||||
UserName: pusherName,
|
||||
UserID: pusherID,
|
||||
GitPushOptions: make(map[string]string),
|
||||
|
||||
UserID: pusherID,
|
||||
UserName: os.Getenv(repo_module.EnvPusherName),
|
||||
UserExtDoerData: os.Getenv(repo_module.EnvPusherExtDoerData),
|
||||
IsWiki: isWiki,
|
||||
}
|
||||
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
|
||||
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
|
||||
@@ -721,7 +752,7 @@ func writeFlushPktLine(ctx context.Context, out io.Writer) error {
|
||||
func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error {
|
||||
hexchar := []byte("0123456789abcdef")
|
||||
hex := func(n uint64) byte {
|
||||
return hexchar[n&15]
|
||||
return hexchar[(n)&15]
|
||||
}
|
||||
|
||||
length := uint64(len(data) + 4)
|
||||
|
||||
@@ -74,7 +74,7 @@ func runKeys(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
authorizedString, extra := private.AuthorizedPublicKeyForSSH(ctx, content)
|
||||
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
|
||||
// do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys
|
||||
if extra.Error != nil {
|
||||
return extra.Error
|
||||
|
||||
@@ -5,6 +5,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -21,11 +22,11 @@ func runSendMail(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
if !confirmSkipped {
|
||||
if len(body) == 0 {
|
||||
cprintln(c, "warning: Content is empty")
|
||||
fmt.Println("warning: Content is empty")
|
||||
}
|
||||
|
||||
if !confirm(c.Reader, c.Writer, "Proceed with sending email? [Y/n] ") {
|
||||
cprintln(c, "The mail was not sent")
|
||||
fmt.Println("The mail was not sent")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -34,6 +35,6 @@ func runSendMail(ctx context.Context, c *cli.Command) error {
|
||||
if extra.HasError() {
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
cprintf(c, "Sent %s email(s) to all users\n", respText.Text)
|
||||
_, _ = fmt.Printf("Sent %s email(s) to all users\n", respText.Text)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,14 +5,60 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func defaultLoggingFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "writer",
|
||||
Usage: "Name of the log writer - will default to mode",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "level",
|
||||
Usage: "Logging level for the new logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "stacktrace-level",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Stacktrace logging level",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "flags",
|
||||
Aliases: []string{"F"},
|
||||
Usage: "Flags for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "expression",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Matching expression for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "prefix",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Prefix for the logger",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Usage: "Use color in the logs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newLoggingCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "logging",
|
||||
@@ -45,6 +91,92 @@ func newLoggingCommand() *cli.Command {
|
||||
},
|
||||
},
|
||||
Action: runReleaseReopenLogging,
|
||||
}, {
|
||||
Name: "remove",
|
||||
Usage: "Remove a logger",
|
||||
ArgsUsage: "[name] Name of logger to remove",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
}, &cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
},
|
||||
},
|
||||
Action: runRemoveLogger,
|
||||
}, {
|
||||
Name: "add",
|
||||
Usage: "Add a logger",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "file",
|
||||
Usage: "Add a file logger",
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "filename",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Filename for the logger - this must be set.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "rotate",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Rotate logs",
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "max-size",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Maximum size in bytes before rotation",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "daily",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Rotate logs daily",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-days",
|
||||
Aliases: []string{"D"},
|
||||
Usage: "Maximum number of daily logs to keep",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "compress",
|
||||
Aliases: []string{"z"},
|
||||
Usage: "Compress rotated logs",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "compression-level",
|
||||
Aliases: []string{"Z"},
|
||||
Usage: "Compression level to use",
|
||||
},
|
||||
}...),
|
||||
Action: runAddFileLogger,
|
||||
}, {
|
||||
Name: "conn",
|
||||
Usage: "Add a net conn logger",
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect-on-message",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Reconnect to host for every message",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Reconnect to host when connection is dropped",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "protocol",
|
||||
Aliases: []string{"P"},
|
||||
Usage: "Set protocol to use: tcp, unix, or udp (defaults to tcp)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Host address and port to connect to (defaults to :7020)",
|
||||
},
|
||||
}...),
|
||||
Action: runAddConnLogger,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "log-sql",
|
||||
Usage: "Set LogSQL",
|
||||
@@ -63,6 +195,107 @@ func newLoggingCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
logger := c.String("logger")
|
||||
if len(logger) == 0 {
|
||||
logger = log.DEFAULT
|
||||
}
|
||||
writer := c.Args().First()
|
||||
|
||||
extra := private.RemoveLogger(ctx, logger, writer)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "conn"
|
||||
vals["net"] = "tcp"
|
||||
if c.IsSet("protocol") {
|
||||
switch c.String("protocol") {
|
||||
case "udp":
|
||||
vals["net"] = "udp"
|
||||
case "unix":
|
||||
vals["net"] = "unix"
|
||||
}
|
||||
}
|
||||
if c.IsSet("address") {
|
||||
vals["address"] = c.String("address")
|
||||
} else {
|
||||
vals["address"] = ":7020"
|
||||
}
|
||||
if c.IsSet("reconnect") {
|
||||
vals["reconnect"] = c.Bool("reconnect")
|
||||
}
|
||||
if c.IsSet("reconnect-on-message") {
|
||||
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
|
||||
}
|
||||
return commonAddLogger(ctx, c, mode, vals)
|
||||
}
|
||||
|
||||
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "file"
|
||||
if c.IsSet("filename") {
|
||||
vals["filename"] = c.String("filename")
|
||||
} else {
|
||||
return errors.New("filename must be set when creating a file logger")
|
||||
}
|
||||
if c.IsSet("rotate") {
|
||||
vals["rotate"] = c.Bool("rotate")
|
||||
}
|
||||
if c.IsSet("max-size") {
|
||||
vals["maxsize"] = c.Int64("max-size")
|
||||
}
|
||||
if c.IsSet("daily") {
|
||||
vals["daily"] = c.Bool("daily")
|
||||
}
|
||||
if c.IsSet("max-days") {
|
||||
vals["maxdays"] = c.Int("max-days")
|
||||
}
|
||||
if c.IsSet("compress") {
|
||||
vals["compress"] = c.Bool("compress")
|
||||
}
|
||||
if c.IsSet("compression-level") {
|
||||
vals["compressionLevel"] = c.Int("compression-level")
|
||||
}
|
||||
return commonAddLogger(ctx, c, mode, vals)
|
||||
}
|
||||
|
||||
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
|
||||
if len(c.String("level")) > 0 {
|
||||
vals["level"] = log.LevelFromString(c.String("level")).String()
|
||||
}
|
||||
if len(c.String("stacktrace-level")) > 0 {
|
||||
vals["stacktraceLevel"] = log.LevelFromString(c.String("stacktrace-level")).String()
|
||||
}
|
||||
if len(c.String("expression")) > 0 {
|
||||
vals["expression"] = c.String("expression")
|
||||
}
|
||||
if len(c.String("prefix")) > 0 {
|
||||
vals["prefix"] = c.String("prefix")
|
||||
}
|
||||
if len(c.String("flags")) > 0 {
|
||||
vals["flags"] = log.FlagsFromString(c.String("flags"))
|
||||
}
|
||||
if c.IsSet("color") {
|
||||
vals["colorize"] = c.Bool("color")
|
||||
}
|
||||
logger := log.DEFAULT
|
||||
if c.IsSet("logger") {
|
||||
logger = c.String("logger")
|
||||
}
|
||||
writer := mode
|
||||
if c.IsSet("writer") {
|
||||
writer = c.String("writer")
|
||||
}
|
||||
|
||||
extra := private.AddLogger(ctx, logger, writer, mode, vals)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runPauseLogging(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
userMsg := private.PauseLogging(ctx)
|
||||
|
||||
119
cmd/serv.go
119
cmd/serv.go
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -28,7 +29,6 @@ import (
|
||||
"gitea.dev/modules/process"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/agit"
|
||||
"gitea.dev/services/lfs"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
@@ -139,13 +139,13 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
if setting.SSH.Disabled {
|
||||
cprintln(c, "Gitea: SSH has been disabled")
|
||||
println("Gitea: SSH has been disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.NArg() < 1 {
|
||||
if err := cli.ShowSubcommandHelp(c); err != nil {
|
||||
cprintf(c, "error showing subcommand help: %v\n", err)
|
||||
fmt.Printf("error showing subcommand help: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -171,19 +171,15 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
if err != nil {
|
||||
return fail(ctx, "Key check failed", "Failed to check provided key: %v", err)
|
||||
}
|
||||
var authSuccessMsg string
|
||||
switch key.Type {
|
||||
case asymkey_model.KeyTypeDeploy:
|
||||
authSuccessMsg = "Hi there! You've successfully authenticated with an SSH deploy key."
|
||||
println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.")
|
||||
case asymkey_model.KeyTypePrincipal:
|
||||
authSuccessMsg = "Hi there! You've successfully authenticated with the SSH principal " + key.Content + "."
|
||||
println("Hi there! You've successfully authenticated with the principal " + key.Content + ", but Gitea does not provide shell access.")
|
||||
default:
|
||||
authSuccessMsg = "Hi there, " + user.Name + "! You've successfully authenticated with the SSH key named " + key.Name + "."
|
||||
println("Hi there, " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.")
|
||||
}
|
||||
_, _ = fmt.Fprintf(c.ErrWriter, "%s\n%s",
|
||||
authSuccessMsg,
|
||||
"Gitea does not provide shell access. If this is unexpected, please setup Gitea under another SSH user or use container to deploy.",
|
||||
)
|
||||
println("If this is unexpected, please log in with password and setup Gitea under another user.")
|
||||
return nil
|
||||
} else if c.Bool("debug") {
|
||||
log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND"))
|
||||
@@ -195,35 +191,45 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
if len(sshCmdArgs) < 2 {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
cprintf(c, "%s", agit.SshInfoJson)
|
||||
return nil
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
fmt.Print(`{"type":"agit","version":1}`)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
|
||||
}
|
||||
|
||||
var reqOwnerName, reqRepoName string
|
||||
{
|
||||
var ok bool
|
||||
reqRepoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
|
||||
reqOwnerName, reqRepoName, ok = strings.Cut(reqRepoPath, "/")
|
||||
if !ok {
|
||||
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", reqRepoPath)
|
||||
}
|
||||
reqRepoName = strings.TrimSuffix(reqRepoName, ".git") // "the-repo-name" or "the-repo-name.wiki"
|
||||
repoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
|
||||
repoPathFields := strings.SplitN(repoPath, "/", 2)
|
||||
if len(repoPathFields) != 2 {
|
||||
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
|
||||
}
|
||||
|
||||
if !repo_model.IsValidSSHAccessRepoName(reqRepoName) {
|
||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reqRepoName)
|
||||
username := repoPathFields[0]
|
||||
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
|
||||
|
||||
if !repo_model.IsValidSSHAccessRepoName(reponame) {
|
||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
||||
}
|
||||
|
||||
if c.Bool("enable-pprof") {
|
||||
stopProfiler, err := pprof.DumpPprofForUsername(setting.PprofDataPath, reqOwnerName)
|
||||
if err != nil {
|
||||
return fail(ctx, "Unable to start pprof profiler", "Unable to start pprof profile: %v", err)
|
||||
if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil {
|
||||
return fail(ctx, "Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err)
|
||||
}
|
||||
defer stopProfiler()
|
||||
|
||||
stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username)
|
||||
if err != nil {
|
||||
return fail(ctx, "Unable to start CPU profiler", "Unable to start CPU profile: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
stopCPUProfiler()
|
||||
err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username)
|
||||
if err != nil {
|
||||
_ = fail(ctx, "Unable to dump Mem profile", "Unable to dump Mem Profile: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
verb, lfsVerb := sshCmdArgs[0], ""
|
||||
@@ -248,38 +254,39 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb)
|
||||
}
|
||||
|
||||
results, extra := private.ServCommand(ctx, keyID, reqOwnerName, reqRepoName, requestedMode, verb, lfsVerb)
|
||||
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
|
||||
}
|
||||
|
||||
// because the original repoPath maybe redirected, we need to use the returned actual repository information
|
||||
if results.IsWiki {
|
||||
repoPath = repo_model.RelativeWikiPath(results.OwnerName, results.RepoName)
|
||||
} else {
|
||||
repoPath = repo_model.RelativePath(results.OwnerName, results.RepoName)
|
||||
}
|
||||
|
||||
// LFS SSH protocol
|
||||
if verb == git.CmdVerbLfsTransfer {
|
||||
if results.IsWiki {
|
||||
return fail(ctx, "LFS Transfer is not supported for wikis", "")
|
||||
}
|
||||
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, UserExtDoerData: results.UserExtDoerData, RepoID: results.RepoID})
|
||||
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return lfstransfer.Main(ctx, results.OwnerName, results.RepoName, lfsVerb, token)
|
||||
return lfstransfer.Main(ctx, repoPath, lfsVerb, token)
|
||||
}
|
||||
|
||||
// LFS token authentication
|
||||
if verb == git.CmdVerbLfsAuthenticate {
|
||||
if results.IsWiki {
|
||||
return fail(ctx, "LFS Authenticate is not supported for wikis", "")
|
||||
}
|
||||
lfsTokenHref := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
||||
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
||||
|
||||
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, UserExtDoerData: results.UserExtDoerData, RepoID: results.RepoID})
|
||||
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenAuthentication := &git_model.LFSTokenResponse{
|
||||
Header: make(map[string]string),
|
||||
Href: lfsTokenHref,
|
||||
Href: url,
|
||||
}
|
||||
tokenAuthentication.Header["Authorization"] = token
|
||||
|
||||
@@ -291,7 +298,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var command *process.Cmd
|
||||
var command *exec.Cmd
|
||||
gitBinPath := filepath.Dir(gitcmd.GitExecutable) // e.g. /usr/bin
|
||||
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
|
||||
if _, err := os.Stat(gitBinVerb); err != nil {
|
||||
@@ -300,14 +307,15 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
verbFields := strings.SplitN(verb, "-", 2)
|
||||
if len(verbFields) == 2 {
|
||||
// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
|
||||
command = process.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], results.RepoStoragePath)
|
||||
command = exec.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], repoPath)
|
||||
}
|
||||
}
|
||||
if command == nil {
|
||||
// by default, use the verb (it has been checked above by allowedCommands)
|
||||
command = process.CommandContext(ctx, gitBinVerb, results.RepoStoragePath)
|
||||
command = exec.CommandContext(ctx, gitBinVerb, repoPath)
|
||||
}
|
||||
|
||||
process.SetSysProcAttribute(command)
|
||||
command.Dir = setting.RepoRootPath
|
||||
command.Stdout = os.Stdout
|
||||
command.Stdin = os.Stdin
|
||||
@@ -315,20 +323,15 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
command.Env = append(command.Env, os.Environ()...)
|
||||
command.Env = append(command.Env,
|
||||
repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
|
||||
|
||||
repo_module.EnvRepoUsername+"="+results.OwnerName,
|
||||
repo_module.EnvRepoName+"="+results.RepoName,
|
||||
repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
|
||||
|
||||
repo_module.EnvKeyID+"="+strconv.FormatInt(results.PublicKeyID, 10),
|
||||
|
||||
repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
|
||||
repo_module.EnvRepoUsername+"="+results.OwnerName,
|
||||
repo_module.EnvPusherName+"="+results.UserName,
|
||||
repo_module.EnvPusherEmail+"="+results.UserEmail,
|
||||
repo_module.EnvPusherExtDoerData+"="+results.UserExtDoerData,
|
||||
|
||||
repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
|
||||
repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
|
||||
repo_module.EnvPRID+"="+strconv.Itoa(0),
|
||||
|
||||
repo_module.EnvDeployKeyID+"="+strconv.FormatInt(results.DeployKeyID, 10),
|
||||
repo_module.EnvKeyID+"="+strconv.FormatInt(results.KeyID, 10),
|
||||
repo_module.EnvAppURL+"="+setting.AppURL,
|
||||
)
|
||||
// to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
|
||||
@@ -340,9 +343,9 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
// Update user key activity.
|
||||
if results.PublicKeyID > 0 {
|
||||
if err = private.UpdatePublicKeyLastUsed(ctx, results.PublicKeyID, results.RepoID); err != nil {
|
||||
return fail(ctx, "Failed to update public key", "UpdatePublicKeyLastUsed: %v", err)
|
||||
if results.KeyID > 0 {
|
||||
if err = private.UpdatePublicKeyInRepo(ctx, results.KeyID, results.RepoID); err != nil {
|
||||
return fail(ctx, "Failed to update public key", "UpdatePublicKeyInRepo: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
cmd/web.go
12
cmd/web.go
@@ -15,7 +15,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/gtprof"
|
||||
@@ -27,7 +26,6 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers"
|
||||
"gitea.dev/routers/install"
|
||||
"gitea.dev/services/audit"
|
||||
|
||||
"github.com/felixge/fgprof"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -228,14 +226,7 @@ func serveInstalled(c *cli.Command) error {
|
||||
|
||||
// Set up Chi routes
|
||||
webRoutes := routers.NormalRoutes()
|
||||
|
||||
auditCtx := cliAuditContext(context.Background())
|
||||
log.Info("Audit record output: %s", setting.Audit.RecordOutput)
|
||||
audit.Record(auditCtx, audit_model.SystemStartup, nil, "version", setting.AppVer)
|
||||
|
||||
err := listen(webRoutes, true)
|
||||
|
||||
audit.Record(auditCtx, audit_model.SystemShutdown, nil)
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
return err
|
||||
@@ -253,8 +244,7 @@ func servePprof() {
|
||||
_, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true)
|
||||
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it.
|
||||
log.Info("Starting pprof server on localhost:6060")
|
||||
server := &http.Server{Addr: "localhost:6060", Handler: mux, ReadHeaderTimeout: 10 * time.Second}
|
||||
log.Info("Stopped pprof server: %v", server.ListenAndServe())
|
||||
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", mux))
|
||||
finished()
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
TrustedRoots: certPool,
|
||||
Email: setting.AcmeEmail,
|
||||
Agreed: setting.AcmeTOS,
|
||||
Profile: setting.AcmeProfile,
|
||||
DisableHTTPChallenge: !enableHTTPChallenge,
|
||||
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
|
||||
ListenHost: setting.HTTPAddr,
|
||||
@@ -100,20 +99,37 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
||||
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
||||
ctx := graceful.GetManager().ShutdownContext()
|
||||
err := magic.ManageSync(ctx, []string{setting.AppDomain})
|
||||
err := magic.ManageSync(ctx, []string{setting.Domain})
|
||||
if err != nil {
|
||||
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
|
||||
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.Domain)
|
||||
if cacheErr != nil || cert.Expired() {
|
||||
return errors.Join(err, cacheErr)
|
||||
}
|
||||
log.Error("ACME certificate manage failed; continuing with existing certificate: %v", err)
|
||||
if err := magic.ManageAsync(ctx, []string{setting.AppDomain}); err != nil {
|
||||
if err := magic.ManageAsync(ctx, []string{setting.Domain}); err != nil {
|
||||
log.Error("Failed to start async ACME management: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// certmagic only advertises its own ACME challenge protocol, applyTLSSettings appends ours
|
||||
tlsConfig := applyTLSSettings(magic.TLSConfig())
|
||||
tlsConfig := magic.TLSConfig()
|
||||
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2")
|
||||
|
||||
if version := toTLSVersion(setting.SSLMinimumVersion); version != 0 {
|
||||
tlsConfig.MinVersion = version
|
||||
}
|
||||
if version := toTLSVersion(setting.SSLMaximumVersion); version != 0 {
|
||||
tlsConfig.MaxVersion = version
|
||||
}
|
||||
|
||||
// Set curve preferences
|
||||
if curves := toCurvePreferences(setting.SSLCurvePreferences); len(curves) > 0 {
|
||||
tlsConfig.CurvePreferences = curves
|
||||
}
|
||||
|
||||
// Set cipher suites
|
||||
if ciphers := toTLSCiphers(setting.SSLCipherSuites); len(ciphers) > 0 {
|
||||
tlsConfig.CipherSuites = ciphers
|
||||
}
|
||||
|
||||
if enableHTTPChallenge {
|
||||
go func() {
|
||||
|
||||
158
cmd/web_https.go
158
cmd/web_https.go
@@ -12,82 +12,122 @@ import (
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
)
|
||||
|
||||
var tlsVersionStringMap = map[string]uint16{
|
||||
"": tls.VersionTLS12, // Default to tls.VersionTLS12
|
||||
"tlsv1.0": tls.VersionTLS10,
|
||||
"tlsv1.1": tls.VersionTLS11,
|
||||
"tlsv1.2": tls.VersionTLS12,
|
||||
"tlsv1.3": tls.VersionTLS13,
|
||||
}
|
||||
|
||||
// toTLSVersion returns 0 when unset or unknown, which keeps the crypto/tls default
|
||||
func toTLSVersion(version string) uint16 {
|
||||
version = strings.TrimSpace(strings.ToLower(version))
|
||||
tlsVersion, ok := tlsVersionStringMap[version]
|
||||
if !ok && version != "" {
|
||||
tlsVersion, ok := tlsVersionStringMap[strings.TrimSpace(strings.ToLower(version))]
|
||||
if !ok {
|
||||
log.Warn("Unknown tls version: %s", version)
|
||||
return 0
|
||||
}
|
||||
return tlsVersion
|
||||
}
|
||||
|
||||
var curveStringMap = map[string]tls.CurveID{
|
||||
"x25519": tls.X25519,
|
||||
"p256": tls.CurveP256,
|
||||
"p384": tls.CurveP384,
|
||||
"p521": tls.CurveP521,
|
||||
"mlkem1024": tls.MLKEM1024,
|
||||
"x25519mlkem768": tls.X25519MLKEM768,
|
||||
"secp256r1mlkem768": tls.SecP256r1MLKEM768,
|
||||
"secp384r1mlkem1024": tls.SecP384r1MLKEM1024,
|
||||
"x25519": tls.X25519,
|
||||
"p256": tls.CurveP256,
|
||||
"p384": tls.CurveP384,
|
||||
"p521": tls.CurveP521,
|
||||
}
|
||||
|
||||
// lookupAll resolves configured names, warning about and skipping the ones crypto/tls does not know
|
||||
func lookupAll[T comparable](names []string, table map[string]T, kind string) []T {
|
||||
values := make([]T, 0, len(names))
|
||||
for _, name := range names {
|
||||
value, ok := table[strings.TrimSpace(strings.ToLower(name))]
|
||||
func toCurvePreferences(preferences []string) []tls.CurveID {
|
||||
ids := make([]tls.CurveID, 0, len(preferences))
|
||||
for _, pref := range preferences {
|
||||
id, ok := curveStringMap[strings.TrimSpace(strings.ToLower(pref))]
|
||||
if !ok {
|
||||
log.Warn("Unknown %s: %s", kind, name)
|
||||
continue
|
||||
log.Warn("Unknown curve: %s", pref)
|
||||
}
|
||||
if id != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values
|
||||
return ids
|
||||
}
|
||||
|
||||
// cipherStringMap is derived from crypto/tls so that suites Go adds or removes are picked up automatically
|
||||
var cipherStringMap = buildCipherStringMap()
|
||||
var cipherStringMap = map[string]uint16{
|
||||
"rsa_with_rc4_128_sha": tls.TLS_RSA_WITH_RC4_128_SHA,
|
||||
"rsa_with_3des_ede_cbc_sha": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"rsa_with_aes_128_cbc_sha": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"rsa_with_aes_256_cbc_sha": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"rsa_with_aes_128_cbc_sha256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"rsa_with_aes_128_gcm_sha256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"rsa_with_aes_256_gcm_sha384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"ecdhe_ecdsa_with_rc4_128_sha": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
|
||||
"ecdhe_ecdsa_with_aes_128_cbc_sha": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"ecdhe_ecdsa_with_aes_256_cbc_sha": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"ecdhe_rsa_with_rc4_128_sha": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
|
||||
"ecdhe_rsa_with_3des_ede_cbc_sha": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"ecdhe_rsa_with_aes_128_cbc_sha": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"ecdhe_rsa_with_aes_256_cbc_sha": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ecdhe_ecdsa_with_aes_128_cbc_sha256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
"ecdhe_rsa_with_aes_128_cbc_sha256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"ecdhe_rsa_with_aes_128_gcm_sha256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"ecdhe_ecdsa_with_aes_128_gcm_sha256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"ecdhe_rsa_with_aes_256_gcm_sha384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"ecdhe_ecdsa_with_aes_256_gcm_sha384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"ecdhe_rsa_with_chacha20_poly1305_sha256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
"ecdhe_ecdsa_with_chacha20_poly1305_sha256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
"ecdhe_rsa_with_chacha20_poly1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
"ecdhe_ecdsa_with_chacha20_poly1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
"aes_128_gcm_sha256": tls.TLS_AES_128_GCM_SHA256,
|
||||
"aes_256_gcm_sha384": tls.TLS_AES_256_GCM_SHA384,
|
||||
"chacha20_poly1305_sha256": tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
}
|
||||
|
||||
func buildCipherStringMap() map[string]uint16 {
|
||||
ciphers := map[string]uint16{
|
||||
// aliases for the two suites Go later renamed with a _SHA256 suffix
|
||||
"ecdhe_rsa_with_chacha20_poly1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
"ecdhe_ecdsa_with_chacha20_poly1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
}
|
||||
for _, cipher := range append(tls.CipherSuites(), tls.InsecureCipherSuites()...) {
|
||||
ciphers[strings.ToLower(strings.TrimPrefix(cipher.Name, "TLS_"))] = cipher.ID
|
||||
func toTLSCiphers(cipherStrings []string) []uint16 {
|
||||
ciphers := make([]uint16, 0, len(cipherStrings))
|
||||
for _, cipherString := range cipherStrings {
|
||||
cipher, ok := cipherStringMap[strings.TrimSpace(strings.ToLower(cipherString))]
|
||||
if !ok {
|
||||
log.Warn("Unknown cipher: %s", cipherString)
|
||||
}
|
||||
if cipher != 0 {
|
||||
ciphers = append(ciphers, cipher)
|
||||
}
|
||||
}
|
||||
|
||||
return ciphers
|
||||
}
|
||||
|
||||
// applyTLSSettings applies the configured [server] TLS options. Every option
|
||||
// left unset keeps the crypto/tls default, which Go keeps current.
|
||||
func applyTLSSettings(tlsConfig *tls.Config) *tls.Config {
|
||||
// the listener is already TLS wrapped when it reaches http.Server, so it never sets ALPN for us
|
||||
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2", "http/1.1")
|
||||
tlsConfig.MinVersion = toTLSVersion(setting.SSLMinimumVersion)
|
||||
tlsConfig.MaxVersion = toTLSVersion(setting.SSLMaximumVersion)
|
||||
|
||||
if curves := lookupAll(setting.SSLCurvePreferences, curveStringMap, "curve"); len(curves) > 0 {
|
||||
tlsConfig.CurvePreferences = curves
|
||||
// defaultCiphers uses hardware support to check if AES is specifically
|
||||
// supported by the CPU.
|
||||
//
|
||||
// If AES is supported AES ciphers will be preferred over ChaCha based ciphers
|
||||
// (This code is directly inspired by the certmagic code.)
|
||||
func defaultCiphers() []uint16 {
|
||||
if cpuid.CPU.Supports(cpuid.AESNI) {
|
||||
return defaultCiphersAESfirst
|
||||
}
|
||||
if ciphers := lookupAll(setting.SSLCipherSuites, cipherStringMap, "cipher suite"); len(ciphers) > 0 {
|
||||
tlsConfig.CipherSuites = ciphers
|
||||
}
|
||||
return tlsConfig
|
||||
return defaultCiphersChaChaFirst
|
||||
}
|
||||
|
||||
var (
|
||||
defaultCiphersAES = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
}
|
||||
|
||||
defaultCiphersChaCha = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
}
|
||||
|
||||
defaultCiphersAESfirst = append(defaultCiphersAES, defaultCiphersChaCha...)
|
||||
defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...)
|
||||
)
|
||||
|
||||
// runHTTPS listens on the provided network address and then calls
|
||||
// Serve to handle requests on incoming TLS connections.
|
||||
//
|
||||
@@ -96,7 +136,33 @@ func applyTLSSettings(tlsConfig *tls.Config) *tls.Config {
|
||||
// certFile should be the concatenation of the server's certificate followed by the
|
||||
// CA's certificate.
|
||||
func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
|
||||
tlsConfig := applyTLSSettings(&tls.Config{})
|
||||
tlsConfig := &tls.Config{}
|
||||
if tlsConfig.NextProtos == nil {
|
||||
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
|
||||
}
|
||||
|
||||
if version := toTLSVersion(setting.SSLMinimumVersion); version != 0 {
|
||||
tlsConfig.MinVersion = version
|
||||
}
|
||||
if version := toTLSVersion(setting.SSLMaximumVersion); version != 0 {
|
||||
tlsConfig.MaxVersion = version
|
||||
}
|
||||
|
||||
// Set curve preferences
|
||||
tlsConfig.CurvePreferences = []tls.CurveID{
|
||||
tls.X25519,
|
||||
tls.CurveP256,
|
||||
}
|
||||
if curves := toCurvePreferences(setting.SSLCurvePreferences); len(curves) > 0 {
|
||||
tlsConfig.CurvePreferences = curves
|
||||
}
|
||||
|
||||
// Set cipher suites
|
||||
tlsConfig.CipherSuites = defaultCiphers()
|
||||
if ciphers := toTLSCiphers(setting.SSLCipherSuites); len(ciphers) > 0 {
|
||||
tlsConfig.CipherSuites = ciphers
|
||||
}
|
||||
|
||||
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
||||
|
||||
certPEMBlock, err := os.ReadFile(certFile)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user