mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-26 21:39:42 +09:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63bc4eae68 | ||
|
|
6b416d6135 | ||
|
|
95a300e1f4 | ||
|
|
2f5a735c75 | ||
|
|
8819eee696 | ||
|
|
0c8be7bee0 | ||
|
|
f25811942c | ||
|
|
c0c11c551c | ||
|
|
e99e24cb04 | ||
|
|
c20df84548 | ||
|
|
24ce5ae082 |
@@ -13,7 +13,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: ./.github/actions/free-disk-space
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -9,7 +9,7 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
- if: ${{ inputs.cache == 'true' }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
gobuild:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend deps-tools
|
||||
- run: TAGS="bindata" make backend
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
include:
|
||||
- { tags: "bindata", target: "lint-backend" }
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
with:
|
||||
lint-cache: "true"
|
||||
|
||||
@@ -12,8 +12,8 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -20,8 +20,8 @@ jobs:
|
||||
if: github.repository == 'go-gitea/gitea' # prevent running on forks
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
|
||||
with:
|
||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||
configurationFile: renovate.json5
|
||||
|
||||
@@ -12,8 +12,8 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
e2e: ${{ steps.changes.outputs.e2e }}
|
||||
shell: ${{ steps.changes.outputs.shell }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
|
||||
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
gitea_fork: giteabot/gitea
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
name: giteabot
|
||||
|
||||
on:
|
||||
# When main advances, rerun merge queue maintenance so the oldest
|
||||
# reviewed/wait-merge PR can be updated against the new base promptly.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# pull_request_target gives this workflow access to GITEABOT_TOKEN on PRs from
|
||||
# forks, which the bot needs to write labels, statuses and comments. Safe here
|
||||
# because the job only runs a pinned action and never checks out PR HEAD.
|
||||
# These PR lifecycle events drive label maintenance, queue maintenance, and
|
||||
# explicit bot actions triggered by relevant label changes.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types:
|
||||
- opened
|
||||
@@ -20,19 +13,13 @@ on:
|
||||
- closed
|
||||
- review_requested
|
||||
- review_request_removed
|
||||
# Review events keep review-derived state such as lgtm labels and status checks
|
||||
# in sync after approvals, edits, or dismissals.
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
- edited
|
||||
- dismissed
|
||||
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
||||
# other housekeeping, even though main pushes now trigger it promptly.
|
||||
schedule:
|
||||
- cron: "15 3 * * *"
|
||||
# Allow maintainers to rerun selected checks manually when debugging bot
|
||||
# behavior without waiting for another repository event.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
checks:
|
||||
@@ -58,7 +45,7 @@ jobs:
|
||||
steps:
|
||||
# pull_request_review runs without repository secrets on fork PRs, so fall
|
||||
# back to the workflow token for the non-backport checks handled here.
|
||||
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
|
||||
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
|
||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
with:
|
||||
lint-cache: "true"
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
with:
|
||||
cache: "false"
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/node-setup
|
||||
- run: make deps-frontend
|
||||
- run: make lint-frontend
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- 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
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
ports:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
- uses: ./.github/actions/pgsql-shard
|
||||
with:
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
ports:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
- uses: ./.github/actions/pgsql-shard
|
||||
with:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
ports:
|
||||
- "7700:7700"
|
||||
redis:
|
||||
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
|
||||
image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5
|
||||
options: >- # wait until redis has started
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 5s
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- 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'
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
- "587:587"
|
||||
- "993:993"
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- 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'
|
||||
@@ -241,7 +241,7 @@ jobs:
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- 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'
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
platform: linux/amd64
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
platform: linux/arm64
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
platform: linux/riscv64
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: ./.github/actions/go-setup
|
||||
- uses: ./.github/actions/node-setup
|
||||
- run: make deps-frontend
|
||||
|
||||
@@ -30,7 +30,7 @@ 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -17,15 +17,11 @@ jobs:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- 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 \
|
||||
|
||||
@@ -13,17 +13,16 @@ jobs:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# 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
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -34,8 +33,6 @@ jobs:
|
||||
- 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
|
||||
@@ -48,7 +45,6 @@ jobs:
|
||||
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
|
||||
@@ -79,7 +75,7 @@ jobs:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# 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
|
||||
|
||||
@@ -14,17 +14,16 @@ jobs:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# 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
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -35,8 +34,6 @@ jobs:
|
||||
- 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
|
||||
@@ -49,7 +46,6 @@ jobs:
|
||||
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
|
||||
@@ -90,7 +86,7 @@ jobs:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# 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
|
||||
|
||||
@@ -17,17 +17,16 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# 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
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -38,8 +37,6 @@ jobs:
|
||||
- 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
|
||||
@@ -52,7 +49,6 @@ jobs:
|
||||
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
|
||||
@@ -93,7 +89,7 @@ jobs:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# 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
|
||||
|
||||
@@ -107,7 +107,6 @@ linters:
|
||||
- -QF1008
|
||||
testifylint:
|
||||
disable:
|
||||
- empty
|
||||
- go-require
|
||||
- require-error
|
||||
usetesting:
|
||||
|
||||
@@ -4,56 +4,6 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21
|
||||
|
||||
* SECURITY
|
||||
* fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009) (#38183)
|
||||
|
||||
* BUGFIXES
|
||||
* fix: walk git log context error handling (#38182) (#38185)
|
||||
|
||||
## [1.26.3](https://github.com/go-gitea/gitea/releases/tag/1.26.3) - 2026-06-18
|
||||
|
||||
* BREAKING
|
||||
* fix(actions)!: require merged PR to bypass fork PR approval gate (#38010) (#38041)
|
||||
|
||||
* SECURITY
|
||||
* fix(hostmatcher): patch incorrect private list (#38170) (#38173)
|
||||
* fix: Various security fixes (#38103) (#38151)
|
||||
* fix: Various sec fixes (#38108) (#38147)
|
||||
* fix: allow git clone of private repos with anonymous code access (#38074) (#38146)
|
||||
* fix(auth): ignore stale OIDC external login links to organizations (#37875) (#38141)
|
||||
* fix(hostmatcher): block reserved IP ranges from external/private filters (#38039) (#38059)
|
||||
* fix(lfs): require Code-unit access for cross-repo LFS object reuse (#38006) (#38050)
|
||||
* fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) (#38015)
|
||||
* fix: bound CODEOWNERS regex match time (#38011) (#38025)
|
||||
* fix: bound debian ParseControlFile to a single control stanza (#38044) (#38055)
|
||||
* fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829)
|
||||
|
||||
* API
|
||||
* feat(api): add Link header in ListForks (#38052) (#38063)
|
||||
|
||||
* BUGFIXES
|
||||
* fix: Fix the panic when ssh remote lfs endpoint parsing failure (#38026) (#38158)
|
||||
* fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115)
|
||||
* fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) (#38096)
|
||||
* fix: parse HEAD ref (#38119)
|
||||
* fix: git cmd (#38084) (#38087)
|
||||
* fix(releases): generate notes for initial tag (#37697) (#37986)
|
||||
* fix(actions): return 404 when job log blob is missing (#38003) (#38004)
|
||||
* fix(actions): exclude `workflow_call` from workflow trigger detection (#37894) (#37899)
|
||||
* fix(actions): keep action run title clickable when commit subject is a URL (#37867) (#37898)
|
||||
* fix(actions): reject workflow_dispatch for workflows without that trigger (#37660) (#37895)
|
||||
* fix(actions): ack re-sent `UpdateLog` finalize idempotently (#37885) (#37892)
|
||||
* fix: http content file render (#37850) (#37856)
|
||||
* fix(issues): clear stale ReviewTypeRequest when submitting pending review (#37809) (#37815)
|
||||
* fix: Fix issue target branch selection for non-collaborators (#36916) (#38164)
|
||||
|
||||
* BUILD
|
||||
* fix(deps): update `@playwright/test` to 1.60.0 (#38144)
|
||||
* ci: add `tools/ci-tools.ts` for the PR labeler workflow (#37831)
|
||||
* fix(build): swagger css import (#37801) (#37803)
|
||||
|
||||
## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20
|
||||
|
||||
* SECURITY
|
||||
|
||||
+35
-15
@@ -2,17 +2,12 @@
|
||||
|
||||
This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find.
|
||||
|
||||
| Topic | Document |
|
||||
|:-----------------------|:-----------------------------------------------------------------|
|
||||
| Setup and requirements | [docs/build-setup.md](docs/build-setup.md) |
|
||||
| Development workflow | [docs/development.md](docs/development.md) |
|
||||
| Build from source | [docs/build-source.md](docs/build-source.md) |
|
||||
| Running the tests | [docs/testing.md](docs/testing.md) |
|
||||
| Frontend guidelines | [docs/guidelines-frontend.md](docs/guidelines-frontend.md) |
|
||||
| Backend guidelines | [docs/guidelines-backend.md](docs/guidelines-backend.md) |
|
||||
| Refactoring | [docs/guidelines-refactoring.md](docs/guidelines-refactoring.md) |
|
||||
| Community Governance | [docs/community-governance.md](docs/community-governance.md) |
|
||||
| Release management | [docs/release-management.md](docs/release-management.md) |
|
||||
| Topic | Document |
|
||||
| :---- | :------- |
|
||||
| Backend (Go modules, API v1) | [docs/guideline-backend.md](docs/guideline-backend.md) |
|
||||
| Frontend (npm, UI guidelines) | [docs/guideline-frontend.md](docs/guideline-frontend.md) |
|
||||
| Maintainers, TOC, labels, merge queue, commit format for mergers | [docs/community-governance.md](docs/community-governance.md) |
|
||||
| Release cycle, backports, tagging releases | [docs/release-management.md](docs/release-management.md) |
|
||||
|
||||
<details><summary>Table of Contents</summary>
|
||||
|
||||
@@ -48,7 +43,7 @@ This document explains how to contribute changes to the Gitea project. Topic-spe
|
||||
It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \
|
||||
Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
|
||||
|
||||
For configuring IDEs for Gitea development, see the [IDE setup notes](docs/development.md#ide-configuration) and the [contributed configurations](contrib/development/).
|
||||
For configuring IDEs for Gitea development, see the [contributed IDE configurations](contrib/ide/).
|
||||
|
||||
## AI Contribution Policy
|
||||
|
||||
@@ -111,8 +106,7 @@ If further discussion is needed, we encourage you to open a new issue instead an
|
||||
|
||||
## Building Gitea
|
||||
|
||||
See [docs/setup.md](docs/setup.md) for prerequisites and [docs/development.md](docs/development.md)
|
||||
for building Gitea and the development workflow.
|
||||
See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
|
||||
|
||||
## Styleguide
|
||||
|
||||
@@ -131,7 +125,33 @@ Afterwards, copyright should only be modified when the copyright author changes.
|
||||
|
||||
## Testing
|
||||
|
||||
Before submitting a pull request, run the linters (`make lint`, or the scoped `make lint-backend` / `make lint-frontend`) and the tests to make sure your changes don't cause a regression elsewhere. See [docs/testing.md](docs/testing.md) for how to run the unit, integration, end-to-end, and migration tests.
|
||||
Before submitting a pull request, run all tests to make sure your changes don't cause a regression elsewhere.
|
||||
|
||||
Here's how to run the test suite:
|
||||
|
||||
- code lint
|
||||
|
||||
| | |
|
||||
| :-------------------- | :--------------------------------------------------------------------------- |
|
||||
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|
||||
|``make lint-frontend`` | lint frontend files |
|
||||
|``make lint-backend`` | lint backend files |
|
||||
|
||||
- run tests (we suggest running them on Linux)
|
||||
|
||||
| Command | Action | |
|
||||
|:----------------------------------------------|:-----------------------------------------------------| ------------------------------------------- |
|
||||
| ``make test-backend[\#SpecificTestName]`` | run unit test(s) | |
|
||||
| ``make test-integration[\#SpecificTestName]`` | run [integration](tests/integration) test(s) | [More details](tests/integration/README.md) |
|
||||
| ``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | |
|
||||
|
||||
- E2E test environment variables
|
||||
|
||||
| Variable | Description |
|
||||
| :-------------------------------- | :---------------------------------------------------------- |
|
||||
| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output |
|
||||
| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` |
|
||||
| ``GITEA_TEST_E2E_TIMEOUT_FACTOR`` | Timeout multiplier (default: 4 on CI, 1 locally) |
|
||||
|
||||
## Translation
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che
|
||||
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
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.0 # 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
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.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
|
||||
|
||||
@@ -127,7 +127,6 @@ BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* mo
|
||||
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
|
||||
|
||||
SVG_DEST_DIR := public/assets/img/svg
|
||||
SVG_DEST_DIRS := $(SVG_DEST_DIR) options/fileicon
|
||||
|
||||
AIR_TMP_DIR := .air
|
||||
|
||||
@@ -634,10 +633,10 @@ svg: node_modules ## build svg files
|
||||
|
||||
.PHONY: svg-check
|
||||
svg-check: svg
|
||||
@git add $(SVG_DEST_DIRS)
|
||||
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIRS)); \
|
||||
@git add $(SVG_DEST_DIR)
|
||||
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIRS)' and commit the result:"; \
|
||||
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
|
||||
printf "%s" "$${diff}"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
The goal of Gitea is to make the easiest, fastest, and most painless way of
|
||||
setting up a self-hosted all-in-one software development service,
|
||||
including Git hosting, code management, code review, issue tracking, project kanban, wiki,
|
||||
team collaboration, package registry and CI/CD which can reuse GitHub Actions.
|
||||
The goal of this project is to make the easiest, fastest, and most
|
||||
painless way of setting up a self-hosted Git service.
|
||||
|
||||
As Gitea is written in Go, it works across **all** the platforms and
|
||||
architectures that are supported by Go, including Linux, macOS, FreeBSD/OpenBSD and Windows
|
||||
on x86, amd64, ARM, RISC-V 64 and PowerPC architectures.
|
||||
architectures that are supported by Go, including Linux, macOS, and
|
||||
Windows on x86, amd64, ARM and PowerPC architectures.
|
||||
This project has been
|
||||
[forked](https://blog.gitea.com/welcome-to-gitea/) from
|
||||
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
|
||||
|
||||
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
|
||||
|
||||
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
|
||||
|
||||
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com),
|
||||
or use container (docker/podman/etc) to deploy on your own server with the [official image](https://hub.docker.com/r/gitea/gitea).
|
||||
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -40,12 +40,27 @@ If you have any suggestions or would like to contribute to it, you can visit the
|
||||
|
||||
## Building
|
||||
|
||||
See [docs/build-setup.md](docs/build-setup.md) for prerequisites
|
||||
and [docs/development.md](docs/development.md) for setting up a local development environment, linting, and testing.
|
||||
From the root of the source tree, run:
|
||||
|
||||
If you'd like to build from source or make a distribution package, see [docs/build-source.md](docs/build-source.md) for more information.
|
||||
TAGS="bindata" make build
|
||||
|
||||
After building, you can run `./gitea web` to start the server, or `./gitea help` to see all available commands.
|
||||
The `build` target is split into two sub-targets:
|
||||
|
||||
- `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod).
|
||||
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and [pnpm](https://pnpm.io/installation).
|
||||
|
||||
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
|
||||
|
||||
More info: https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## Using
|
||||
|
||||
After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!NOTE]
|
||||
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -54,8 +69,7 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
|
||||
> [!NOTE]
|
||||
>
|
||||
> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
|
||||
> 2. New to the codebase? The [development guide](docs/development.md) walks through setting up a local environment and building from source.
|
||||
> 3. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
|
||||
> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
|
||||
|
||||
## Translating
|
||||
|
||||
@@ -112,19 +126,14 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
||||
|
||||
Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
|
||||
|
||||
**How do I configure Gitea?**
|
||||
**Why is this not hosted on a Gitea instance?**
|
||||
|
||||
For dynamic config options, you can change it on your admin panel's configuration section.
|
||||
|
||||
For static config options, you can edit your `app.ini` file and resart the instance.
|
||||
See [app.example.ini](https://github.com/go-gitea/gitea/blob/main/custom/conf/app.example.ini) or [configuration documentation](https://docs.gitea.com/administration/config-cheat-sheet) for more details.
|
||||
We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
|
||||
|
||||
**Where can I find the security patches?**
|
||||
|
||||
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
|
||||
|
||||
(more FAQs are listed in [FAQ documentation](https://docs.gitea.com/help/faq))
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -134,7 +143,7 @@ for the full license text.
|
||||
## Further information
|
||||
|
||||
<details>
|
||||
<summary>Looking for an overview of the interface? Check it out the screenshots!</summary>
|
||||
<summary>Looking for an overview of the interface? Check it out!</summary>
|
||||
|
||||
### Login/Register Page
|
||||
|
||||
|
||||
@@ -158,8 +158,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
isAdmin := c.Bool("admin")
|
||||
// Only local, existing, regular users should be forced to update their password. Bot users for example are non-interactive
|
||||
mustChangePassword := userType == user_model.UserTypeIndividual
|
||||
mustChangePassword := true // always default to true
|
||||
if c.IsSet("must-change-password") {
|
||||
if userType != user_model.UserTypeIndividual {
|
||||
return errors.New("must-change-password flag can only be set for individual users")
|
||||
|
||||
@@ -63,7 +63,6 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
|
||||
+1
-12
@@ -203,7 +203,7 @@ func TestCliCmdError(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n", r.Stderr)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
@@ -235,14 +235,3 @@ func TestCliCmdBefore(t *testing.T) {
|
||||
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||
assert.Equal(t, "/dev/null", configValues["action"])
|
||||
}
|
||||
|
||||
func TestCliCmdCompletion(t *testing.T) {
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error { return nil },
|
||||
})
|
||||
res, err := runTestApp(app, "./gitea", "completion", "bash", "--nonexist")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, res.ExitCode)
|
||||
assert.Equal(t, "", res.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -nonexist\n", res.Stderr)
|
||||
}
|
||||
|
||||
+4
-25
@@ -5,10 +5,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -154,37 +154,16 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
return app
|
||||
}
|
||||
|
||||
// usageErr marks a usage error already reported by cliOnUsageError, so RunMainApp does not print it again.
|
||||
type usageErr struct{ err error }
|
||||
|
||||
func (e usageErr) Error() string { return e.err.Error() }
|
||||
func (e usageErr) Unwrap() error { return e.err }
|
||||
|
||||
// cliOnUsageError reports usage errors itself instead of letting urfave/cli dump the full help to stdout (since urfave/cli v3.10).
|
||||
func cliOnUsageError(_ context.Context, cmd *cli.Command, err error, _ bool) error {
|
||||
_, _ = fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n", err.Error())
|
||||
return usageErr{err}
|
||||
}
|
||||
|
||||
func setCLIOnUsageError(cmd *cli.Command) {
|
||||
_ = cmd.Walk(func(c *cli.Command) error {
|
||||
c.OnUsageError = cliOnUsageError
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func RunMainApp(app *cli.Command, args ...string) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
setCLIOnUsageError(app)
|
||||
// the completion subcommands are built during app.Run, after the Walk above, so cover them via this hook
|
||||
app.ConfigureShellCompletionCommand = setCLIOnUsageError
|
||||
err := app.Run(ctx, args)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := errors.AsType[usageErr](err); ok {
|
||||
cli.OsExiter(1) // cliOnUsageError already reported it
|
||||
if strings.HasPrefix(err.Error(), "flag provided but not defined:") {
|
||||
// the cli package should already have output the error message, so just exit
|
||||
cli.OsExiter(1)
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# IDE and code editor configuration
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [IDE and code editor configuration](#ide-and-code-editor-configuration)
|
||||
- [Microsoft Visual Studio Code](#microsoft-visual-studio-code)
|
||||
|
||||
## Microsoft Visual Studio Code
|
||||
|
||||
Download Microsoft Visual Studio Code at https://code.visualstudio.com/ and follow instructions at https://code.visualstudio.com/docs/languages/go to setup Go extension for it.
|
||||
|
||||
Create new directory `.vscode` in Gitea root folder and copy contents of folder [contrib/development/vscode](vscode/) to it. You can now use `Ctrl`+`Shift`+`B` to build gitea executable and `F5` to run it in debug mode.
|
||||
Create new directory `.vscode` in Gitea root folder and copy contents of folder [contrib/ide/vscode](vscode/) to it. You can now use `Ctrl`+`Shift`+`B` to build gitea executable and `F5` to run it in debug mode.
|
||||
|
||||
Supported on Debian, Ubuntu, Red Hat, Fedora, SUSE Linux, MacOS and Microsoft Windows.
|
||||
|
||||
@@ -762,12 +762,7 @@ LEVEL = Info
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Git config options
|
||||
;; The format is `some.configKey = value`.
|
||||
;; These options will be written into the gitconfig file under "[git] HOME_PATH" when Gitea web starts.
|
||||
;; ATTENTION:
|
||||
;; * It only does "set" config, a removed config key from this section won't be removed from git config automatically.
|
||||
;; * Some config options might affect the behavior of git and fail Gitea's git operation,
|
||||
;; make sure you know what you are doing before making changes.
|
||||
;; This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`.
|
||||
;[git.config]
|
||||
;diff.algorithm = histogram
|
||||
;core.logAllRefUpdates = true
|
||||
@@ -3002,10 +2997,6 @@ LEVEL = Info
|
||||
;; Comma-separated list of workflow directories, the first one to exist
|
||||
;; in a repo is used to find Actions workflow files
|
||||
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
|
||||
;; Comma-separated list of scoped workflow directories in a source repository, the first one to exist is used.
|
||||
;; Files here are picked up only when the repo is registered as a scoped-workflow source; in any other repo they neither run repo-level nor scope-level.
|
||||
;; Must not overlap with WORKFLOW_DIRS. Leave empty so no directory is scanned; no scoped workflows are found or run.
|
||||
;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows
|
||||
;; Maximum number of attempts a single workflow run can have. Default value is 50.
|
||||
;MAX_RERUN_ATTEMPTS = 50
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# Setup and requirements
|
||||
|
||||
This document lists the tools you need to build Gitea from source and how to get
|
||||
the code. Once your environment is ready, see [development.md](development.md) for
|
||||
the build and development workflow, and [testing.md](testing.md) for running tests.
|
||||
|
||||
For the contribution workflow and review process, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
### Go
|
||||
|
||||
[Install Go](https://go.dev/doc/install) and set up your Go environment. The
|
||||
required version is the one declared in [`go.mod`](../go.mod); installing the same
|
||||
version your continuous integration uses avoids `gofmt` differences between Go
|
||||
releases.
|
||||
|
||||
> [!NOTE]
|
||||
> Some `make` tasks build external Go tools on demand (for example `make
|
||||
> watch-backend`). To use them, the `"$GOPATH"/bin` directory must be on your
|
||||
> executable `PATH`; otherwise you have to manage those tools yourself.
|
||||
|
||||
### Node.js and pnpm
|
||||
|
||||
[Install Node.js](https://nodejs.org/en/download/) to build the JavaScript and CSS
|
||||
files. The minimum supported version is the one declared in
|
||||
[`package.json`](../package.json) (`engines.node`); the latest LTS is recommended.
|
||||
|
||||
Gitea manages frontend dependencies with [pnpm](https://pnpm.io/). The `make`
|
||||
targets invoke it for you, so installing pnpm manually is only needed if you want
|
||||
to run `pnpm` commands directly.
|
||||
|
||||
### Make
|
||||
|
||||
Gitea uses [Make](https://www.gnu.org/software/make/) to drive builds, linting, and
|
||||
tests. On Windows it can be installed via [MSYS2](https://www.msys2.org/) or
|
||||
[Chocolatey](https://chocolatey.org/packages/make).
|
||||
|
||||
### Python with uv (optional)
|
||||
|
||||
Linting the templates, workflow files, and YAML requires Python tooling that Gitea
|
||||
runs through [uv](https://docs.astral.sh/uv/). After installing uv, `make` creates
|
||||
the environment automatically (`uv sync`); you only need this if you run
|
||||
`make lint-templates`, `make lint-yaml`, or `make lint-actions` locally.
|
||||
|
||||
### Git LFS
|
||||
|
||||
The integration tests require [Git LFS](https://git-lfs.com/) to be installed.
|
||||
|
||||
## Getting the source code
|
||||
|
||||
Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/go-gitea/gitea
|
||||
```
|
||||
|
||||
To contribute changes, [fork the repository](https://github.com/go-gitea/gitea) on
|
||||
GitHub and add your fork as a git remote so you can push branches and open pull
|
||||
requests. See GitHub's [working with forks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks)
|
||||
documentation for the details.
|
||||
|
||||
## Installing dependencies
|
||||
|
||||
Most build and test targets install the dependencies they need on their own. To
|
||||
fetch everything up front, run `make deps` (or the per-group `make deps-frontend`,
|
||||
`make deps-backend`, `make deps-tools`, `make deps-py`).
|
||||
@@ -1,92 +0,0 @@
|
||||
# Prepare build environment
|
||||
|
||||
Complete the steps in [build-setup.md](build-setup.md) to prepare your environment for building Gitea from source.
|
||||
|
||||
## Choose a branch
|
||||
|
||||
By default, the cloned repository is on main branch (the current development branch for next major release, aka: main nightly).
|
||||
|
||||
You can switch to a versioned branch (the branch for the next minor stable release, aka: stable nightly )
|
||||
or a versioned tag (matches the official releases with version numbers)
|
||||
|
||||
To test a Pull Request, you can fetch its code by its Pull Request number (take `PR #123456` as example):
|
||||
|
||||
```bash
|
||||
git fetch origin pull/123456/head:pr-123456
|
||||
```
|
||||
|
||||
# Build
|
||||
|
||||
Various [make tasks](https://github.com/go-gitea/gitea/blob/main/Makefile)
|
||||
are provided to keep the build process as simple as possible.
|
||||
|
||||
Depending on requirements, the following build tags can be included.
|
||||
|
||||
- `bindata`: Build a single monolithic binary, with all assets included. Required for distribution and production build.
|
||||
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules).
|
||||
Can be used to authenticate local users or extend authentication to methods available to PAM.
|
||||
- `gogit`: (EXPERIMENTAL) Use go-git variants of Git commands.
|
||||
|
||||
To include all assets, use the `bindata` tag:
|
||||
|
||||
```bash
|
||||
TAGS="bindata" make build
|
||||
```
|
||||
|
||||
Tag `gogit` is used to try to resolve some Windows-specific performance problems, POSIX systems don't need it.
|
||||
You can build a Windows binary by:
|
||||
|
||||
```bash
|
||||
GOOS=windows TAGS="bindata gogit" make build
|
||||
```
|
||||
|
||||
## Changing default paths
|
||||
|
||||
Gitea will search for a number of things from the _`CustomPath`_.
|
||||
By default, this is the `custom/` directory in the current working directory when running Gitea.
|
||||
It will also look for its configuration file _`CustomConf`_ in `$(CustomPath)/conf/app.ini`,
|
||||
and will use the current working directory as the relative base path _`AppWorkPath`_.
|
||||
|
||||
These values, although useful when developing, may conflict with downstream users preferences.
|
||||
|
||||
For packagers who need to use paths like `/etc/gitea/app.ini`,
|
||||
they should define these values at build time for `make build` by environment variable like
|
||||
`LDFLAGS='-X "module.Var1=Value1" -X "module.Var2=Value2"' TAGS="bindata" make build`.
|
||||
|
||||
- _`CustomConf`_: `-X "code.gitea.io/gitea/modules/setting.CustomConf=/etc/gitea/app.ini"`
|
||||
- _`AppWorkPath`_: `-X "code.gitea.io/gitea/modules/setting.AppWorkPath=/var/lib/gitea"`
|
||||
- _`CustomPath`_: `-X "code.gitea.io/gitea/modules/setting.CustomPath=/var/lib/gitea/custom"`
|
||||
- Default PID file location: `-X "code.gitea.io/gitea/cmd.PIDFile=/run/gitea.pid"`
|
||||
|
||||
Add as many of the strings with their preceding `-X` to the `LDFLAGS` variable and run `make build`
|
||||
with the appropriate `TAGS` as above.
|
||||
|
||||
Running `gitea help` will allow you to review what the computed settings will be for your `gitea`.
|
||||
|
||||
## Cross Build
|
||||
|
||||
Gitea use's Golang's toolchain variables for cross-building.
|
||||
|
||||
For example, to cross build for Linux ARM64:
|
||||
|
||||
```
|
||||
GOOS=linux GOARCH=arm64 TAGS="bindata" make build
|
||||
```
|
||||
|
||||
### Adding shell autocompletion
|
||||
|
||||
Shell completion can be generated directly from binary with:
|
||||
```sh
|
||||
gitea completion <shell>
|
||||
```
|
||||
|
||||
Supported values for `<shell>` are `bash`, `fish`, `pwsh` and `zsh`.
|
||||
Details on how to load the completion for your shell can be found in the completion command help.
|
||||
|
||||
## Source Maps
|
||||
|
||||
By default, gitea generates reduced source maps for frontend files to conserve space. This can be controlled with the `ENABLE_SOURCEMAP` environment variable:
|
||||
|
||||
- `ENABLE_SOURCEMAP=true` generates all source maps, the default for development builds
|
||||
- `ENABLE_SOURCEMAP=reduced` generates limited source maps, the default for production builds
|
||||
- `ENABLE_SOURCEMAP=false` generates no source maps
|
||||
@@ -1,141 +0,0 @@
|
||||
# Development
|
||||
|
||||
This document describes how to build Gitea from source and the day-to-day
|
||||
development workflow. For prerequisites and how to obtain the code, see
|
||||
[build-setup.md](build-setup.md). For running tests, see [testing.md](testing.md). For the
|
||||
contribution workflow and review process, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
Area-specific guidelines:
|
||||
|
||||
- [Backend development guidelines](guidelines-backend.md)
|
||||
- [Frontend development guidelines](guidelines-frontend.md)
|
||||
- [Refactoring guidelines](guidelines-refactoring.md)
|
||||
|
||||
## Building
|
||||
|
||||
To build Gitea for development, run:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
No build tags are required: SQLite support is compiled in by default, which is
|
||||
enough for local development. The `build` target runs two sub-targets, `frontend`
|
||||
and `backend`. The `bindata` tag embeds the frontend assets into the binary and is
|
||||
only needed when packaging a self-contained build, so leave it out during
|
||||
development.
|
||||
|
||||
See `make help` for all available targets, and the workflows in
|
||||
[`.github/workflows`](https://github.com/go-gitea/gitea/tree/main/.github/workflows)
|
||||
to see how continuous integration builds and checks Gitea.
|
||||
|
||||
## Building continuously
|
||||
|
||||
To rebuild automatically when source files change:
|
||||
|
||||
```bash
|
||||
# watch both frontend and backend
|
||||
make watch
|
||||
|
||||
# or watch only the frontend (starts the Vite dev server)
|
||||
make watch-frontend
|
||||
|
||||
# or watch only the backend (Go)
|
||||
make watch-backend
|
||||
```
|
||||
|
||||
Watching all backend source files may hit the default open-files limit on macOS or
|
||||
Linux; raise it with `ulimit -n 12288` for the current shell, or in your shell
|
||||
startup file to make it permanent.
|
||||
|
||||
## Formatting, linting and checks
|
||||
|
||||
Continuous integration rejects pull requests that fail formatting, linting, or
|
||||
consistency checks. Format your code first:
|
||||
|
||||
```bash
|
||||
make fmt
|
||||
```
|
||||
|
||||
Then lint:
|
||||
|
||||
```bash
|
||||
# lint everything
|
||||
make lint
|
||||
# or only one side
|
||||
make lint-backend
|
||||
make lint-frontend
|
||||
```
|
||||
|
||||
Many linters can fix issues automatically with `make lint-fix` (or the scoped
|
||||
`make lint-backend-fix` / `make lint-frontend-fix`). The combined consistency
|
||||
checks that CI runs are available as `make checks`.
|
||||
|
||||
## Building and adding SVGs
|
||||
|
||||
SVG icons are built with `make svg`, which compiles the icon sources into
|
||||
`public/assets/img/svg`. Custom icons can be added under `web_src/svg`.
|
||||
|
||||
## Updating the API
|
||||
|
||||
When you create or change API routes, you **must** update the
|
||||
[Swagger](https://swagger.io/docs/specification/2-0/what-is-swagger/) documentation
|
||||
using [go-swagger](https://goswagger.io/) comments. See the
|
||||
[backend development guidelines](guidelines-backend.md) for how API routes,
|
||||
request/response structs, and swagger definitions fit together.
|
||||
|
||||
Regenerate and validate the spec after changing an endpoint, then commit the
|
||||
updated JSON:
|
||||
|
||||
```bash
|
||||
make generate-swagger
|
||||
make swagger-validate
|
||||
```
|
||||
|
||||
CI verifies the committed spec is up to date with:
|
||||
|
||||
```bash
|
||||
make swagger-check
|
||||
```
|
||||
|
||||
## Creating new configuration options
|
||||
|
||||
When adding configuration options it is not enough to add them to the
|
||||
`modules/setting` files. Also update
|
||||
[`custom/conf/app.example.ini`](../custom/conf/app.example.ini), and document them in
|
||||
the [configuration cheat sheet](https://docs.gitea.com/administration/config-cheat-sheet),
|
||||
which lives in the [documentation repository](https://gitea.com/gitea/docs).
|
||||
|
||||
## Database migrations
|
||||
|
||||
If you make breaking changes to a database-persisted struct under `models/`, add a
|
||||
new migration in `models/migrations/`. See [testing.md](testing.md#migration-tests)
|
||||
for running the migration tests.
|
||||
|
||||
## Testing
|
||||
|
||||
For unit, integration, end-to-end, and migration tests, see [testing.md](testing.md).
|
||||
|
||||
## IDE configuration
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
A `launch.json` and `tasks.json` are provided in
|
||||
[`contrib/development/vscode`](../contrib/development/vscode). See
|
||||
[`contrib/development/README.md`](../contrib/development/README.md) for details.
|
||||
|
||||
### GoLand
|
||||
|
||||
Clicking the `Run Application` arrow on `func main()` in `/main.go` starts a
|
||||
debuggable Gitea instance.
|
||||
|
||||
The `Output Directory` in `Run/Debug Configuration` **must** be set to the Gitea
|
||||
project directory (the one containing `main.go` and `go.mod`). Otherwise the working
|
||||
directory is a GoLand temporary directory, which prevents Gitea from loading dynamic
|
||||
resources (such as templates) in development.
|
||||
|
||||
## Submitting your changes
|
||||
|
||||
Push your branch and open a pull request. See [CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||
for the review process and PR requirements. For help, join the `#Develop` channel on
|
||||
[Discord](https://discord.gg/gitea).
|
||||
@@ -0,0 +1,63 @@
|
||||
# Backend development
|
||||
|
||||
This document covers backend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
For coding style and architecture, see also the [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) on the documentation site.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \
|
||||
You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules).
|
||||
|
||||
Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \
|
||||
Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies.
|
||||
|
||||
The `go.mod`, `go.sum` update needs to be justified as part of the PR description,
|
||||
and must be verified by the reviewers and/or merger to always reference
|
||||
an existing upstream commit.
|
||||
|
||||
## API v1
|
||||
|
||||
The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
|
||||
|
||||
### GitHub API compatibility
|
||||
|
||||
Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \
|
||||
If Gitea provides functionality that GitHub does not, a new endpoint can be created. \
|
||||
If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \
|
||||
Updating an existing API should not remove existing fields unless there is a really good reason to do so. \
|
||||
The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned).
|
||||
|
||||
### Adding/Maintaining API routes
|
||||
|
||||
All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \
|
||||
All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \
|
||||
and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \
|
||||
They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \
|
||||
All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \
|
||||
and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \
|
||||
They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279).
|
||||
|
||||
### When to use what HTTP method
|
||||
|
||||
In general, HTTP methods are chosen as follows:
|
||||
|
||||
- **GET** endpoints return the requested object(s) and status **OK (200)**
|
||||
- **DELETE** endpoints return the status **No Content (204)** and no content either
|
||||
- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object
|
||||
- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either
|
||||
- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)**
|
||||
|
||||
### Requirements for API routes
|
||||
|
||||
All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required).
|
||||
|
||||
Endpoints returning lists must
|
||||
|
||||
- support pagination (`page` & `limit` options in query)
|
||||
- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
|
||||
|
||||
### Knowledge
|
||||
|
||||
- Partially database table migration must use `SyncWithOptions(IgnoreDrop...)`
|
||||
- Template variables with "camelCase" or "snake_case" are used for restoring the form values from a submitted form
|
||||
@@ -0,0 +1,17 @@
|
||||
# Frontend development
|
||||
|
||||
This document covers frontend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
## Dependencies
|
||||
|
||||
For the frontend, we use [npm](https://www.npmjs.com/).
|
||||
|
||||
The same restrictions apply for frontend dependencies as for [backend dependencies](guideline-backend.md#dependencies), with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version.
|
||||
|
||||
## Design guideline
|
||||
|
||||
Depending on your change, please read the
|
||||
|
||||
- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend)
|
||||
- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend)
|
||||
- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring)
|
||||
@@ -1,132 +0,0 @@
|
||||
# Backend development guidelines
|
||||
|
||||
This document covers backend-specific architecture and contribution expectations.
|
||||
For the general workflow see [CONTRIBUTING.md](../CONTRIBUTING.md), and for building
|
||||
and testing see [development.md](development.md) and [testing.md](testing.md).
|
||||
|
||||
## Background
|
||||
|
||||
The backend is written in Go. Web routing is handled by
|
||||
[chi](https://github.com/go-chi/chi) and database access goes through the
|
||||
[XORM](https://xorm.io/) ORM. Understanding how the packages depend on each other is
|
||||
essential before contributing backend code.
|
||||
|
||||
## Package design
|
||||
|
||||
### Package layout
|
||||
|
||||
The backend is split into top-level packages, each with a focused responsibility:
|
||||
|
||||
- `build`: helper scripts used at compile time
|
||||
- `cmd`: subcommands such as `web`, `serv`, `hooks`, `doctor`, and admin utilities
|
||||
- `models`: data structures and database operations (XORM); keeps external
|
||||
dependencies to a minimum
|
||||
- `models/db`: core database operations
|
||||
- `models/fixtures`: sample data used by tests
|
||||
- `models/migrations`: schema migration scripts
|
||||
- `modules`: standalone functionality with few dependencies
|
||||
- `modules/setting`: configuration handling
|
||||
- `modules/git`: interaction with the Git command line
|
||||
- `routers`: request handlers, split into `api`, `web`, `install`, and `private`
|
||||
- `services`: business logic that ties routers and models together
|
||||
- `templates`: Go HTML templates
|
||||
- `public`: compiled frontend assets
|
||||
- `tests`: integration and end-to-end test helpers
|
||||
|
||||
### Dependency direction
|
||||
|
||||
Dependencies only flow in one direction:
|
||||
|
||||
```text
|
||||
cmd → routers → services → models → modules
|
||||
```
|
||||
|
||||
A package on the left may import a package on its right, but never the reverse.
|
||||
|
||||
### Naming conventions
|
||||
|
||||
- Top-level packages use the plural form: `services`, `models`, `routers`.
|
||||
- Subpackages use the singular form: `services/user`, `models/repository`.
|
||||
|
||||
When packages from different layers share a name, use a snake_case import alias to
|
||||
disambiguate:
|
||||
|
||||
```go
|
||||
import user_service "gitea.dev/services/user"
|
||||
```
|
||||
|
||||
### Database transactions
|
||||
|
||||
Operations that must roll back together should run inside `db.WithTx()` (or
|
||||
`db.WithTx2()` when a value must be returned), defined in `models/db/context.go`.
|
||||
Functions that participate in a transaction take a `context.Context` as their first
|
||||
parameter so the transaction can be propagated.
|
||||
|
||||
### XORM gotchas
|
||||
|
||||
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
|
||||
every row in the table.
|
||||
- Partial table migrations must use `SyncWithOptions(IgnoreDrop...)` rather than a
|
||||
plain `Sync`.
|
||||
- When inserting rows with preset IDs, MSSQL requires `SET IDENTITY_INSERT` to be
|
||||
enabled and PostgreSQL requires the sequence to be updated afterwards.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Go dependencies are managed with [Go Modules](https://go.dev/ref/mod).
|
||||
|
||||
Pull requests should only modify `go.mod` and `go.sum` where it relates to the
|
||||
change at hand, be it a bug fix or a new feature. Otherwise, these files should only
|
||||
be touched by pull requests whose sole purpose is updating dependencies. Run
|
||||
`make tidy` after any change to `go.mod`.
|
||||
|
||||
Any `go.mod` / `go.sum` update must be justified in the PR description and must be
|
||||
verified by reviewers and the merger to reference an existing upstream commit.
|
||||
|
||||
## API v1
|
||||
|
||||
The API is documented with [Swagger](https://gitea.com/api/swagger) and is modelled
|
||||
on [the GitHub API](https://docs.github.com/en/rest).
|
||||
|
||||
### GitHub API compatibility
|
||||
|
||||
Gitea's API should use the same endpoints and fields as the GitHub API where
|
||||
possible, unless there is a good reason to deviate.
|
||||
|
||||
- If Gitea offers functionality GitHub does not, a new endpoint may be added.
|
||||
- If Gitea exposes information the GitHub API does not, a new field may be added as
|
||||
long as it does not collide with a GitHub field.
|
||||
- Existing fields should not be removed unless there is a strong reason; the same
|
||||
applies to status responses.
|
||||
|
||||
If you notice a problem that would require a breaking change, leave a comment in the
|
||||
code for a future refactor to API v2 (which is currently not planned) rather than
|
||||
breaking v1.
|
||||
|
||||
### Adding and maintaining API routes
|
||||
|
||||
- All possible results (errors, success, and failure messages) must be documented in
|
||||
the swagger comments on the route.
|
||||
- Every JSON request body must be defined as a struct in `modules/structs/` and
|
||||
registered in [`routers/api/v1/swagger/options.go`](../routers/api/v1/swagger/options.go).
|
||||
- Every JSON response must be defined as a struct in `modules/structs/` and
|
||||
registered with its category under [`routers/api/v1/swagger/`](../routers/api/v1/swagger).
|
||||
|
||||
### HTTP methods and status codes
|
||||
|
||||
In general, choose HTTP methods as follows:
|
||||
|
||||
- **GET** returns the requested object(s) with status **200 OK**.
|
||||
- **POST** creates a new object (e.g. a user) and returns **201 Created** with the
|
||||
created object.
|
||||
- **PUT** adds or assigns an existing object (e.g. a user to a team) and returns
|
||||
**204 No Content** with no body.
|
||||
- **PATCH** edits an existing object and returns the changed object with **200 OK**.
|
||||
- **DELETE** removes an object and returns **204 No Content** with no body.
|
||||
|
||||
### Requirements for API routes
|
||||
|
||||
- All parameters of endpoints that edit an object must be optional, except those
|
||||
needed to identify the object, which are required.
|
||||
- Endpoints returning lists must support pagination (`page` and `limit` query
|
||||
options) and set the `X-Total-Count` header via `ctx.SetTotalCountHeader(...)`.
|
||||
@@ -1,98 +0,0 @@
|
||||
# Frontend development guidelines
|
||||
|
||||
This document covers frontend-specific architecture and contribution expectations.
|
||||
For the general workflow see [CONTRIBUTING.md](../CONTRIBUTING.md), and for building
|
||||
and testing see [development.md](development.md) and [testing.md](testing.md).
|
||||
|
||||
## Background
|
||||
|
||||
The frontend uses [Vue 3](https://vuejs.org/), [Fomantic-UI](https://fomantic-ui.com/) (built on jQuery)
|
||||
and [Tailwind CSS](https://tailwindcss.com/). Pages are rendered with Go HTML templates.
|
||||
Source files live in:
|
||||
|
||||
- `web_src/css/`: CSS styles
|
||||
- `web_src/js/`: JavaScript and TypeScript
|
||||
- `web_src/js/components/`: Vue components
|
||||
- `web_src/js/features/`: feature modules wired up at page load
|
||||
- `templates/`: Go HTML templates
|
||||
|
||||
## Dependencies
|
||||
|
||||
Frontend dependencies are managed with [pnpm](https://pnpm.io/). The same rules as
|
||||
for [backend dependencies](guidelines-backend.md#dependencies) apply, except the
|
||||
relevant files are `package.json` and `pnpm-lock.yaml`, and new versions must always
|
||||
reference an existing published version.
|
||||
|
||||
## Framework usage
|
||||
|
||||
Mixing frameworks arbitrarily makes code hard to maintain. Recommended combinations:
|
||||
|
||||
- Vue3
|
||||
- Vanilla JavaScript
|
||||
- Fomantic-UI (jQuery), deprecated, we vendored a specific version with a lot of changes.
|
||||
|
||||
Avoid combinations such as Vue with Fomantic-UI.
|
||||
Vue components may reuse Fomantic-UI CSS classes for visual consistency.
|
||||
Use Go templates for simple or SEO-relevant pages and Vue for complex, interactive pages.
|
||||
Gitea uses Vue 3 **without** JSX to keep HTML and JavaScript separate.
|
||||
|
||||
> [!NOTE]
|
||||
> Fomantic-UI is not an accessibility-friendly framework. Gitea patches some ARIA
|
||||
> behavior, but accessibility work is ongoing — prefer semantic HTML and test
|
||||
> keyboard/screen-reader behavior where you can.
|
||||
|
||||
## Gitea-specific conventions
|
||||
|
||||
- Keep features in their own files or directories.
|
||||
- Use kebab-case for HTML `id`s and classes, ideally with 2-3 feature keywords.
|
||||
- Prefix classes to avoid short-name conflicts between different frameworks.
|
||||
- Create a new class name when overriding framework styles instead of editing the framework's own classes,
|
||||
or fix the framework's source to fix all cases.
|
||||
- Prefer semantic elements such as `<button>` over generic `<div>`s.
|
||||
- Avoid `!important`; when it is unavoidable, document why.
|
||||
- Prefix custom DOM events with `ce-`.
|
||||
|
||||
## CSS
|
||||
|
||||
Prefer Tailwind utility classes with the `tw-` prefix, and the `flex-*` layout
|
||||
helpers over per-child margins. Gitea also ships a small set of custom helpers:
|
||||
`gt-` for general helpers and `g-` for framework-level helpers (see
|
||||
`web_src/css/helpers.css`); use these only when a Tailwind utility does not exist.
|
||||
|
||||
Write class attributes as a single readable unit in templates:
|
||||
|
||||
```html
|
||||
<div class="flex-text-inline {{if .IsFoo}}tw-hidden{{end}}"></div>
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
- Use `import type` for type-only imports.
|
||||
- Prefer `@ts-expect-error` over `@ts-ignore`.
|
||||
- Use the `!` non-null assertion (rather than `?.`/`??`) when a value is known to always exist.
|
||||
- Only mark a function `async` when it actually uses `await` or returns a `Promise`.
|
||||
Avoid async event listeners; if unavoidable, call `e.preventDefault()` before the
|
||||
first `await`. For a deliberately un-awaited call, assign it: `const _promise = asyncFoo()`.
|
||||
|
||||
## Data fetching
|
||||
|
||||
Use the `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` wrappers from
|
||||
[`web_src/js/modules/fetch.ts`](../web_src/js/modules/fetch.ts).
|
||||
|
||||
## DOM attributes
|
||||
|
||||
Avoid `node.dataset` because of its camel-casing behavior; use `node.getAttribute`
|
||||
in new code. Never bind user-provided data directly onto DOM nodes.
|
||||
|
||||
## Showing and hiding elements
|
||||
|
||||
- In Vue, use `v-if` and `v-show`.
|
||||
- In Go templates and plain JavaScript, use the `.tw-hidden` class together with the
|
||||
`showElem()`, `hideElem()`, and `toggleElem()` helpers from
|
||||
[`web_src/js/utils/dom.ts`](../web_src/js/utils/dom.ts).
|
||||
|
||||
## UI component gallery
|
||||
|
||||
When running Gitea in development mode, standardized UI components are available at
|
||||
`/devtest` (for example `http://localhost:3000/devtest`). These pages are also used
|
||||
by the e2e tests.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Refactoring guidelines
|
||||
|
||||
This document covers expectations for refactoring work. For the general workflow see
|
||||
[CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
## Background
|
||||
|
||||
Gitea is a large, long-lived project. Over time the codebase has accumulated
|
||||
outdated mechanisms, mixed frameworks, and legacy code that can cause bugs or slow
|
||||
down new features. Refactoring keeps the codebase maintainable, but it needs to be
|
||||
done carefully so it improves things without introducing regressions.
|
||||
|
||||
## Writing a refactoring PR
|
||||
|
||||
- Be forward-looking: address the root cause, not just the immediate symptom.
|
||||
- Aim to reduce ambiguity and conflicts and to improve maintainability.
|
||||
- Explain the rationale in the PR description: why the refactor is necessary, how it
|
||||
resolves the legacy problem, and its advantages and disadvantages.
|
||||
- Keep the scope tight: preserve existing behavior where feasible and avoid bundling
|
||||
unrelated changes.
|
||||
- Break large refactors into intermediate steps across multiple PRs so each one is
|
||||
easy to review.
|
||||
- Include tests that verify the behavior stays correct.
|
||||
- Prefer scheduling non-bugfix refactoring early in a milestone, so any issues
|
||||
surface well before a release.
|
||||
- If there is disagreement about a refactor, escalate to the Technical Oversight
|
||||
Committee (TOC) for a decision.
|
||||
|
||||
## Reviewing and merging
|
||||
|
||||
- Keep refactoring PRs short-lived (typically no more than 7 days) with quick review
|
||||
cycles, and merge them promptly so they do not block on unrelated work.
|
||||
- A non-author core member may approve and merge a refactoring PR after 7 days if the
|
||||
TOC has raised no objection.
|
||||
- Accept imperfect intermediate implementations as long as the final result improves
|
||||
the codebase.
|
||||
- A temporary regression caused by a necessary refactor is acceptable if it is fixed
|
||||
promptly afterwards.
|
||||
-151
@@ -1,151 +0,0 @@
|
||||
# Testing
|
||||
|
||||
Gitea has four kinds of automated tests: backend unit tests, integration tests,
|
||||
end-to-end (e2e) tests, and migration tests. Local runs default to SQLite, so no
|
||||
extra services are required to get started.
|
||||
|
||||
For prerequisites see [build-setup.md](build-setup.md); for the build workflow see
|
||||
[development.md](development.md).
|
||||
|
||||
## Unit tests
|
||||
|
||||
Backend unit tests live in `*_test.go` files next to the code they cover. Set
|
||||
`GITEA_TEST_LOG_SQL=1` to log all SQL statements executed during the tests.
|
||||
|
||||
```bash
|
||||
make test-backend
|
||||
```
|
||||
|
||||
To run a single backend test, use `go test` directly or the `#` selector:
|
||||
|
||||
```bash
|
||||
go test -run '^TestName$' ./modulepath/
|
||||
make test-backend#TestName
|
||||
```
|
||||
|
||||
Frontend unit tests run with [Vitest](https://vitest.dev/):
|
||||
|
||||
```bash
|
||||
make test-frontend
|
||||
# single file:
|
||||
pnpm exec vitest <path-filter>
|
||||
```
|
||||
|
||||
## Integration tests
|
||||
|
||||
Integration tests exercise Gitea against a real database. They live in
|
||||
`tests/integration/` and require [Git LFS](https://git-lfs.com/) to be installed.
|
||||
The database is selected with `GITEA_TEST_DATABASE`; an empty value defaults to
|
||||
SQLite, which needs no external service:
|
||||
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
Run a single integration test with the `#` selector:
|
||||
|
||||
```bash
|
||||
make test-integration#TestName
|
||||
```
|
||||
|
||||
If you hit errors such as a mismatched database version or SSH push failures, try a
|
||||
clean rebuild first:
|
||||
|
||||
```bash
|
||||
make clean build
|
||||
```
|
||||
|
||||
### Running against other databases
|
||||
|
||||
Set `GITEA_TEST_DATABASE` together with the matching `TEST_*` connection variables.
|
||||
The commands below start a throwaway database container (press `Ctrl-C` to stop and
|
||||
remove it) and then run the tests against it.
|
||||
|
||||
#### MySQL
|
||||
|
||||
```bash
|
||||
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest
|
||||
```
|
||||
|
||||
```bash
|
||||
GITEA_TEST_DATABASE=mysql TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-integration
|
||||
```
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
PostgreSQL tests also use a MinIO container for object storage:
|
||||
|
||||
```bash
|
||||
docker run -e "POSTGRES_DB=test" -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=postgres" -p 5432:5432 --rm --name pgsql postgres:latest
|
||||
docker run --rm -p 9000:9000 -e MINIO_ROOT_USER=123456 -e MINIO_ROOT_PASSWORD=12345678 --name minio bitnamilegacy/minio:2023.8.31
|
||||
```
|
||||
|
||||
```bash
|
||||
GITEA_TEST_DATABASE=pgsql TEST_MINIO_ENDPOINT=localhost:9000 TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=postgres TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-integration
|
||||
```
|
||||
|
||||
#### MSSQL
|
||||
|
||||
```bash
|
||||
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest
|
||||
```
|
||||
|
||||
```bash
|
||||
GITEA_TEST_DATABASE=mssql TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-integration
|
||||
```
|
||||
|
||||
### Running the database test workflow with Gitea Runner
|
||||
|
||||
The CI database test jobs can be run locally with
|
||||
[Gitea Runner](https://gitea.com/gitea/runner). Running every job is
|
||||
resource-intensive and not recommended:
|
||||
|
||||
```bash
|
||||
gitea-runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
|
||||
```
|
||||
|
||||
List the available job names, then run a single one:
|
||||
|
||||
```bash
|
||||
gitea-runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest -l
|
||||
gitea-runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest -j <job_name>
|
||||
```
|
||||
|
||||
## End-to-end tests
|
||||
|
||||
End-to-end tests drive a running Gitea instance with [Playwright](https://playwright.dev/):
|
||||
|
||||
```bash
|
||||
make test-e2e
|
||||
```
|
||||
|
||||
To run a single e2e test file, pass it via `GITEA_TEST_E2E_FLAGS`:
|
||||
|
||||
```bash
|
||||
GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e
|
||||
```
|
||||
|
||||
Useful environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
| :--- | :--- |
|
||||
| `GITEA_TEST_E2E_DEBUG` | When set, show the Gitea server output. |
|
||||
| `GITEA_TEST_E2E_FLAGS` | Additional flags passed to Playwright, e.g. `--ui`. |
|
||||
| `GITEA_TEST_E2E_TIMEOUT_FACTOR` | Timeout multiplier (default: 4 on CI, 1 locally). |
|
||||
|
||||
## Migration tests
|
||||
|
||||
If you change a database-persisted struct under `models/` you will usually need a
|
||||
new migration in `models/migrations/`. Run the migration tests with:
|
||||
|
||||
```bash
|
||||
make test-migration
|
||||
```
|
||||
|
||||
## Continuous integration
|
||||
|
||||
CI runs the unit tests, runs the integration tests against every supported database,
|
||||
and tests migration from several recent Gitea versions. Please submit your pull
|
||||
request with additional unit and integration tests as appropriate. Prefer unit tests
|
||||
when the logic can be tested in isolation, and keep local integration and e2e tests
|
||||
fast (aim for sub-2s runtime).
|
||||
+51
-186
@@ -1,6 +1,7 @@
|
||||
import arrayFunc from 'eslint-plugin-array-func';
|
||||
import comments from '@eslint-community/eslint-plugin-eslint-comments';
|
||||
import deMorgan from 'eslint-plugin-de-morgan';
|
||||
import github from 'eslint-plugin-github';
|
||||
import globals from 'globals';
|
||||
import importPlugin from 'eslint-plugin-import-x';
|
||||
import playwright from 'eslint-plugin-playwright';
|
||||
@@ -71,6 +72,7 @@ export default defineConfig([
|
||||
regexp,
|
||||
sonarjs,
|
||||
unicorn,
|
||||
github,
|
||||
wc,
|
||||
},
|
||||
settings: {
|
||||
@@ -84,6 +86,7 @@ export default defineConfig([
|
||||
'@eslint-community/eslint-comments/no-duplicate-disable': [2],
|
||||
'@eslint-community/eslint-comments/no-restricted-disable': [0],
|
||||
'@eslint-community/eslint-comments/no-unlimited-disable': [2],
|
||||
'@eslint-community/eslint-comments/no-unused-disable': [2],
|
||||
'@eslint-community/eslint-comments/no-unused-enable': [2],
|
||||
'@eslint-community/eslint-comments/no-use': [0],
|
||||
'@eslint-community/eslint-comments/require-description': [0],
|
||||
@@ -190,6 +193,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-duplicate-type-constituents': [2, {ignoreUnions: true}],
|
||||
'@typescript-eslint/no-dynamic-delete': [0],
|
||||
'@typescript-eslint/no-empty-function': [0],
|
||||
'@typescript-eslint/no-empty-interface': [0],
|
||||
'@typescript-eslint/no-empty-object-type': [2],
|
||||
'@typescript-eslint/no-explicit-any': [0],
|
||||
'@typescript-eslint/no-extra-non-null-assertion': [2],
|
||||
@@ -202,6 +206,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-invalid-this': [0],
|
||||
'@typescript-eslint/no-invalid-void-type': [0],
|
||||
'@typescript-eslint/no-loop-func': [0],
|
||||
'@typescript-eslint/no-loss-of-precision': [0],
|
||||
'@typescript-eslint/no-magic-numbers': [0],
|
||||
'@typescript-eslint/no-meaningless-void-operator': [0],
|
||||
'@typescript-eslint/no-misused-new': [2],
|
||||
@@ -230,7 +235,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-unsafe-assignment': [0],
|
||||
'@typescript-eslint/no-unsafe-call': [0],
|
||||
'@typescript-eslint/no-unsafe-declaration-merging': [2],
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': [0],
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': [2],
|
||||
'@typescript-eslint/no-unsafe-function-type': [2],
|
||||
'@typescript-eslint/no-unsafe-member-access': [0],
|
||||
'@typescript-eslint/no-unsafe-return': [0],
|
||||
@@ -274,6 +279,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/strict-void-return': [0],
|
||||
'@typescript-eslint/switch-exhaustiveness-check': [0],
|
||||
'@typescript-eslint/triple-slash-reference': [2],
|
||||
'@typescript-eslint/typedef': [0],
|
||||
'@typescript-eslint/unbound-method': [0], // too many false-positives
|
||||
'@typescript-eslint/unified-signatures': [2],
|
||||
'accessor-pairs': [2],
|
||||
@@ -306,9 +312,32 @@ export default defineConfig([
|
||||
'func-names': [0],
|
||||
'func-style': [0],
|
||||
'getter-return': [2],
|
||||
'github/a11y-aria-label-is-well-formatted': [0],
|
||||
'github/a11y-no-title-attribute': [0],
|
||||
'github/a11y-no-visually-hidden-interactive-element': [0],
|
||||
'github/a11y-role-supports-aria-props': [0],
|
||||
'github/a11y-svg-has-accessible-name': [0],
|
||||
'github/array-foreach': [0],
|
||||
'github/async-currenttarget': [2],
|
||||
'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599
|
||||
'github/authenticity-token': [0],
|
||||
'github/get-attribute': [0],
|
||||
'github/js-class-name': [0],
|
||||
'github/no-blur': [0],
|
||||
'github/no-d-none': [0],
|
||||
'github/no-dataset': [2],
|
||||
'github/no-dynamic-script-tag': [2],
|
||||
'github/no-implicit-buggy-globals': [2],
|
||||
'github/no-inner-html': [0],
|
||||
'github/no-innerText': [2],
|
||||
'github/no-then': [2],
|
||||
'github/no-useless-passive': [2],
|
||||
'github/prefer-observers': [0],
|
||||
'github/require-passive-events': [2],
|
||||
'gitea/unescaped-html-literal': [2],
|
||||
'grouped-accessor-pairs': [2],
|
||||
'guard-for-in': [0],
|
||||
'id-denylist': [0],
|
||||
'id-blacklist': [0],
|
||||
'id-length': [0],
|
||||
'id-match': [0],
|
||||
'import-x/consistent-type-specifier-style': [0],
|
||||
@@ -355,6 +384,7 @@ export default defineConfig([
|
||||
'import-x/prefer-default-export': [0],
|
||||
'import-x/unambiguous': [0],
|
||||
'init-declarations': [0],
|
||||
'line-comment-position': [0],
|
||||
'logical-assignment-operators': [0],
|
||||
'max-classes-per-file': [0],
|
||||
'max-depth': [0],
|
||||
@@ -363,12 +393,14 @@ export default defineConfig([
|
||||
'max-nested-callbacks': [0],
|
||||
'max-params': [0],
|
||||
'max-statements': [0],
|
||||
'multiline-comment-style': [0],
|
||||
'new-cap': [0],
|
||||
'no-alert': [0],
|
||||
'no-array-constructor': [0], // handled by @typescript-eslint/no-array-constructor
|
||||
'no-async-promise-executor': [0],
|
||||
'no-await-in-loop': [0],
|
||||
'no-bitwise': [0],
|
||||
'no-buffer-constructor': [0],
|
||||
'no-caller': [2],
|
||||
'no-case-declarations': [2],
|
||||
'no-class-assign': [2],
|
||||
@@ -525,11 +557,12 @@ export default defineConfig([
|
||||
'no-nested-ternary': [0],
|
||||
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
|
||||
'no-new-native-nonconstructor': [2],
|
||||
'no-new-object': [2],
|
||||
'no-new-symbol': [0], // handled by no-new-native-nonconstructor
|
||||
'no-new-wrappers': [2],
|
||||
'no-new': [0],
|
||||
'no-nonoctal-decimal-escape': [2],
|
||||
'no-obj-calls': [2],
|
||||
'no-object-constructor': [2],
|
||||
'no-octal-escape': [2],
|
||||
'no-octal': [2],
|
||||
'no-param-reassign': [0],
|
||||
@@ -589,8 +622,10 @@ export default defineConfig([
|
||||
'no-warning-comments': [0],
|
||||
'no-with': [0], // handled by no-restricted-syntax
|
||||
'object-shorthand': [2, 'always'],
|
||||
'one-var-declaration-per-line': [0],
|
||||
'one-var': [0],
|
||||
'operator-assignment': [2, 'always'],
|
||||
'operator-linebreak': [0], // handled by @stylistic/operator-linebreak
|
||||
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
|
||||
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
|
||||
'prefer-destructuring': [0],
|
||||
@@ -726,309 +761,139 @@ export default defineConfig([
|
||||
'strict': [0],
|
||||
'symbol-description': [2],
|
||||
'unicode-bom': [2, 'never'],
|
||||
'unicorn/better-dom-traversing': [2],
|
||||
'unicorn/better-regex': [0],
|
||||
'unicorn/catch-error-name': [0],
|
||||
'unicorn/class-reference-in-static-methods': [2],
|
||||
'unicorn/comment-content': [0],
|
||||
'unicorn/consistent-assert': [0],
|
||||
'unicorn/consistent-boolean-name': [0],
|
||||
'unicorn/consistent-class-member-order': [0],
|
||||
'unicorn/consistent-compound-words': [0], // too opinionated
|
||||
'unicorn/consistent-conditional-object-spread': [2],
|
||||
'unicorn/consistent-date-clone': [2],
|
||||
'unicorn/consistent-destructuring': [2],
|
||||
'unicorn/consistent-empty-array-spread': [2],
|
||||
'unicorn/consistent-existence-index-check': [0],
|
||||
'unicorn/consistent-export-decorator-position': [2],
|
||||
'unicorn/consistent-function-scoping': [0],
|
||||
'unicorn/consistent-function-style': [2],
|
||||
'unicorn/consistent-json-file-read': [2],
|
||||
'unicorn/consistent-optional-chaining': [2],
|
||||
'unicorn/consistent-template-literal-escape': [2],
|
||||
'unicorn/consistent-existence-index-check': [0],
|
||||
'unicorn/consistent-function-scoping': [0],
|
||||
'unicorn/custom-error-definition': [0],
|
||||
'unicorn/default-export-style': [2],
|
||||
'unicorn/dom-node-dataset': [2, {preferAttributes: true}],
|
||||
'unicorn/empty-brace-spaces': [2],
|
||||
'unicorn/error-message': [0],
|
||||
'unicorn/escape-case': [0],
|
||||
'unicorn/expiring-todo-comments': [0],
|
||||
'unicorn/explicit-length-check': [0],
|
||||
'unicorn/explicit-timer-delay': [2],
|
||||
'unicorn/filename-case': [0],
|
||||
'unicorn/id-match': [2],
|
||||
'unicorn/import-index': [0],
|
||||
'unicorn/import-style': [0],
|
||||
'unicorn/isolated-functions': [2, {functions: []}],
|
||||
'unicorn/logical-assignment-operators': [0],
|
||||
'unicorn/max-nested-calls': [0],
|
||||
'unicorn/name-replacements': [0],
|
||||
'unicorn/new-for-builtins': [2],
|
||||
'unicorn/no-abusive-eslint-disable': [0],
|
||||
'unicorn/no-accessor-recursion': [2],
|
||||
'unicorn/no-accidental-bitwise-operator': [2],
|
||||
'unicorn/no-anonymous-default-export': [0],
|
||||
'unicorn/no-array-callback-reference': [0],
|
||||
'unicorn/no-array-concat-in-loop': [2],
|
||||
'unicorn/no-array-fill-with-reference-type': [2],
|
||||
'unicorn/no-array-from-fill': [2],
|
||||
'unicorn/no-array-front-mutation': [0],
|
||||
'unicorn/no-array-for-each': [2],
|
||||
'unicorn/no-array-method-this-argument': [2],
|
||||
'unicorn/no-array-push-push': [2],
|
||||
'unicorn/no-array-reduce': [2],
|
||||
'unicorn/no-array-reverse': [0],
|
||||
'unicorn/no-array-sort': [0],
|
||||
'unicorn/no-array-sort-for-min-max': [2],
|
||||
'unicorn/no-array-splice': [0],
|
||||
'unicorn/no-asterisk-prefix-in-documentation-comments': [0],
|
||||
'unicorn/no-await-expression-member': [0],
|
||||
'unicorn/no-await-in-promise-methods': [2],
|
||||
'unicorn/no-blob-to-file': [2],
|
||||
'unicorn/no-boolean-sort-comparator': [2],
|
||||
'unicorn/no-break-in-nested-loop': [0],
|
||||
'unicorn/no-canvas-to-image': [2],
|
||||
'unicorn/no-chained-comparison': [2],
|
||||
'unicorn/no-collection-bracket-access': [2],
|
||||
'unicorn/no-computed-property-existence-check': [0],
|
||||
'unicorn/no-confusing-array-splice': [2],
|
||||
'unicorn/no-confusing-array-with': [2],
|
||||
'unicorn/no-console-spaces': [0],
|
||||
'unicorn/no-constant-zero-expression': [2],
|
||||
'unicorn/no-declarations-before-early-exit': [0],
|
||||
'unicorn/no-document-cookie': [2],
|
||||
'unicorn/no-double-comparison': [2],
|
||||
'unicorn/no-duplicate-if-branches': [2],
|
||||
'unicorn/no-duplicate-logical-operands': [2],
|
||||
'unicorn/no-duplicate-loops': [0],
|
||||
'unicorn/no-duplicate-set-values': [2],
|
||||
'unicorn/no-empty-file': [2],
|
||||
'unicorn/no-error-property-assignment': [2],
|
||||
'unicorn/no-exports-in-scripts': [2],
|
||||
'unicorn/no-for-each': [2],
|
||||
'unicorn/no-for-loop': [0],
|
||||
'unicorn/no-global-object-property-assignment': [0],
|
||||
'unicorn/no-hex-escape': [0],
|
||||
'unicorn/no-immediate-mutation': [0],
|
||||
'unicorn/no-impossible-length-comparison': [2],
|
||||
'unicorn/no-incorrect-query-selector': [2],
|
||||
'unicorn/no-incorrect-template-string-interpolation': [0],
|
||||
'unicorn/no-instanceof-builtins': [2],
|
||||
'unicorn/no-invalid-argument-count': [0],
|
||||
'unicorn/no-invalid-character-comparison': [2],
|
||||
'unicorn/no-instanceof-array': [0],
|
||||
'unicorn/no-invalid-fetch-options': [2],
|
||||
'unicorn/no-invalid-file-input-accept': [2],
|
||||
'unicorn/no-invalid-remove-event-listener': [2],
|
||||
'unicorn/no-keyword-prefix': [0],
|
||||
'unicorn/no-late-current-target-access': [2],
|
||||
'unicorn/no-length-as-slice-end': [2],
|
||||
'unicorn/no-lonely-if': [2],
|
||||
'unicorn/no-loop-iterable-mutation': [2],
|
||||
'unicorn/no-magic-array-flat-depth': [0],
|
||||
'unicorn/no-manually-wrapped-comments': [0], // too opinionated
|
||||
'unicorn/no-mismatched-map-key': [2],
|
||||
'unicorn/no-misrefactored-assignment': [2],
|
||||
'unicorn/no-named-default': [2],
|
||||
'unicorn/no-negated-array-predicate': [2],
|
||||
'unicorn/no-negated-comparison': [2],
|
||||
'unicorn/no-negated-condition': [0],
|
||||
'unicorn/no-negation-in-equality-check': [2],
|
||||
'unicorn/no-nested-ternary': [0],
|
||||
'unicorn/no-new-array': [0],
|
||||
'unicorn/no-new-buffer': [0],
|
||||
'unicorn/no-non-function-verb-prefix': [0],
|
||||
'unicorn/no-nonstandard-builtin-properties': [2],
|
||||
'unicorn/no-null': [0],
|
||||
'unicorn/no-object-as-default-parameter': [0],
|
||||
'unicorn/no-object-methods-with-collections': [2],
|
||||
'unicorn/no-optional-chaining-on-undeclared-variable': [2],
|
||||
'unicorn/no-process-exit': [0],
|
||||
'unicorn/no-redundant-comparison': [2],
|
||||
'unicorn/no-return-array-push': [2],
|
||||
'unicorn/no-selector-as-dom-name': [2],
|
||||
'unicorn/no-single-promise-in-promise-methods': [2],
|
||||
'unicorn/no-static-only-class': [2],
|
||||
'unicorn/no-subtraction-comparison': [2],
|
||||
'unicorn/no-thenable': [2],
|
||||
'unicorn/no-this-assignment': [2],
|
||||
'unicorn/no-this-outside-of-class': [0], // gitea uses `this` in non-class functions
|
||||
'unicorn/no-top-level-assignment-in-function': [0],
|
||||
'unicorn/no-top-level-side-effects': [0],
|
||||
'unicorn/no-typeof-undefined': [2],
|
||||
'unicorn/no-uncalled-method': [2],
|
||||
'unicorn/no-undeclared-class-members': [2],
|
||||
'unicorn/no-unnecessary-array-flat-depth': [2],
|
||||
'unicorn/no-unnecessary-array-splice-count': [2],
|
||||
'unicorn/no-unnecessary-await': [2],
|
||||
'unicorn/no-unnecessary-boolean-comparison': [2],
|
||||
'unicorn/no-unnecessary-global-this': [0],
|
||||
'unicorn/no-unnecessary-nested-ternary': [2],
|
||||
'unicorn/no-unnecessary-polyfills': [2],
|
||||
'unicorn/no-unnecessary-slice-end': [2],
|
||||
'unicorn/no-unnecessary-splice': [2],
|
||||
'unicorn/no-unreadable-array-destructuring': [0],
|
||||
'unicorn/no-unreadable-for-of-expression': [0],
|
||||
'unicorn/no-unreadable-iife': [0],
|
||||
'unicorn/no-unreadable-new-expression': [0],
|
||||
'unicorn/no-unreadable-object-destructuring': [0],
|
||||
'unicorn/no-unsafe-buffer-conversion': [2],
|
||||
'unicorn/no-unsafe-dom-html': [0],
|
||||
'unicorn/no-unsafe-property-key': [0],
|
||||
'unicorn/no-unsafe-string-replacement': [0],
|
||||
'unicorn/no-unused-array-method-return': [2],
|
||||
'unicorn/no-unused-properties': [2],
|
||||
'unicorn/no-useless-boolean-cast': [2],
|
||||
'unicorn/no-useless-coercion': [2],
|
||||
'unicorn/no-useless-collection-argument': [2],
|
||||
'unicorn/no-useless-compound-assignment': [2],
|
||||
'unicorn/no-useless-concat': [2],
|
||||
'unicorn/no-useless-continue': [2],
|
||||
'unicorn/no-useless-delete-check': [2],
|
||||
'unicorn/no-useless-else': [0],
|
||||
'unicorn/no-useless-error-capture-stack-trace': [2],
|
||||
'unicorn/no-useless-fallback-in-spread': [2],
|
||||
'unicorn/no-useless-iterator-to-array': [2],
|
||||
'unicorn/no-useless-length-check': [2],
|
||||
'unicorn/no-useless-logical-operand': [2],
|
||||
'unicorn/no-useless-override': [2],
|
||||
'unicorn/no-useless-promise-resolve-reject': [2],
|
||||
'unicorn/no-useless-recursion': [0],
|
||||
'unicorn/no-useless-spread': [2],
|
||||
'unicorn/no-useless-switch-case': [2],
|
||||
'unicorn/no-useless-template-literals': [2],
|
||||
'unicorn/no-useless-undefined': [0],
|
||||
'unicorn/no-xor-as-exponentiation': [2],
|
||||
'unicorn/no-zero-fractions': [2],
|
||||
'unicorn/number-literal-case': [0],
|
||||
'unicorn/numeric-separators-style': [0],
|
||||
'unicorn/operator-assignment': [2],
|
||||
'unicorn/prefer-add-event-listener': [2],
|
||||
'unicorn/prefer-add-event-listener-options': [2],
|
||||
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
||||
'unicorn/prefer-array-flat': [2],
|
||||
'unicorn/prefer-array-flat-map': [2],
|
||||
'unicorn/prefer-array-from-async': [2],
|
||||
'unicorn/prefer-array-from-map': [2],
|
||||
'unicorn/prefer-array-index-of': [2],
|
||||
'unicorn/prefer-array-iterable-methods': [2],
|
||||
'unicorn/prefer-array-last-methods': [2],
|
||||
'unicorn/prefer-array-slice': [2],
|
||||
'unicorn/prefer-array-some': [2],
|
||||
'unicorn/prefer-at': [0],
|
||||
'unicorn/prefer-await': [2],
|
||||
'unicorn/prefer-bigint-literals': [2],
|
||||
'unicorn/prefer-blob-reading-methods': [2],
|
||||
'unicorn/prefer-boolean-return': [2],
|
||||
'unicorn/prefer-class-fields': [2],
|
||||
'unicorn/prefer-classlist-toggle': [2],
|
||||
'unicorn/prefer-code-point': [0],
|
||||
'unicorn/prefer-continue': [0],
|
||||
'unicorn/prefer-date-now': [2],
|
||||
'unicorn/prefer-default-parameters': [0],
|
||||
'unicorn/prefer-direct-iteration': [2],
|
||||
'unicorn/prefer-dispose': [2],
|
||||
'unicorn/prefer-dom-node-append': [2],
|
||||
'unicorn/prefer-dom-node-html-methods': [0],
|
||||
'unicorn/prefer-dom-node-dataset': [0],
|
||||
'unicorn/prefer-dom-node-remove': [2],
|
||||
'unicorn/prefer-dom-node-text-content': [2],
|
||||
'unicorn/prefer-early-return': [0],
|
||||
'unicorn/prefer-else-if': [2],
|
||||
'unicorn/prefer-event-target': [2],
|
||||
'unicorn/prefer-export-from': [0],
|
||||
'unicorn/prefer-flat-math-min-max': [2],
|
||||
'unicorn/prefer-get-or-insert-computed': [2],
|
||||
'unicorn/prefer-global-number-constants': [2],
|
||||
'unicorn/prefer-global-this': [0],
|
||||
'unicorn/prefer-has-check': [2],
|
||||
'unicorn/prefer-hoisting-branch-code': [2],
|
||||
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
|
||||
'unicorn/prefer-identifier-import-export-specifiers': [2],
|
||||
'unicorn/prefer-import-meta-properties': [2],
|
||||
'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes
|
||||
'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated
|
||||
'unicorn/prefer-iterable-in-constructor': [2],
|
||||
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
||||
'unicorn/prefer-iterator-to-array': [2],
|
||||
'unicorn/prefer-iterator-to-array-at-end': [2],
|
||||
'unicorn/prefer-json-parse-buffer': [0],
|
||||
'unicorn/prefer-keyboard-event-key': [2],
|
||||
'unicorn/prefer-location-assign': [2],
|
||||
'unicorn/prefer-logical-operator-over-ternary': [2],
|
||||
'unicorn/prefer-map-from-entries': [0],
|
||||
'unicorn/prefer-math-abs': [2],
|
||||
'unicorn/prefer-math-constants': [2],
|
||||
'unicorn/prefer-math-min-max': [2],
|
||||
'unicorn/prefer-math-trunc': [2],
|
||||
'unicorn/prefer-minimal-ternary': [0],
|
||||
'unicorn/prefer-modern-dom-apis': [0],
|
||||
'unicorn/prefer-modern-math-apis': [2],
|
||||
'unicorn/prefer-module': [2],
|
||||
'unicorn/prefer-native-coercion-functions': [2],
|
||||
'unicorn/prefer-negative-index': [2],
|
||||
'unicorn/prefer-node-protocol': [2],
|
||||
'unicorn/prefer-number-coercion': [0],
|
||||
'unicorn/prefer-number-is-safe-integer': [0],
|
||||
'unicorn/prefer-number-properties': [0],
|
||||
'unicorn/prefer-object-define-properties': [2],
|
||||
'unicorn/prefer-object-destructuring-defaults': [2],
|
||||
'unicorn/prefer-object-from-entries': [2],
|
||||
'unicorn/prefer-object-iterable-methods': [2],
|
||||
'unicorn/prefer-object-has-own': [0],
|
||||
'unicorn/prefer-optional-catch-binding': [2],
|
||||
'unicorn/prefer-path2d': [2],
|
||||
'unicorn/prefer-private-class-fields': [0],
|
||||
'unicorn/prefer-promise-with-resolvers': [2],
|
||||
'unicorn/prefer-prototype-methods': [0],
|
||||
'unicorn/prefer-query-selector': [2],
|
||||
'unicorn/prefer-queue-microtask': [2],
|
||||
'unicorn/prefer-reflect-apply': [0],
|
||||
'unicorn/prefer-regexp-escape': [0],
|
||||
'unicorn/prefer-regexp-test': [2],
|
||||
'unicorn/prefer-response-static-json': [2],
|
||||
'unicorn/prefer-scoped-selector': [0],
|
||||
'unicorn/prefer-set-has': [0],
|
||||
'unicorn/prefer-set-size': [2],
|
||||
'unicorn/prefer-short-arrow-method': [2],
|
||||
'unicorn/prefer-simple-condition-first': [0],
|
||||
'unicorn/prefer-simple-sort-comparator': [2],
|
||||
'unicorn/prefer-single-array-predicate': [2],
|
||||
'unicorn/prefer-single-call': [2],
|
||||
'unicorn/prefer-single-object-destructuring': [2],
|
||||
'unicorn/prefer-single-replace': [2],
|
||||
'unicorn/prefer-smaller-scope': [2],
|
||||
'unicorn/prefer-split-limit': [0], // too opinionated
|
||||
'unicorn/prefer-spread': [0],
|
||||
'unicorn/prefer-string-match-all': [2],
|
||||
'unicorn/prefer-string-pad-start-end': [2],
|
||||
'unicorn/prefer-string-raw': [0],
|
||||
'unicorn/prefer-string-repeat': [2],
|
||||
'unicorn/prefer-string-replace-all': [0],
|
||||
'unicorn/prefer-string-slice': [0],
|
||||
'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with
|
||||
'unicorn/prefer-string-trim-start-end': [2],
|
||||
'unicorn/prefer-structured-clone': [2],
|
||||
'unicorn/prefer-switch': [0],
|
||||
'unicorn/prefer-temporal': [0],
|
||||
'unicorn/prefer-ternary': [0],
|
||||
'unicorn/prefer-top-level-await': [0],
|
||||
'unicorn/prefer-type-error': [0],
|
||||
'unicorn/prefer-type-literal-last': [0],
|
||||
'unicorn/prefer-uint8array-base64': [0],
|
||||
'unicorn/prefer-unary-minus': [2],
|
||||
'unicorn/prefer-unicode-code-point-escapes': [0],
|
||||
'unicorn/prefer-url-can-parse': [2],
|
||||
'unicorn/prefer-url-href': [2],
|
||||
'unicorn/prefer-while-loop-condition': [2],
|
||||
'unicorn/prevent-abbreviations': [0],
|
||||
'unicorn/relative-url-style': [2],
|
||||
'unicorn/require-array-join-separator': [2],
|
||||
'unicorn/require-array-sort-compare': [0],
|
||||
'unicorn/require-css-escape': [2],
|
||||
'unicorn/require-module-attributes': [2],
|
||||
'unicorn/require-module-specifiers': [0],
|
||||
'unicorn/require-number-to-fixed-digits-argument': [2],
|
||||
'unicorn/require-passive-events': [2],
|
||||
'unicorn/require-post-message-target-origin': [0],
|
||||
'unicorn/require-proxy-trap-boolean-return': [2],
|
||||
'unicorn/string-content': [0],
|
||||
'unicorn/switch-case-braces': [0],
|
||||
'unicorn/switch-case-break-position': [2],
|
||||
'unicorn/template-indent': [2],
|
||||
'unicorn/text-encoding-identifier-case': [0],
|
||||
'unicorn/throw-new-error': [2],
|
||||
'unicorn/try-complexity': [0],
|
||||
'use-isnan': [2],
|
||||
'valid-typeof': [2, {requireStringLiterals: true}],
|
||||
'vars-on-top': [0],
|
||||
@@ -1091,7 +956,6 @@ export default defineConfig([
|
||||
languageOptions: {globals: globals.vitest},
|
||||
rules: {
|
||||
'gitea/unescaped-html-literal': [0],
|
||||
'unicorn/no-error-property-assignment': [0],
|
||||
'vitest/consistent-test-filename': [0],
|
||||
'vitest/consistent-test-it': [0],
|
||||
'vitest/expect-expect': [0],
|
||||
@@ -1103,6 +967,7 @@ export default defineConfig([
|
||||
'vitest/no-conditional-in-test': [0],
|
||||
'vitest/no-conditional-tests': [0],
|
||||
'vitest/no-disabled-tests': [0],
|
||||
'vitest/no-done-callback': [0],
|
||||
'vitest/no-duplicate-hooks': [0],
|
||||
'vitest/no-focused-tests': [2],
|
||||
'vitest/no-hooks': [0],
|
||||
|
||||
@@ -23,17 +23,17 @@ require (
|
||||
github.com/ProtonMail/go-crypto v1.4.1
|
||||
github.com/PuerkitoBio/goquery v1.12.0
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/alecthomas/chroma/v2 v2.26.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
|
||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.6.0
|
||||
github.com/bohde/codel v0.2.0
|
||||
github.com/buildkite/terminal-to-html/v3 v3.16.8
|
||||
github.com/caddyserver/certmagic v0.25.4
|
||||
github.com/caddyserver/certmagic v0.25.3
|
||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635
|
||||
github.com/chi-middleware/proxy v1.1.1
|
||||
github.com/dlclark/regexp2/v2 v2.2.2
|
||||
github.com/dlclark/regexp2/v2 v2.2.1
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
|
||||
@@ -73,7 +73,7 @@ require (
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/mattn/go-sqlite3 v1.14.47
|
||||
github.com/mattn/go-sqlite3 v1.14.45
|
||||
github.com/meilisearch/meilisearch-go v0.36.3
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
@@ -86,7 +86,7 @@ require (
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
github.com/redis/go-redis/v9 v9.21.0
|
||||
github.com/redis/go-redis/v9 v9.20.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
@@ -96,15 +96,15 @@ require (
|
||||
github.com/tstranex/u2f v1.0.0
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/urfave/cli-docs/v3 v3.1.0
|
||||
github.com/urfave/cli/v3 v3.10.0
|
||||
github.com/urfave/cli/v3 v3.9.1
|
||||
github.com/wneessen/go-mail v0.7.3
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0
|
||||
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.5
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/image v0.43.0
|
||||
golang.org/x/image v0.42.0
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
@@ -114,7 +114,7 @@ require (
|
||||
google.golang.org/grpc v1.81.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/ini.v1 v1.67.3
|
||||
modernc.org/sqlite v1.53.0
|
||||
modernc.org/sqlite v1.52.0
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
|
||||
xorm.io/builder v0.3.13
|
||||
@@ -273,7 +273,7 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/libc v1.73.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -76,8 +76,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqK
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
|
||||
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
@@ -187,8 +187,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
|
||||
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
|
||||
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
|
||||
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
|
||||
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
@@ -240,8 +240,8 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@@ -514,8 +514,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
|
||||
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
||||
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
||||
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
|
||||
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||
@@ -618,8 +618,8 @@ github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4Ul
|
||||
github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
|
||||
github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
|
||||
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY=
|
||||
github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
|
||||
github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w=
|
||||
@@ -709,8 +709,8 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
|
||||
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
||||
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=
|
||||
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.9.1 h1:OLU13atWZ0M+a4xmyBuBNOLZsSRYXyPeMeNjOvgYP54=
|
||||
github.com/urfave/cli/v3 v3.9.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0=
|
||||
github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk=
|
||||
@@ -740,8 +740,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0 h1:Bq5YIYgUJVbt4Hbh7ibBwNR4SNEafsyDVhIXl7dXDdg=
|
||||
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
|
||||
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0 h1:gZSMTTnLcUeY5mH4z3G6GEzbaBTOCUfBCAJXMRyuzEM=
|
||||
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
@@ -780,8 +780,8 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
|
||||
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -940,8 +940,8 @@ modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
modernc.org/libc v1.73.0 h1:Y/KmTxbIN5T3x+NFjYOzV/+Ha7wKClfIecmTCTuYlqQ=
|
||||
modernc.org/libc v1.73.0/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -950,8 +950,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+10
-21
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,8 +21,6 @@ import (
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ActionRun represents a run of a workflow file
|
||||
@@ -51,13 +48,6 @@ type ActionRun struct {
|
||||
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
|
||||
RawConcurrency string // raw concurrency
|
||||
|
||||
// WorkflowRepoID/WorkflowCommitSHA record the (repo, commit) the run's workflow file content came from.
|
||||
// Always filled (repo-level run = the repo itself; scoped run = the source repo).
|
||||
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
|
||||
|
||||
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"` // IsScopedRun explicitly classifies scoped runs.
|
||||
|
||||
// Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced.
|
||||
// When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops.
|
||||
Started timeutil.TimeStamp
|
||||
@@ -96,11 +86,7 @@ func (run *ActionRun) WorkflowLink() string {
|
||||
if run.Repo == nil {
|
||||
return ""
|
||||
}
|
||||
// A scoped run's workflow is disambiguated by its source repo, so carry scoped_workflow_source_repo_id back to the run list
|
||||
if run.IsScopedRun {
|
||||
return fmt.Sprintf("%s/actions/?workflow=%s&scoped_workflow_source_repo_id=%d", run.Repo.Link(), url.QueryEscape(run.WorkflowID), run.WorkflowRepoID)
|
||||
}
|
||||
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), url.QueryEscape(run.WorkflowID))
|
||||
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
|
||||
}
|
||||
|
||||
// RefLink return the url of run's ref
|
||||
@@ -278,7 +264,11 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
|
||||
}
|
||||
|
||||
func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) {
|
||||
run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex})
|
||||
run := &ActionRun{
|
||||
RepoID: repoID,
|
||||
Index: runIndex,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -289,7 +279,9 @@ func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionR
|
||||
}
|
||||
|
||||
func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
|
||||
run := &ActionRun{}
|
||||
run := &ActionRun{
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Desc("index").Get(run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -303,10 +295,7 @@ func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branc
|
||||
var run ActionRun
|
||||
q := db.GetEngine(ctx).Where("repo_id=?", repoID).
|
||||
And("ref = ?", branch).
|
||||
And("workflow_id = ?", workflowFile).
|
||||
// TODO: the badge only reflects the repo's own (repo-level) runs; a same-named scoped run must not leak in.
|
||||
// Support a scoped-workflow badge later by making this source-aware.
|
||||
And("is_scoped_run = ?", false)
|
||||
And("workflow_id = ?", workflowFile)
|
||||
if event != "" {
|
||||
q.And("event = ?", event)
|
||||
}
|
||||
|
||||
+10
-48
@@ -108,10 +108,6 @@ type ActionRunJob struct {
|
||||
// ParentJobID scopes `Needs` resolution: name lookups happen only among rows sharing the same ParentJobID. 0 for top-level rows.
|
||||
ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||
|
||||
// ContinueOnError mirrors the job-level continue-on-error field from the workflow YAML.
|
||||
// When true, a failure of this job does not fail the overall workflow run.
|
||||
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
|
||||
Started timeutil.TimeStamp
|
||||
Stopped timeutil.TimeStamp
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
@@ -361,14 +357,6 @@ func CollectAllDescendantJobs(parent *ActionRunJob, allJobs []*ActionRunJob) []*
|
||||
return out
|
||||
}
|
||||
|
||||
// hasWaitingJobsToPick reports whether any waiting, unclaimed, non-reusable job
|
||||
// remains in the repo, i.e. work that an idle runner could still pick up.
|
||||
func hasWaitingJobsToPick(ctx context.Context, repoID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("repo_id = ? AND task_id = ? AND status = ? AND is_reusable_caller = ?", repoID, 0, StatusWaiting, false).
|
||||
Exist(&ActionRunJob{})
|
||||
}
|
||||
|
||||
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
@@ -393,41 +381,18 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
if job.RunID == 0 {
|
||||
var err error
|
||||
if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil {
|
||||
// Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump.
|
||||
if statusUpdated && job.Status.IsWaiting() && !job.IsReusableCaller {
|
||||
// if the status of job changes to waiting again, increase tasks version.
|
||||
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump.
|
||||
if statusUpdated && !job.IsReusableCaller {
|
||||
switch {
|
||||
case job.Status.IsWaiting():
|
||||
// A job returning to the waiting queue is work a runner can pick up, so bump the
|
||||
// version to wake idle runners whose tasksVersion already equals latestVersion.
|
||||
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case job.Status.IsDone():
|
||||
// When a job finishes, bump the version so that idle runners — whose
|
||||
// tasksVersion already equals the current latestVersion — learn that
|
||||
// remaining waiting jobs are still available and attempt PickTask again.
|
||||
// Without this bump, runners that completed their tasks would see
|
||||
// tasksVersion==latestVersion and skip PickTask, leaving the other jobs
|
||||
// permanently unassigned until the version changes for another reason.
|
||||
// Only bump when waiting work actually remains for this repo, otherwise
|
||||
// every job completion would needlessly bump the global version and wake
|
||||
// every idle runner instance-wide for nothing.
|
||||
hasWaiting, err := hasWaitingJobsToPick(ctx, job.RepoID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if hasWaiting {
|
||||
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if job.RunID == 0 {
|
||||
var err error
|
||||
if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,12 +500,9 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
allSkipped := len(jobs) != 0
|
||||
var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked bool
|
||||
for _, job := range jobs {
|
||||
// A failed job with continue-on-error:true does not fail the workflow run.
|
||||
// It counts as a "continued failure" and is treated like success for aggregation.
|
||||
isContinuedFailure := job.ContinueOnError && job.Status == StatusFailure
|
||||
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped || isContinuedFailure)
|
||||
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
|
||||
allSkipped = allSkipped && job.Status == StatusSkipped
|
||||
hasFailure = hasFailure || (job.Status == StatusFailure && !job.ContinueOnError)
|
||||
hasFailure = hasFailure || job.Status == StatusFailure
|
||||
hasCancelled = hasCancelled || job.Status == StatusCancelled
|
||||
hasCancelling = hasCancelling || job.Status == StatusCancelling
|
||||
hasWaiting = hasWaiting || job.Status == StatusWaiting
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/translation"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
@@ -62,9 +61,7 @@ type FindRunOptions struct {
|
||||
RepoID int64
|
||||
OwnerID int64
|
||||
WorkflowID string
|
||||
WorkflowRepoID int64 // source-aware filter: the repo a run's workflow content came from (0 = any)
|
||||
IsScopedRun optional.Option[bool] // is the run from a scoped workflow
|
||||
Ref string // the commit/tag/… that caused this workflow
|
||||
Ref string // the commit/tag/… that caused this workflow
|
||||
TriggerUserID int64
|
||||
TriggerEvent webhook_module.HookEventType
|
||||
Status []Status
|
||||
@@ -80,12 +77,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
||||
if opts.WorkflowID != "" {
|
||||
cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID})
|
||||
}
|
||||
if opts.WorkflowRepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"`action_run`.workflow_repo_id": opts.WorkflowRepoID})
|
||||
}
|
||||
if opts.IsScopedRun.Has() {
|
||||
cond = cond.And(builder.Eq{"`action_run`.is_scoped_run": opts.IsScopedRun.Value()})
|
||||
}
|
||||
if opts.TriggerUserID > 0 {
|
||||
cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
|
||||
}
|
||||
@@ -115,15 +106,6 @@ func (opts FindRunOptions) ToJoins() []db.JoinFunc {
|
||||
}
|
||||
|
||||
func (opts FindRunOptions) ToOrders() string {
|
||||
// When scoped to a repo, sort by `index`: it reuses the unique
|
||||
// `repo_index` (repo_id, index) index, so the query seeks repo_id and
|
||||
// walks index descending instead of filesorting all matching rows.
|
||||
// Within a repo `index` is co-monotonic with `id`, so the order is the same.
|
||||
if opts.RepoID > 0 {
|
||||
return "`action_run`.`index` DESC"
|
||||
}
|
||||
// `index` is scoped per repo, so it is meaningless across repos. With no
|
||||
// RepoID, sort by the global, PK-indexed `id` for a deterministic order.
|
||||
return "`action_run`.`id` DESC"
|
||||
}
|
||||
|
||||
@@ -165,20 +147,9 @@ func GetRunBranches(ctx context.Context, repoID int64) ([]string, error) {
|
||||
// GetRunWorkflowIDs returns all distinct WorkflowIDs that have at least
|
||||
// one ActionRun in the given repo.
|
||||
func GetRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
|
||||
return getRunWorkflowIDs(ctx, repoID, builder.NewCond())
|
||||
}
|
||||
|
||||
// GetRepoRunWorkflowIDs returns all distinct WorkflowIDs that have at least
|
||||
// one repo-level ActionRun in the given repo.
|
||||
func GetRepoRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
|
||||
return getRunWorkflowIDs(ctx, repoID, builder.Eq{"is_scoped_run": false})
|
||||
}
|
||||
|
||||
func getRunWorkflowIDs(ctx context.Context, repoID int64, extraCond builder.Cond) ([]string, error) {
|
||||
ids := make([]string, 0, 10)
|
||||
cond := builder.Eq{"repo_id": repoID}
|
||||
return ids, db.GetEngine(ctx).Table("action_run").
|
||||
Where(cond.And(extraCond)).
|
||||
Where(builder.Eq{"repo_id": repoID}).
|
||||
Distinct("workflow_id").
|
||||
Cols("workflow_id").
|
||||
Asc("workflow_id").
|
||||
|
||||
@@ -6,13 +6,10 @@ package actions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetRunWorkflowIDs(t *testing.T) {
|
||||
@@ -27,46 +24,6 @@ func TestGetRunWorkflowIDs(t *testing.T) {
|
||||
assert.Empty(t, ids)
|
||||
}
|
||||
|
||||
func TestGetRepoRunWorkflowIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const (
|
||||
repoID = int64(4)
|
||||
repoWorkflowID = "repo-orphan.yaml"
|
||||
scopedWorkflowID = "scoped-only.yaml"
|
||||
sharedWorkflowID = "shared-name.yaml"
|
||||
scopedWorkflowRepo = int64(111)
|
||||
)
|
||||
for _, spec := range []struct {
|
||||
id int64
|
||||
workflowID string
|
||||
workflowRepoID int64
|
||||
isScopedRun bool
|
||||
}{
|
||||
{99811, repoWorkflowID, repoID, false},
|
||||
{99812, scopedWorkflowID, scopedWorkflowRepo, true},
|
||||
{99813, sharedWorkflowID, repoID, false},
|
||||
{99814, sharedWorkflowID, scopedWorkflowRepo, true},
|
||||
} {
|
||||
require.NoError(t, db.Insert(t.Context(), &ActionRun{
|
||||
ID: spec.id,
|
||||
Index: spec.id,
|
||||
RepoID: repoID,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: spec.workflowID,
|
||||
WorkflowRepoID: spec.workflowRepoID,
|
||||
IsScopedRun: spec.isScopedRun,
|
||||
}))
|
||||
}
|
||||
|
||||
ids, err := GetRepoRunWorkflowIDs(t.Context(), repoID)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, ids, repoWorkflowID)
|
||||
assert.Contains(t, ids, sharedWorkflowID)
|
||||
assert.NotContains(t, ids, scopedWorkflowID)
|
||||
}
|
||||
|
||||
func TestGetStatusInfoList(t *testing.T) {
|
||||
statusInfoList := GetStatusInfoList(t.Context(), translation.MockLocale{})
|
||||
|
||||
@@ -78,85 +35,3 @@ func TestGetStatusInfoList(t *testing.T) {
|
||||
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
|
||||
}, statusInfoList)
|
||||
}
|
||||
|
||||
// TestFindRunOptions_WorkflowRepoID: two runs share the bare WorkflowID but come from different content-source repos;
|
||||
// the source-aware WorkflowRepoID filter must separate them.
|
||||
func TestFindRunOptions_WorkflowRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const (
|
||||
repoID = int64(4)
|
||||
sourceA = int64(111)
|
||||
sourceB = int64(222)
|
||||
workflowID = "u3-shared.yaml"
|
||||
)
|
||||
for _, spec := range []struct{ id, workflowRepoID int64 }{
|
||||
{99801, sourceA},
|
||||
{99802, sourceB},
|
||||
} {
|
||||
require.NoError(t, db.Insert(t.Context(), &ActionRun{
|
||||
ID: spec.id,
|
||||
Index: spec.id,
|
||||
RepoID: repoID,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: workflowID,
|
||||
WorkflowRepoID: spec.workflowRepoID,
|
||||
IsScopedRun: true,
|
||||
}))
|
||||
}
|
||||
|
||||
// no source filter -> both
|
||||
all, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, all, 2)
|
||||
|
||||
// filter by source A -> only the run whose content came from A
|
||||
onlyA, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceA})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, onlyA, 1)
|
||||
assert.EqualValues(t, 99801, onlyA[0].ID)
|
||||
|
||||
// filter by source B -> only the run whose content came from B
|
||||
onlyB, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceB})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, onlyB, 1)
|
||||
assert.EqualValues(t, 99802, onlyB[0].ID)
|
||||
}
|
||||
|
||||
func TestFindRunOptions_IsScopedRun(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const (
|
||||
repoID = int64(4)
|
||||
workflowID = "scoped-flag.yaml"
|
||||
)
|
||||
for _, spec := range []struct {
|
||||
id int64
|
||||
scoped bool
|
||||
}{
|
||||
{99821, false},
|
||||
{99822, true},
|
||||
} {
|
||||
require.NoError(t, db.Insert(t.Context(), &ActionRun{
|
||||
ID: spec.id,
|
||||
Index: spec.id,
|
||||
RepoID: repoID,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: workflowID,
|
||||
WorkflowRepoID: repoID,
|
||||
IsScopedRun: spec.scoped,
|
||||
}))
|
||||
}
|
||||
|
||||
repoLevel, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(false)})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, repoLevel, 1)
|
||||
assert.EqualValues(t, 99821, repoLevel[0].ID)
|
||||
|
||||
scoped, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(true)})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, scoped, 1)
|
||||
assert.EqualValues(t, 99822, scoped[0].ID)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateRepoRunsNumbers(t *testing.T) {
|
||||
@@ -45,57 +44,3 @@ func TestActionRun_Duration_NonNegative(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, time.Duration(0), run.Duration())
|
||||
}
|
||||
|
||||
func TestActionRun_WorkflowLink(t *testing.T) {
|
||||
repo := &repo_model.Repository{OwnerName: "org", Name: "consumer"}
|
||||
|
||||
// a repo-level run links by file name only
|
||||
repoLevel := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: repo.ID}
|
||||
assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml", repoLevel.WorkflowLink())
|
||||
|
||||
// a scoped run carries its source repo id back, so the list stays filtered to that source
|
||||
scoped := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: 42, IsScopedRun: true}
|
||||
assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml&scoped_workflow_source_repo_id=42", scoped.WorkflowLink())
|
||||
}
|
||||
|
||||
func TestGetWorkflowLatestRun_RepoLevelOnly(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const (
|
||||
repoID = int64(4)
|
||||
workflowID = "badge-source-aware.yaml"
|
||||
ref = "refs/heads/main"
|
||||
)
|
||||
require.NoError(t, db.Insert(t.Context(), &ActionRun{
|
||||
ID: 99811,
|
||||
Index: 99811,
|
||||
RepoID: repoID,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: workflowID,
|
||||
Ref: ref,
|
||||
Event: "push",
|
||||
Status: StatusSuccess,
|
||||
WorkflowRepoID: repoID,
|
||||
WorkflowCommitSHA: "repo-level-sha",
|
||||
}))
|
||||
require.NoError(t, db.Insert(t.Context(), &ActionRun{
|
||||
ID: 99812,
|
||||
Index: 99812,
|
||||
RepoID: repoID,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: workflowID,
|
||||
Ref: ref,
|
||||
Event: "push",
|
||||
Status: StatusFailure,
|
||||
WorkflowRepoID: 111,
|
||||
WorkflowCommitSHA: "scoped-sha",
|
||||
IsScopedRun: true,
|
||||
}))
|
||||
|
||||
run, err := GetWorkflowLatestRun(t.Context(), repoID, workflowID, ref, "push")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 99811, run.ID)
|
||||
assert.False(t, run.IsScopedRun)
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ActionScopedWorkflowSource registers a repository as a source of scoped workflows, either for an owner (user/org) or for the whole instance.
|
||||
type ActionScopedWorkflowSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
|
||||
// OwnerID is the scope the source applies to: a user/org ID (applies to that owner's repos), or 0 for instance-level (applies to every repo).
|
||||
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
|
||||
// SourceRepoID is the source repository providing the workflow files; always non-zero.
|
||||
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
|
||||
|
||||
// WorkflowConfigs maps a workflow ID (entry name) to its merge-gate config.
|
||||
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// ScopedWorkflowConfig is one scoped workflow's config within a source registration.
|
||||
type ScopedWorkflowConfig struct {
|
||||
Required bool `json:"required"`
|
||||
Patterns []string `json:"patterns"` // the status-check patterns that must be present and pass, only effective when Required is true
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ActionScopedWorkflowSource))
|
||||
}
|
||||
|
||||
// IsWorkflowRequired reports whether the given workflow ID (entry name) is marked required in this source.
|
||||
func (s *ActionScopedWorkflowSource) IsWorkflowRequired(workflowID string) bool {
|
||||
c, ok := s.WorkflowConfigs[workflowID]
|
||||
return ok && c.Required
|
||||
}
|
||||
|
||||
type FindScopedWorkflowSourceOpts struct {
|
||||
db.ListOptions
|
||||
OwnerIDs []int64
|
||||
SourceRepoID int64
|
||||
}
|
||||
|
||||
func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if len(opts.OwnerIDs) > 0 {
|
||||
cond = cond.And(builder.In("owner_id", opts.OwnerIDs))
|
||||
}
|
||||
if opts.SourceRepoID != 0 {
|
||||
cond = cond.And(builder.Eq{"source_repo_id": opts.SourceRepoID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// GetEffectiveScopedWorkflowSources returns the scoped-workflow sources effective for a repo owned by repoOwnerID:
|
||||
// the owner's own sources plus instance-level (owner_id=0) sources.
|
||||
func GetEffectiveScopedWorkflowSources(ctx context.Context, repoOwnerID int64) ([]*ActionScopedWorkflowSource, error) {
|
||||
owners := []int64{0}
|
||||
if repoOwnerID != 0 {
|
||||
owners = append(owners, repoOwnerID)
|
||||
}
|
||||
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners})
|
||||
}
|
||||
|
||||
// IsScopedWorkflowSourceEffective reports whether sourceRepoID is a scoped-workflow source effective for a repo owned by repoOwnerID.
|
||||
func IsScopedWorkflowSourceEffective(ctx context.Context, repoOwnerID, sourceRepoID int64) (bool, error) {
|
||||
owners := []int64{0}
|
||||
if repoOwnerID != 0 {
|
||||
owners = append(owners, repoOwnerID)
|
||||
}
|
||||
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners, SourceRepoID: sourceRepoID}.ToConds())
|
||||
}
|
||||
|
||||
// IsWorkflowRequiredInSources reports whether workflowID from sourceRepoID is required by any of the given sources.
|
||||
func IsWorkflowRequiredInSources(sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool {
|
||||
for _, s := range sources {
|
||||
if s.SourceRepoID == sourceRepoID && s.IsWorkflowRequired(workflowID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ScopedStatusContextPrefix returns the source-repo prefix that makes a scoped run's commit-status context distinct from same-named workflows.
|
||||
func ScopedStatusContextPrefix(ctx context.Context, sourceRepoID int64) string {
|
||||
if sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID); err == nil {
|
||||
return sourceRepo.FullName()
|
||||
}
|
||||
return fmt.Sprintf("scoped:%d", sourceRepoID)
|
||||
}
|
||||
|
||||
// IsScopedWorkflowRequired reports whether workflowID from sourceRepoID is required for a repo owned by consumerOwnerID.
|
||||
func IsScopedWorkflowRequired(ctx context.Context, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) {
|
||||
sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID), nil
|
||||
}
|
||||
|
||||
// IsScopedWorkflowOptedOutloads the consumer's effective sources then calls ScopedWorkflowOptedOut
|
||||
func IsScopedWorkflowOptedOut(ctx context.Context, cfg *repo_model.ActionsConfig, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) {
|
||||
if !cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID) {
|
||||
return false, nil
|
||||
}
|
||||
sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ScopedWorkflowOptedOut(cfg, sources, sourceRepoID, workflowID), nil
|
||||
}
|
||||
|
||||
// ScopedWorkflowOptedOut reports whether a consumer's opt-out of (sourceRepoID, workflowID) is in effect.
|
||||
func ScopedWorkflowOptedOut(cfg *repo_model.ActionsConfig, sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool {
|
||||
return !IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID) && cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID)
|
||||
}
|
||||
|
||||
// GetScopedWorkflowSourcesByOwner returns the sources an owner (user/org, or 0 for instance) registered.
|
||||
func GetScopedWorkflowSourcesByOwner(ctx context.Context, ownerID int64) ([]*ActionScopedWorkflowSource, error) {
|
||||
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{ownerID}})
|
||||
}
|
||||
|
||||
// GetScopedWorkflowSource returns the (owner, repo) source registration or a NotExist error.
|
||||
func GetScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) (*ActionScopedWorkflowSource, error) {
|
||||
src := &ActionScopedWorkflowSource{}
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Get(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, util.NewNotExistErrorf("scoped workflow source (owner %d, repo %d) does not exist", ownerID, repoID)
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
|
||||
// AddScopedWorkflowSource registers repoID as a source for ownerID (no-op if already registered).
|
||||
func AddScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error {
|
||||
exists, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := db.Insert(ctx, &ActionScopedWorkflowSource{OwnerID: ownerID, SourceRepoID: repoID}); err != nil {
|
||||
// Re-check and treat an already-present row as the intended no-op.
|
||||
if exists, existErr := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource)); existErr == nil && exists {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetScopedWorkflowSourceConfigs replaces the per-workflow merge-gate configs (workflow ID -> config).
|
||||
func SetScopedWorkflowSourceConfigs(ctx context.Context, ownerID, repoID int64, configs map[string]*ScopedWorkflowConfig) error {
|
||||
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).
|
||||
Cols("workflow_configs").
|
||||
Update(&ActionScopedWorkflowSource{WorkflowConfigs: configs})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveScopedWorkflowSource removes the (owner, repo) source registration.
|
||||
func RemoveScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Delete(new(ActionScopedWorkflowSource))
|
||||
return err
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScopedWorkflowSource_IsWorkflowRequired(t *testing.T) {
|
||||
src := &ActionScopedWorkflowSource{WorkflowConfigs: map[string]*ScopedWorkflowConfig{
|
||||
"a.yml": {Required: true, Patterns: []string{"p"}},
|
||||
"b.yml": {Required: true, Patterns: []string{"p"}},
|
||||
"c.yml": {Required: false, Patterns: []string{"p"}}, // patterns kept as history, not required
|
||||
}}
|
||||
assert.True(t, src.IsWorkflowRequired("a.yml"))
|
||||
assert.True(t, src.IsWorkflowRequired("b.yml"))
|
||||
assert.False(t, src.IsWorkflowRequired("c.yml"), "config kept as history but not required")
|
||||
assert.False(t, src.IsWorkflowRequired("d.yml"))
|
||||
|
||||
empty := &ActionScopedWorkflowSource{}
|
||||
assert.False(t, empty.IsWorkflowRequired("a.yml"))
|
||||
}
|
||||
|
||||
func TestIsWorkflowRequiredInSources(t *testing.T) {
|
||||
// repo 100 registered twice (org optional + instance required).
|
||||
sources := []*ActionScopedWorkflowSource{
|
||||
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil},
|
||||
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}},
|
||||
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}},
|
||||
}
|
||||
|
||||
assert.True(t, IsWorkflowRequiredInSources(sources, 100, "a.yml"), "required at instance level wins over org optional")
|
||||
assert.False(t, IsWorkflowRequiredInSources(sources, 100, "z.yml"))
|
||||
assert.False(t, IsWorkflowRequiredInSources(sources, 200, "a.yml"), "a.yml is required for repo 100, not repo 200")
|
||||
assert.True(t, IsWorkflowRequiredInSources(sources, 200, "b.yml"))
|
||||
assert.False(t, IsWorkflowRequiredInSources(sources, 999, "a.yml"), "unknown source repo")
|
||||
}
|
||||
|
||||
func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
rows := []*ActionScopedWorkflowSource{
|
||||
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, // org 2 registers repo 100 (optional)
|
||||
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, // instance also registers repo 100 (required)
|
||||
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, // instance source 200
|
||||
{OwnerID: 3, SourceRepoID: 300, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true, Patterns: []string{"p"}}}}, // a different owner's source
|
||||
}
|
||||
for _, r := range rows {
|
||||
require.NoError(t, db.Insert(ctx, r))
|
||||
}
|
||||
|
||||
// owner 2 sees its own sources plus instance-level ones, but not owner 3's.
|
||||
owner2, err := GetEffectiveScopedWorkflowSources(ctx, 2)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, owner2, 3)
|
||||
|
||||
required, err := IsScopedWorkflowRequired(ctx, 2, 100, "a.yml")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, required, "instance marks a.yml required → required for owner 2 even though org left it optional")
|
||||
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 100, "x.yml")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, required)
|
||||
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 200, "b.yml")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, required)
|
||||
|
||||
// owner 3's source must not be effective for owner 2.
|
||||
required, err = IsScopedWorkflowRequired(ctx, 2, 300, "c.yml")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, required)
|
||||
|
||||
// IsScopedWorkflowSourceEffective: owner-level and instance-level sources are effective; another owner's is not.
|
||||
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 100)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, effective, "owner 2's own source")
|
||||
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 200)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, effective, "instance-level source is effective for any owner")
|
||||
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 300)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, effective, "owner 3's source is not effective for owner 2")
|
||||
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 999)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, effective, "unknown source repo")
|
||||
|
||||
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 300)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, effective, "owner 3's own source is effective for owner 3")
|
||||
}
|
||||
|
||||
func TestScopedWorkflowSourceCRUD(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
// add is idempotent
|
||||
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
|
||||
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
|
||||
sources, err := GetScopedWorkflowSourcesByOwner(ctx, 5)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, sources, 1)
|
||||
|
||||
// set the per-workflow configs (entry name -> {required, patterns}); a.yml required, b.yml kept as history (not required)
|
||||
configs := map[string]*ScopedWorkflowConfig{
|
||||
"a.yml": {Required: true, Patterns: []string{"src: a.yml / *"}},
|
||||
"b.yml": {Required: false, Patterns: []string{"src: b.yml / build (push)"}},
|
||||
}
|
||||
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, configs))
|
||||
src, err := GetScopedWorkflowSource(ctx, 5, 10)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, configs, src.WorkflowConfigs)
|
||||
|
||||
// clearing the configs works
|
||||
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, nil))
|
||||
src, err = GetScopedWorkflowSource(ctx, 5, 10)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, src.WorkflowConfigs)
|
||||
|
||||
// remove
|
||||
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 10))
|
||||
_, err = GetScopedWorkflowSource(ctx, 5, 10)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
sources, err = GetScopedWorkflowSourcesByOwner(ctx, 5)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, sources)
|
||||
}
|
||||
@@ -48,57 +48,3 @@ func TestStatusFromResult(t *testing.T) {
|
||||
assert.Equal(t, tt.want, StatusFromResult(tt.result), "result=%s", tt.result)
|
||||
}
|
||||
}
|
||||
|
||||
func newJob(status Status, continueOnError bool) *ActionRunJob {
|
||||
return &ActionRunJob{Status: status, ContinueOnError: continueOnError}
|
||||
}
|
||||
|
||||
func TestAggregateJobStatusContinueOnError(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
jobs []*ActionRunJob
|
||||
want Status
|
||||
}{
|
||||
{
|
||||
name: "all success",
|
||||
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusSuccess, false)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
{
|
||||
name: "one failure without continue-on-error",
|
||||
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, false)},
|
||||
want: StatusFailure,
|
||||
},
|
||||
{
|
||||
name: "one failure with continue-on-error",
|
||||
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, true)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
{
|
||||
name: "only continued-failure",
|
||||
jobs: []*ActionRunJob{newJob(StatusFailure, true)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
{
|
||||
name: "continued-failure plus real failure",
|
||||
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusFailure, false)},
|
||||
want: StatusFailure,
|
||||
},
|
||||
{
|
||||
name: "all skipped",
|
||||
jobs: []*ActionRunJob{newJob(StatusSkipped, false), newJob(StatusSkipped, false)},
|
||||
want: StatusSkipped,
|
||||
},
|
||||
{
|
||||
name: "continued-failure plus skipped counts as success",
|
||||
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusSkipped, false)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, AggregateJobStatus(tt.jobs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+68
-134
@@ -227,18 +227,13 @@ func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
||||
return util.EllipsisDisplayString(name, limit) // database column has a length limit
|
||||
}
|
||||
|
||||
// errJobAlreadyClaimed is a sentinel used inside claimJobForRunner to signal that
|
||||
// another runner won the optimistic-lock race; it is never returned to callers.
|
||||
var errJobAlreadyClaimed = errors.New("job already claimed by another runner")
|
||||
|
||||
// CreateTaskForRunner finds a waiting job that matches the runner's labels and
|
||||
// atomically claims it. It iterates through all matching jobs so that a
|
||||
// concurrent claim by another runner (which would lose the optimistic lock on
|
||||
// job #1) does not leave the remaining jobs permanently unassigned.
|
||||
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
||||
if db.InTransaction(ctx) {
|
||||
return nil, false, errors.New("CreateTaskForRunner must not be called within a database transaction")
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
jobCond := builder.NewCond()
|
||||
@@ -259,144 +254,83 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
}
|
||||
|
||||
// TODO: a more efficient way to filter labels
|
||||
var job *ActionRunJob
|
||||
log.Trace("runner labels: %v", runner.AgentLabels)
|
||||
for _, v := range jobs {
|
||||
if !runner.CanMatchLabels(v.RunsOn) {
|
||||
continue
|
||||
if runner.CanMatchLabels(v.RunsOn) {
|
||||
job = v
|
||||
break
|
||||
}
|
||||
task, ok, err := claimJobForRunner(ctx, runner, v)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if ok {
|
||||
return task, true, nil
|
||||
}
|
||||
// Another runner claimed this job concurrently; try the next one.
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// claimJobForRunner attempts to atomically claim job for runner inside its own
|
||||
// transaction. Returns (task, true, nil) on success, or (nil, false, nil) when
|
||||
// another runner wins the optimistic-lock race (the caller should try the next
|
||||
// candidate job).
|
||||
func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (*ActionTask, bool, error) {
|
||||
var resultTask *ActionTask
|
||||
|
||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
if err := job.LoadAttributes(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := timeutil.TimeStampNow()
|
||||
job.Started = now
|
||||
job.Status = StatusRunning
|
||||
|
||||
task := &ActionTask{
|
||||
JobID: job.ID,
|
||||
Attempt: job.Attempt,
|
||||
RunnerID: runner.ID,
|
||||
Started: now,
|
||||
Status: StatusRunning,
|
||||
RepoID: job.RepoID,
|
||||
OwnerID: job.OwnerID,
|
||||
CommitSHA: job.CommitSHA,
|
||||
IsForkPullRequest: job.IsForkPullRequest,
|
||||
}
|
||||
task.GenerateAndFillToken()
|
||||
|
||||
workflowJob, err := job.ParseJob()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load job %d: %w", job.ID, err)
|
||||
}
|
||||
|
||||
if _, err := e.Insert(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID)
|
||||
if err := UpdateTask(ctx, task, "log_filename"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(workflowJob.Steps) > 0 {
|
||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||
for i, v := range workflowJob.Steps {
|
||||
steps[i] = &ActionTaskStep{
|
||||
Name: makeTaskStepDisplayName(v, 255),
|
||||
TaskID: task.ID,
|
||||
Index: int64(i),
|
||||
RepoID: task.RepoID,
|
||||
Status: StatusWaiting,
|
||||
}
|
||||
}
|
||||
if _, err := e.Insert(steps); err != nil {
|
||||
return err
|
||||
}
|
||||
task.Steps = steps
|
||||
}
|
||||
|
||||
job.TaskID = task.ID
|
||||
n, err := UpdateRunJob(ctx, job, builder.And(builder.Eq{"task_id": 0}, builder.Eq{"status": StatusWaiting}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
// Another runner claimed this job between our scan and this update;
|
||||
// signal the outer loop to move on without treating this as an error.
|
||||
return errJobAlreadyClaimed
|
||||
}
|
||||
|
||||
task.Job = job
|
||||
resultTask = task
|
||||
return nil
|
||||
})
|
||||
|
||||
if errors.Is(err, errJobAlreadyClaimed) {
|
||||
if job == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
if err := job.LoadAttributes(ctx); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return resultTask, true, nil
|
||||
}
|
||||
|
||||
// ReleaseTaskForRunner reverts a freshly-claimed but undelivered task: it deletes
|
||||
// the task together with its steps and returns the job to the waiting queue. It is
|
||||
// used when assembling the runner response fails after the job was already claimed,
|
||||
// so the job is not stranded in running state with no runner ever executing it.
|
||||
func ReleaseTaskForRunner(ctx context.Context, task *ActionTask) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
e := db.GetEngine(ctx)
|
||||
now := timeutil.TimeStampNow()
|
||||
job.Started = now
|
||||
job.Status = StatusRunning
|
||||
|
||||
job, err := GetRunJobByRepoAndID(ctx, task.RepoID, task.JobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task := &ActionTask{
|
||||
JobID: job.ID,
|
||||
Attempt: job.Attempt,
|
||||
RunnerID: runner.ID,
|
||||
Started: now,
|
||||
Status: StatusRunning,
|
||||
RepoID: job.RepoID,
|
||||
OwnerID: job.OwnerID,
|
||||
CommitSHA: job.CommitSHA,
|
||||
IsForkPullRequest: job.IsForkPullRequest,
|
||||
}
|
||||
task.GenerateAndFillToken()
|
||||
|
||||
job.Status = StatusWaiting
|
||||
job.Started = 0
|
||||
job.TaskID = 0
|
||||
// Guard on task_id and status so we only release while the job still
|
||||
// references this task and has not progressed past running.
|
||||
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": task.ID, "status": StatusRunning}, "status", "started", "task_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return fmt.Errorf("release task %d: job %d no longer references it", task.ID, task.JobID)
|
||||
}
|
||||
workflowJob, err := job.ParseJob()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("load job %d: %w", job.ID, err)
|
||||
}
|
||||
|
||||
if _, err := e.Delete(&ActionTaskStep{TaskID: task.ID}); err != nil {
|
||||
return err
|
||||
if _, err := e.Insert(task); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID)
|
||||
if err := UpdateTask(ctx, task, "log_filename"); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if len(workflowJob.Steps) > 0 {
|
||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||
for i, v := range workflowJob.Steps {
|
||||
steps[i] = &ActionTaskStep{
|
||||
Name: makeTaskStepDisplayName(v, 255),
|
||||
TaskID: task.ID,
|
||||
Index: int64(i),
|
||||
RepoID: task.RepoID,
|
||||
Status: StatusWaiting,
|
||||
}
|
||||
}
|
||||
if _, err := e.ID(task.ID).Delete(&ActionTask{}); err != nil {
|
||||
return err
|
||||
if _, err := e.Insert(steps); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
task.Steps = steps
|
||||
}
|
||||
|
||||
job.TaskID = task.ID
|
||||
if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil {
|
||||
return nil, false, err
|
||||
} else if n != 1 {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
task.Job = job
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return task, true, nil
|
||||
}
|
||||
|
||||
func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
|
||||
|
||||
@@ -306,68 +306,3 @@ func TestStopTaskCancellingFallsBackForMissingRunner(t *testing.T) {
|
||||
assert.Equal(t, StatusCancelled, jobAfterStop.Status)
|
||||
assert.NotZero(t, jobAfterStop.Stopped)
|
||||
}
|
||||
|
||||
// TestReleaseTaskForRunner verifies that releasing a freshly-claimed task returns
|
||||
// its job to the waiting queue and deletes the task and its steps, so a failure
|
||||
// while assembling the runner response cannot strand the job in running state.
|
||||
func TestReleaseTaskForRunner(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
run := &ActionRun{
|
||||
Title: "release-task-test-run",
|
||||
RepoID: 1,
|
||||
OwnerID: 2,
|
||||
WorkflowID: "test.yaml",
|
||||
Index: 9902,
|
||||
TriggerUserID: 2,
|
||||
Ref: "refs/heads/main",
|
||||
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||
Event: "push",
|
||||
TriggerEvent: "push",
|
||||
Status: StatusWaiting,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), run))
|
||||
|
||||
job := &ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RepoID: run.RepoID,
|
||||
OwnerID: run.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: "release-job",
|
||||
Attempt: 1,
|
||||
JobID: "release-job",
|
||||
Status: StatusWaiting,
|
||||
RunsOn: []string{"ubuntu-latest"},
|
||||
WorkflowPayload: []byte("on: push\njobs:\n release-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"),
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
|
||||
runner := &ActionRunner{
|
||||
UUID: "release-runner-uuid",
|
||||
Name: "release-runner",
|
||||
AgentLabels: []string{"ubuntu-latest"},
|
||||
}
|
||||
runner.GenerateAndFillToken()
|
||||
require.NoError(t, db.Insert(t.Context(), runner))
|
||||
|
||||
task, ok, err := CreateTaskForRunner(t.Context(), runner)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, task)
|
||||
|
||||
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
||||
require.Equal(t, StatusRunning, claimed.Status)
|
||||
require.Equal(t, task.ID, claimed.TaskID)
|
||||
|
||||
require.NoError(t, ReleaseTaskForRunner(t.Context(), task))
|
||||
|
||||
// Job is back in the waiting queue with no task assigned.
|
||||
released := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
||||
assert.Equal(t, StatusWaiting, released.Status)
|
||||
assert.Zero(t, released.TaskID)
|
||||
assert.Zero(t, released.Started)
|
||||
|
||||
// The task and its steps are gone.
|
||||
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
|
||||
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func (a *Action) TableIndices() []*schemas.Index {
|
||||
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
|
||||
|
||||
cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
|
||||
cuIndex.AddColumn("user_id", "is_deleted", "created_unix")
|
||||
cuIndex.AddColumn("user_id", "is_deleted")
|
||||
|
||||
actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
|
||||
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Task represents a task
|
||||
@@ -174,13 +172,17 @@ func (err ErrTaskDoesNotExist) Unwrap() error {
|
||||
|
||||
// GetMigratingTask returns the migrating task by repo's id
|
||||
func GetMigratingTask(ctx context.Context, repoID int64) (*Task, error) {
|
||||
task, has, err := db.Get[Task](ctx, builder.Eq{"repo_id": repoID, "`type`": structs.TaskTypeMigrateRepo})
|
||||
task := Task{
|
||||
RepoID: repoID,
|
||||
Type: structs.TaskTypeMigrateRepo,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(&task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrTaskDoesNotExist{0, repoID, structs.TaskTypeMigrateRepo}
|
||||
return nil, ErrTaskDoesNotExist{0, repoID, task.Type}
|
||||
}
|
||||
return task, nil
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// CreateTask creates a task on database
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"strk.kbt.io/projects/go/libravatar"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -100,13 +99,12 @@ func HashEmail(email string) string {
|
||||
// GetEmailForHash converts a provided md5sum to the email
|
||||
func GetEmailForHash(ctx context.Context, md5Sum string) (string, error) {
|
||||
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
|
||||
emailHash, has, err := db.Get[EmailHash](ctx, builder.Eq{"`hash`": strings.ToLower(strings.TrimSpace(md5Sum))})
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if !has {
|
||||
return "", nil
|
||||
emailHash := EmailHash{
|
||||
Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
|
||||
}
|
||||
return emailHash.Email, nil
|
||||
|
||||
_, err := db.GetEngine(ctx).Get(&emailHash)
|
||||
return emailHash.Email, err
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,13 @@ type RenamedBranch struct {
|
||||
|
||||
// FindRenamedBranch check if a branch was renamed
|
||||
func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) {
|
||||
return db.Get[RenamedBranch](ctx, builder.Eq{"repo_id": repoID, "`from`": from})
|
||||
branch = &RenamedBranch{
|
||||
RepoID: repoID,
|
||||
From: from,
|
||||
}
|
||||
exist, err = db.GetEngine(ctx).Get(branch)
|
||||
|
||||
return branch, exist, err
|
||||
}
|
||||
|
||||
// RenameBranch rename a branch
|
||||
|
||||
+2
-1
@@ -126,7 +126,8 @@ func GetLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (*LFSM
|
||||
return nil, ErrLFSObjectNotExist
|
||||
}
|
||||
|
||||
m, has, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": oid})
|
||||
m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID}
|
||||
has, err := db.GetEngine(ctx).Get(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ProtectedTag struct
|
||||
@@ -113,7 +111,8 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
|
||||
|
||||
// GetProtectedTagByNamePattern gets protected tag by name_pattern
|
||||
func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) {
|
||||
tag, has, err := db.Get[ProtectedTag](ctx, builder.Eq{"name_pattern": pattern, "repo_id": repoID})
|
||||
tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID}
|
||||
has, err := db.GetEngine(ctx).Get(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -79,14 +79,6 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comments.loadResolveDoers(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comments.loadReactions(ctx, issue.Repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find all reviews by ReviewID
|
||||
reviews := make(map[int64]*Review)
|
||||
ids := make([]int64, 0, len(comments))
|
||||
@@ -115,6 +107,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
comments[n] = comment
|
||||
n++
|
||||
|
||||
if err := comment.LoadResolveDoer(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comment.LoadReactions(ctx, issue.Repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
|
||||
FootnoteContextID: strconv.FormatInt(comment.ID, 10),
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// CommentList defines a list of comments
|
||||
@@ -445,73 +444,6 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadResolveDoers bulk-loads the resolve doer for all code comments that have one.
|
||||
func (comments CommentList) loadResolveDoers(ctx context.Context) error {
|
||||
resolveDoerIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||
return c.ResolveDoerID, c.ResolveDoerID != 0 && c.Type == CommentTypeCode
|
||||
})
|
||||
if len(resolveDoerIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
userMaps, err := user_model.GetUsersMapByIDs(ctx, resolveDoerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.ResolveDoerID == 0 || comment.Type != CommentTypeCode {
|
||||
continue
|
||||
}
|
||||
if u, ok := userMaps[comment.ResolveDoerID]; ok {
|
||||
comment.ResolveDoer = u
|
||||
} else {
|
||||
comment.ResolveDoer = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadReactions bulk-loads reactions for all comments in the list.
|
||||
func (comments CommentList) loadReactions(ctx context.Context, repo *repo_model.Repository) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
commentIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||
return c.ID, c.Reactions == nil
|
||||
})
|
||||
if len(commentIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var allReactions ReactionList
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("`comment_id` > 0").
|
||||
In("comment_id", commentIDs).
|
||||
In("`type`", setting.UI.Reactions).
|
||||
Asc("issue_id", "comment_id", "created_unix", "id").
|
||||
Find(&allReactions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := allReactions.LoadUsers(ctx, repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reactByComment := make(map[int64]ReactionList, len(commentIDs))
|
||||
for _, r := range allReactions {
|
||||
reactByComment[r.CommentID] = append(reactByComment[r.CommentID], r)
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.Reactions == nil {
|
||||
comment.Reactions = reactByComment[comment.ID]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the comments, except for attachments and
|
||||
// comments
|
||||
func (comments CommentList) LoadAttributes(ctx context.Context) (err error) {
|
||||
|
||||
@@ -361,8 +361,8 @@ func (issue *Issue) ResetAttributesLoaded() {
|
||||
|
||||
// GetIsRead load the `IsRead` field of the issue
|
||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||
issueUser, has, err := db.Get[IssueUser](ctx, builder.Eq{"issue_id": issue.ID, "uid": userID})
|
||||
if err != nil {
|
||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
||||
if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
issue.IsRead = false
|
||||
@@ -499,7 +499,11 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
||||
if index < 1 {
|
||||
return nil, ErrIssueNotExist{}
|
||||
}
|
||||
issue, has, err := db.Get[Issue](ctx, builder.Eq{"repo_id": repoID, "`index`": index})
|
||||
issue := &Issue{
|
||||
RepoID: repoID,
|
||||
Index: index,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -591,12 +591,9 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev
|
||||
|
||||
func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {
|
||||
issueIDs := issues.getIssueIDs()
|
||||
if len(issueIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
issueUsers := make([]*IssueUser, 0, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).Where("uid =?", userID).
|
||||
In("issue_id", issueIDs).
|
||||
In("issue_id").
|
||||
Find(&issueUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIssueList_LoadRepositories(t *testing.T) {
|
||||
@@ -31,22 +30,6 @@ func TestIssueList_LoadRepositories(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList_LoadIsRead(t *testing.T) {
|
||||
// Regression: In("issue_id") was missing the issueIDs argument, causing
|
||||
// xorm to generate "0=1" and never mark any issue as read.
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
|
||||
// Fixture: uid=1 has is_read=true on issue 1 only.
|
||||
issueList := issues_model.IssueList{issue1, issue2}
|
||||
require.NoError(t, issueList.LoadIsRead(t.Context(), 1))
|
||||
|
||||
assert.True(t, issue1.IsRead, "issue 1 should be marked read for user 1")
|
||||
assert.False(t, issue2.IsRead, "issue 2 should not be marked read for user 1")
|
||||
}
|
||||
|
||||
func TestIssueList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
setting.Service.EnableTimetracking = true
|
||||
|
||||
@@ -599,7 +599,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
|
||||
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
|
||||
continue
|
||||
}
|
||||
has, err := db.Exist[organization.TeamUnit](ctx, builder.Eq{"org_id": issue.Repo.Owner.ID, "team_id": team.ID, "`type`": unittype})
|
||||
has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/references"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type crossReference struct {
|
||||
@@ -191,11 +189,11 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
|
||||
func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository,
|
||||
ref references.IssueReference,
|
||||
) (*Issue, references.XRefAction, error) {
|
||||
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
|
||||
refAction := ref.Action
|
||||
refIssue, has, err := db.Get[Issue](stdCtx, builder.Eq{"repo_id": repo.ID, "`index`": ref.Index})
|
||||
if err != nil {
|
||||
return nil, references.XRefActionNone, err
|
||||
} else if !has {
|
||||
e := db.GetEngine(stdCtx)
|
||||
|
||||
if has, _ := e.Get(refIssue); !has {
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
if err := refIssue.LoadRepo(stdCtx); err != nil {
|
||||
|
||||
@@ -535,8 +535,12 @@ func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullReque
|
||||
if index < 1 {
|
||||
return nil, ErrPullRequestNotExist{}
|
||||
}
|
||||
pr := &PullRequest{
|
||||
BaseRepoID: repoID,
|
||||
Index: index,
|
||||
}
|
||||
|
||||
pr, has, err := db.Get[PullRequest](ctx, builder.Eq{"base_repo_id": repoID, "`index`": index})
|
||||
has, err := db.GetEngine(ctx).Get(pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -415,11 +415,6 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(335, "Add reusable workflow fields and action_run_attempt_job_id_index table for ActionRunJob", v1_27.AddReusableWorkflowFieldsToActionRunJob),
|
||||
newMigration(336, "Add ActionRunJobSummary table", v1_27.AddActionRunJobSummaryTable),
|
||||
newMigration(337, "Add visibility to team", v1_27.AddVisibilityToTeam),
|
||||
newMigration(338, "Expand legacy MSSQL issue/comment long-text columns", v1_27.ExpandIssueAndCommentLongTextFieldsForMSSQL),
|
||||
newMigration(339, "Extend action c_u index to include created_unix for faster dashboard feed queries", v1_27.AddCreatedUnixToActionUserIsDeletedIndex),
|
||||
newMigration(340, "Add ContinueOnError column to ActionRunJob", v1_27.AddContinueOnErrorToActionRunJob),
|
||||
newMigration(341, "Convert legacy MSSQL DATETIME columns to DATETIME2", v1_27.FixLegacyMSSQLDateTimeColumns),
|
||||
newMigration(342, "Add scoped workflows schema", v1_27.AddScopedWorkflowsSchema),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/migrations/base"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type issueWithLongTextContent struct {
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
}
|
||||
|
||||
func (issueWithLongTextContent) TableName() string {
|
||||
return "issue"
|
||||
}
|
||||
|
||||
type commentWithLongTextFields struct {
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
PatchQuoted string `xorm:"LONGTEXT patch"`
|
||||
}
|
||||
|
||||
func (commentWithLongTextFields) TableName() string {
|
||||
return "comment"
|
||||
}
|
||||
|
||||
func isMSSQLMaxTextColumn(column *schemas.Column) bool {
|
||||
if column.Length != -1 {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(column.SQLType.Name, schemas.Varchar) || strings.EqualFold(column.SQLType.Name, schemas.NVarchar)
|
||||
}
|
||||
|
||||
func modifyLongTextColumnsForMSSQL(x db.EngineMigration, bean any, columnNames ...string) error {
|
||||
table, err := x.TableInfo(bean)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, columnName := range columnNames {
|
||||
column := table.GetColumn(columnName)
|
||||
if column == nil {
|
||||
return fmt.Errorf("column %s does not exist in table %s", columnName, table.Name)
|
||||
}
|
||||
if isMSSQLMaxTextColumn(column) {
|
||||
continue
|
||||
}
|
||||
if err := base.ModifyColumn(x, table.Name, column); err != nil {
|
||||
return fmt.Errorf("modify %s.%s: %w", table.Name, columnName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpandIssueAndCommentLongTextFieldsForMSSQL expands legacy MSSQL nvarchar(4000)
|
||||
// columns to nvarchar(max) so PR push comments and long issue content are not truncated.
|
||||
func ExpandIssueAndCommentLongTextFieldsForMSSQL(x db.EngineMigration) error {
|
||||
if x.Dialect().URI().DBType != schemas.MSSQL {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := modifyLongTextColumnsForMSSQL(x, new(issueWithLongTextContent), "content"); err != nil {
|
||||
return err
|
||||
}
|
||||
return modifyLongTextColumnsForMSSQL(x, new(commentWithLongTextFields), "content", "patch")
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/migrations/migrationtest"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type issueBeforeLongTextMSSQLMigration struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Content string `xorm:"VARCHAR(4000)"`
|
||||
}
|
||||
|
||||
func (issueBeforeLongTextMSSQLMigration) TableName() string {
|
||||
return "issue"
|
||||
}
|
||||
|
||||
type commentBeforeLongTextMSSQLMigration struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Content string `xorm:"VARCHAR(4000)"`
|
||||
Patch string `xorm:"VARCHAR(4000) patch"`
|
||||
}
|
||||
|
||||
func (commentBeforeLongTextMSSQLMigration) TableName() string {
|
||||
return "comment"
|
||||
}
|
||||
|
||||
func Test_ExpandIssueAndCommentLongTextFieldsForMSSQL(t *testing.T) {
|
||||
if !setting.Database.Type.IsMSSQL() {
|
||||
t.Skip("Only MSSQL needs to expand legacy nvarchar(4000) long-text columns")
|
||||
}
|
||||
|
||||
x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(issueBeforeLongTextMSSQLMigration), new(commentBeforeLongTextMSSQLMigration))
|
||||
defer deferrable()
|
||||
|
||||
require.NoError(t, ExpandIssueAndCommentLongTextFieldsForMSSQL(x))
|
||||
require.NoError(t, ExpandIssueAndCommentLongTextFieldsForMSSQL(x))
|
||||
|
||||
longText := strings.Repeat("x", 5000)
|
||||
_, err := x.Insert(&issueBeforeLongTextMSSQLMigration{Content: longText})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = x.Insert(&commentBeforeLongTextMSSQLMigration{Content: longText, Patch: longText})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// AddCreatedUnixToActionUserIsDeletedIndex extends the c_u composite index on
|
||||
// the action table to include created_unix, enabling efficient ORDER BY on the
|
||||
// dashboard feed query without a full sort of all matching rows.
|
||||
func AddCreatedUnixToActionUserIsDeletedIndex(x db.EngineMigration) error {
|
||||
// xorm Sync cannot reliably update an index when another index already
|
||||
// covers the same columns in a different order (Equal() is order-insensitive).
|
||||
// Drop the old c_u index explicitly, then recreate it with the new column set.
|
||||
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
if idx.Name == "c_u" {
|
||||
if _, err := x.Exec(x.Dialect().DropIndexSQL("action", idx)); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newIndex := schemas.NewIndex("c_u", schemas.IndexType)
|
||||
newIndex.AddColumn("user_id", "is_deleted", "created_unix")
|
||||
if _, err := x.Exec(x.Dialect().CreateIndexSQL("action", newIndex)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/migrations/migrationtest"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type actionBeforeV339 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
OpType int
|
||||
ActUserID int64
|
||||
RepoID int64
|
||||
CommentID int64 `xorm:"INDEX"`
|
||||
IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
RefName string
|
||||
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Content string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
func (actionBeforeV339) TableName() string { return "action" }
|
||||
|
||||
func (actionBeforeV339) TableIndices() []*schemas.Index {
|
||||
repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
|
||||
repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
|
||||
|
||||
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
|
||||
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
|
||||
|
||||
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
|
||||
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
|
||||
|
||||
// old 2-column index, before the migration
|
||||
cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
|
||||
cuIndex.AddColumn("user_id", "is_deleted")
|
||||
|
||||
actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
|
||||
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
|
||||
|
||||
return []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex}
|
||||
}
|
||||
|
||||
func Test_AddCreatedUnixToActionUserIsDeletedIndex(t *testing.T) {
|
||||
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(actionBeforeV339))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted"}, false), "old c_u index should exist before migration")
|
||||
assert.False(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted", "created_unix"}, false), "new c_u index should not exist before migration")
|
||||
|
||||
require.NoError(t, AddCreatedUnixToActionUserIsDeletedIndex(x))
|
||||
|
||||
indexes, err = x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted"}, false), "old 2-column c_u index should be gone after migration")
|
||||
assert.True(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted", "created_unix"}, false), "new 3-column c_u index must exist after migration")
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddContinueOnErrorToActionRunJob adds the ContinueOnError column to ActionRunJob,
|
||||
// storing the job-level continue-on-error value from the workflow YAML.
|
||||
func AddContinueOnErrorToActionRunJob(x db.EngineMigration) error {
|
||||
type ActionRunJob struct {
|
||||
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
}
|
||||
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(ActionRunJob))
|
||||
return err
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/migrations/base"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// legacyDateTimeColumns are the persisted real datetime columns that old Gitea
|
||||
// versions created as MSSQL DATETIME. Every other time value is stored as a
|
||||
// unix timestamp integer, so these are the only columns affected.
|
||||
var legacyDateTimeColumns = []struct {
|
||||
bean any
|
||||
column string
|
||||
}{
|
||||
{new(externalLoginUserWithExpiresAt), "expires_at"},
|
||||
{new(lfsLockWithCreated), "created"},
|
||||
}
|
||||
|
||||
type externalLoginUserWithExpiresAt struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (externalLoginUserWithExpiresAt) TableName() string {
|
||||
return "external_login_user"
|
||||
}
|
||||
|
||||
type lfsLockWithCreated struct {
|
||||
Created time.Time `xorm:"created"`
|
||||
}
|
||||
|
||||
func (lfsLockWithCreated) TableName() string {
|
||||
return "lfs_lock"
|
||||
}
|
||||
|
||||
// FixLegacyMSSQLDateTimeColumns converts legacy locale-dependent DATETIME columns
|
||||
// to DATETIME2. Databases created by old Gitea versions stored these columns as
|
||||
// DATETIME, which fails to parse ISO datetime strings ('YYYY-MM-DD HH:MM:SS')
|
||||
// when the MSSQL session language is not English, breaking external account
|
||||
// linking and LFS lock creation. New installs already use DATETIME2, so only
|
||||
// legacy MSSQL columns need converting.
|
||||
func FixLegacyMSSQLDateTimeColumns(x db.EngineMigration) error {
|
||||
if x.Dialect().URI().DBType != schemas.MSSQL {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, c := range legacyDateTimeColumns {
|
||||
table, err := x.TableInfo(c.bean)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dataType string
|
||||
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table.Name, c.column).Get(&dataType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has || !strings.EqualFold(dataType, "datetime") {
|
||||
continue
|
||||
}
|
||||
|
||||
column := table.GetColumn(c.column)
|
||||
if column == nil {
|
||||
return fmt.Errorf("column %s does not exist in table %s", c.column, table.Name)
|
||||
}
|
||||
if err := base.ModifyColumn(x, table.Name, column); err != nil {
|
||||
return fmt.Errorf("modify %s.%s: %w", table.Name, c.column, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/migrations/migrationtest"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type externalLoginUserBeforeDateTimeMigration struct {
|
||||
ExternalID string `xorm:"pk NOT NULL"`
|
||||
LoginSourceID int64 `xorm:"pk NOT NULL"`
|
||||
ExpiresAt time.Time // sync creates DATETIME2; downgraded to legacy DATETIME via raw SQL below
|
||||
}
|
||||
|
||||
func (externalLoginUserBeforeDateTimeMigration) TableName() string {
|
||||
return "external_login_user"
|
||||
}
|
||||
|
||||
type lfsLockBeforeDateTimeMigration struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Created time.Time `xorm:"created"`
|
||||
}
|
||||
|
||||
func (lfsLockBeforeDateTimeMigration) TableName() string {
|
||||
return "lfs_lock"
|
||||
}
|
||||
|
||||
func Test_FixLegacyMSSQLDateTimeColumns(t *testing.T) {
|
||||
if !setting.Database.Type.IsMSSQL() {
|
||||
t.Skip("Only MSSQL needs to convert the legacy locale-dependent DATETIME columns")
|
||||
}
|
||||
|
||||
x, deferrable := migrationtest.PrepareTestEnv(t, 0,
|
||||
new(externalLoginUserBeforeDateTimeMigration),
|
||||
new(lfsLockBeforeDateTimeMigration),
|
||||
)
|
||||
defer deferrable()
|
||||
|
||||
// Force the legacy DATETIME column type that old Gitea versions created.
|
||||
_, err := x.Exec("ALTER TABLE [external_login_user] ALTER COLUMN [expires_at] DATETIME")
|
||||
require.NoError(t, err)
|
||||
_, err = x.Exec("ALTER TABLE [lfs_lock] ALTER COLUMN [created] DATETIME")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "datetime", mssqlColumnType(t, x, "external_login_user", "expires_at"))
|
||||
require.Equal(t, "datetime", mssqlColumnType(t, x, "lfs_lock", "created"))
|
||||
|
||||
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x))
|
||||
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x)) // idempotent
|
||||
|
||||
require.Equal(t, "datetime2", mssqlColumnType(t, x, "external_login_user", "expires_at"))
|
||||
require.Equal(t, "datetime2", mssqlColumnType(t, x, "lfs_lock", "created"))
|
||||
|
||||
// Inserting an ISO-formatted datetime must succeed even under a non-English
|
||||
// locale, which is the failure the legacy DATETIME columns produced. The
|
||||
// SET LANGUAGE and INSERT run in one Exec so they share a single connection.
|
||||
_, err = x.Exec("SET LANGUAGE German; " +
|
||||
"INSERT INTO [external_login_user] ([external_id], [login_source_id], [expires_at]) " +
|
||||
"VALUES ('ext-id', 1, '2026-06-25 11:58:39')")
|
||||
require.NoError(t, err)
|
||||
_, err = x.Exec("SET LANGUAGE German; " +
|
||||
"INSERT INTO [lfs_lock] ([created]) VALUES ('2026-06-25 11:58:39')")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mssqlColumnType(t *testing.T, x db.EngineMigration, table, column string) string {
|
||||
t.Helper()
|
||||
var dataType string
|
||||
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table, column).Get(&dataType)
|
||||
require.NoError(t, err)
|
||||
require.True(t, has)
|
||||
return dataType
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddScopedWorkflowsSchema(x db.EngineMigration) error {
|
||||
// Create the action_scoped_workflow_source table
|
||||
type ScopedWorkflowConfig struct {
|
||||
Required bool `json:"required"`
|
||||
Patterns []string `json:"patterns"`
|
||||
}
|
||||
type ActionScopedWorkflowSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
|
||||
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
|
||||
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
if err := x.Sync(new(ActionScopedWorkflowSource)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the columns that record where a run's workflow content came from
|
||||
type ActionRun struct {
|
||||
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
|
||||
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(ActionRun))
|
||||
return err
|
||||
}
|
||||
@@ -411,8 +411,11 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, ErrOrgNotExist{0, name}
|
||||
}
|
||||
|
||||
u, has, err := db.Get[Organization](ctx, builder.Eq{"lower_name": strings.ToLower(name), "`type`": user_model.UserTypeOrganization})
|
||||
u := &Organization{
|
||||
LowerName: strings.ToLower(name),
|
||||
Type: user_model.UserTypeOrganization,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -58,7 +58,7 @@ func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*P
|
||||
// GetPropertiesByName gets all properties with a specific name
|
||||
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
|
||||
pps := make([]*PackageProperty, 0, 10)
|
||||
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND `name` = ?", refType, refID, name).OrderBy("id").Find(&pps)
|
||||
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps)
|
||||
}
|
||||
|
||||
// UpdateProperty updates a property
|
||||
@@ -68,12 +68,13 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error {
|
||||
}
|
||||
|
||||
func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error {
|
||||
pp, ok, err := db.Get[PackageProperty](ctx, builder.Eq{"ref_type": refType, "ref_id": refID, "`name`": name})
|
||||
pp := PackageProperty{RefType: refType, RefID: refID, Name: name}
|
||||
ok, err := db.GetEngine(ctx).Get(&pp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
_, err = db.GetEngine(ctx).ID(pp.ID).Cols("value").Update(&PackageProperty{Value: value})
|
||||
_, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value})
|
||||
return err
|
||||
}
|
||||
_, err = InsertProperty(ctx, refType, refID, name, value)
|
||||
|
||||
@@ -164,7 +164,9 @@ func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error {
|
||||
|
||||
// HasVersionFileReferences checks if there are associated files
|
||||
func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) {
|
||||
return db.Exist[PackageFile](ctx, builder.Eq{"version_id": versionID})
|
||||
return db.GetEngine(ctx).Get(&PackageFile{
|
||||
VersionID: versionID,
|
||||
})
|
||||
}
|
||||
|
||||
// SearchValue describes a value to search
|
||||
|
||||
@@ -95,40 +95,6 @@ func TestGetActionsUserRepoPermission(t *testing.T) {
|
||||
assert.False(t, perm.CanRead(unit.TypeCode))
|
||||
})
|
||||
|
||||
t.Run("CollaborativeOwner_ForkPR_Denied", func(t *testing.T) {
|
||||
// Target repo15 trusts repo2's owner as a collaborative owner.
|
||||
repo15ActionsUnit := repo15.MustGetUnit(ctx, unit.TypeActions)
|
||||
repo15ActionsUnit.ActionsConfig().AddCollaborativeOwner(owner2.ID)
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
|
||||
|
||||
// Owner cross-repo policy does not allow repo15, so the only branch that
|
||||
// could grant access is the collaborative-owner one.
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, actions_model.OwnerActionsConfig{}))
|
||||
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
|
||||
// Non-fork task is legitimately granted code-read via collaborative owner.
|
||||
task53.IsForkPullRequest = false
|
||||
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, perm.CanRead(unit.TypeCode))
|
||||
|
||||
// Fork PR must NOT be able to read a third private repo through the
|
||||
// collaborative-owner branch.
|
||||
task53.IsForkPullRequest = true
|
||||
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
||||
perm, err = GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, perm.CanRead(unit.TypeCode))
|
||||
|
||||
// Restore state for subsequent subtests.
|
||||
task53.IsForkPullRequest = false
|
||||
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
||||
repo15ActionsUnit.ActionsConfig().RemoveCollaborativeOwner(owner2.ID)
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
|
||||
})
|
||||
|
||||
t.Run("Inheritance_And_Clamping", func(t *testing.T) {
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
task53.IsForkPullRequest = false
|
||||
|
||||
@@ -369,9 +369,7 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
|
||||
// 2. The Actions Bot user has been explicitly granted access and repository is private
|
||||
// 3. The repository is public (handled by botPerm above)
|
||||
|
||||
// Fork PRs are never allowed cross-repo access to other private repositories,
|
||||
// matching the discriminator enforced by checkSameOwnerCrossRepoAccess above.
|
||||
if taskRepo.IsPrivate && !task.IsForkPullRequest {
|
||||
if taskRepo.IsPrivate {
|
||||
actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||
if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
|
||||
return maxPerm, nil
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ViewedState stores for a file in which state it is currently viewed
|
||||
@@ -68,14 +66,9 @@ func (rs *ReviewState) GetViewedFileCount() int {
|
||||
// If the review didn't exist before in the database, it won't afterwards either.
|
||||
// The returned boolean shows whether the review exists in the database
|
||||
func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, bool, error) {
|
||||
review, has, err := db.Get[ReviewState](ctx, builder.Eq{"user_id": userID, "pull_id": pullID, "commit_sha": commitSHA})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if review == nil {
|
||||
review = &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA}
|
||||
}
|
||||
return review, has, nil
|
||||
review := &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA}
|
||||
has, err := db.GetEngine(ctx).Get(review)
|
||||
return review, has, err
|
||||
}
|
||||
|
||||
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Attachment represent a attachment of issue/comment/release.
|
||||
@@ -158,7 +156,8 @@ func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachm
|
||||
|
||||
// GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName.
|
||||
func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) {
|
||||
attach, has, err := db.Get[Attachment](ctx, builder.Eq{"release_id": releaseID, "`name`": fileName})
|
||||
attach := &Attachment{ReleaseID: releaseID, Name: fileName}
|
||||
has, err := db.GetEngine(ctx).Get(attach)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -102,13 +102,20 @@ func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*C
|
||||
|
||||
// GetCollaboration get collaboration for a repository id with a user id
|
||||
func GetCollaboration(ctx context.Context, repoID, uid int64) (*Collaboration, error) {
|
||||
collaboration, _, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": uid})
|
||||
collaboration := &Collaboration{
|
||||
RepoID: repoID,
|
||||
UserID: uid,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(collaboration)
|
||||
if !has {
|
||||
collaboration = nil
|
||||
}
|
||||
return collaboration, err
|
||||
}
|
||||
|
||||
// IsCollaborator check if a user is a collaborator of a repository
|
||||
func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
|
||||
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": userID})
|
||||
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
|
||||
}
|
||||
|
||||
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
|
||||
@@ -119,7 +126,13 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
collaboration, has, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": uid})
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
collaboration := &Collaboration{
|
||||
RepoID: repo.ID,
|
||||
UserID: uid,
|
||||
}
|
||||
has, err := e.Get(collaboration)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get collaboration: %w", err)
|
||||
} else if !has {
|
||||
@@ -131,12 +144,12 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
|
||||
}
|
||||
collaboration.Mode = mode
|
||||
|
||||
if _, err = db.GetEngine(ctx).
|
||||
if _, err = e.
|
||||
ID(collaboration.ID).
|
||||
Cols("mode").
|
||||
Update(collaboration); err != nil {
|
||||
return fmt.Errorf("update collaboration: %w", err)
|
||||
} else if _, err = db.Exec(ctx, "UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
|
||||
} else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
|
||||
return fmt.Errorf("update access table: %w", err)
|
||||
}
|
||||
|
||||
@@ -161,5 +174,5 @@ func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": userID})
|
||||
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repo.ID, UserID: userID})
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrMirrorNotExist mirror does not exist error
|
||||
@@ -78,7 +76,8 @@ func (m *Mirror) ScheduleNextUpdate() {
|
||||
|
||||
// GetMirrorByRepoID returns mirror information of a repository.
|
||||
func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) {
|
||||
m, has, err := db.Get[Mirror](ctx, builder.Eq{"repo_id": repoID})
|
||||
m := &Mirror{RepoID: repoID}
|
||||
has, err := db.GetEngine(ctx).Get(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrRedirectNotExist represents a "RedirectNotExist" kind of error.
|
||||
@@ -54,8 +52,8 @@ func init() {
|
||||
// LookupRedirect look up if a repository has a redirect name
|
||||
func LookupRedirect(ctx context.Context, ownerID int64, repoName string) (int64, error) {
|
||||
repoName = strings.ToLower(repoName)
|
||||
redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"owner_id": ownerID, "lower_name": repoName})
|
||||
if err != nil {
|
||||
redirect := &Redirect{OwnerID: ownerID, LowerName: repoName}
|
||||
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
|
||||
return 0, err
|
||||
} else if !has {
|
||||
return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName}
|
||||
|
||||
@@ -216,7 +216,8 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
||||
|
||||
// GetRelease returns release by given ID.
|
||||
func GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) {
|
||||
rel, has, err := db.Get[Release](ctx, builder.Eq{"repo_id": repoID, "lower_tag_name": strings.ToLower(tagName)})
|
||||
rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
|
||||
has, err := db.GetEngine(ctx).Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
+3
-3
@@ -849,9 +849,9 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos
|
||||
}
|
||||
|
||||
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
|
||||
return db.Exist[Repository](ctx, builder.Eq{
|
||||
"owner_id": u.ID,
|
||||
"lower_name": strings.ToLower(repoName),
|
||||
return db.GetEngine(ctx).Get(&Repository{
|
||||
OwnerID: u.ID,
|
||||
LowerName: strings.ToLower(repoName),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,6 @@ func MakeRestrictedPermissions() ActionsTokenPermissions {
|
||||
|
||||
type ActionsConfig struct {
|
||||
DisabledWorkflows []string
|
||||
// DisabledScopedWorkflows maps a scoped workflow's source repository ID to the entry names opted out of in this repository.
|
||||
DisabledScopedWorkflows map[int64][]string
|
||||
// CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos.
|
||||
// Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions.
|
||||
CollaborativeOwnerIDs []int64
|
||||
@@ -100,29 +98,6 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) {
|
||||
cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file)
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) IsScopedWorkflowDisabled(sourceRepoID int64, workflowID string) bool {
|
||||
return slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) DisableScopedWorkflow(sourceRepoID int64, workflowID string) {
|
||||
if slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) {
|
||||
return
|
||||
}
|
||||
if cfg.DisabledScopedWorkflows == nil {
|
||||
cfg.DisabledScopedWorkflows = make(map[int64][]string)
|
||||
}
|
||||
cfg.DisabledScopedWorkflows[sourceRepoID] = append(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) EnableScopedWorkflow(sourceRepoID int64, workflowID string) {
|
||||
workflowIDs := util.SliceRemoveAll(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
|
||||
if len(workflowIDs) == 0 {
|
||||
delete(cfg.DisabledScopedWorkflows, sourceRepoID)
|
||||
return
|
||||
}
|
||||
cfg.DisabledScopedWorkflows[sourceRepoID] = workflowIDs
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) {
|
||||
if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) {
|
||||
cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsConfig_ScopedWorkflowOptOut(t *testing.T) {
|
||||
cfg := &ActionsConfig{}
|
||||
|
||||
assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
|
||||
|
||||
cfg.DisableScopedWorkflow(100, "ci.yml")
|
||||
assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
|
||||
|
||||
// idempotent
|
||||
cfg.DisableScopedWorkflow(100, "ci.yml")
|
||||
assert.Len(t, cfg.DisabledScopedWorkflows, 1)
|
||||
|
||||
// keyed by source repo: the same filename from a different source repo is independent
|
||||
assert.False(t, cfg.IsScopedWorkflowDisabled(200, "ci.yml"))
|
||||
|
||||
// must not collide with the repo-level DisabledWorkflows list (bare filename)
|
||||
assert.False(t, cfg.IsWorkflowDisabled("ci.yml"))
|
||||
cfg.DisableWorkflow("ci.yml")
|
||||
assert.True(t, cfg.IsWorkflowDisabled("ci.yml"))
|
||||
assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"), "repo-level disable must not touch the scoped entry")
|
||||
|
||||
cfg.EnableScopedWorkflow(100, "ci.yml")
|
||||
assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
|
||||
assert.True(t, cfg.IsWorkflowDisabled("ci.yml"), "enabling the scoped entry must not touch the repo-level disable")
|
||||
}
|
||||
|
||||
func TestActionsConfig_ScopedWorkflowSerialization(t *testing.T) {
|
||||
cfg := &ActionsConfig{}
|
||||
cfg.DisableScopedWorkflow(100, "ci.yml")
|
||||
cfg.DisableWorkflow("repo.yml")
|
||||
|
||||
bs, err := cfg.ToDB()
|
||||
require.NoError(t, err)
|
||||
|
||||
got := &ActionsConfig{}
|
||||
require.NoError(t, got.FromDB(bs))
|
||||
assert.True(t, got.IsScopedWorkflowDisabled(100, "ci.yml"))
|
||||
assert.True(t, got.IsWorkflowDisabled("repo.yml"))
|
||||
}
|
||||
+1
-3
@@ -9,8 +9,6 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Star represents a starred repo by a user.
|
||||
@@ -70,7 +68,7 @@ func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star
|
||||
|
||||
// IsStaring checks if user has starred given repository.
|
||||
func IsStaring(ctx context.Context, userID, repoID int64) bool {
|
||||
has, _ := db.Exist[Star](ctx, builder.Eq{"uid": userID, "repo_id": repoID})
|
||||
has, _ := db.GetEngine(ctx).Get(&Star{UID: userID, RepoID: repoID})
|
||||
return has
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// WatchMode specifies what kind of watch the user has on a repository
|
||||
@@ -43,14 +41,12 @@ func init() {
|
||||
}
|
||||
|
||||
// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
|
||||
func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) {
|
||||
watch, has, err := db.Get[Watch](ctx, builder.Eq{"user_id": userID, "repo_id": repoID})
|
||||
func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) {
|
||||
watch := Watch{UserID: userID, RepoID: repoID}
|
||||
has, err := db.GetEngine(ctx).Get(&watch)
|
||||
if err != nil {
|
||||
return watch, err
|
||||
}
|
||||
if watch == nil {
|
||||
watch = &Watch{UserID: userID, RepoID: repoID}
|
||||
}
|
||||
if !has {
|
||||
watch.Mode = WatchModeNone
|
||||
}
|
||||
@@ -68,7 +64,7 @@ func IsWatching(ctx context.Context, userID, repoID int64) bool {
|
||||
return err == nil && IsWatchMode(watch.Mode)
|
||||
}
|
||||
|
||||
func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
|
||||
func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) {
|
||||
if watch.Mode == mode {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// AppState represents a state record in database
|
||||
@@ -46,7 +44,9 @@ func SaveAppStateContent(ctx context.Context, key, content string) error {
|
||||
|
||||
// GetAppStateContent gets an app state from database
|
||||
func GetAppStateContent(ctx context.Context, key string) (content string, err error) {
|
||||
appState, has, err := db.Get[AppState](ctx, builder.Eq{"id": key})
|
||||
e := db.GetEngine(ctx)
|
||||
appState := &AppState{ID: key}
|
||||
has, err := e.Get(appState)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if !has {
|
||||
|
||||
@@ -348,8 +348,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
|
||||
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
|
||||
data := makeTimeLimitCodeHashData(opts, user)
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||
emailAddress, has, _ := db.Get[EmailAddress](ctx, builder.Eq{"uid": user.ID, "email": email})
|
||||
if has {
|
||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||
return emailAddress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Follow represents relations of user and their followers.
|
||||
@@ -26,7 +24,7 @@ func init() {
|
||||
|
||||
// IsFollowing returns true if user is following followID.
|
||||
func IsFollowing(ctx context.Context, userID, followID int64) bool {
|
||||
has, _ := db.Exist[Follow](ctx, builder.Eq{"user_id": userID, "follow_id": followID})
|
||||
has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID})
|
||||
return has
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// UserOpenID is the list of all OpenID identities of a user.
|
||||
@@ -45,7 +43,7 @@ func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri})
|
||||
return db.GetEngine(ctx).Get(&UserOpenID{URI: uri})
|
||||
}
|
||||
|
||||
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error.
|
||||
@@ -52,8 +50,8 @@ func init() {
|
||||
// LookupUserRedirect look up userID if a user has a redirect name
|
||||
func LookupUserRedirect(ctx context.Context, userName string) (int64, error) {
|
||||
userName = strings.ToLower(userName)
|
||||
redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"lower_name": userName})
|
||||
if err != nil {
|
||||
redirect := &Redirect{LowerName: userName}
|
||||
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
|
||||
return 0, err
|
||||
} else if !has {
|
||||
return 0, ErrUserRedirectNotExist{Name: userName}
|
||||
|
||||
@@ -125,7 +125,8 @@ func GetUserSetting(ctx context.Context, userID int64, key string, def ...string
|
||||
return "", err
|
||||
}
|
||||
|
||||
setting, has, err := db.Get[Setting](ctx, builder.Eq{"user_id": userID, "setting_key": key})
|
||||
setting := &Setting{UserID: userID, SettingKey: key}
|
||||
has, err := db.GetEngine(ctx).Get(setting)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+6
-21
@@ -1276,7 +1276,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
|
||||
email = strings.ToLower(email)
|
||||
// Otherwise, check in alternative list for activated email addresses
|
||||
emailAddress, has, err := db.Get[EmailAddress](ctx, builder.Eq{"lower_email": email, "is_activated": true})
|
||||
emailAddress := &EmailAddress{LowerEmail: email, IsActivated: true}
|
||||
has, err := db.GetEngine(ctx).Get(emailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1296,29 +1297,13 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
|
||||
func GetIndividualUserByPrimaryEmail(ctx context.Context, email string) (*User, error) {
|
||||
email = strings.ToLower(strings.TrimSpace(email))
|
||||
if len(email) == 0 {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
|
||||
user, has, err := db.Get[User](ctx, builder.Eq{"email": email, "type": UserTypeIndividual})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, loginSource int64, loginName string) (*User, bool, error) {
|
||||
user, has, err := db.Get[User](ctx, builder.Eq{"login_type": loginType, "login_source": loginSource, "login_name": loginName})
|
||||
func GetIndividualUser(ctx context.Context, user *User) (bool, error) {
|
||||
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
|
||||
has, err := db.GetEngine(ctx).Get(user)
|
||||
if has && user.Type != UserTypeIndividual {
|
||||
has = false
|
||||
user = nil
|
||||
}
|
||||
return user, has, err
|
||||
return has, err
|
||||
}
|
||||
|
||||
// GetUserByOpenID returns the user object by given OpenID if exists.
|
||||
|
||||
@@ -69,9 +69,6 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||
runsOn[i] = evaluator.Interpolate(v)
|
||||
}
|
||||
job.RawRunsOn = encodeRunsOn(runsOn)
|
||||
if err := evaluator.EvaluateYamlNode(&job.RawContinueOnError); err != nil {
|
||||
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", id, err)
|
||||
}
|
||||
swf := &SingleWorkflow{
|
||||
Name: workflow.Name,
|
||||
RawOn: workflow.RawOn,
|
||||
|
||||
@@ -58,11 +58,6 @@ func TestParse(t *testing.T) {
|
||||
options: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "continue_on_error_expr",
|
||||
options: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
invalidFileTests := []struct {
|
||||
name string
|
||||
|
||||
@@ -79,37 +79,23 @@ func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
RawNeeds yaml.Node `yaml:"needs,omitempty"`
|
||||
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Steps []*Step `yaml:"steps,omitempty"`
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"`
|
||||
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
|
||||
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
With map[string]any `yaml:"with,omitempty"`
|
||||
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
|
||||
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
|
||||
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// GetContinueOnError decodes the continue-on-error field to a bool.
|
||||
// The field may be a literal bool or an already-evaluated expression node.
|
||||
func (j *Job) GetContinueOnError() bool {
|
||||
if j.RawContinueOnError.Kind == 0 {
|
||||
return false
|
||||
}
|
||||
var v bool
|
||||
if err := j.RawContinueOnError.Decode(&v); err != nil {
|
||||
return false
|
||||
}
|
||||
return v
|
||||
Name string `yaml:"name,omitempty"`
|
||||
RawNeeds yaml.Node `yaml:"needs,omitempty"`
|
||||
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Steps []*Step `yaml:"steps,omitempty"`
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
|
||||
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
With map[string]any `yaml:"with,omitempty"`
|
||||
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
|
||||
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
|
||||
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Job) Clone() *Job {
|
||||
@@ -117,24 +103,23 @@ func (j *Job) Clone() *Job {
|
||||
return nil
|
||||
}
|
||||
return &Job{
|
||||
Name: j.Name,
|
||||
RawNeeds: j.RawNeeds,
|
||||
RawRunsOn: j.RawRunsOn,
|
||||
Env: j.Env,
|
||||
If: j.If,
|
||||
Steps: j.Steps,
|
||||
TimeoutMinutes: j.TimeoutMinutes,
|
||||
RawContinueOnError: j.RawContinueOnError,
|
||||
Services: j.Services,
|
||||
Strategy: j.Strategy,
|
||||
RawContainer: j.RawContainer,
|
||||
Defaults: j.Defaults,
|
||||
Outputs: j.Outputs,
|
||||
Uses: j.Uses,
|
||||
With: j.With,
|
||||
RawSecrets: j.RawSecrets,
|
||||
RawConcurrency: j.RawConcurrency,
|
||||
RawPermissions: j.RawPermissions,
|
||||
Name: j.Name,
|
||||
RawNeeds: j.RawNeeds,
|
||||
RawRunsOn: j.RawRunsOn,
|
||||
Env: j.Env,
|
||||
If: j.If,
|
||||
Steps: j.Steps,
|
||||
TimeoutMinutes: j.TimeoutMinutes,
|
||||
Services: j.Services,
|
||||
Strategy: j.Strategy,
|
||||
RawContainer: j.RawContainer,
|
||||
Defaults: j.Defaults,
|
||||
Outputs: j.Outputs,
|
||||
Uses: j.Uses,
|
||||
With: j.With,
|
||||
RawSecrets: j.RawSecrets,
|
||||
RawConcurrency: j.RawConcurrency,
|
||||
RawPermissions: j.RawPermissions,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user