Merge remote-tracking branch 'upstream/main' into add-matrix-dynamic-job-input-support

This commit is contained in:
ZPascal
2026-06-16 16:54:55 +02:00
511 changed files with 20499 additions and 6127 deletions
+1
View File
@@ -4,6 +4,7 @@
/assets/*.json linguist-generated
/public/assets/img/svg/*.svg linguist-generated
/templates/swagger/v1_json.tmpl linguist-generated
/templates/swagger/v1_openapi3_json.tmpl linguist-generated
/options/fileicon/** linguist-generated
/vendor/** -text -eol linguist-vendored
/web_src/js/vendor/** -text -eol linguist-vendored
+4 -4
View File
@@ -9,10 +9,10 @@ inputs:
runs:
using: composite
steps:
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Build regular image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: ${{ inputs.platform }}
@@ -20,7 +20,7 @@ runs:
file: Dockerfile
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: ${{ inputs.platform }}
+10 -2
View File
@@ -1,9 +1,17 @@
name: free-disk-space
description: Free space on / before large cache restores
# Delete preinstalled toolchains which gitea doesn't use
# Delete preinstalled toolchains which gitea doesn't use and show disk space usage
runs:
using: composite
steps:
- shell: bash
run: sudo rm -rf /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet
run: |
echo "free space before cleanup:"
df -h /
for dir in /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet; do
sudo rm -rf "$dir" &
done
wait
echo "free space after cleanup:"
df -h /
+2 -3
View File
@@ -4,6 +4,8 @@ description: Restore the go module, build, and golangci-lint caches. Save only o
# Only the cache-seeder workflow saves; rename requires updating cache-seeder.yml.
# The lint job restores but does not save the gobuild cache, so only one writer
# (the gobuild job) populates it and there is no contention on the cache key.
# Seeder restores by exact key only (no restore-keys) so each go.sum seeds a clean
# cache and size stays bounded; do not add restore-keys here. PR runs keep them.
inputs:
lint-cache:
@@ -18,7 +20,6 @@ runs:
with:
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
@@ -30,7 +31,6 @@ runs:
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
@@ -42,7 +42,6 @@ runs:
with:
path: ~/.cache/golangci-lint
key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golint-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
gobuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
+3 -3
View File
@@ -20,13 +20,13 @@ jobs:
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }}
env:
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg nolyfill)$"]'
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]'
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with:
upload_sources: true
+6 -1
View File
@@ -49,7 +49,7 @@ jobs:
e2e: ${{ steps.changes.outputs.e2e }}
shell: ${{ steps.changes.outputs.shell }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
@@ -64,6 +64,11 @@ jobs:
- ".golangci.yml"
- ".editorconfig"
- "options/locale/locale_en-US.json"
- "models/fixtures/**"
- "tests/*.ini.tmpl"
- "tests/gitea-repositories-meta/**"
- "tests/testdata/**"
- "tools/test-integration.sh"
frontend:
- "*.ts"
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
+14 -1
View File
@@ -1,9 +1,16 @@
name: giteabot
on:
# When main advances, rerun merge queue maintenance so the oldest
# reviewed/wait-merge PR can be updated against the new base promptly.
push:
branches:
- main
# pull_request_target gives this workflow access to GITEABOT_TOKEN on PRs from
# forks, which the bot needs to write labels, statuses and comments. Safe here
# because the job only runs a pinned action and never checks out PR HEAD.
# These PR lifecycle events drive label maintenance, queue maintenance, and
# explicit bot actions triggered by relevant label changes.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
@@ -13,13 +20,19 @@ 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:
@@ -45,7 +58,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@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot
- 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' }}
+6 -6
View File
@@ -19,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
with:
cache: "false"
@@ -42,7 +42,7 @@ jobs:
- run: make lint-spell
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: 3.14
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
@@ -62,7 +62,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
+13 -9
View File
@@ -42,7 +42,7 @@ jobs:
ports:
- "9000:9000"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- run: make deps-backend
- run: make backend
@@ -119,7 +119,9 @@ jobs:
env:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: "-Xms512m -Xmx512m" # reduce from ES default of 50%
xpack.ml.enabled: false
ingest.geoip.downloader.enabled: false
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
ports:
- "9200:9200"
meilisearch:
@@ -129,7 +131,7 @@ jobs:
ports:
- "7700:7700"
redis:
image: redis:latest@sha256:48e78eb9d1e1adcfb10184b2cc3c7fc5ed21e5a3be08875f239257d194bab8c9
image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
@@ -149,7 +151,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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'
@@ -193,7 +195,9 @@ jobs:
env:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: "-Xms512m -Xmx512m" # reduce from ES default of 50%
xpack.ml.enabled: false
ingest.geoip.downloader.enabled: false
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
ports:
- "9200:9200"
smtpimap:
@@ -204,7 +208,7 @@ jobs:
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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'
@@ -237,7 +241,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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'
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/riscv64
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/node-setup
- run: make deps-frontend
+1 -1
View File
@@ -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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -17,7 +17,7 @@ jobs:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install snapcraft
run: sudo snap install snapcraft --classic
+11 -11
View File
@@ -14,7 +14,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
@@ -57,7 +57,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -75,12 +75,12 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Get cleaned branch name
id: clean_name
env:
@@ -88,7 +88,7 @@ jobs:
run: |
REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta
with:
images: |-
@@ -98,7 +98,7 @@ jobs:
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta_rootless
with:
images: |-
@@ -112,18 +112,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -133,7 +133,7 @@ jobs:
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
+11 -11
View File
@@ -15,7 +15,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
@@ -58,7 +58,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -86,13 +86,13 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta
with:
images: |-
@@ -105,7 +105,7 @@ jobs:
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta_rootless
with:
images: |-
@@ -121,18 +121,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -140,7 +140,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
+11 -11
View File
@@ -18,7 +18,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
@@ -61,7 +61,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -89,13 +89,13 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta
with:
images: |-
@@ -112,7 +112,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
id: meta_rootless
with:
images: |-
@@ -133,18 +133,18 @@ jobs:
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -152,7 +152,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
+2 -1
View File
@@ -16,4 +16,5 @@
- In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist
- For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important`
- Include authorship attribution in issue and pull request comments
- Add `Co-Authored-By` lines to all commits, indicating name and model used
- Always add `Assisted-By` trailers to commit messages in format `Assisted-by: AGENT_NAME:MODEL_VERSION`
- Never add `Co-Authored-By` `Signed-off-by` trailer to commit messages. Sign off must be done by a human.
+3 -3
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -9,7 +9,7 @@ COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
@@ -44,7 +44,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/.s6-svscan/* \
/go/src/gitea.dev/gitea
FROM docker.io/library/alpine:3.23 AS gitea
FROM docker.io/library/alpine:3.24 AS gitea
EXPOSE 22 3000
+3 -3
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
@@ -9,7 +9,7 @@ COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
FROM docker.io/library/golang:1.26-alpine3.24 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
@@ -39,7 +39,7 @@ COPY docker/rootless /tmp/local
RUN chmod 755 /tmp/local/usr/local/bin/* \
/go/src/gitea.dev/gitea
FROM docker.io/library/alpine:3.23 AS gitea-rootless
FROM docker.io/library/alpine:3.24 AS gitea-rootless
EXPOSE 2222 3000
+1 -1
View File
@@ -64,4 +64,4 @@ metiftikci <metiftikci@hotmail.com> (@metiftikci)
Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
Nicolas <bircni@icloud.com> (@bircni)
Nicolas <bircni@icloud.com> (@bircni)
+3 -11
View File
@@ -11,12 +11,12 @@ COMMA := ,
XGO_VERSION := go-1.26.x
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.2 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go
AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
@@ -608,14 +608,6 @@ update-js: node_modules ## update js dependencies
rm -rf node_modules pnpm-lock.yaml
pnpm install
@touch node_modules
$(MAKE) --no-print-directory nolyfill
.PHONY: nolyfill
nolyfill: node_modules ## apply nolyfill overrides to package.json and relock
pnpm exec nolyfill install
node tools/migrate-nolyfills.ts
pnpm install
@touch node_modules
.PHONY: update-py
update-py: node_modules ## update py dependencies
+11 -51
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -57,8 +57,8 @@ func main() {
log.Fatalf("scanning swagger:enum annotations: %v", err)
}
names := make([]string, 0, len(astEnumMap))
for _, n := range astEnumMap {
names = append(names, n)
for _, ns := range astEnumMap {
names = append(names, ns...)
}
sort.Strings(names)
fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", "))
+123 -18
View File
@@ -6,6 +6,7 @@ package openapi3gen
import (
"fmt"
"regexp"
"sort"
"strings"
"gitea.dev/modules/json"
@@ -25,10 +26,12 @@ var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
// Gitea-specific post-processing: file-schema fixups, URI formats,
// deprecated flags, and shared-enum extraction.
//
// astEnumMap is a value-set-key → Go-type-name map (built by
// ScanSwaggerEnumTypes). If a shared enum in the spec has no entry in the
// map, Convert returns an error — no fallback naming.
func Convert(swaggerJSON []byte, astEnumMap map[string]string) (*openapi3.T, error) {
// astEnumMap is a value-set-key → Go-type-name(s) map (built by
// ScanSwaggerEnumTypes). When a value set is shared by multiple Go types,
// per-property disambiguation uses the x-go-enum-desc extension. If a shared
// enum in the spec has no matching entry, Convert returns an error — no
// fallback naming.
func Convert(swaggerJSON []byte, astEnumMap map[string][]string) (*openapi3.T, error) {
var swagger2 openapi2.T
if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil {
return nil, fmt.Errorf("parsing swagger 2.0: %w", err)
@@ -176,12 +179,24 @@ type enumUsage struct {
// If the derived enum name collides with an existing component schema, or
// no // swagger:enum annotation matches the value set, generation aborts
// with an actionable error — there are no silent fallbacks.
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string][]string) error {
if doc.Components == nil {
return nil
}
enumGroups := map[string][]enumUsage{}
type groupKey struct {
valueSet string
typeName string
}
enumGroups := map[groupKey][]enumUsage{}
groupOrder := []groupKey{} // deterministic iteration
addUsage := func(key groupKey, u enumUsage) {
if _, seen := enumGroups[key]; !seen {
groupOrder = append(groupOrder, key)
}
enumGroups[key] = append(enumGroups[key], u)
}
for schemaName, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
@@ -192,24 +207,31 @@ func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
continue
}
if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") {
key := EnumKey(propRef.Value.Enum)
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, false})
key := groupKey{
valueSet: EnumKey(propRef.Value.Enum),
typeName: extractEnumTypeName(propRef.Value, astEnumMap),
}
addUsage(key, enumUsage{schemaName, propName, propRef, false})
}
if propRef.Value.Type.Is("array") && propRef.Value.Items != nil &&
propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" &&
len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") {
key := EnumKey(propRef.Value.Items.Value.Enum)
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, true})
key := groupKey{
valueSet: EnumKey(propRef.Value.Items.Value.Enum),
typeName: extractEnumTypeName(propRef.Value.Items.Value, astEnumMap),
}
addUsage(key, enumUsage{schemaName, propName, propRef, true})
}
}
}
for key, usages := range enumGroups {
for _, key := range groupOrder {
usages := enumGroups[key]
if len(usages) < 2 {
continue
}
enumName, err := deriveEnumName(key, usages, astEnumMap)
enumName, err := deriveEnumName(key.valueSet, key.typeName, usages, astEnumMap)
if err != nil {
return err
}
@@ -257,12 +279,17 @@ func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
return nil
}
// deriveEnumName looks up a shared enum's Go type name from astEnumMap by
// value-set key. If no annotation matches, returns an error identifying the
// offending properties and the fix.
func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string) (string, error) {
if name, ok := astEnumMap[key]; ok {
return name, nil
// deriveEnumName looks up a shared enum's Go type name. If typeName is
// non-empty (because we recovered it from x-go-enum-desc), it is used
// directly. Otherwise the value-set must map to exactly one known type. On
// failure, returns an error identifying the offending properties.
func deriveEnumName(key, typeName string, usages []enumUsage, astEnumMap map[string][]string) (string, error) {
if typeName != "" {
return typeName, nil
}
names := astEnumMap[key]
if len(names) == 1 {
return names[0], nil
}
props := map[string]bool{}
@@ -273,9 +300,87 @@ func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string
for p := range props {
propList = append(propList, p)
}
if len(names) > 1 {
return "", fmt.Errorf(
"value-set %q is shared by multiple swagger:enum types %v and could not be disambiguated for properties: %v; "+
"ensure go-swagger emits x-go-enum-desc for those properties",
key, names, propList,
)
}
return "", fmt.Errorf(
"no swagger:enum annotation matches value-set %q used by %d properties: %v; "+
"fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus",
key, len(usages), propList,
)
}
// extractEnumTypeName recovers the Go type name a schema's enum came from by
// parsing the property's x-go-enum-desc extension. go-swagger emits one line
// per value as "<value> <ConstName>[ <free text>]"; the type is the longest
// common prefix of the const names, narrowed to the candidate set in
// astEnumMap. Returns "" if extraction is inconclusive.
func extractEnumTypeName(s *openapi3.Schema, astEnumMap map[string][]string) string {
if s == nil || s.Extensions == nil {
return ""
}
raw, ok := s.Extensions["x-go-enum-desc"]
if !ok {
return ""
}
desc, ok := raw.(string)
if !ok {
return ""
}
candidates := astEnumMap[EnumKey(s.Enum)]
if len(candidates) == 0 {
return ""
}
// Collect the const names (second whitespace-separated field per line).
var consts []string
for line := range strings.SplitSeq(desc, "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 {
consts = append(consts, fields[1])
}
}
if len(consts) == 0 {
return ""
}
// A candidate matches when it is a prefix of every const name AND the
// first character after the prefix is an uppercase ASCII letter — this
// rejects e.g. "Alpha" matching "Alphabet" (suffix "bet" starts lower)
// while still accepting both "Alpha" and "AlphaPlus" against "AlphaPlusX"
// (both prefixes valid). The most specific (longest) wins; ties return
// "" so deriveEnumName surfaces the ambiguity rather than silently
// picking a winner.
ordered := append([]string(nil), candidates...)
sort.Slice(ordered, func(i, j int) bool { return len(ordered[i]) > len(ordered[j]) })
var matches []string
for _, name := range ordered {
ok := true
for _, c := range consts {
if !strings.HasPrefix(c, name) {
ok = false
break
}
suffix := c[len(name):]
// Empty suffix means the const name exactly equals the type name — valid exact match.
// A non-empty suffix must begin with an uppercase letter to reject incidental
// prefix matches (e.g. "Alpha" should not match "Alphabet").
if len(suffix) > 0 && (suffix[0] < 'A' || suffix[0] > 'Z') {
ok = false
break
}
}
if ok {
matches = append(matches, name)
}
}
if len(matches) == 0 {
return ""
}
if len(matches) > 1 && len(matches[0]) == len(matches[1]) {
return ""
}
return matches[0]
}
+53 -5
View File
@@ -12,9 +12,9 @@ import (
func TestDeriveEnumName_hit(t *testing.T) {
key := EnumKey([]any{"red", "green", "blue"})
astMap := map[string]string{key: "Color"}
astMap := map[string][]string{key: {"Color"}}
usages := []enumUsage{{schemaName: "Paint", propName: "color"}}
got, err := deriveEnumName(key, usages, astMap)
got, err := deriveEnumName(key, "", usages, astMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -26,7 +26,7 @@ func TestDeriveEnumName_hit(t *testing.T) {
func TestDeriveEnumName_miss(t *testing.T) {
key := EnumKey([]any{"x", "y"})
usages := []enumUsage{{schemaName: "Thing", propName: "kind"}}
_, err := deriveEnumName(key, usages, map[string]string{})
_, err := deriveEnumName(key, "", usages, map[string][]string{})
if err == nil {
t.Fatal("expected miss error, got nil")
}
@@ -64,7 +64,7 @@ func TestExtractSharedEnums_usesASTMap(t *testing.T) {
},
},
}
astMap := map[string]string{EnumKey([]any{"red", "green", "blue"}): "Color"}
astMap := map[string][]string{EnumKey([]any{"red", "green", "blue"}): {"Color"}}
if err := extractSharedEnums(doc, astMap); err != nil {
t.Fatalf("extractSharedEnums: %v", err)
}
@@ -139,6 +139,54 @@ func TestFixFileSchemas_recursesIntoNested(t *testing.T) {
}
}
func TestExtractEnumTypeName_TeamVisibility(t *testing.T) {
enum := []any{"public", "limited", "private"}
key := EnumKey(enum)
astMap := map[string][]string{key: {"UserVisibility", "TeamVisibility"}}
schema := &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: enum,
Extensions: map[string]any{
"x-go-enum-desc": "public TeamVisibilityPublic\nlimited TeamVisibilityLimited\nprivate TeamVisibilityPrivate",
},
}
if got := extractEnumTypeName(schema, astMap); got != "TeamVisibility" {
t.Fatalf("got %q, want %q", got, "TeamVisibility")
}
}
func TestExtractEnumTypeName_ambiguousPrefixTie(t *testing.T) {
enum := []any{"one", "two"}
key := EnumKey(enum)
astMap := map[string][]string{key: {"AB", "AC"}}
schema := &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: enum,
Extensions: map[string]any{
"x-go-enum-desc": "one ABOne\ntwo ACTwo",
},
}
if got := extractEnumTypeName(schema, astMap); got != "" {
t.Fatalf("got %q, want empty string for ambiguous tie", got)
}
}
func TestExtractEnumTypeName_rejectsIncidentalPrefix(t *testing.T) {
enum := []any{"a", "b"}
key := EnumKey(enum)
astMap := map[string][]string{key: {"Alpha", "Alphabet"}}
schema := &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: enum,
Extensions: map[string]any{
"x-go-enum-desc": "a AlphabetA\nb AlphabetB",
},
}
if got := extractEnumTypeName(schema, astMap); got != "Alphabet" {
t.Fatalf("got %q, want %q", got, "Alphabet")
}
}
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
@@ -164,7 +212,7 @@ func TestExtractSharedEnums_missReturnsError(t *testing.T) {
},
},
}
if err := extractSharedEnums(doc, map[string]string{}); err == nil {
if err := extractSharedEnums(doc, map[string][]string{}); err == nil {
t.Fatal("expected miss error")
}
}
+15 -11
View File
@@ -34,13 +34,16 @@ func EnumKey(values []any) string {
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
// a canonical value-set key (see EnumKey) to the Go type name declared with
// // swagger:enum TypeName.
// a canonical value-set key (see EnumKey) to the Go type names declared with
// // swagger:enum TypeName. Multiple type names per key are allowed (e.g.
// distinct enum types that happen to share a value set such as
// {public, limited, private}); callers must disambiguate per-usage (typically
// by parsing the property's x-go-enum-desc extension to recover the const
// type prefix).
//
// Returns an error on parse failure, on an annotation for a type whose
// constants can't be extracted, or on value-set collisions between two
// different enum types.
func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) {
// Returns an error on parse failure or on an annotation for a type whose
// constants can't be extracted.
func ScanSwaggerEnumTypes(dirs []string) (map[string][]string, error) {
fset := token.NewFileSet()
parsed := []*ast.File{}
@@ -92,17 +95,18 @@ func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) {
}
}
result := map[string]string{}
result := map[string][]string{}
for typeName := range enumTypes {
values, ok := enumValues[typeName]
if !ok || len(values) == 0 {
return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName)
}
key := EnumKey(values)
if existing, ok := result[key]; ok && existing != typeName {
return nil, fmt.Errorf("swagger:enum value-set collision: %s and %s both use %q", existing, typeName, key)
}
result[key] = typeName
result[key] = append(result[key], typeName)
}
for key, names := range result {
sort.Strings(names)
result[key] = names
}
return result, nil
}
+21 -11
View File
@@ -6,10 +6,19 @@ package openapi3gen
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
func single(got map[string][]string, key string) string {
v := got[key]
if len(v) != 1 {
return ""
}
return v[0]
}
func TestEnumKey_sortsAndJoins(t *testing.T) {
key := EnumKey([]any{"b", "a", "c"})
if key != "a|b|c" {
@@ -47,7 +56,7 @@ const (
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"red", "green", "blue"})
if got[wantKey] != "Color" {
if single(got, wantKey) != "Color" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
}
}
@@ -98,13 +107,14 @@ const (
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected collision error, got nil")
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
msg := err.Error()
if !strings.Contains(msg, "Alpha") || !strings.Contains(msg, "Beta") {
t.Fatalf("error %q should mention both Alpha and Beta", msg)
key := EnumKey([]any{"x", "y"})
names := got[key]
if !slices.Equal(names, []string{"Alpha", "Beta"}) {
t.Fatalf("map[%q] = %v, want [Alpha Beta]", key, names)
}
}
@@ -168,7 +178,7 @@ type Hue string
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"a", "b"})
if got[wantKey] != "Hue" {
if single(got, wantKey) != "Hue" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue")
}
}
@@ -194,7 +204,7 @@ type Shade string
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"dark", "light"})
if got[wantKey] != "Shade" {
if single(got, wantKey) != "Shade" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
}
}
@@ -230,10 +240,10 @@ const (
}
colorKey := EnumKey([]any{"red", "blue"})
shadeKey := EnumKey([]any{"dark", "light"})
if got[colorKey] != "Color" {
if single(got, colorKey) != "Color" {
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
}
if got[shadeKey] != "Shade" {
if single(got, shadeKey) != "Shade" {
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
}
}
+76
View File
@@ -6,10 +6,12 @@ package cmd
import (
"context"
"errors"
"fmt"
"os"
"gitea.dev/modules/generate"
"gitea.dev/modules/ssh"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v3"
@@ -21,6 +23,7 @@ func newGenerateCommand() *cli.Command {
Usage: "Generate Gitea's secrets/keys/tokens",
Commands: []*cli.Command{
newGenerateSecretCommand(),
newGenerateSSHCommand(),
},
}
}
@@ -37,6 +40,17 @@ func newGenerateSecretCommand() *cli.Command {
}
}
func newGenerateSSHCommand() *cli.Command {
return &cli.Command{
Name: "ssh",
Usage: "Generate ssh keys",
Commands: []*cli.Command{
newGenerateSSHKeyCommand(),
newGenerateSSHHostKeysCommand(),
},
}
}
func newGenerateInternalTokenCommand() *cli.Command {
return &cli.Command{
Name: "INTERNAL_TOKEN",
@@ -62,6 +76,30 @@ func newGenerateSecretKeyCommand() *cli.Command {
}
}
func newGenerateSSHKeyCommand() *cli.Command {
return &cli.Command{
Name: "key",
Usage: "Generate a new ssh key",
Flags: []cli.Flag{
&cli.IntFlag{Name: "bits", Aliases: []string{"b"}, Usage: "Number of bits in the key, ignored when key is ed25519"},
&cli.StringFlag{Name: "type", Aliases: []string{"t"}, Value: "ed25519", Usage: "Specifies the type of key to create."},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Specifies the path or base directory for the key file", Required: true},
},
Action: runGenerateKeyPair,
}
}
func newGenerateSSHHostKeysCommand() *cli.Command {
return &cli.Command{
Name: "host-keys",
Usage: "Generate host keys of all default key types (rsa, ecdsa, and ed25519) if they do not already exist.",
Flags: []cli.Flag{
&cli.StringFlag{Name: "dir", Aliases: []string{"d"}, Usage: "Specifies the base directory for the key files", Required: true},
},
Action: runGenerateHostKey,
}
}
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
internalToken, err := generate.NewInternalToken()
if err != nil {
@@ -103,3 +141,41 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
return nil
}
func runGenerateHostKey(_ context.Context, c *cli.Command) error {
file := c.String("dir")
info, err := os.Stat(file)
if errors.Is(err, os.ErrNotExist) {
if err = os.MkdirAll(file, 0o644); err != nil {
return err
}
} else if err != nil {
return err
} else if !info.IsDir() {
return errors.New("file already exists and is not a directory")
}
fmt.Fprintf(c.Writer, "Generating host keys in %s\n", file)
_, err = ssh.InitDefaultHostKeys(file)
return err
}
func runGenerateKeyPair(_ context.Context, c *cli.Command) error {
file := c.String("file")
keyType := c.String("type")
fmt.Fprintf(c.Writer, "Generating public/private %s key pair.\n", keyType)
// Check if file exists to prevent overwriting
if _, err := os.Stat(file); err == nil {
if !confirm(c.Reader, c.Writer, "%s already exists.\nOverwrite (y/n)? ", file) {
fmt.Println("Aborting")
return nil
}
}
bits := c.Int("bits")
err := ssh.GenKeyPair(file, generate.SSHKeyType(keyType), bits)
if err == nil {
fmt.Printf("Your SSH key has been saved in %s\n", file)
}
return err
}
+8 -15
View File
@@ -38,22 +38,15 @@ func argsSet(c *cli.Command, args ...string) error {
}
// confirm waits for user input which confirms an action
func confirm() (bool, error) {
func confirm(stdin io.Reader, stdout io.Writer, msg string, args ...any) bool {
var response string
_, err := fmt.Scanln(&response)
if err != nil {
return false, err
}
_, _ = fmt.Fprintf(stdout, msg, args...)
_, _ = fmt.Fscanln(stdin, &response)
switch strings.ToLower(response) {
case "y", "yes":
return true, nil
case "n", "no":
return false, nil
default:
return false, errors.New(response + " isn't a correct confirmation string")
return true
}
return false
}
func initDB(ctx context.Context) error {
@@ -142,9 +135,9 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
// Dirty patch for urfave/cli's strange design.
// "./gitea bad-cmd" should not start the web server.
rootArgs := cmd.Root().Args().Slice()
if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
return rootArgs[0], false
args := cmd.Args().Slice()
if len(args) != 0 {
return args[0], false
}
return "", true
}
+41 -96
View File
@@ -151,9 +151,6 @@ func (d *delayWriter) WriteString(s string) (n int, err error) {
}
func (d *delayWriter) Close() error {
if d == nil {
return nil
}
stopped := d.timer.Stop()
if stopped || d.buf == nil {
return nil
@@ -163,16 +160,6 @@ func (d *delayWriter) Close() error {
return err
}
type nilWriter struct{}
func (n *nilWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil
}
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
fields := strings.Split(line, " ")
if len(fields) != 3 {
@@ -227,8 +214,7 @@ Gitea or set your environment appropriately.`, "")
total := 0
lastline := 0
var out io.Writer
out = &nilWriter{}
out := io.Discard
if setting.Git.VerbosePush {
if setting.Git.VerbosePushDelay > 0 {
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
@@ -350,12 +336,10 @@ Gitea or set your environment appropriately.`, "")
return nil
}
var out io.Writer
var dWriter *delayWriter
out = &nilWriter{}
out := io.Discard
if setting.Git.VerbosePush {
if setting.Git.VerbosePushDelay > 0 {
dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
defer dWriter.Close()
out = dWriter
} else {
@@ -382,101 +366,62 @@ Gitea or set your environment appropriately.`, "")
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
IsWiki: isWiki,
}
oldCommitIDs := make([]string, hookBatchSize)
newCommitIDs := make([]string, hookBatchSize)
refFullNames := make([]git.RefName, hookBatchSize)
count := 0
total := 0
wasEmpty := false
masterPushed := false
oldCommitIDs := make([]string, 0, hookBatchSize)
newCommitIDs := make([]string, 0, hookBatchSize)
refFullNames := make([]git.RefName, 0, hookBatchSize)
results := make([]private.HookPostReceiveBranchResult, 0)
defer func() {
hookPrintResults(results)
}()
processBatch := func() error {
if len(refFullNames) == 0 {
return nil
}
_, _ = fmt.Fprintf(out, " Processing %d references\n", len(refFullNames))
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
if extra.HasError() {
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
}
results = append(results, resp.Results...)
oldCommitIDs = oldCommitIDs[:0]
newCommitIDs = newCommitIDs[:0]
refFullNames = refFullNames[:0]
return nil
}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
// TODO: support news feeds for wiki
// wiki doesn't need "post-receive" at the moment
if isWiki {
continue
}
var ok bool
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
if !ok {
continue
}
_, _ = fmt.Fprintf(out, ".")
fmt.Fprintf(out, ".")
commitID, _ := git.NewIDFromString(newCommitIDs[count])
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
masterPushed = true
}
count++
total++
if count >= hookBatchSize {
fmt.Fprintf(out, " Processing %d references\n", count)
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
if extra.HasError() {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
oldCommitIDs = append(oldCommitIDs, oldCommitID)
newCommitIDs = append(newCommitIDs, newCommitID)
refFullNames = append(refFullNames, refFullName)
if len(refFullNames) >= hookBatchSize {
// process and start a new batch
if err := processBatch(); err != nil {
return err
}
wasEmpty = wasEmpty || resp.RepoWasEmpty
results = append(results, resp.Results...)
count = 0
}
}
if err := scanner.Err(); err != nil {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
}
if count == 0 {
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
if extra.HasError() {
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
}
}
fmt.Fprintf(out, "Processed %d references in total\n", total)
_ = dWriter.Close()
hookPrintResults(results)
return nil
}
hookOptions.OldCommitIDs = oldCommitIDs[:count]
hookOptions.NewCommitIDs = newCommitIDs[:count]
hookOptions.RefFullNames = refFullNames[:count]
fmt.Fprintf(out, " Processing %d references\n", count)
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
if resp == nil {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
}
wasEmpty = wasEmpty || resp.RepoWasEmpty
results = append(results, resp.Results...)
fmt.Fprintf(out, "Processed %d references in total\n", total)
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
if extra.HasError() {
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
}
}
_ = dWriter.Close()
hookPrintResults(results)
return nil
return processBatch()
}
func hookPrintResults(results []private.HookPostReceiveBranchResult) {
+2 -6
View File
@@ -22,14 +22,10 @@ func runSendMail(ctx context.Context, c *cli.Command) error {
if !confirmSkipped {
if len(body) == 0 {
fmt.Print("warning: Content is empty")
fmt.Println("warning: Content is empty")
}
fmt.Print("Proceed with sending email? [Y/n] ")
isConfirmed, err := confirm()
if err != nil {
return err
} else if !isConfirmed {
if !confirm(c.Reader, c.Writer, "Proceed with sending email? [Y/n] ") {
fmt.Println("The mail was not sent")
return nil
}
+1
View File
@@ -41,4 +41,5 @@ func TestDefaultCommand(t *testing.T) {
test(t, []string{"./gitea"}, "", true)
test(t, []string{"./gitea", "test"}, "", true)
test(t, []string{"./gitea", "other"}, "other", false)
test(t, []string{"./gitea", "test", "extra"}, "extra", false)
}
+14 -9
View File
@@ -113,23 +113,25 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
return nil
}
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
// getAccessMode maps an SSH git/LFS verb to the access mode it requires, with
// ok=false for an unrecognised verb. Callers MUST reject the request when ok is
// false: AccessModeNone would otherwise pass the `userMode < mode` permission
// check in routers/private/serv.go and grant access.
func getAccessMode(verb, lfsVerb string) (mode perm.AccessMode, ok bool) {
switch verb {
case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
return perm.AccessModeRead
return perm.AccessModeRead, true
case git.CmdVerbReceivePack:
return perm.AccessModeWrite
return perm.AccessModeWrite, true
case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
switch lfsVerb {
case git.CmdSubVerbLfsUpload:
return perm.AccessModeWrite
return perm.AccessModeWrite, true
case git.CmdSubVerbLfsDownload:
return perm.AccessModeRead
return perm.AccessModeRead, true
}
}
// should be unreachable
setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
return perm.AccessModeNone
return perm.AccessModeNone, false
}
func runServ(ctx context.Context, c *cli.Command) error {
@@ -247,7 +249,10 @@ func runServ(ctx context.Context, c *cli.Command) error {
}
}
requestedMode := getAccessMode(verb, lfsVerb)
requestedMode, ok := getAccessMode(verb, lfsVerb)
if !ok {
return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb)
}
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
if extra.HasError() {
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"gitea.dev/models/perm"
"gitea.dev/modules/git"
"github.com/stretchr/testify/assert"
)
func TestGetAccessMode(t *testing.T) {
cases := []struct {
verb, lfsVerb string
expected perm.AccessMode
}{
{git.CmdVerbUploadPack, "", perm.AccessModeRead},
{git.CmdVerbUploadArchive, "", perm.AccessModeRead},
{git.CmdVerbReceivePack, "", perm.AccessModeWrite},
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
}
for _, tc := range cases {
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
assert.True(t, ok)
assert.Equal(t, tc.expected, mode)
})
}
}
// TestGetAccessModeUnknownVerb locks in the invariant that getAccessMode reports
// ok=false for unrecognised verbs and LFS sub-verbs, so runServ rejects them. An
// unknown verb has no valid access mode; if it were treated as AccessModeNone (0)
// it would pass the `userMode < mode` permission check in routers/private/serv.go
// and hand out valid LFS JWTs for any private repository.
func TestGetAccessModeUnknownVerb(t *testing.T) {
cases := []struct{ verb, lfsVerb string }{
{git.CmdVerbLfsAuthenticate, ""},
{git.CmdVerbLfsAuthenticate, "badverb"},
{git.CmdVerbLfsTransfer, "badverb"},
{"git-unknown-verb", ""},
}
for _, tc := range cases {
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
assert.False(t, ok)
assert.Equal(t, perm.AccessModeNone, mode)
})
}
}
+9 -3
View File
@@ -531,6 +531,10 @@ INTERNAL_TOKEN =
;;
;; The value of the X-Content-Type-Options HTTP header for all responses. Use "unset" to remove the header.
;X_CONTENT_TYPE_OPTIONS = nosniff
;;
;; The value of the general Content-Security-Policy for most web pages.
;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy.
;CONTENT_SECURITY_POLICY_GENERAL =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2668,19 +2672,21 @@ LEVEL = Info
;FILE_EXTENSIONS = .adoc,.asciidoc
;; External command to render all matching extensions
;RENDER_COMMAND = "asciidoc --out-file=- -"
;; Don't pass the file on STDIN, pass the filename as argument instead.
;; Whether Gitea should write the content into a local temp file for the render command's input.
;; * false: the content will be passed via STDIN to the command.
;; * true: write the content into a local temp file, and pass the temp filename as argument to the command.
;IS_INPUT_FILE = false
;; How the content will be rendered.
;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
;RENDER_CONTENT_MODE = sanitized
;; The sandbox applied to the iframe and Content-Security-Policy header when RENDER_CONTENT_MODE is `iframe`.
;; The sandbox applied to the Content-Security-Policy for the rendered content when RENDER_CONTENT_MODE is `iframe`.
;; It defaults to a safe set of "allow-*" restrictions (space separated).
;; You can also set it by your requirements or use "disabled" to disable the sandbox completely.
;; When set it, make sure there is no security risk:
;; * PDF-only content: generally safe to use "disabled", and it needs to be "disabled" because PDF only renders with no sandbox.
;; * HTML content with JS: if the "RENDER_COMMAND" can guarantee there is no XSS, then it is safe, otherwise, you need to fine tune the "allow-*" restrictions.
;; * HTML content with JS: do not set "allow-same-origin" unless the "RENDER_COMMAND" can guarantee there is no XSS.
;RENDER_CONTENT_SANDBOX =
;; Whether post-process the rendered HTML content, including:
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
+72 -21
View File
@@ -2,6 +2,36 @@
This document describes maintainer expectations, project governance, and the detailed pull request review workflow (labels, merge queue, commit message format for mergers). For what contributors should do when opening and updating a PR, see [CONTRIBUTING.md](../CONTRIBUTING.md).
## Table of contents
- [Community governance and review process](#community-governance-and-review-process)
- [Table of contents](#table-of-contents)
- [Code review](#code-review)
- [Milestone](#milestone)
- [Labels](#labels)
- [Reviewing PRs](#reviewing-prs)
- [For reviewers](#for-reviewers)
- [Getting PRs merged](#getting-prs-merged)
- [Final call](#final-call)
- [Commit messages](#commit-messages)
- [PR Co-authors](#pr-co-authors)
- [PRs targeting `main`](#prs-targeting-main)
- [Backport PRs](#backport-prs)
- [Contribution Roles](#contribution-roles)
- [Maintainers](#maintainers)
- [Review expectations](#review-expectations)
- [Becoming a maintainer](#becoming-a-maintainer)
- [Stepping down, advisors, and inactivity](#stepping-down-advisors-and-inactivity)
- [Account security](#account-security)
- [Mergers](#mergers)
- [Becoming a merger](#becoming-a-merger)
- [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
- [TOC election process](#toc-election-process)
- [Current TOC members](#current-toc-members)
- [Previous TOC/owners members](#previous-tocowners-members)
- [Governance Compensation](#governance-compensation)
- [Roadmap](#roadmap)
## Code review
### Milestone
@@ -92,7 +122,9 @@ $PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX)
$REWRITTEN_PR_SUMMARY
```
## Maintainers
## Contribution Roles
### Maintainers
We list [maintainers](../MAINTAINERS) so every PR gets proper review.
@@ -121,12 +153,30 @@ For security, maintainers should enable 2FA and sign commits with GPG when possi
Any account with write access (including bots and TOC members) **must** use [2FA](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication).
## Technical Oversight Committee (TOC)
### Mergers
Mergers are the maintainers who carry out the final merge of approved PRs. Their responsibilities, described throughout this guide, are:
- Merging PRs from the [merge queue](#getting-prs-merged) in order, once a PR has `lgtm/done`, no open discussions, and no merge conflicts.
- Rewriting the PR title and summary so the squash [commit message](#commit-messages) is clear, removing false-positive co-authors while keeping every true co-author.
- Assigning the correct labels (including `type/…`) needed for changelog and backport decisions.
- Agreeing, together with the owners, on when a release is ready (see [release management](release-management.md)).
#### Becoming a merger
A merger must already be a Gitea maintainer.
To apply, use the [Discord](https://discord.gg/Gitea) `#maintainers` channel.
The minimum requirement for applications to become a merger is to have participated actively in the community for at least four months before applying.
Ultimately, regardless of previous participation, you can only become a merger if the TOC votes in your favor.
You may also be invited by the TOC to become a merger.
### Technical Oversight Committee (TOC)
At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
https://blog.gitea.com/quarterly-23q1/
### TOC election process
#### TOC election process
Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
@@ -140,17 +190,30 @@ As long as seats are empty in the TOC, members of the previous TOC can fill them
If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
If multiple persons have the same amount of votes, a random draw will be used to determine the order of the candidates with the same amount of votes, and thus who gets the seat first.
The candidates will be placed in the list in an alphabetical insensitive order by their username.
We use this script to determine the order of candidates with the same amount of votes:
```python
import random
random.seed("Gitea TOC <YEAR> Election")
random.choice([<CANDIDATE_1>, <CANDIDATE_2>, ...])
```
The result of this script needs then to be published in the TOC election issue to ensure transparency of the process.
### Current TOC members
- 2024-01-01 ~ 2024-12-31
- 2026-06-14 ~ 2026-12-31
- Company
- [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
- Community
- [6543](https://gitea.com/6543) <6543@obermui.de>
- [bircni](https://gitea.com/bircni) <bircni@icloud.com>
- [delvh](https://gitea.com/delvh) <dev.lh@web.de>
- [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
- [TheFox0x7](https://gitea.com/TheFox0x7) <thefox0x7@gmail.com>
### Previous TOC/owners members
@@ -159,11 +222,11 @@ Here's the history of the owners and the time they served:
- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
- [Lauris Bukšis](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), 2025
- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
- [6543](https://gitea.com/6543) - 2023
- [John Olheiser](https://gitea.com/jolheiser) - 2023
- [6543](https://gitea.com/6543) - 2023, 2025
- [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024
- [Jason Song](https://gitea.com/wolfogre) - 2023
## Governance Compensation
@@ -176,18 +239,6 @@ These funds will come from community sources like the OpenCollective rather than
Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties.
Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself.
## TOC & Working groups
With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places.
The currently proposed groups are:
- **Core Group**: maintain the primary Gitea repository
- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc.
- **Documentation Group**: maintain related documents and repositories
- **Translation Group**: coordinate with translators and maintain translations
- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items
## Roadmap
Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders.
+5 -2
View File
@@ -18,6 +18,8 @@ import wc from 'eslint-plugin-wc';
import {defineConfig, globalIgnores} from 'eslint/config';
import type {ESLint} from 'eslint';
import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts';
const jsExts = ['js', 'mjs', 'cjs'] as const;
const tsExts = ['ts', 'mts', 'cts'] as const;
@@ -65,6 +67,7 @@ export default defineConfig([
'@typescript-eslint': typescriptPlugin.plugin,
'array-func': arrayFunc,
'de-morgan': deMorgan,
'gitea': {rules: {'unescaped-html-literal': unescapedHtmlLiteral}},
'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203
regexp,
sonarjs,
@@ -331,7 +334,7 @@ export default defineConfig([
'github/no-useless-passive': [2],
'github/prefer-observers': [0],
'github/require-passive-events': [2],
'github/unescaped-html-literal': [2],
'gitea/unescaped-html-literal': [2],
'grouped-accessor-pairs': [2],
'guard-for-in': [0],
'id-blacklist': [0],
@@ -952,7 +955,7 @@ export default defineConfig([
plugins: {vitest},
languageOptions: {globals: globals.vitest},
rules: {
'github/unescaped-html-literal': [0],
'gitea/unescaped-html-literal': [0],
'vitest/consistent-test-filename': [0],
'vitest/consistent-test-it': [0],
'vitest/expect-expect': [0],
+64 -73
View File
@@ -1,31 +1,31 @@
module gitea.dev
go 1.26.3
go 1.26.4
require (
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.20.0
gitea.com/gitea/runner v1.0.5
gitea.com/gitea/runner v1.0.8
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
gitea.dev/actions-proto-go v0.5.0
gitea.dev/sdk v1.0.1
gitea.dev/actions-proto-go v0.6.0
gitea.dev/sdk v1.1.0
github.com/42wim/httpsig v1.2.4
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0
github.com/Azure/go-ntlmssp v0.1.1
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347
github.com/ProtonMail/go-crypto v1.4.1
github.com/PuerkitoBio/goquery v1.12.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3
github.com/alecthomas/chroma/v2 v2.25.0
github.com/aws/aws-sdk-go-v2/credentials v1.19.16
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.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
@@ -33,7 +33,7 @@ require (
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.1.0
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
@@ -42,9 +42,9 @@ require (
github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.10.1
github.com/getkin/kin-openapi v0.138.0
github.com/getkin/kin-openapi v0.140.0
github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/chi/v5 v5.3.0
github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.21.2
github.com/go-enry/go-enry/v2 v2.9.6
@@ -53,32 +53,32 @@ require (
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-redsync/redsync/v4 v4.16.0
github.com/go-sql-driver/mysql v1.10.0
github.com/go-webauthn/webauthn v0.17.3
github.com/go-webauthn/webauthn v0.17.4
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/go-github/v87 v87.0.0
github.com/google/go-github/v88 v88.0.0
github.com/google/licenseclassifier/v2 v2.0.0
github.com/google/pprof v0.0.0-20260507013755-92041b743c96
github.com/google/pprof v0.0.0-20260604005048-7023385849c0
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-version v1.9.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.5.0
github.com/jhillyerd/enmime/v2 v2.4.0
github.com/jhillyerd/enmime/v2 v2.4.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.18.6
github.com/klauspost/cpuid/v2 v2.3.0
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.44
github.com/meilisearch/meilisearch-go v0.36.2
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
github.com/microsoft/go-mssqldb v1.9.7
github.com/minio/minio-go/v7 v7.1.0
github.com/microsoft/go-mssqldb v1.10.0
github.com/minio/minio-go/v7 v7.2.0
github.com/msteinert/pam/v2 v2.1.0
github.com/niklasfasching/go-org v1.9.1
github.com/opencontainers/go-digest v1.0.0
@@ -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.19.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,24 +96,25 @@ 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.6.1
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.30.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/crypto v0.52.0
golang.org/x/image v0.40.0
golang.org/x/net v0.55.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.42.0
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/text v0.37.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
golang.org/x/text v0.38.0
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.2
modernc.org/sqlite v1.50.1
gopkg.in/ini.v1 v1.67.3
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
@@ -124,24 +125,24 @@ require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/DataDog/zstd v1.5.7 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring/v2 v2.16.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
github.com/aws/smithy-go v1.27.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/blevesearch/bleve_index_api v1.3.11 // indirect
github.com/bits-and-blooms/bitset v1.24.5 // indirect
github.com/blevesearch/bleve_index_api v1.3.12 // indirect
github.com/blevesearch/geo v0.2.5 // indirect
github.com/blevesearch/go-faiss v1.1.0 // indirect
github.com/blevesearch/go-faiss v1.1.4 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.2.0 // indirect
@@ -156,13 +157,13 @@ require (
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
github.com/blevesearch/zapx/v17 v17.1.2 // indirect
github.com/blevesearch/zapx/v17 v17.1.6 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -176,26 +177,22 @@ require (
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.5 // indirect
github.com/go-webauthn/x v0.2.6 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect
@@ -209,46 +206,42 @@ require (
github.com/inbucket/html2text v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-shellwords v1.0.13 // indirect
github.com/mholt/acmez/v3 v3.1.6 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minlz v1.1.0 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.12 // indirect
github.com/nwaples/rardecode/v2 v2.2.3 // indirect
github.com/oasdiff/yaml v0.1.0 // indirect
github.com/oasdiff/yaml3 v0.0.13 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/common v0.68.1 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rhysd/actionlint v1.7.12 // indirect
@@ -260,9 +253,9 @@ require (
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
@@ -271,18 +264,16 @@ require (
go.etcd.io/bbolt v1.4.3 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap v1.28.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect
golang.org/x/tools v0.45.0 // indirect
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.72.3 // indirect
modernc.org/libc v1.73.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
+134 -150
View File
@@ -10,8 +10,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
gitea.com/gitea/runner v1.0.5 h1:9Idm8XXxXp4aHoLp0Ba0AEwv87YUblp9gk4qbih+DlE=
gitea.com/gitea/runner v1.0.5/go.mod h1:ff8SKVat/6FywBGpN4dKi2x3AlHZUfLOA/vVWHf7+Cw=
gitea.com/gitea/runner v1.0.8 h1:zKfC4+zyyGIDagqhII3WVw52P+A9iAa63It0lniN4SI=
gitea.com/gitea/runner v1.0.8/go.mod h1:AGLQXo8ELz9WPzNJ5W1SvnCik8ZX3jF0o6yCPCmwLtM=
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M=
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ=
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
@@ -26,20 +26,20 @@ gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHq
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitea.dev/actions-proto-go v0.5.0 h1:Fc3DI4Fm3B3JBRXFUjegql+usoNAjjAw1cxMansfA2I=
gitea.dev/actions-proto-go v0.5.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs=
gitea.dev/sdk v1.0.1 h1:CWXQUQvp2I6YKOWkhYo1Flx2sRNfMK1X9Op4oR2awXs=
gitea.dev/sdk v1.0.1/go.mod h1:jCf5Uzz0Jkb61jxNgMxLOCWwle1J1B2nKdcRtxuK9rY=
gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY=
gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs=
gitea.dev/sdk v1.1.0 h1:wLlz03WkLEiXa2bQpO1JQBTlYf7tQI2neYtZK1kU+TE=
gitea.dev/sdk v1.1.0/go.mod h1:Zfl+EZXdsGGCLkryDfsmvYrQo6GKMl4U3BJA8Beu+cs=
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432/go.mod h1:BLWe6Nol65Xxncvaw07yYMxiyk02We1lBrbRYsMYsjE=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
@@ -50,8 +50,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZY
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
@@ -67,17 +67,17 @@ github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
github.com/RoaringBitmap/roaring/v2 v2.16.0 h1:Kys1UNf49d5W8Tq3bpuAhIr/Z8/yPB+59CO8A6c/BbE=
github.com/RoaringBitmap/roaring/v2 v2.16.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
github.com/RoaringBitmap/roaring/v2 v2.18.2/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3 h1:ikrUPushSxbpr9mmDhSgz1KyUTChjwkDrxY/jzv2OTQ=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3/go.mod h1:bnXbvnI9Mfqdj4L3Y9aCsB1A/ztuYLNRzgVKsJFgR/U=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7xGekf8vTkcqTvyBBHaLYIMxx0g=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo=
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.25.0 h1:DWkVlxrNpxPf+Qcfe04LBqUArxUiybK8ZQ9T7OFu68E=
github.com/alecthomas/chroma/v2 v2.25.0/go.mod h1:+95AZrRWlpW9g6qXD7S7UdHviopsGP/kCIrtJcU3QoQ=
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=
@@ -85,45 +85,45 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14 h1:3N664oayz66ttIkc8B9/OLntMWhoGhKqPudRRfsZQ20=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14/go.mod h1:M+6j5lOmtDMjLlFMO8lfTs0gI3qBsSjM8L7F1cQF5Ng=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4 h1:Uu+wqrOXozYYvaxcNIqjFsMTjoIJIZDN3R0f70ZIjyQ=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4/go.mod h1:pYrBdL1tMTZO7PaKRsa1cTUB8HtQh3fFM3zJHGhTQcE=
github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk=
github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZFtopqJ4w=
github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM=
github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4=
github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY=
github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
github.com/blevesearch/bleve_index_api v1.3.11 h1:x29vbV8OjWfLcrDVd7Lr1q+BkLNS0JWNEig0MCVnKH4=
github.com/blevesearch/bleve_index_api v1.3.11/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/bleve_index_api v1.3.12 h1:MirVNltwGq8z0PhOgiQp+bKL5qq8OvCxEwOOC7NnHNE=
github.com/blevesearch/bleve_index_api v1.3.12/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
github.com/blevesearch/go-faiss v1.1.0 h1:xM7Jc0ZUCv5lssG9Ohj3Jv0SdTpxcUABU1dDt9XVsc4=
github.com/blevesearch/go-faiss v1.1.0/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-faiss v1.1.4 h1:wGHK+yiOSIvBAQMr4LcTaHBFf9v1dBebs3WpFqT93Rg=
github.com/blevesearch/go-faiss v1.1.4/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
@@ -163,15 +163,15 @@ github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2l
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
github.com/blevesearch/zapx/v17 v17.1.2 h1:avbOk2igaASNoiy0BE/jPgcxAnRI2PGeydeP4hg7Ikk=
github.com/blevesearch/zapx/v17 v17.1.2/go.mod h1:WQObxKrqUX7cd0G1GMvDfc/bmZzQvoy7APOPimx7DiI=
github.com/blevesearch/zapx/v17 v17.1.6 h1:rVGeyH0EPElBXM4PvjrCdt8LDdRLpa4GC1gMRQkCWUE=
github.com/blevesearch/zapx/v17 v17.1.6/go.mod h1:c+mPvbZgZnDPOUS5Z9EXhntMcJnpIVjQTM9TF5yEGJM=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E=
@@ -179,8 +179,8 @@ github.com/bohde/codel v0.2.0/go.mod h1:Idb1IRvTdwkRjIjguLIo+FXhIBhcpGl94o7xra6g
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -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.1.0 h1:jHXRmHRZGbuQzDZjMlCAXOvQb75iv3HyLDzXGj5H1AY=
github.com/dlclark/regexp2/v2 v2.1.0/go.mod h1:Bz5TMy5d8fPK0ximH0Yi9KvsRHNnvXqUx9XG6a4wB+I=
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=
@@ -274,8 +274,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.138.0 h1:ebfE0JAmF6AqHrNBy1KO3Fs68K9tPs48HalvLPo7Rv4=
github.com/getkin/kin-openapi v0.138.0/go.mod h1:vUYWaKyMqj7PfTybelXtLuLN9tReS12vxnzMRK+z2GY=
github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g=
github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -285,8 +285,8 @@ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1T
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
@@ -295,8 +295,6 @@ github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUD
github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
@@ -305,16 +303,16 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
@@ -330,10 +328,10 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.17.3 h1:XHZ0TXV7k8vChcE4TFgPitOPJ5cb7h1dpAeFDS0cjCo=
github.com/go-webauthn/webauthn v0.17.3/go.mod h1:PlkMgmuL9McCT7dvgBj/Sz/fgs3V6ZID6/KnFkEcPvQ=
github.com/go-webauthn/x v0.2.5 h1:wEVTfU04XFyPTXGQbKOQwMKhcDWfDAkdsDDBsDaG9yY=
github.com/go-webauthn/x v0.2.5/go.mod h1:Qna/yJz9rV6lRzwl5BfYbmTJpVGxcBIds3gJtw2tlGg=
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
@@ -367,8 +365,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
@@ -380,8 +376,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v87 v87.0.0 h1:9Ck3dcOxWJyfsN8tzdah4YvmqB/7ZsstMglv/PkOsl0=
github.com/google/go-github/v87 v87.0.0/go.mod h1:hGUoT5pwm/ck5uLL+wroSVQfg8mpe+buxllCcGV4VaM=
github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M=
github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -394,8 +390,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA=
github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M=
github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20260604005048-7023385849c0 h1:h1QTMDl6q9wDvDCJVpKQSjgleGFYnd2fOxmg2K+6BGE=
github.com/google/pprof v0.0.0-20260604005048-7023385849c0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -417,8 +413,8 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls=
github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM=
github.com/graph-gophers/graphql-go v1.10.2 h1:HXu6Wu5klCH4ALn1fQHVI20cjEIa4wftavHIgbLA4Fo=
github.com/graph-gophers/graphql-go v1.10.2/go.mod h1:AsADheC4CCFwd8n1/QbkduTlHgYYMsRgtPihYVAlEsk=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -460,11 +456,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jhillyerd/enmime/v2 v2.4.0 h1:6bPyyg2OPXEK1fKsLT89DntZf05LqaL2cIx+cvkEXTo=
github.com/jhillyerd/enmime/v2 v2.4.0/go.mod h1:TLpvqImPiumRecsJK5TYseRw2bPg3g0EtWc+SfU7cMs=
github.com/jhillyerd/enmime/v2 v2.4.1 h1:VkBX8GJJ/wbQofWsKP3egRqgXcwmxlY94YUmXTj08kE=
github.com/jhillyerd/enmime/v2 v2.4.1/go.mod h1:TLpvqImPiumRecsJK5TYseRw2bPg3g0EtWc+SfU7cMs=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -506,24 +501,23 @@ github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE=
github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/meilisearch/meilisearch-go v0.36.2 h1:MYaMPCpdLh2aYPt+zK+19mLoA4dfBY3S1L7T0FADCjU=
github.com/meilisearch/meilisearch-go v0.36.2/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
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.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=
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
@@ -540,10 +534,10 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
github.com/minio/minlz v1.1.0 h1:rUOGu3EP4EqJC5k3qCsIwEnZiJULKqtRyDdqbhlvMmQ=
github.com/minio/minlz v1.1.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -551,8 +545,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -566,15 +558,15 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/nwaples/rardecode/v2 v2.2.3 h1:qaVuy3ChZDbAQZshPLjHeNJKF3Cru8uo9jmgveKIy2A=
github.com/nwaples/rardecode/v2 v2.2.3/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M=
github.com/oasdiff/yaml3 v0.0.12/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg=
github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0=
github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg=
github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
@@ -599,13 +591,11 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -621,15 +611,15 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
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.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.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=
@@ -680,6 +670,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -710,8 +702,6 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -719,13 +709,11 @@ 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.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/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=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -752,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.30.0 h1:guXC+uOA325P1FIwUVesPoDcnogHnbPrhV8c+wMHZPE=
gitlab.com/gitlab-org/api/client-go/v2 v2.30.0/go.mod h1:0upZTKi9YMMvhavTlKJ3YgQUZE/GJkcLmXT/+6knTXU=
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=
@@ -765,8 +753,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -787,14 +775,13 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
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.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
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=
@@ -803,8 +790,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -820,9 +807,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -836,8 +822,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -867,10 +853,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -879,10 +864,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -893,8 +877,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -906,14 +890,14 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 h1:YedBIttDguBl/zy2wJauEUm+DZZg4UXseWj0g/3N+yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -929,8 +913,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
@@ -944,20 +928,20 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
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.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
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=
@@ -966,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.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
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=
+1 -1
View File
@@ -15,9 +15,9 @@ import (
"gitea.dev/modules/setting"
// register supported doc types
_ "gitea.dev/modules/markup/asciicast"
_ "gitea.dev/modules/markup/console"
_ "gitea.dev/modules/markup/csv"
_ "gitea.dev/modules/markup/jupyter"
_ "gitea.dev/modules/markup/markdown"
_ "gitea.dev/modules/markup/orgmode"
-92
View File
@@ -21,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
@@ -253,96 +251,6 @@ func UpdateRepoRunsNumbers(ctx context.Context, repoID int64) {
}
}
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID,
Ref: ref,
WorkflowID: workflowID,
TriggerEvent: event,
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked, StatusCancelling},
})
if err != nil {
return nil, err
}
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
if total == 0 {
return nil, nil
}
cancelledJobs := make([]*ActionRunJob, 0, total)
// Iterate over each found run and cancel its associated jobs.
for _, run := range runs {
// Find all jobs associated with the current run.
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
RunID: run.ID,
})
if err != nil {
return cancelledJobs, err
}
cjs, err := CancelJobs(ctx, jobs)
if err != nil {
return cancelledJobs, err
}
cancelledJobs = append(cancelledJobs, cjs...)
}
// Return nil to indicate successful cancellation of all running and waiting jobs.
return cancelledJobs, nil
}
func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) {
cancelledJobs := make([]*ActionRunJob, 0, len(jobs))
// Iterate over each job and attempt to cancel it.
for _, job := range jobs {
// Skip jobs that are already in a terminal state (completed, cancelled, etc.).
status := job.Status
if status.IsDone() {
continue
}
// If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it.
if job.TaskID == 0 {
job.Status = StatusCancelled
job.Stopped = timeutil.TimeStampNow()
// Update the job's status and stopped time in the database.
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil {
return cancelledJobs, err
}
// If the update affected 0 rows, it means the job has changed in the meantime
if n == 0 {
log.Error("Failed to cancel job %d because it has changed", job.ID)
continue
}
cancelledJobs = append(cancelledJobs, job)
// Continue with the next job.
continue
}
// If the job has an associated task, try to stop the task, effectively cancelling the job.
if err := StopTask(ctx, job.TaskID, StatusCancelling); err != nil {
return cancelledJobs, err
}
updatedJob, err := GetRunJobByRunAndID(ctx, job.RunID, job.ID)
if err != nil {
return cancelledJobs, fmt.Errorf("get job: %w", err)
}
cancelledJobs = append(cancelledJobs, updatedJob)
}
// Return nil to indicate successful cancellation of all running and waiting jobs.
return cancelledJobs, nil
}
func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
var run ActionRun
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
+318 -1
View File
@@ -4,6 +4,7 @@
package actions
import (
"cmp"
"context"
"fmt"
"slices"
@@ -12,8 +13,10 @@ import (
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
webhook_module "gitea.dev/modules/webhook"
"xorm.io/builder"
)
@@ -82,14 +85,55 @@ type ActionRunJob struct {
// A value of 0 indicates a legacy job created before ActionRunAttempt existed.
AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
// WorkflowSourceRepoID + WorkflowSourceCommitSHA record the (repo, commit) this job's containing workflow file came from.
WorkflowSourceRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowSourceCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
// IsReusableCaller marks this job as a reusable workflow caller.
// Caller jobs do not run on a runner; their status is derived from their child jobs.
IsReusableCaller bool `xorm:"index NOT NULL DEFAULT FALSE"`
// IsExpanded reports whether this job's lazy expansion (children-row insertion) is complete.
// For a reusable workflow caller, true means children rows exist and CallPayload is populated.
IsExpanded bool `xorm:"NOT NULL DEFAULT FALSE"`
// CallUses stores the raw "uses:" string of a reusable workflow caller job.
// Only set when IsReusableCaller is true.
CallUses string `xorm:"VARCHAR(512) NOT NULL DEFAULT ''"`
// ReusableWorkflowContent is the content of the reusable workflow specified by "uses:".
// Only set when IsReusableCaller is true.
ReusableWorkflowContent []byte `xorm:"LONGBLOB"`
// CallSecrets encodes the reusable workflow caller's "secrets:" section:
// - "" : no "secrets:" section (children only see auto-generated tokens).
// - "inherit" : the caller wrote "secrets: inherit".
// - JSON object : explicit mapping {alias: source_name}; names only, no values.
// Only set when IsReusableCaller is true.
CallSecrets string `xorm:"LONGTEXT"`
// CallPayload is the JSON-encoded WorkflowCallPayload exposed to children as gitea.event.
// Populated atomically with IsExpanded at the end of expandReusableWorkflowCaller.
// Only set when IsReusableCaller is true.
CallPayload string `xorm:"LONGTEXT"`
// 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"`
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated index"`
}
// ActionRunAttemptJobIDIndex backs the run-wide AttemptJobID counter, keyed by ActionRun.ID.
// Use GetNextAttemptJobID to allocate the next ID for a run.
type ActionRunAttemptJobIDIndex db.ResourceIndex
// GetNextAttemptJobID atomically allocates the next AttemptJobID for a job in the given run.
// AttemptJobIDs are unique within a single attempt and stable across attempts for the same logical job
func GetNextAttemptJobID(ctx context.Context, runID int64) (int64, error) {
return db.GetNextResourceIndex(ctx, "action_run_attempt_job_id_index", runID)
}
func init() {
db.RegisterModel(new(ActionRunJob))
db.RegisterModel(new(ActionRunAttemptJobIDIndex))
}
func (job *ActionRunJob) Duration() time.Duration {
@@ -225,6 +269,101 @@ func GetRunJobsByRunAndAttemptID(ctx context.Context, runID, runAttemptID int64)
return jobs, nil
}
// GetPriorAttemptChildrenByParent returns the children of the most recent prior attempt where
// the parent (identified by parentAttemptJobID) actually had children, indexed by child JobID then child Name.
// Returns (nil, nil) when no such attempt exists.
// The (JobID, Name) key disambiguates both reusable-workflow subtrees and matrix-expanded instances (whose Name carries the matrix suffix).
func GetPriorAttemptChildrenByParent(ctx context.Context, runID, currentAttemptID, parentAttemptJobID int64) (map[string]map[string]*ActionRunJob, error) {
// query every prior caller row sharing this AttemptJobID, newest first.
var priorCallers []*ActionRunJob
if err := db.GetEngine(ctx).
Where("run_id = ? AND attempt_job_id = ? AND run_attempt_id < ?", runID, parentAttemptJobID, currentAttemptID).
Desc("run_attempt_id").
Find(&priorCallers); err != nil {
return nil, fmt.Errorf("find prior callers: %w", err)
}
if len(priorCallers) == 0 {
return nil, nil //nolint:nilnil // caller is brand new in this attempt
}
// query for every child of every prior caller
callerIDs := make([]int64, len(priorCallers))
for i, c := range priorCallers {
callerIDs[i] = c.ID
}
var allChildren []*ActionRunJob
if err := db.GetEngine(ctx).
Where("run_id = ?", runID).
In("parent_job_id", callerIDs).
Find(&allChildren); err != nil {
return nil, fmt.Errorf("find prior children: %w", err)
}
childrenByCallerID := make(map[int64][]*ActionRunJob, len(callerIDs))
for _, c := range allChildren {
childrenByCallerID[c.ParentJobID] = append(childrenByCallerID[c.ParentJobID], c)
}
// Walk priorCallers in run_attempt_id-desc order and return the children of the first caller that actually had any.
// Skipped attempts (caller exists but no children) are bypassed.
for _, caller := range priorCallers {
children := childrenByCallerID[caller.ID]
if len(children) == 0 {
continue
}
out := make(map[string]map[string]*ActionRunJob)
for _, c := range children {
if out[c.JobID] == nil {
out[c.JobID] = make(map[string]*ActionRunJob)
}
out[c.JobID][c.Name] = c
}
return out, nil
}
return nil, nil //nolint:nilnil // every prior attempt skipped this caller
}
// GetDirectChildJobsByParent returns the direct child jobs of a parent job (e.g. a reusable workflow caller).
func GetDirectChildJobsByParent(ctx context.Context, parentJob *ActionRunJob) (ActionJobList, error) {
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).
Where("run_id=? AND parent_job_id=?", parentJob.RunID, parentJob.ID).
OrderBy("id").
Find(&jobs); err != nil {
return nil, err
}
return jobs, nil
}
// CollectAllDescendantJobs returns every job in `allJobs` that lives under parent's subtree (recursively), excluding `parent` itself
func CollectAllDescendantJobs(parent *ActionRunJob, allJobs []*ActionRunJob) []*ActionRunJob {
parents := map[int64]bool{parent.ID: true}
for {
grew := false
for _, j := range allJobs {
if j.ParentJobID == 0 {
continue
}
if parents[j.ParentJobID] && !parents[j.ID] {
parents[j.ID] = true
grew = true
}
}
if !grew {
break
}
}
out := make([]*ActionRunJob, 0)
for _, j := range allJobs {
if j.ID == parent.ID || !parents[j.ID] {
continue
}
out = append(out, j)
}
return out
}
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
e := db.GetEngine(ctx)
@@ -249,7 +388,8 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return affected, nil
}
if slices.Contains(cols, "status") && job.Status.IsWaiting() {
// 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
@@ -263,6 +403,15 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
}
}
if statusUpdated && job.ParentJobID > 0 {
// Reusable workflow caller's children cascade their status changes upward to the parent caller.
parent, err := GetRunJobByRunAndID(ctx, job.RunID, job.ParentJobID)
if err != nil {
return affected, fmt.Errorf("load parent caller %d: %w", job.ParentJobID, err)
}
return affected, RefreshReusableCallerStatus(ctx, parent)
}
{
// Other goroutines may aggregate the status of the attempt/run and update it too.
// So we need to load the current jobs before updating the aggregate state.
@@ -315,6 +464,44 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return affected, nil
}
// RefreshReusableCallerStatus recomputes a reusable workflow caller's Status, Started and Stopped from its current direct children and persists the change.
// No-op if caller is not a reusable caller.
//
// Concurrency: two sibling children finishing at roughly the same time can each invoke this for the same parent caller.
// No row-level lock is taken because AggregateJobStatus is a pure function of the children's statuses (order-independent), so racing callers arrive at the same Status.
func RefreshReusableCallerStatus(ctx context.Context, caller *ActionRunJob) error {
if !caller.IsReusableCaller {
return nil
}
children, err := GetDirectChildJobsByParent(ctx, caller)
if err != nil {
return err
}
newStatus := AggregateJobStatus(children)
cols := make([]string, 0, 3)
if caller.Status != newStatus {
caller.Status = newStatus
cols = append(cols, "status")
}
if newStatus != StatusSkipped {
now := timeutil.TimeStampNow()
if caller.Started.IsZero() && newStatus == StatusRunning {
caller.Started = now
cols = append(cols, "started")
}
if caller.Stopped.IsZero() && newStatus.IsDone() {
caller.Stopped = now
cols = append(cols, "stopped")
}
}
if len(cols) == 0 {
return nil
}
_, err = UpdateRunJob(ctx, caller, nil, cols...)
return err
}
func AggregateJobStatus(jobs []*ActionRunJob) Status {
allSuccessOrSkipped := len(jobs) != 0
allSkipped := len(jobs) != 0
@@ -353,6 +540,49 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
}
}
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID,
Ref: ref,
WorkflowID: workflowID,
TriggerEvent: event,
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked, StatusCancelling},
})
if err != nil {
return nil, err
}
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
if total == 0 {
return nil, nil
}
cancelledJobs := make([]*ActionRunJob, 0, total)
// Iterate over each found run and cancel its associated jobs.
for _, run := range runs {
// Find all jobs associated with the current run.
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
RunID: run.ID,
})
if err != nil {
return cancelledJobs, err
}
cjs, err := CancelJobs(ctx, jobs)
if err != nil {
return cancelledJobs, err
}
cancelledJobs = append(cancelledJobs, cjs...)
}
// Return nil to indicate successful cancellation of all running and waiting jobs.
return cancelledJobs, nil
}
func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) {
if job.RawConcurrency == "" {
return nil, nil
@@ -410,3 +640,90 @@ func GetMaxAttemptJobID(ctx context.Context, runID, runAttemptID int64) (int64,
}
return job.AttemptJobID, nil
}
func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) {
cancelledJobs := make([]*ActionRunJob, 0, len(jobs))
for _, job := range jobs {
if job.IsReusableCaller {
sub, err := cancelReusableCaller(ctx, job)
if err != nil {
return cancelledJobs, err
}
cancelledJobs = append(cancelledJobs, sub...)
continue
}
c, err := cancelOneJob(ctx, job)
if err != nil {
return cancelledJobs, err
}
if c != nil {
cancelledJobs = append(cancelledJobs, c)
}
}
return cancelledJobs, nil
}
// cancelOneJob cancels a single job and returns the post-cancel row
func cancelOneJob(ctx context.Context, job *ActionRunJob) (*ActionRunJob, error) {
if job.Status.IsDone() {
return nil, nil //nolint:nilnil // signal "nothing to cancel; not an error"
}
// No associated task: mark Cancelled directly. This includes reusable callers and jobs that never reached PickTask.
if job.TaskID == 0 {
job.Status = StatusCancelled
job.Stopped = timeutil.TimeStampNow()
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil {
return nil, err
}
if n == 0 {
log.Error("Failed to cancel job %d because it has changed", job.ID)
return nil, nil //nolint:nilnil // signal "nothing to cancel; not an error"
}
return job, nil
}
// Has a task: stop the task and re-read the row.
if err := StopTask(ctx, job.TaskID, StatusCancelling); err != nil {
return nil, err
}
updated, err := GetRunJobByRunAndID(ctx, job.RunID, job.ID)
if err != nil {
return nil, fmt.Errorf("get job: %w", err)
}
return updated, nil
}
// cancelReusableCaller cancels `caller` and all its child jobs
func cancelReusableCaller(ctx context.Context, caller *ActionRunJob) ([]*ActionRunJob, error) {
cancelledJobs := make([]*ActionRunJob, 0)
attemptJobs, err := GetRunJobsByRunAndAttemptID(ctx, caller.RunID, caller.RunAttemptID)
if err != nil {
return cancelledJobs, err
}
// Cancel descendants deepest-first, then the caller: a caller's status is aggregated from its children,
// so each child must reach its final state before its parent caller is re-aggregated.
// A child's ID always exceeds its parent's, so descending ID is a valid deepest-first order.
descendants := CollectAllDescendantJobs(caller, attemptJobs)
slices.SortFunc(descendants, func(a, b *ActionRunJob) int { return cmp.Compare(b.ID, a.ID) })
for _, c := range descendants {
cancelled, err := cancelOneJob(ctx, c)
if err != nil {
return cancelledJobs, err
}
if cancelled != nil {
cancelledJobs = append(cancelledJobs, cancelled)
}
}
if c, err := cancelOneJob(ctx, caller); err != nil {
return cancelledJobs, err
} else if c != nil {
cancelledJobs = append(cancelledJobs, c)
}
return cancelledJobs, nil
}
+207
View File
@@ -0,0 +1,207 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
)
const (
// JobSummaryCapability is the runner-declare capability string for job summaries.
JobSummaryCapability = "job-summary"
// JobSummaryContentTypeMarkdown is the only accepted content type for job summaries.
JobSummaryContentTypeMarkdown = "text/markdown"
// MaxJobSummarySize is the maximum accepted per-step summary payload size in bytes.
MaxJobSummarySize = 1024 * 1024 // 1 MiB
// MaxJobSummaryAggregateSize is the maximum aggregate size of all step summaries within
// a single job attempt. Matches GitHub's documented per-job summary cap of 1 MiB.
MaxJobSummaryAggregateSize = 1024 * 1024 // 1 MiB
)
// RunnerCapabilities returns the value advertised in the X-Gitea-Actions-Capabilities header.
// When more capabilities are added, return them comma-separated so runners can split on ", ".
func RunnerCapabilities() string {
return JobSummaryCapability
}
type ActionRunJobSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(summary_key)"`
RunID int64 `xorm:"UNIQUE(summary_key)"`
RunAttemptID int64 `xorm:"UNIQUE(summary_key) NOT NULL DEFAULT 0"`
JobID int64 `xorm:"UNIQUE(summary_key)"`
StepIndex int64 `xorm:"UNIQUE(summary_key)"`
Content string `xorm:"LONGTEXT"`
ContentType string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'text/markdown'"`
// ContentSize is the byte length of Content. Stored explicitly because LENGTH()
// counts characters (not bytes) on PostgreSQL, SQLite and MSSQL, which would let
// multibyte UTF-8 content bypass the aggregate cap.
ContentSize int64 `xorm:"NOT NULL DEFAULT 0"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(ActionRunJobSummary))
}
func GetActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID, stepIndex int64) (*ActionRunJobSummary, error) {
var s ActionRunJobSummary
has, err := db.GetEngine(ctx).
Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=? AND step_index=?", repoID, runID, runAttemptID, jobID, stepIndex).
Get(&s)
if err != nil {
return nil, err
}
if !has {
return nil, util.ErrNotExist
}
return &s, nil
}
// ErrJobSummaryAggregateExceeded is returned when a step summary upload would push the
// aggregate size of summaries for a single job attempt over MaxJobSummaryAggregateSize.
var ErrJobSummaryAggregateExceeded = util.NewInvalidArgumentErrorf("job summary aggregate size exceeded")
func UpsertActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID, stepIndex int64, contentType string, content []byte) error {
if runID <= 0 || jobID <= 0 || repoID <= 0 || stepIndex < 0 {
return util.ErrInvalidArgument
}
if len(content) == 0 {
// Treat empty summaries as no-op; runner may create SUMMARY.md but never write to it.
return nil
}
if len(content) > MaxJobSummarySize {
return util.ErrInvalidArgument
}
if contentType != JobSummaryContentTypeMarkdown {
return util.ErrInvalidArgument
}
// The aggregate check is best-effort: a tx wouldn't actually serialize concurrent
// step uploads (no row-level lock on the parent job), so wrapping these two
// statements only adds round-trip cost without changing the race semantics.
// The current step is excluded because the upsert below replaces its size with len(content).
otherSize, err := sumOtherJobSummarySizes(ctx, repoID, runID, runAttemptID, jobID, stepIndex)
if err != nil {
return err
}
if otherSize+int64(len(content)) > MaxJobSummaryAggregateSize {
return ErrJobSummaryAggregateExceeded
}
now := timeutil.TimeStampNow()
return upsertActionRunJobSummary(ctx, &ActionRunJobSummary{
RepoID: repoID,
RunID: runID,
RunAttemptID: runAttemptID,
JobID: jobID,
StepIndex: stepIndex,
Content: string(content),
ContentSize: int64(len(content)),
ContentType: contentType,
Created: now,
Updated: now,
})
}
// sumOtherJobSummarySizes returns the total stored size of all step summaries for a job
// except excludeStepIndex, computed in the database to avoid loading every row.
func sumOtherJobSummarySizes(ctx context.Context, repoID, runID, runAttemptID, jobID, excludeStepIndex int64) (int64, error) {
return db.GetEngine(ctx).
Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=? AND step_index<>?", repoID, runID, runAttemptID, jobID, excludeStepIndex).
SumInt(new(ActionRunJobSummary), "content_size")
}
// DeleteActionRunJobSummary removes the stored summary for a specific step. Used when
// a runner PUTs an empty body to clear a previously-uploaded step summary.
func DeleteActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID, stepIndex int64) error {
_, err := db.GetEngine(ctx).
Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=? AND step_index=?", repoID, runID, runAttemptID, jobID, stepIndex).
Delete(new(ActionRunJobSummary))
return err
}
func upsertActionRunJobSummary(ctx context.Context, summary *ActionRunJobSummary) error {
engine := db.GetEngine(ctx)
columns := "`repo_id`, `run_id`, `run_attempt_id`, `job_id`, `step_index`, `content`, `content_type`, `content_size`, `created`, `updated`"
values := []any{
summary.RepoID,
summary.RunID,
summary.RunAttemptID,
summary.JobID,
summary.StepIndex,
summary.Content,
summary.ContentType,
summary.ContentSize,
summary.Created,
summary.Updated,
}
if setting.Database.Type.IsPostgreSQL() || setting.Database.Type.IsSQLite3() {
args := append([]any{"INSERT INTO `action_run_job_summary` (" + columns + ") VALUES (?,?,?,?,?,?,?,?,?,?) " +
"ON CONFLICT (`repo_id`, `run_id`, `run_attempt_id`, `job_id`, `step_index`) DO UPDATE SET " +
"`content` = excluded.`content`, `content_type` = excluded.`content_type`, `content_size` = excluded.`content_size`, `updated` = excluded.`updated`"}, values...)
_, err := engine.Exec(args...)
return err
}
if setting.Database.Type.IsMySQL() {
args := append([]any{
"INSERT INTO `action_run_job_summary` (" + columns + ") VALUES (?,?,?,?,?,?,?,?,?,?) " +
"ON DUPLICATE KEY UPDATE `content` = VALUES(`content`), `content_type` = VALUES(`content_type`), `content_size` = VALUES(`content_size`), `updated` = VALUES(`updated`)",
}, values...)
_, err := engine.Exec(args...)
return err
}
if setting.Database.Type.IsMSSQL() {
_, err := engine.Exec(`
MERGE INTO action_run_job_summary WITH (HOLDLOCK) AS target
USING (SELECT ? AS repo_id, ? AS run_id, ? AS run_attempt_id, ? AS job_id, ? AS step_index) AS source
ON target.repo_id = source.repo_id
AND target.run_id = source.run_id
AND target.run_attempt_id = source.run_attempt_id
AND target.job_id = source.job_id
AND target.step_index = source.step_index
WHEN MATCHED THEN
UPDATE SET content = ?, content_type = ?, content_size = ?, updated = ?
WHEN NOT MATCHED THEN
INSERT (repo_id, run_id, run_attempt_id, job_id, step_index, content, content_type, content_size, created, updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`,
summary.RepoID, summary.RunID, summary.RunAttemptID, summary.JobID, summary.StepIndex,
summary.Content, summary.ContentType, summary.ContentSize, summary.Updated,
summary.RepoID, summary.RunID, summary.RunAttemptID, summary.JobID, summary.StepIndex, summary.Content, summary.ContentType, summary.ContentSize, summary.Created, summary.Updated)
return err
}
return util.ErrInvalidArgument
}
// ListActionRunJobSummaries lists the stored summaries for a run attempt, ordered by job
// then step. A positive jobID scopes the lookup to that single job, used by the job view to
// avoid rendering every job's summary on each poll; jobID<=0 returns all jobs in the attempt.
func ListActionRunJobSummaries(ctx context.Context, repoID, runID, runAttemptID, jobID int64) ([]*ActionRunJobSummary, error) {
sess := db.GetEngine(ctx).Where("repo_id=? AND run_id=? AND run_attempt_id=?", repoID, runID, runAttemptID)
if jobID > 0 {
sess = sess.And("job_id=?", jobID)
}
var summaries []*ActionRunJobSummary
if err := sess.OrderBy("job_id ASC, step_index ASC").Find(&summaries); err != nil {
return nil, err
}
return summaries, nil
}
+199
View File
@@ -0,0 +1,199 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetPriorAttemptChildrenByParent(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
// 3 attempts of one run:
// 1: caller expanded with 3 matrix instances of "work" + non-matrix sibling "summary".
// 2: caller skipped, no children rows.
// 3: placeholder "current" attempt for the walkback subtest.
run := &ActionRun{
Title: "prior-children-test",
RepoID: 4,
Index: 9501,
OwnerID: 1,
WorkflowID: "matrix.yaml",
TriggerUserID: 1,
Ref: "refs/heads/master",
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
Event: "push",
TriggerEvent: "push",
EventPayload: "{}",
Status: StatusSuccess,
}
require.NoError(t, db.Insert(ctx, run))
const callerAttemptJobID int64 = 9001
insertAttempt := func(t *testing.T, num int64, status Status) *ActionRunAttempt {
t.Helper()
a := &ActionRunAttempt{
RepoID: run.RepoID,
RunID: run.ID,
Attempt: num,
TriggerUserID: 1,
Status: status,
}
require.NoError(t, db.Insert(ctx, a))
return a
}
insertCaller := func(t *testing.T, attemptID int64, status Status, expanded bool) *ActionRunJob {
t.Helper()
caller := &ActionRunJob{
RunID: run.ID,
RunAttemptID: attemptID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: "caller",
JobID: "caller",
Attempt: 1,
Status: status,
AttemptJobID: callerAttemptJobID,
IsReusableCaller: true,
IsExpanded: expanded,
}
require.NoError(t, db.Insert(ctx, caller))
return caller
}
insertChild := func(t *testing.T, attemptID, parentID, attemptJobID int64, name, jobID string) {
t.Helper()
require.NoError(t, db.Insert(ctx, &ActionRunJob{
RunID: run.ID,
RunAttemptID: attemptID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: name,
JobID: jobID,
Attempt: 1,
Status: StatusSuccess,
AttemptJobID: attemptJobID,
ParentJobID: parentID,
}))
}
attempt1 := insertAttempt(t, 1, StatusSuccess)
caller1 := insertCaller(t, attempt1.ID, StatusSuccess, true)
insertChild(t, attempt1.ID, caller1.ID, 101, "work (alpha)", "work")
insertChild(t, attempt1.ID, caller1.ID, 102, "work (beta)", "work")
insertChild(t, attempt1.ID, caller1.ID, 103, "work (gamma)", "work")
insertChild(t, attempt1.ID, caller1.ID, 104, "summary", "summary")
attempt2 := insertAttempt(t, 2, StatusSkipped)
insertCaller(t, attempt2.ID, StatusSkipped, false) // no children intentionally
// both subtests expect attempt 1's expansion, differing only in the "current" attempt id
assertAttempt1Children := func(t *testing.T, out map[string]map[string]*ActionRunJob) {
t.Helper()
// outer map keyed by JobID: "work" has 3 matrix instances, "summary" 1
assert.Len(t, out, 2)
assert.Len(t, out["work"], 3, "matrix instances must each get their own inner-map entry")
assert.Len(t, out["summary"], 1)
require.NotNil(t, out["work"]["work (alpha)"])
require.NotNil(t, out["work"]["work (beta)"])
require.NotNil(t, out["work"]["work (gamma)"])
require.NotNil(t, out["summary"]["summary"])
assert.Equal(t, int64(101), out["work"]["work (alpha)"].AttemptJobID)
assert.Equal(t, int64(102), out["work"]["work (beta)"].AttemptJobID)
assert.Equal(t, int64(103), out["work"]["work (gamma)"].AttemptJobID)
assert.Equal(t, int64(104), out["summary"]["summary"].AttemptJobID)
}
t.Run("matrix instances and non-matrix sibling are indexed by (JobID, Name)", func(t *testing.T) {
// "current" = attempt 2; prior = attempt 1, which is the immediately preceding attempt.
out, err := GetPriorAttemptChildrenByParent(ctx, run.ID, attempt2.ID, callerAttemptJobID)
require.NoError(t, err)
assertAttempt1Children(t, out)
})
t.Run("walkback past an attempt where the caller had no children", func(t *testing.T) {
attempt3 := insertAttempt(t, 3, StatusRunning)
// "current" = attempt 3; the immediately preceding attempt 2 has no children, so the lookup must walk further back to attempt 1.
out, err := GetPriorAttemptChildrenByParent(ctx, run.ID, attempt3.ID, callerAttemptJobID)
require.NoError(t, err)
assertAttempt1Children(t, out)
})
}
// A reusable caller subtree with a Blocked descendant (e.g. a nested caller stuck on an invalid `uses:`) must aggregate to Cancelled, when the run is cancelled.
func TestCancelJobs_NestedBlockedReusableCaller(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
run := &ActionRun{
Title: "cancel-nested-caller",
RepoID: 4,
Index: 9701,
OwnerID: 1,
WorkflowID: "caller.yaml",
TriggerUserID: 1,
Ref: "refs/heads/master",
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
Event: "push",
TriggerEvent: "push",
EventPayload: "{}",
Status: StatusBlocked,
}
require.NoError(t, db.Insert(ctx, run))
attempt := &ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: 1, Status: StatusBlocked}
require.NoError(t, db.Insert(ctx, attempt))
run.LatestAttemptID = attempt.ID
require.NoError(t, UpdateRun(ctx, run, "latest_attempt_id"))
newJob := func(name string, attemptJobID, parentID int64, callUses string) *ActionRunJob {
job := &ActionRunJob{
RunID: run.ID,
RunAttemptID: attempt.ID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: name,
JobID: name,
Attempt: 1,
Status: StatusBlocked,
AttemptJobID: attemptJobID,
IsReusableCaller: true,
CallUses: callUses,
ParentJobID: parentID,
}
require.NoError(t, db.Insert(ctx, job))
return job
}
// outer: a valid top-level caller that expanded; inner: a nested caller stuck Blocked (invalid uses, never expands).
outer := newJob("outer", 1, 0, "./.gitea/workflows/lib.yml")
inner := newJob("inner", 2, outer.ID, "https://other.example.com/o/r/.gitea/workflows/ci.yml@v1")
// Cancel all jobs of the attempt, ordered by id (parent before child).
jobs, err := GetRunJobsByRunAndAttemptID(ctx, run.ID, attempt.ID)
require.NoError(t, err)
_, err = CancelJobs(ctx, jobs)
require.NoError(t, err)
for _, j := range []*ActionRunJob{outer, inner} {
got := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: j.ID})
assert.Equal(t, StatusCancelled, got.Status, "job %q should be cancelled", j.JobID)
}
gotAttempt := unittest.AssertExistsAndLoadBean(t, &ActionRunAttempt{ID: attempt.ID})
assert.Equal(t, StatusCancelled, gotAttempt.Status, "attempt must aggregate to Cancelled")
gotRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: run.ID})
assert.Equal(t, StatusCancelled, gotRun.Status, "run must aggregate to Cancelled, not stay Blocked")
}
+2 -4
View File
@@ -64,7 +64,6 @@ type FindRunOptions struct {
Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Approved bool // not util.OptionalBool, it works only when it's true
Status []Status
ConcurrencyGroup string
CommitSHA string
@@ -81,9 +80,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.TriggerUserID > 0 {
cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
}
if opts.Approved {
cond = cond.And(builder.Gt{"`action_run`.approved_by": 0})
}
if len(opts.Status) > 0 {
cond = cond.And(builder.In("`action_run`.status", opts.Status))
}
@@ -115,6 +111,7 @@ func (opts FindRunOptions) ToOrders() string {
type StatusInfo struct {
Status int
StatusName string
DisplayedStatus string
}
@@ -126,6 +123,7 @@ func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInf
for _, s := range allStatus {
statusInfoList = append(statusInfoList, StatusInfo{
Status: int(s),
StatusName: s.String(),
DisplayedStatus: s.LocaleString(lang),
})
}
+13
View File
@@ -7,6 +7,7 @@ import (
"testing"
"gitea.dev/models/unittest"
"gitea.dev/modules/translation"
"github.com/stretchr/testify/assert"
)
@@ -22,3 +23,15 @@ func TestGetRunWorkflowIDs(t *testing.T) {
assert.NoError(t, err)
assert.Empty(t, ids)
}
func TestGetStatusInfoList(t *testing.T) {
statusInfoList := GetStatusInfoList(t.Context(), translation.MockLocale{})
assert.Equal(t, []StatusInfo{
{Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"},
{Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"},
{Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"},
{Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"},
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
}, statusInfoList)
}
+2 -2
View File
@@ -249,7 +249,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
}
var jobs []*ActionRunJob
if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
if err := e.Where("task_id=? AND status=? AND is_reusable_caller=?", 0, StatusWaiting, false).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
return nil, false, err
}
@@ -390,7 +390,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
RepoID: task.RepoID,
Status: task.Status,
Stopped: task.Stopped,
}, nil); err != nil {
}, nil, "status", "stopped"); err != nil {
return nil, err
}
} else {
@@ -8,6 +8,7 @@ import (
"fmt"
"hash"
"gitea.dev/models/gituser"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
@@ -32,8 +33,8 @@ type CommitVerification struct {
// SignCommit represents a commit with validation of signature.
type SignCommit struct {
Verification *CommitVerification
*user_model.UserCommit
Verification *CommitVerification
*gituser.UserCommit // TODO: need to use a explicit field name, avoid anonymous field
}
const (
+6 -54
View File
@@ -20,42 +20,6 @@ import (
"xorm.io/builder"
)
// ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error.
type ErrAccessTokenNotExist struct {
Token string
}
// IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist.
func IsErrAccessTokenNotExist(err error) bool {
_, ok := err.(ErrAccessTokenNotExist)
return ok
}
func (err ErrAccessTokenNotExist) Error() string {
return fmt.Sprintf("access token does not exist [sha: %s]", err.Token)
}
func (err ErrAccessTokenNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error.
type ErrAccessTokenEmpty struct{}
// IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty.
func IsErrAccessTokenEmpty(err error) bool {
_, ok := err.(ErrAccessTokenEmpty)
return ok
}
func (err ErrAccessTokenEmpty) Error() string {
return "access token is empty"
}
func (err ErrAccessTokenEmpty) Unwrap() error {
return util.ErrInvalidArgument
}
var successfulAccessTokenCache *lru.Cache[string, any]
// AccessToken represents a personal access token.
@@ -134,21 +98,11 @@ func getAccessTokenIDFromCache(token string) int64 {
// GetAccessTokenBySHA returns access token by given token value
func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) {
if token == "" {
return nil, ErrAccessTokenEmpty{}
}
// A token is defined as being SHA1 sum these are 40 hexadecimal bytes long
if len(token) != 40 {
return nil, ErrAccessTokenNotExist{token}
}
for _, x := range []byte(token) {
if x < '0' || (x > '9' && x < 'a') || x > 'f' {
return nil, ErrAccessTokenNotExist{token}
}
if len(token) < 8 {
return nil, util.NewNotExistErrorf("access token not found")
}
lastEight := token[len(token)-8:]
if id := getAccessTokenIDFromCache(token); id > 0 {
accessToken := &AccessToken{
TokenLastEight: lastEight,
@@ -169,7 +123,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
if err != nil {
return nil, err
} else if len(tokens) == 0 {
return nil, ErrAccessTokenNotExist{token}
return nil, util.NewNotExistErrorf("access token not found")
}
for _, t := range tokens {
@@ -181,7 +135,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
return &t, nil
}
}
return nil, ErrAccessTokenNotExist{token}
return nil, util.NewNotExistErrorf("access token not found")
}
// AccessTokenByNameExists checks if a token name has been used already by a user.
@@ -218,13 +172,11 @@ func UpdateAccessToken(ctx context.Context, t *AccessToken) error {
// DeleteAccessTokenByID deletes access token by given ID.
func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error {
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{
UID: userID,
})
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{UID: userID})
if err != nil {
return err
} else if cnt != 1 {
return ErrAccessTokenNotExist{}
return util.NewNotExistErrorf("access token not found")
}
return nil
}
+4 -3
View File
@@ -9,6 +9,7 @@ import (
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -76,11 +77,11 @@ func TestGetAccessTokenBySHA(t *testing.T) {
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "notahash")
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
assert.ErrorIs(t, err, util.ErrNotExist)
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "")
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenEmpty(err))
assert.ErrorIs(t, err, util.ErrNotExist)
}
func TestListAccessTokens(t *testing.T) {
@@ -128,5 +129,5 @@ func TestDeleteAccessTokenByID(t *testing.T) {
err = auth_model.DeleteAccessTokenByID(t.Context(), 100, 100)
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
assert.ErrorIs(t, err, util.ErrNotExist)
}
+20 -45
View File
@@ -4,8 +4,10 @@
package db
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"sync"
"gitea.dev/modules/setting"
@@ -14,61 +16,34 @@ import (
"xorm.io/xorm/dialects"
)
var registerOnce sync.Once
type postgresSchemaDriver struct{}
func registerPostgresSchemaDriver() {
registerOnce.Do(func() {
sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{})
dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres"))
})
}
var registerPostgresSchemaDriver = sync.OnceFunc(func() {
sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{})
dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres"))
})
type postgresSchemaDriver struct {
pq.Driver
}
// Open opens a new connection to the database. name is a connection string.
// This function opens the postgres connection in the default manner but immediately
// runs set_config to set the search_path appropriately
func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
conn, err := d.Driver.Open(name)
// Open opens the postgres connection in the default manner with default schema support.
// It immediately runs "set_config" to set the search_path appropriately.
func (*postgresSchemaDriver) Open(connStr string) (driver.Conn, error) {
conn, err := pq.Driver{}.Open(connStr)
if err != nil {
return conn, err
return nil, err
}
schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema)
// golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here
// and in any case pq does not implement it
if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck // see above
_, err := execer.Exec(`SELECT set_config(
connExec, ok := conn.(driver.ExecerContext)
if !ok {
return nil, errors.New("postgres driver does not implement ExecerContext interface")
}
_, err = connExec.ExecContext(context.Background(), `SELECT set_config(
'search_path',
$1 || ',' || current_setting('search_path'),
false)`, []driver.Value{schemaValue})
if err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
stmt, err := conn.Prepare(`SELECT set_config(
'search_path',
$1 || ',' || current_setting('search_path'),
false)`)
false)`,
[]driver.NamedValue{{Ordinal: 1, Value: setting.Database.Schema}},
)
if err != nil {
_ = conn.Close()
return nil, err
}
defer stmt.Close()
// driver.String.ConvertValue will never return err for string
// golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here
_, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck // see above
if err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
+12 -7
View File
@@ -28,9 +28,8 @@ var (
registeredInitFuncs []func() error
)
// Engine represents a xorm engine or session.
type Engine interface {
Table(tableNameOrBean any) *xorm.Session
// SQLSession represents a common interface for engine and session to execute SQLs
type SQLSession interface {
Count(...any) (int64, error)
Decr(column string, arg ...any) *xorm.Session
Delete(...any) (int64, error)
@@ -52,7 +51,6 @@ type Engine interface {
Limit(limit int, start ...int) *xorm.Session
NoAutoTime() *xorm.Session
SumInt(bean any, columnName string) (res int64, err error)
Sync(...any) error
Select(string) *xorm.Session
SetExpr(string, any) *xorm.Session
NotIn(string, ...any) *xorm.Session
@@ -61,12 +59,20 @@ type Engine interface {
Distinct(...string) *xorm.Session
Query(...any) ([]map[string][]byte, error)
Cols(...string) *xorm.Session
Table(tableNameOrBean any) *xorm.Session
Context(ctx context.Context) *xorm.Session
Ping() error
QueryInterface(sqlOrArgs ...any) ([]map[string]any, error)
IsTableExist(tableNameOrBean any) (bool, error)
}
// Session represents a xorm session interface, used as an abstraction over *xorm.Session.
// Engine represents a xorm engine
type Engine interface {
SQLSession
Sync(...any) error
Ping() error
}
// Session represents a xorm session interface
type Session interface {
Engine
And(query any, args ...any) *xorm.Session
@@ -89,7 +95,6 @@ type EngineMigration interface {
Dialect() dialects.Dialect
DropTables(beans ...any) error
NewSession() *xorm.Session
QueryInterface(sqlOrArgs ...any) ([]map[string]any, error)
SetMapper(mapper names.Mapper)
SyncWithOptions(opts xorm.SyncOptions, beans ...any) (*xorm.SyncResult, error)
TableInfo(bean any) (*schemas.Table, error)
+2 -3
View File
@@ -24,10 +24,9 @@ type Paginator interface {
}
// SetSessionPagination sets pagination for a database session
func SetSessionPagination(sess Engine, p Paginator) Session {
func SetSessionPagination(sess Engine, p Paginator) {
skip, take := p.GetSkipTake()
return sess.Limit(take, skip)
sess.Limit(take, skip)
}
// ListOptions options to paginate results
+14 -4
View File
@@ -23,6 +23,7 @@ import (
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
"xorm.io/builder"
)
@@ -119,7 +120,7 @@ WHEN NOT MATCHED
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
_, err := git.NewIDFromString(sha)
if err != nil {
return 0, git.ErrInvalidSHA{SHA: sha}
return 0, util.NewInvalidArgumentErrorf("invalid sha: %v", err)
}
switch {
@@ -505,13 +506,19 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
opts.CommitStatus.ContextHash = strings.TrimSpace(opts.CommitStatus.ContextHash)
opts.CommitStatus.SHA = opts.SHA.String()
opts.CommitStatus.CreatorID = opts.Creator.ID
opts.CommitStatus.RepoID = opts.Repo.ID
opts.CommitStatus.Index = idx
log.Debug("NewCommitStatus[%s, %s]: %d", opts.Repo.FullName(), opts.SHA, opts.CommitStatus.Index)
opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context)
// Callers may pre-compute a ContextHash to keep entries that share a
// human-readable Context separated (e.g. two workflow files with the
// same `name:` — issue #35699). Only derive from Context when unset.
if opts.CommitStatus.ContextHash == "" {
opts.CommitStatus.ContextHash = HashCommitStatusContext(opts.CommitStatus.Context)
}
// Insert new CommitStatus
if err = db.Insert(ctx, opts.CommitStatus); err != nil {
@@ -529,8 +536,11 @@ type SignCommitWithStatuses struct {
*asymkey_model.SignCommit
}
// hashCommitStatusContext hash context
func hashCommitStatusContext(context string) string {
// HashCommitStatusContext returns the sha1 hash used to dedupe commit statuses
// by Context. Callers that need to keep statuses with the same display Context
// separated (e.g. distinct workflow files sharing a `name:`) can mix extra
// disambiguating data into the input.
func HashCommitStatusContext(context string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(context)))
}
+5 -2
View File
@@ -196,7 +196,10 @@ func LFSObjectAccessible(ctx context.Context, user *user_model.User, oid string)
count, err := db.GetEngine(ctx).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
return count > 0, err
}
cond := repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)
// LFS objects are repository code content, so authorization must require
// Code-unit access; other unit accesses (e.g. Issues) must not authorize
// reuse of an existing LFS object across repositories.
cond := repo_model.AccessibleRepositoryCondition(user, unit.TypeCode)
count, err := db.GetEngine(ctx).Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
return count > 0, err
}
@@ -220,7 +223,7 @@ func LFSAutoAssociate(ctx context.Context, metas []*LFSMetaObject, user *user_mo
newMetas := make([]*LFSMetaObject, 0, len(metas))
cond := builder.In(
"`lfs_meta_object`.repository_id",
builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)),
builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeCode)),
)
if err := db.GetEngine(ctx).Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil {
return err
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gituser
import (
"context"
"gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
)
// AvatarStackData is the view-model for the AvatarStack render helpers. Participants[0] is
// the primary participant (commit author), painted on top; the rest follow.
type AvatarStackData struct {
Participants []*CommitParticipant
SearchByEmailLink string
}
func BuildAvatarStackData(ctx context.Context, allParticipants []*git.CommitIdentity, emailUserMap *user.EmailUserMap) *AvatarStackData {
if emailUserMap == nil {
emails := make([]string, len(allParticipants))
for i, sig := range allParticipants {
emails[i] = sig.Email
}
var err error
emailUserMap, err = user.GetUsersByEmails(ctx, emails)
if err != nil {
log.Error("GetUsersByEmails failed: %v", err)
}
}
ret := &AvatarStackData{
Participants: make([]*CommitParticipant, 0, len(allParticipants)),
}
for _, p := range allParticipants {
var giteaUser *user.User
if emailUserMap != nil {
giteaUser = emailUserMap.GetByEmail(p.Email)
}
ret.Participants = append(ret.Participants, &CommitParticipant{GiteaUser: giteaUser, GitIdentity: p})
}
return ret
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gituser
import (
"context"
"net/url"
"gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
)
// CommitParticipant is one participant of a commit (its author or a co-author):
// a git identity, optionally matched to a Gitea user.
type CommitParticipant struct {
GitIdentity *git.CommitIdentity // git identity (name/email), never nil
GiteaUser *user.User // matched Gitea user, nil if unmatched
}
// UserCommit represents a commit with matched of database "author" user.
type UserCommit struct {
GitCommit *git.Commit
AuthorUser *user.User
AvatarStackData *AvatarStackData
}
func RepoCommitSearchByEmailLink(repoLink string, ref git.RefName) string {
if curRefWebLinkPath := ref.RefWebLinkPath(); curRefWebLinkPath != "" {
return repoLink + "/commits/" + curRefWebLinkPath + "/search?q=" + url.QueryEscape("author:") + "{email}"
}
return ""
}
// GetUserCommitsByGitCommits checks if authors' e-mails of commits are corresponding to users.
func GetUserCommitsByGitCommits(ctx context.Context, gitCommits []*git.Commit, repoLink string, currentRef git.RefName) ([]*UserCommit, error) {
userCommits := make([]*UserCommit, 0, len(gitCommits))
emailSet := make(container.Set[string])
for _, c := range gitCommits {
emailSet.Add(c.Author.Email)
emailSet.Add(c.Committer.Email)
for _, p := range c.AllParticipantIdentities() {
emailSet.Add(p.Email)
}
}
emailUserMap, err := user.GetUsersByEmails(ctx, emailSet.Values())
if err != nil {
return nil, err
}
searchByEmailLink := RepoCommitSearchByEmailLink(repoLink, currentRef)
for _, c := range gitCommits {
uc := &UserCommit{
AuthorUser: emailUserMap.GetByEmail(c.Author.Email), // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"?
GitCommit: c,
AvatarStackData: BuildAvatarStackData(ctx, c.AllParticipantIdentities(), emailUserMap),
}
uc.AvatarStackData.SearchByEmailLink = searchByEmailLink
userCommits = append(userCommits, uc)
}
return userCommits, nil
}
+4 -6
View File
@@ -64,8 +64,8 @@ func GetAssigneeIDsByIssue(ctx context.Context, issueID int64) ([]int64, error)
}
// IsUserAssignedToIssue returns true when the user is assigned to the issue
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) {
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, userID int64) (isAssigned bool, err error) {
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": userID, "issue_id": issue.ID})
}
type AssignedIssuesOptions struct {
@@ -170,7 +170,7 @@ func toggleUserAssignee(ctx context.Context, issue *Issue, assigneeID int64) (re
}
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multipleAssignees []string) ([]int64, error) {
var requestAssignees []string
// Keeping the old assigning method for compatibility reasons
@@ -184,7 +184,5 @@ func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multi
}
// Get the IDs of all assignees
assigneeIDs, err = user_model.GetUserIDsByNames(ctx, requestAssignees, false)
return assigneeIDs, err
return user_model.GetUserIDsByNames(ctx, requestAssignees, false)
}
+3 -3
View File
@@ -40,7 +40,7 @@ func TestUpdateAssignee(t *testing.T) {
assert.NoError(t, err)
// Check if he got removed
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1)
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1.ID)
assert.NoError(t, err)
assert.False(t, isAssigned)
@@ -56,12 +56,12 @@ func TestUpdateAssignee(t *testing.T) {
}
// Check if the user is assigned
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, user2)
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, user2.ID)
assert.NoError(t, err)
assert.True(t, isAssigned)
// This user should not be assigned
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, &user_model.User{ID: 4})
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, 4)
assert.NoError(t, err)
assert.False(t, isAssigned)
}
+9 -1
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"strings"
"time"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
@@ -413,7 +414,7 @@ func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer)
}
// GetGitHeadRefName returns git ref for hidden pull request branch
func (pr *PullRequest) GetGitHeadRefName() string {
func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefName but not string
return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)
}
@@ -860,6 +861,11 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul
return rules, warnings
}
// codeOwnerMatchTimeout bounds a single pattern match so a crafted pattern
// cannot stall via catastrophic backtracking. See also the aggregate budget
// enforced by the caller across the whole rules×files match loop.
const codeOwnerMatchTimeout = 150 * time.Millisecond
type CodeOwnerRule struct {
Rule *regexp2.Regexp // it supports negative lookahead, does better for end users
Negative bool
@@ -888,6 +894,8 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
return nil, warnings
}
// Bound matching time so user-supplied patterns cannot stall PR creation via catastrophic backtracking.
rule.Rule.MatchTimeout = codeOwnerMatchTimeout
for _, user := range tokens[1:] {
user = strings.TrimPrefix(user, "@")
+1 -1
View File
@@ -181,7 +181,7 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
findSession := listPullRequestStatement(ctx, baseRepoID, opts)
applySorts(findSession, opts.SortType, 0)
findSession = db.SetSessionPagination(findSession, opts)
db.SetSessionPagination(findSession, opts)
prs := make([]*PullRequest, 0, opts.PageSize)
found := findSession.Find(&prs)
return prs, maxResults, found
+19
View File
@@ -4,7 +4,9 @@
package issues_test
import (
"strings"
"testing"
"time"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
@@ -39,6 +41,7 @@ func TestPullRequest(t *testing.T) {
t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects)
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
t.Run("CodeOwnerPatternMatchTimeout", testCodeOwnerPatternMatchTimeout)
t.Run("GetApprovers", testGetApprovers)
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
@@ -376,6 +379,22 @@ func testCodeOwnerAbsolutePathPatterns(t *testing.T) {
}
}
// testCodeOwnerPatternMatchTimeout ensures user-supplied CODEOWNERS patterns
// cannot stall pull request processing through catastrophic regex backtracking:
// each compiled rule must enforce a bounded match time.
func testCodeOwnerPatternMatchTimeout(t *testing.T) {
rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), "(a+)+ @user5\n")
require.Len(t, rules, 1)
maliciousInput := strings.Repeat("a", 30) + "X"
start := time.Now()
_, err := rules[0].Rule.MatchString(maliciousInput)
elapsed := time.Since(start)
require.Error(t, err, "expected MatchTimeout error on pathological input")
assert.Less(t, elapsed, time.Second, "match timeout did not bound regex evaluation; took %s", elapsed)
}
func testGetApprovers(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5})
// Official reviews are already deduplicated. Allow unofficial reviews
+5 -1
View File
@@ -412,7 +412,11 @@ func prepareMigrationTasks() []*migration {
newMigration(332, "Add last_sync_unix to mirror", v1_27.AddLastSyncUnixToMirror),
newMigration(333, "Add bypass allowlist to branch protection", v1_27.AddBranchProtectionBypassAllowlist),
newMigration(334, "Add cancelling support to action runners", v1_27.AddCancellingSupportToActionRunner),
newMigration(335, "Add support for matrix actions evaluation", v1_27.AddMatrixEvaluationColumnsToActionRunJob),
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, "Add job max-parallel support", v1_27.AddJobMaxParallel),
newMigration(339, "Add support for matrix actions evaluation", v1_27.AddMatrixEvaluationColumnsToActionRunJob),
}
return preparedMigrations
}
+19 -10
View File
@@ -9,16 +9,25 @@ import (
"xorm.io/xorm"
)
// AddMatrixEvaluationColumnsToActionRunJob adds RawStrategy and IsMatrixEvaluated columns
// to support deferred matrix expansion for jobs whose matrix depends on other jobs' outputs.
func AddMatrixEvaluationColumnsToActionRunJob(x db.EngineMigration) error {
// AddReusableWorkflowFieldsToActionRunJob adds the ActionRunJob columns that describe the reusable workflow caller hierarchy,
// and the ActionRunAttemptJobIDIndex table backing run-wide AttemptJobID allocation.
func AddReusableWorkflowFieldsToActionRunJob(x db.EngineMigration) error {
type ActionRunJob struct {
RawStrategy string `xorm:"TEXT"`
IsMatrixEvaluated bool
WorkflowSourceRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowSourceCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsReusableCaller bool `xorm:"index NOT NULL DEFAULT FALSE"`
ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
CallUses string `xorm:"VARCHAR(512) NOT NULL DEFAULT ''"`
CallSecrets string `xorm:"LONGTEXT"`
CallPayload string `xorm:"LONGTEXT"`
IsExpanded bool `xorm:"NOT NULL DEFAULT FALSE"`
ReusableWorkflowContent []byte `xorm:"LONGBLOB"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRunJob))
return err
type ActionRunAttemptJobIDIndex db.ResourceIndex
if _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunJob)); err != nil {
return err
}
return x.Sync(new(ActionRunAttemptJobIDIndex))
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
)
func AddActionRunJobSummaryTable(x db.EngineMigration) error {
type ActionRunJobSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(summary_key)"`
RunID int64 `xorm:"UNIQUE(summary_key)"`
RunAttemptID int64 `xorm:"UNIQUE(summary_key) NOT NULL DEFAULT 0"`
JobID int64 `xorm:"UNIQUE(summary_key)"`
StepIndex int64 `xorm:"UNIQUE(summary_key)"`
Content string `xorm:"LONGTEXT"`
ContentType string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'text/markdown'"`
ContentSize int64 `xorm:"NOT NULL DEFAULT 0"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(ActionRunJobSummary))
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
type VisibleType int
type teamWithVisibility struct {
Visibility VisibleType `xorm:"NOT NULL DEFAULT 2"`
}
func (teamWithVisibility) TableName() string {
return "team"
}
func AddVisibilityToTeam(x db.EngineMigration) error {
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(teamWithVisibility)); err != nil {
return err
}
// Owner teams must remain listable to all org members; new orgs create
// them as "limited", so make existing owner teams limited too.
// Filter on authorize=4 (AccessModeOwner) so a user-created team that
// happens to share the name "owners" is not accidentally affected.
_, err := x.Exec("UPDATE `team` SET visibility = ? WHERE lower_name = ? AND authorize = ?", 1, "owners", 4)
return err
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
// AddJobMaxParallel adds the MaxParallel column to ActionRunJob to support
// limiting how many matrix jobs from the same job definition run concurrently.
func AddJobMaxParallel(x db.EngineMigration) error {
type ActionRunJob struct {
MaxParallel int `xorm:"NOT NULL DEFAULT 0"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreDropIndices: true,
}, new(ActionRunJob))
return err
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
// AddMatrixEvaluationColumnsToActionRunJob adds RawStrategy and IsMatrixEvaluated columns
// to support deferred matrix expansion for jobs whose matrix depends on other jobs' outputs.
func AddMatrixEvaluationColumnsToActionRunJob(x db.EngineMigration) error {
type ActionRunJob struct {
RawStrategy string `xorm:"TEXT"`
IsMatrixEvaluated bool
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRunJob))
return err
}
+36
View File
@@ -183,12 +183,42 @@ type FindOrgMembersOpts struct {
Doer *user_model.User
IsDoerMember bool
OrgID int64
Keyword string
}
func (opts FindOrgMembersOpts) PublicOnly() bool {
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
}
// applyKeywordFilter adds keyword search conditions to session
func (opts FindOrgMembersOpts) applyKeywordFilter(sess db.Session) bool {
if opts.Keyword == "" {
return false
}
keywordCond := builder.Or(
db.BuildCaseInsensitiveLike("`user`.lower_name", opts.Keyword),
db.BuildCaseInsensitiveLike("`user`.full_name", opts.Keyword),
)
emailCond := db.BuildCaseInsensitiveLike("`user`.email", opts.Keyword)
switch {
case opts.Doer == nil:
emailCond = emailCond.And(builder.Eq{"`user`.keep_email_private": false})
case !opts.Doer.IsAdmin:
emailCond = emailCond.And(
builder.Or(
builder.Eq{"`user`.keep_email_private": false},
builder.Eq{"`user`.id": opts.Doer.ID},
),
)
}
keywordCond = keywordCond.Or(emailCond)
_ = sess.Join("INNER", "`user`", "org_user.uid = `user`.id").And(keywordCond)
return true
}
// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess db.Session) {
if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted {
@@ -212,6 +242,7 @@ func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, erro
} else {
opts.applyTeamMatesOnlyFilter(sess)
}
_ = opts.applyKeywordFilter(sess)
return sess.Count(new(OrgUser))
}
@@ -339,6 +370,7 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
NumMembers: 1,
IncludesAllRepositories: true,
CanCreateOrgRepo: true,
Visibility: structs.VisibleTypeLimited,
}
if err = db.Insert(ctx, t); err != nil {
return fmt.Errorf("insert owner team: %w", err)
@@ -460,7 +492,11 @@ func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUs
} else {
opts.applyTeamMatesOnlyFilter(sess)
}
if opts.applyKeywordFilter(sess) {
sess = sess.Select("org_user.*")
}
sess = sess.OrderBy("org_user.uid ASC")
if opts.ListOptions.PageSize > 0 {
db.SetSessionPagination(sess, opts)
+70
View File
@@ -288,6 +288,76 @@ func TestGetOrgUsersByOrgID(t *testing.T) {
assert.Empty(t, orgUsers)
}
func TestOrgMembersSearch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
testCases := []struct {
name string
opts *organization.FindOrgMembersOpts
expectedUIDs []int64
}{
{
name: "match by username",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user4",
},
expectedUIDs: []int64{4},
},
{
name: "match by full name",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user27",
},
expectedUIDs: []int64{28},
},
{
name: "private email hidden",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user2@example.com",
},
expectedUIDs: []int64{},
},
{
name: "admin can search private email",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: admin,
Keyword: "user2@example.com",
},
expectedUIDs: []int64{2},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
count, err := organization.CountOrgMembers(t.Context(), tc.opts)
assert.NoError(t, err)
assert.EqualValues(t, len(tc.expectedUIDs), count)
members, err := organization.GetOrgUsersByOrgID(t.Context(), tc.opts)
assert.NoError(t, err)
memberUIDs := make([]int64, 0, len(members))
for _, member := range members {
memberUIDs = append(memberUIDs, member.UID)
}
slices.Sort(memberUIDs)
assert.Equal(t, tc.expectedUIDs, memberUIDs)
})
}
}
func TestChangeOrgUserStatus(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+31 -3
View File
@@ -14,6 +14,7 @@ import (
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"xorm.io/builder"
@@ -81,9 +82,36 @@ type Team struct {
Members []*user_model.User `xorm:"-"`
NumRepos int
NumMembers int
Units []*TeamUnit `xorm:"-"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
Units []*TeamUnit `xorm:"-"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 2"`
}
func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() }
func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() }
func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() }
// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read
// the team's metadata, based on the team's visibility tier and the parent org's
// visibility. Privileged callers (site admins, org owners, team members) are
// decided by the caller before reaching here.
func (t *Team) CanNonMemberReadMeta(ctx context.Context, org, doer *user_model.User) (bool, error) {
switch t.Visibility {
case structs.VisibleTypePublic:
return HasOrgOrUserVisible(ctx, org, doer), nil
case structs.VisibleTypeLimited:
return IsOrganizationMember(ctx, t.OrgID, doer.ID)
default:
return false, nil
}
}
func NormalizeTeamVisibility(s string) structs.VisibleType {
if vt, ok := structs.VisibilityModes[s]; ok {
return vt
}
return structs.VisibleTypePrivate
}
func init() {
+53 -8
View File
@@ -10,6 +10,8 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/structs"
"xorm.io/builder"
)
@@ -50,9 +52,15 @@ type SearchTeamOptions struct {
Keyword string
OrgID int64
IncludeDesc bool
// IncludeVisibilities, when combined with UserID, also returns teams whose
// visibility is in this list, even if UserID is not a member. Typical values:
// - {limited,public} for org members
// - {public} for signed-in users who are not org members
// Leave empty to return only teams the user is a member of.
IncludeVisibilities []structs.VisibleType
}
func (opts *SearchTeamOptions) toCond() builder.Cond {
func (opts *SearchTeamOptions) applyToSession(sess db.SQLSession) {
cond := builder.NewCond()
if len(opts.Keyword) > 0 {
@@ -68,11 +76,51 @@ func (opts *SearchTeamOptions) toCond() builder.Cond {
cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
}
if opts.UserID > 0 {
switch {
case opts.UserID > 0 && len(opts.IncludeVisibilities) > 0:
sess = sess.Join("LEFT", "team_user", "team_user.team_id = team.id AND team_user.uid = ?", opts.UserID)
cond = cond.And(builder.Or(
builder.Eq{"team_user.uid": opts.UserID},
builder.In("`team`.visibility", opts.IncludeVisibilities),
))
case opts.UserID > 0:
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
case len(opts.IncludeVisibilities) > 0:
cond = cond.And(builder.In("`team`.visibility", opts.IncludeVisibilities))
}
sess.Where(cond)
}
return cond
func VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn bool) []structs.VisibleType {
switch {
case isOrgMember:
return []structs.VisibleType{structs.VisibleTypeLimited, structs.VisibleTypePublic}
case isSignedIn:
return []structs.VisibleType{structs.VisibleTypePublic}
default:
return nil
}
}
func ApplyTeamListFilter(ctx context.Context, orgID int64, viewer *user_model.User, isSignedIn bool, opts *SearchTeamOptions) error {
if viewer.IsAdmin {
return nil
}
isOwner, err := IsOrganizationOwner(ctx, orgID, viewer.ID)
if err != nil {
return err
}
if isOwner {
return nil
}
isOrgMember, err := IsOrganizationMember(ctx, orgID, viewer.ID)
if err != nil {
return err
}
opts.UserID = viewer.ID
opts.IncludeVisibilities = VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn)
return nil
}
// SearchTeam search for teams. Caller is responsible to check permissions.
@@ -80,15 +128,12 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64,
sess := db.GetEngine(ctx)
opts.SetDefaultValues()
cond := opts.toCond()
opts.applyToSession(sess)
if opts.UserID > 0 {
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
}
db.SetSessionPagination(sess, opts)
teams := make([]*Team, 0, opts.PageSize)
count, err := sess.Where(cond).OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams)
count, err := sess.OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams)
if err != nil {
return nil, 0, err
}
+85
View File
@@ -10,6 +10,8 @@ import (
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/structs"
"github.com/stretchr/testify/assert"
)
@@ -38,6 +40,43 @@ func TestTeam_IsMember(t *testing.T) {
assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID))
}
func TestTeam_CanNonMemberReadMeta(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // public org
org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 35}) // private org
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // member of org 3 and org 35
outsider := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // member of neither org
test := func(name string, team *organization.Team, org, doer *user_model.User, expected bool) {
t.Run(name, func(t *testing.T) {
ok, err := team.CanNonMemberReadMeta(t.Context(), org, doer)
assert.NoError(t, err)
assert.Equal(t, expected, ok)
})
}
// Public team is gated only by the parent org's visibility.
publicTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePublic}
test("public team, public org, member", publicTeam, org3, member, true)
test("public team, public org, outsider", publicTeam, org3, outsider, true)
// Public team inside a private org: only org members may see it.
publicTeamPrivOrg := &organization.Team{OrgID: 35, Visibility: structs.VisibleTypePublic}
test("public team, private org, org member", publicTeamPrivOrg, org35, member, true)
test("public team, private org, outsider", publicTeamPrivOrg, org35, outsider, false)
// Limited team: any org member, but never outsiders.
limitedTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypeLimited}
test("limited team, org member", limitedTeam, org3, member, true)
test("limited team, outsider", limitedTeam, org3, outsider, false)
// Private team is never visible to non-members; members/owners are admitted by the caller.
privateTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePrivate}
test("private team, org member", privateTeam, org3, member, false)
test("private team, outsider", privateTeam, org3, outsider, false)
}
func TestTeam_GetRepositories(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
@@ -172,6 +211,52 @@ func TestGetUserOrgTeams(t *testing.T) {
test(3, unittest.NonexistentID)
}
func TestSearchTeamIncludeVisible(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const orgID int64 = 3
// User 5 is an org member but only belongs to team 1 (Owners) — make sure
// they don't see team 2 (default private) but do see a freshly added
// limited team they are not a member of.
visible := &organization.Team{
OrgID: orgID,
LowerName: "visible-team",
Name: "visible-team",
AccessMode: 1, // read
Visibility: structs.VisibleTypeLimited,
}
assert.NoError(t, db.Insert(t.Context(), visible))
teams, _, err := organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{
OrgID: orgID,
UserID: 2,
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true),
})
assert.NoError(t, err)
ids := make(map[int64]bool, len(teams))
for _, team := range teams {
assert.Equal(t, orgID, team.OrgID)
ids[team.ID] = true
}
// user 2 is in team 1 and team 2 in org 3, plus should see the new visible team.
assert.True(t, ids[1], "expected to see team 1 (member)")
assert.True(t, ids[2], "expected to see team 2 (member)")
assert.True(t, ids[visible.ID], "expected to see visible team")
// user 5 is only an org member in team 1, must not see secret team 2 but must see the visible one.
teams, _, err = organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{
OrgID: orgID,
UserID: 5,
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true),
})
assert.NoError(t, err)
ids = make(map[int64]bool, len(teams))
for _, team := range teams {
ids[team.ID] = true
}
assert.False(t, ids[2], "user 5 must not see private team 2")
assert.True(t, ids[visible.ID], "user 5 must see the limited team")
}
func TestHasTeamRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+36 -2
View File
@@ -567,9 +567,9 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.
// CanBeAssigned return true if user can be assigned to issue or pull requests in repo
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (bool, error) {
if user.IsOrganization() {
return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
return false, util.NewInvalidArgumentErrorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
}
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
if err != nil {
@@ -655,3 +655,37 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u
func PermissionNoAccess() Permission {
return Permission{AccessMode: perm_model.AccessModeNone}
}
// CanReadWorkflowCrossRepo checks whether the run can read workflow files from targetRepo.
func CanReadWorkflowCrossRepo(ctx context.Context, targetRepo *repo_model.Repository, run *actions_model.ActionRun) (bool, error) {
if err := run.LoadRepo(ctx); err != nil {
return false, err
}
// (1) Same owner: always allowed (fork-PR scrubbing handled inside).
if checkSameOwnerCrossRepoAccess(ctx, run.Repo, targetRepo, run.IsForkPullRequest) {
return true, nil
}
// (2) Cross-owner: respect the target repo's collaborative-owner allowlist on its Actions unit.
// The caller (run.Repo) must itself be private. The collaborative-owner grant is owner-level, so without this
// guard a public caller owned by a grantee could pull a private reusable workflow and expose its definition and
// logs in a publicly visible run; requiring a private caller keeps private content flowing private -> private.
// This is intentionally stricter than GitHub, which gates on the target repo's access setting (introduced in #32562):
// https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-a-private-repository
if run.Repo.IsPrivate {
if actionsUnit, err := targetRepo.GetUnit(ctx, unit.TypeActions); err == nil {
if actionsUnit.ActionsConfig().IsCollaborativeOwner(run.Repo.OwnerID) {
return true, nil
}
}
}
// (3) Public target: the Actions user's individual permission gives read on any public repo, so `uses:` to a public reusable-workflow library is permitted by default.
// Matches GitHub's behavior: public reusable workflows are universally readable.
botPerm, err := GetIndividualUserRepoPermission(ctx, targetRepo, user_model.NewActionsUser())
if err != nil {
return false, err
}
return botPerm.AccessMode >= perm_model.AccessModeRead, nil
}
+2 -6
View File
@@ -23,6 +23,7 @@ import (
"gitea.dev/modules/base"
"gitea.dev/modules/git"
giturl "gitea.dev/modules/git/url"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/markup"
@@ -641,12 +642,7 @@ func (repo *Repository) CanContentChange() bool {
// DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
if err != nil {
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
return template.HTML(markup.SanitizeDescription(repo.Description))
}
return template.HTML(markup.SanitizeDescription(desc))
return markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), htmlutil.EscapeString(repo.Description))
}
// CloneLink represents different types of clone URLs of repository.
+2 -1
View File
@@ -783,7 +783,8 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor
sess = sess.Where(cond).OrderBy(opts.OrderBy.String())
repos := make(RepositoryList, 0, opts.PageSize)
return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos)
db.SetSessionPagination(sess, &opts)
return repos, count, sess.Find(&repos)
}
func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int64) (RepositoryList, error) {
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secret
import (
"testing"
"gitea.dev/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
+58 -6
View File
@@ -11,6 +11,8 @@ import (
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
secret_module "gitea.dev/modules/secret"
"gitea.dev/modules/setting"
@@ -152,16 +154,16 @@ func UpdateSecret(ctx context.Context, secretID int64, data, description string)
}
func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
secrets := map[string]string{}
baseSecrets := map[string]string{}
secrets["GITHUB_TOKEN"] = task.Token
secrets["GITEA_TOKEN"] = task.Token
baseSecrets["GITHUB_TOKEN"] = task.Token
baseSecrets["GITEA_TOKEN"] = task.Token
if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
return secrets, nil
return baseSecrets, nil
}
ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
@@ -181,10 +183,60 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[
log.Error("Unable to decrypt Actions secret %v %q, maybe SECRET_KEY is wrong: %v", secret.ID, secret.Name, err)
continue
}
secrets[secret.Name] = v
baseSecrets[secret.Name] = v
}
return secrets, nil
return getScopedSecretsForJob(ctx, task.Job, baseSecrets)
}
// getScopedSecretsForJob walks up the caller chain (ParentJobID) and applies
// each caller's secrets policy:
// - "secrets: inherit" passes the parent scope's secrets through unchanged.
// - explicit mapping {alias: SOURCE} only forwards the named secrets, plus the auto-generated tokens.
//
// For top-level jobs (ParentJobID == 0) the base secrets are returned as-is.
func getScopedSecretsForJob(ctx context.Context, job *actions_model.ActionRunJob, baseSecrets map[string]string) (map[string]string, error) {
if job.ParentJobID == 0 {
return baseSecrets, nil
}
caller, err := actions_model.GetRunJobByRunAndID(ctx, job.RunID, job.ParentJobID)
if err != nil {
return nil, fmt.Errorf("load caller job %d: %w", job.ParentJobID, err)
}
parentScope, err := getScopedSecretsForJob(ctx, caller, baseSecrets)
if err != nil {
return nil, err
}
if caller.CallSecrets == jobparser.SecretsInherit {
return parentScope, nil
}
// Empty or explicit-mapping path: only auto-tokens + (any) mapped aliases are exposed.
scoped := map[string]string{
"GITHUB_TOKEN": baseSecrets["GITHUB_TOKEN"],
"GITEA_TOKEN": baseSecrets["GITEA_TOKEN"],
}
if caller.CallSecrets == "" {
return scoped, nil
}
var mapping map[string]string
if err := json.Unmarshal([]byte(caller.CallSecrets), &mapping); err != nil {
return nil, fmt.Errorf("decode caller %d secret map: %w", caller.ID, err)
}
for alias, source := range mapping {
if v, ok := parentScope[source]; ok {
scoped[alias] = v
continue
}
// Secret names are case-insensitive in storage (uppercased).
if v, ok := parentScope[strings.ToUpper(source)]; ok {
scoped[alias] = v
}
}
return scoped, nil
}
func CountWrongRepoLevelSecrets(ctx context.Context) (int64, error) {
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secret
import (
"testing"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetScopedSecretsForJob(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
base := map[string]string{
"GITHUB_TOKEN": "tok",
"GITEA_TOKEN": "tok",
"PROD_API_KEY": "prod-secret",
"DEV_API_KEY": "dev-secret",
}
// insertCaller create an ActionRunJob caller row with the given CallSecrets policy
insertCaller := func(t *testing.T, runID, parentJobID int64, callSecrets string) *actions_model.ActionRunJob {
t.Helper()
job := &actions_model.ActionRunJob{
RunID: runID,
RepoID: 1,
IsReusableCaller: true,
ParentJobID: parentJobID,
CallSecrets: callSecrets,
Status: actions_model.StatusBlocked,
}
require.NoError(t, db.Insert(t.Context(), job))
return job
}
t.Run("TopLevelJob_ReturnsBaseUnchanged", func(t *testing.T) {
const runID = 9001
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: 0}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, base, got, "top-level jobs should see the full base scope")
})
t.Run("CallerInherit_PassesParentScopeThrough", func(t *testing.T) {
const runID = 9002
caller := insertCaller(t, runID, 0, "inherit")
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: caller.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, base, got, "secrets: inherit forwards everything from parent scope")
})
t.Run("CallerEmptySecrets_ExposesOnlyAutoTokens", func(t *testing.T) {
const runID = 9003
caller := insertCaller(t, runID, 0, "")
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: caller.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"GITHUB_TOKEN": "tok",
"GITEA_TOKEN": "tok",
}, got)
})
t.Run("CallerMapping_OnlyMappedAliasesPlusTokens", func(t *testing.T) {
const runID = 9004
// {alias: source} - the called workflow sees `secrets.MY_KEY` resolved to PROD_API_KEY's value.
caller := insertCaller(t, runID, 0, `{"MY_KEY":"PROD_API_KEY"}`)
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: caller.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"GITHUB_TOKEN": "tok",
"GITEA_TOKEN": "tok",
"MY_KEY": "prod-secret",
// no "dev-secret"
}, got)
})
t.Run("CallerMapping_CaseInsensitiveSource", func(t *testing.T) {
const runID = 9005
caller := insertCaller(t, runID, 0, `{"alias":"prod_api_key"}`)
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: caller.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, "prod-secret", got["alias"])
})
t.Run("CallerMapping_UnknownSourceDropsAlias", func(t *testing.T) {
const runID = 9006
// alias points at a non-existent secret name, so it must be dropped.
caller := insertCaller(t, runID, 0, `{"MAPPED_ALIAS":"DOES_NOT_EXIST"}`)
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: caller.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
_, present := got["MAPPED_ALIAS"]
assert.False(t, present)
})
t.Run("Nested_InheritThenInherit_FullScope", func(t *testing.T) {
const runID = 9007
outer := insertCaller(t, runID, 0, "inherit")
inner := insertCaller(t, runID, outer.ID, "inherit")
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: inner.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, base, got, "inherit-then-inherit should pass the full base scope through")
})
t.Run("Nested_InheritThenMapping_InnerNarrows", func(t *testing.T) {
const runID = 9008
// inner mapping narrows the full scope it inherited from outer.
outer := insertCaller(t, runID, 0, "inherit")
inner := insertCaller(t, runID, outer.ID, `{"ALIAS_OUT":"PROD_API_KEY"}`)
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: inner.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"GITHUB_TOKEN": "tok",
"GITEA_TOKEN": "tok",
"ALIAS_OUT": "prod-secret",
// no "dev-secret"
}, got)
})
t.Run("Nested_MappingThenInherit_OuterNarrows", func(t *testing.T) {
const runID = 9009
// inner inherits outer's already-narrowed scope, so leaf sees only auto-tokens + OUTER_ALIAS.
outer := insertCaller(t, runID, 0, `{"OUTER_ALIAS":"PROD_API_KEY"}`)
inner := insertCaller(t, runID, outer.ID, "inherit")
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: inner.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"GITHUB_TOKEN": "tok",
"GITEA_TOKEN": "tok",
"OUTER_ALIAS": "prod-secret",
// no "dev-secret"
}, got)
})
t.Run("Nested_MappingThenMapping_InnerSourceMustExistInOuterScope", func(t *testing.T) {
const runID = 9010
// inner can rename ALIAS_A (in outer's scope) to ALIAS_C, but cannot forward DEV_API_KEY, which outer dropped.
outer := insertCaller(t, runID, 0, `{"ALIAS_A":"PROD_API_KEY"}`)
inner := insertCaller(t, runID, outer.ID, `{"ALIAS_B":"DEV_API_KEY","ALIAS_C":"ALIAS_A"}`)
leaf := &actions_model.ActionRunJob{RunID: runID, ParentJobID: inner.ID}
got, err := getScopedSecretsForJob(ctx, leaf, base)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"GITHUB_TOKEN": "tok",
"GITEA_TOKEN": "tok",
"ALIAS_C": "prod-secret",
// no "dev-secret"
}, got)
})
}
+1 -1
View File
@@ -47,7 +47,7 @@ func OrderBy(orderBy string) any {
}
func whereOrderConditions(e db.Engine, conditions []any) db.Engine {
orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic
orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic. FIXME: some tables do not have "id" column
for _, condition := range conditions {
switch cond := condition.(type) {
case *testCond:
+11 -2
View File
@@ -80,8 +80,11 @@ func init() {
}
// GetExternalLogin checks if a externalID in loginSourceID scope already exists
func GetExternalLogin(ctx context.Context, externalLoginUser *ExternalLoginUser) (bool, error) {
return db.GetEngine(ctx).Get(externalLoginUser)
func GetExternalLogin(ctx context.Context, loginSourceID int64, externalID string) (*ExternalLoginUser, bool, error) {
return db.Get[ExternalLoginUser](ctx, builder.Eq{
"external_id": externalID,
"login_source_id": loginSourceID,
})
}
// LinkExternalToUser link the external user to the user
@@ -118,6 +121,12 @@ func RemoveAllAccountLinks(ctx context.Context, user *User) error {
return err
}
// RemoveExternalLoginByExternalID removes a specific external login link by its provider-side identifier.
func RemoveExternalLoginByExternalID(ctx context.Context, loginSourceID int64, externalID string) error {
_, err := db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", externalID, loginSourceID).Delete(new(ExternalLoginUser))
return err
}
// GetUserIDByExternalUserID get user id according to provider and userID
func GetUserIDByExternalUserID(ctx context.Context, provider, userID string) (int64, error) {
var id int64
+2 -36
View File
@@ -1148,14 +1148,7 @@ func GetUsersBySource(ctx context.Context, s *auth.Source) ([]*User, error) {
return users, err
}
// UserCommit represents a commit with validation of user.
type UserCommit struct { //revive:disable-line:exported
User *User
*git.Commit
}
// ValidateCommitWithEmail check if author's e-mail of commit is corresponding to a user.
func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User {
func GetUserByGitAuthor(ctx context.Context, c *git.Commit) *User {
if c.Author == nil {
return nil
}
@@ -1166,33 +1159,6 @@ func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User {
return u
}
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([]*UserCommit, error) {
var (
newCommits = make([]*UserCommit, 0, len(oldCommits))
emailSet = make(container.Set[string])
)
for _, c := range oldCommits {
if c.Author != nil {
emailSet.Add(c.Author.Email)
}
}
emailUserMap, err := GetUsersByEmails(ctx, emailSet.Values())
if err != nil {
return nil, err
}
for _, c := range oldCommits {
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
newCommits = append(newCommits, &UserCommit{
User: user,
Commit: c,
})
}
return newCommits, nil
}
type EmailUserMap struct {
m map[string]*User
}
@@ -1203,7 +1169,7 @@ func (eum *EmailUserMap) GetByEmail(email string) *User {
func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
if len(emails) == 0 {
return nil, nil //nolint:nilnil // return nil when there are no emails to look up
return &EmailUserMap{}, nil
}
needCheckEmails := make(container.Set[string])
+21
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
"go.yaml.in/yaml/v4"
)
@@ -466,6 +467,26 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
}
}
func EvaluateJobIfExpression(jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (bool, error) {
actJob := &model.Job{
Strategy: &model.Strategy{
FailFastString: job.Strategy.FailFastString,
MaxParallelString: job.Strategy.MaxParallelString,
RawMatrix: job.Strategy.RawMatrix,
},
}
evaluator := NewExpressionEvaluator(NewInterpeter(jobID, actJob, nil, toGitContext(gitCtx), results, vars, inputs))
expr, err := rewriteSubExpression(job.If.Value, false)
if err != nil {
return false, err
}
result, err := evaluator.evaluate(expr, exprparser.DefaultStatusCheckSuccess)
if err != nil {
return false, err
}
return exprparser.IsTruthy(result), nil
}
// parseMappingNode parse a mapping node and preserve order.
func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) {
if node.Kind != yaml.MappingNode {
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package jobparser
import (
"errors"
"fmt"
"path"
"regexp"
"strings"
)
// UsesKind enumerates the supported forms of a reusable workflow "uses:" value.
type UsesKind int
const (
// UsesKindLocalSameRepo is "./.gitea/workflows/foo.yml" - a path inside the calling repository.
UsesKindLocalSameRepo UsesKind = iota + 1
// UsesKindLocalCrossRepo is "owner/repo/.gitea/workflows/foo.yml@ref" - a workflow in another repo on the same instance.
UsesKindLocalCrossRepo
)
// UsesRef is the parsed form of a reusable workflow "uses:" value.
type UsesRef struct {
Kind UsesKind
Owner string // empty for UsesKindLocalSameRepo
Repo string // empty for UsesKindLocalSameRepo
Path string // workflow file path inside the source repo
Ref string // git ref; empty for UsesKindLocalSameRepo
}
var (
reLocalSameRepo = regexp.MustCompile(`^\./\.(gitea|github)/workflows/([^@]+\.ya?ml)$`)
reLocalCrossRepo = regexp.MustCompile(`^([-.\w]+)/([-.\w]+)/\.(gitea|github)/workflows/([^@]+\.ya?ml)@(.+)$`)
)
// ParseUses parses a reusable workflow "uses:" value.
// Only two forms are supported:
// - "./.gitea/workflows/foo.yml" (UsesKindLocalSameRepo, no @ref)
// - "OWNER/REPO/.gitea/workflows/foo.yml@REF" (UsesKindLocalCrossRepo)
func ParseUses(s string) (*UsesRef, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, errors.New("empty uses value")
}
if strings.HasPrefix(s, "./") {
m := reLocalSameRepo.FindStringSubmatch(s)
if m == nil {
return nil, fmt.Errorf(`invalid local "uses:" %q (expect ./.gitea/workflows/<file>.yml)`, s)
}
p := fmt.Sprintf(".%s/workflows/%s", m[1], m[2])
if path.Clean(p) != p {
return nil, fmt.Errorf("invalid workflow path %q", s)
}
return &UsesRef{Kind: UsesKindLocalSameRepo, Path: p}, nil
}
m := reLocalCrossRepo.FindStringSubmatch(s)
if m == nil {
return nil, fmt.Errorf(`invalid cross-repo "uses:" %q (expect owner/repo/.gitea/workflows/<file>.yml@ref)`, s)
}
p := fmt.Sprintf(".%s/workflows/%s", m[3], m[4])
if path.Clean(p) != p {
return nil, fmt.Errorf("invalid workflow path %q", s)
}
return &UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: m[1],
Repo: m[2],
Path: p,
Ref: m[5],
}, nil
}
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package jobparser
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseUses(t *testing.T) {
t.Run("LocalSameRepo", func(t *testing.T) {
cases := []struct {
name string
in string
want UsesRef
}{
{
name: "gitea dir, .yml",
in: "./.gitea/workflows/build.yml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/build.yml"},
},
{
name: "github dir, .yml",
in: "./.github/workflows/build.yml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".github/workflows/build.yml"},
},
{
name: "gitea dir, .yaml",
in: "./.gitea/workflows/build.yaml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/build.yaml"},
},
{
name: "filename containing dots is allowed",
in: "./.gitea/workflows/foo..bar.yml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/foo..bar.yml"},
},
{
name: "nested subdirectory",
in: "./.gitea/workflows/sub/build.yml",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/sub/build.yml"},
},
{
name: "leading/trailing whitespace is trimmed",
in: " ./.gitea/workflows/build.yml ",
want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/build.yml"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := ParseUses(c.in)
require.NoError(t, err)
assert.Equal(t, c.want, *got)
})
}
})
t.Run("LocalCrossRepo", func(t *testing.T) {
cases := []struct {
name string
in string
want UsesRef
}{
{
name: "gitea dir, simple ref",
in: "owner/repo/.gitea/workflows/build.yml@v1",
want: UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: "owner",
Repo: "repo",
Path: ".gitea/workflows/build.yml",
Ref: "v1",
},
},
{
name: "github dir, branch ref",
in: "owner/repo/.github/workflows/build.yml@main",
want: UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: "owner",
Repo: "repo",
Path: ".github/workflows/build.yml",
Ref: "main",
},
},
{
name: ".yaml extension",
in: "owner/repo/.gitea/workflows/build.yaml@abc123",
want: UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: "owner",
Repo: "repo",
Path: ".gitea/workflows/build.yaml",
Ref: "abc123",
},
},
{
name: "ref with slashes (refs/heads/feature)",
in: "owner/repo/.gitea/workflows/build.yml@refs/heads/feature",
want: UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: "owner",
Repo: "repo",
Path: ".gitea/workflows/build.yml",
Ref: "refs/heads/feature",
},
},
{
name: "nested subdirectory under workflows",
in: "owner/repo/.gitea/workflows/sub/build.yml@v1",
want: UsesRef{
Kind: UsesKindLocalCrossRepo,
Owner: "owner",
Repo: "repo",
Path: ".gitea/workflows/sub/build.yml",
Ref: "v1",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := ParseUses(c.in)
require.NoError(t, err)
assert.Equal(t, c.want, *got)
})
}
})
t.Run("Errors", func(t *testing.T) {
cases := []struct {
name string
in string
}{
{name: "empty string", in: ""},
{name: "whitespace only", in: " "},
// Same-repo malformed
{name: "same-repo with @ref", in: "./.gitea/workflows/build.yml@v1"},
{name: "same-repo wrong directory", in: "./not-workflows/build.yml"},
{name: "same-repo wrong extension", in: "./.gitea/workflows/build.txt"},
{name: "same-repo missing extension", in: "./.gitea/workflows/build"},
{name: "same-repo absolute path", in: "/.gitea/workflows/build.yml"},
{name: "same-repo path traversal", in: "./.gitea/workflows/../escape.yml"},
{name: "same-repo double slash", in: "./.gitea/workflows//build.yml"},
{name: "same-repo redundant ./", in: "./.gitea/workflows/./build.yml"},
{name: "same-repo no filename", in: "./.gitea/workflows/.yml"},
// Cross-repo malformed
{name: "cross-repo missing @ref", in: "owner/repo/.gitea/workflows/build.yml"},
{name: "cross-repo empty ref", in: "owner/repo/.gitea/workflows/build.yml@"},
{name: "cross-repo missing owner", in: "/repo/.gitea/workflows/build.yml@v1"},
{name: "cross-repo missing repo", in: "owner//.gitea/workflows/build.yml@v1"},
{name: "cross-repo wrong workflows dir", in: "owner/repo/workflows/build.yml@v1"},
{name: "cross-repo wrong extension", in: "owner/repo/.gitea/workflows/build.txt@v1"},
{name: "cross-repo path traversal", in: "owner/repo/.gitea/workflows/../escape.yml@v1"},
{name: "cross-repo double slash in path", in: "owner/repo/.gitea/workflows//build.yml@v1"},
// owner/repo with chars Gitea's name validators reject
{name: "cross-repo owner with space", in: "bad owner/repo/.gitea/workflows/build.yml@v1"},
{name: "cross-repo repo with @", in: "owner/re@po/.gitea/workflows/build.yml@v1"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, err := ParseUses(c.in)
assert.Error(t, err)
})
}
})
}
+401
View File
@@ -0,0 +1,401 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package jobparser
import (
"errors"
"fmt"
"regexp"
"strings"
"gitea.dev/modules/container"
"gitea.dev/modules/util"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
"go.yaml.in/yaml/v4"
)
// InputType enumerates the allowed types for a workflow_call input.
type InputType string
const (
InputTypeString InputType = "string"
InputTypeBoolean InputType = "boolean"
InputTypeNumber InputType = "number"
)
// InputSpec describes a single workflow_call input declaration.
type InputSpec struct {
Description string `yaml:"description"`
Required bool `yaml:"required"`
Default yaml.Node `yaml:"default"`
Type InputType `yaml:"type"`
}
// SecretSpec describes a single workflow_call secret declaration.
type SecretSpec struct {
Description string `yaml:"description"`
Required bool `yaml:"required"`
}
// OutputSpec describes a single workflow_call output declaration.
type OutputSpec struct {
Description string `yaml:"description"`
Value string `yaml:"value"`
}
// WorkflowCallSpec is the parsed "on.workflow_call" schema of a called workflow.
type WorkflowCallSpec struct {
Inputs map[string]InputSpec
Secrets map[string]SecretSpec
Outputs map[string]OutputSpec
}
// JobOutputs is the per-job-id outputs map used for evaluating workflow_call outputs.
type JobOutputs map[string]map[string]string
// ParseWorkflowCallSpec extracts on.workflow_call.{inputs,secrets,outputs} from a workflow YAML.
// Returns an error if the workflow does not declare on.workflow_call at all.
func ParseWorkflowCallSpec(content []byte) (*WorkflowCallSpec, error) {
var doc struct {
On yaml.Node `yaml:"on"`
}
if err := yaml.Unmarshal(content, &doc); err != nil {
return nil, fmt.Errorf("parse workflow yaml: %w", err)
}
wcNode, ok := findWorkflowCallNode(&doc.On)
if !ok {
return nil, errors.New("workflow does not declare on.workflow_call")
}
spec := &WorkflowCallSpec{
Inputs: map[string]InputSpec{},
Secrets: map[string]SecretSpec{},
Outputs: map[string]OutputSpec{},
}
if wcNode == nil || wcNode.Kind != yaml.MappingNode {
return spec, nil
}
for i := 0; i+1 < len(wcNode.Content); i += 2 {
key := wcNode.Content[i]
val := wcNode.Content[i+1]
switch key.Value {
case "inputs":
if err := decodeWorkflowCallMapping(val, spec.Inputs); err != nil {
return nil, fmt.Errorf("parse workflow_call.inputs: %w", err)
}
case "secrets":
if err := decodeWorkflowCallMapping(val, spec.Secrets); err != nil {
return nil, fmt.Errorf("parse workflow_call.secrets: %w", err)
}
case "outputs":
if err := decodeWorkflowCallMapping(val, spec.Outputs); err != nil {
return nil, fmt.Errorf("parse workflow_call.outputs: %w", err)
}
}
}
for name, in := range spec.Inputs {
if in.Type == "" {
return nil, fmt.Errorf("workflow_call input %q is missing required field \"type\"", name)
}
switch in.Type {
case InputTypeString, InputTypeBoolean, InputTypeNumber:
default:
return nil, fmt.Errorf("workflow_call input %q has unsupported type %q", name, in.Type)
}
}
return spec, nil
}
// findWorkflowCallNode walks the "on:" node and returns the value mapping (or nil) for "workflow_call".
// "ok" is true when the workflow declares workflow_call (even with an empty body).
func findWorkflowCallNode(on *yaml.Node) (val *yaml.Node, ok bool) {
if on == nil || on.Kind == 0 {
return nil, false
}
switch on.Kind {
case yaml.ScalarNode:
return nil, on.Value == "workflow_call"
case yaml.SequenceNode:
for _, item := range on.Content {
if item.Kind == yaml.ScalarNode && item.Value == "workflow_call" {
return nil, true
}
}
return nil, false
case yaml.MappingNode:
for i := 0; i+1 < len(on.Content); i += 2 {
k := on.Content[i]
v := on.Content[i+1]
if k.Value != "workflow_call" {
continue
}
if v.Kind == yaml.MappingNode {
return v, true
}
return nil, true
}
}
return nil, false
}
func decodeWorkflowCallMapping[T any](node *yaml.Node, dst map[string]T) error {
if node == nil || node.Kind != yaml.MappingNode {
return nil
}
for i := 0; i+1 < len(node.Content); i += 2 {
name := node.Content[i].Value
var v T
if err := node.Content[i+1].Decode(&v); err != nil {
return fmt.Errorf("%q: %w", name, err)
}
dst[name] = v
}
return nil
}
// EvaluateCallerWith evaluates the caller-side expressions in `job.With` against the provided contexts
func EvaluateCallerWith(
jobID string,
job *Job,
gitCtx map[string]any,
results map[string]*JobResult,
vars map[string]string,
inputs map[string]any,
) (map[string]any, error) {
actJob := &model.Job{Strategy: &model.Strategy{
FailFastString: job.Strategy.FailFastString,
MaxParallelString: job.Strategy.MaxParallelString,
RawMatrix: job.Strategy.RawMatrix,
}}
var matrix map[string]any
matrixes, err := actJob.GetMatrixes()
if err != nil {
return nil, fmt.Errorf("get caller %q matrix: %w", jobID, err)
}
if len(matrixes) > 0 {
matrix = matrixes[0]
}
evaluator := NewExpressionEvaluator(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs))
out := make(map[string]any, len(job.With))
for k, raw := range job.With {
var evaluated any
switch v := raw.(type) {
case string:
node := yaml.Node{}
if err := node.Encode(v); err != nil {
return nil, fmt.Errorf("encode caller %q with[%q]: %w", jobID, k, err)
}
if err := evaluator.EvaluateYamlNode(&node); err != nil {
return nil, fmt.Errorf("evaluate caller %q with[%q]: %w", jobID, k, err)
}
if err := node.Decode(&evaluated); err != nil {
return nil, fmt.Errorf("decode caller %q with[%q]: %w", jobID, k, err)
}
default:
evaluated = v
}
out[k] = evaluated
}
return out, nil
}
// MatchCallerInputsAgainstSpec checks the caller's already-evaluated `with:` values against the callee's declared `on.workflow_call.inputs` schema
func MatchCallerInputsAgainstSpec(spec *WorkflowCallSpec, evaluated map[string]any) (map[string]any, error) {
resolved := make(map[string]any, len(spec.Inputs))
// fill defaults first
for name, in := range spec.Inputs {
if in.Default.IsZero() {
continue
}
var defaultVal any
if err := in.Default.Decode(&defaultVal); err != nil {
return nil, fmt.Errorf("decode workflow_call input %q default: %w", name, err)
}
v, err := parseWorkflowCallInput(name, in.Type, defaultVal)
if err != nil {
return nil, err
}
resolved[name] = v
}
for k, raw := range evaluated {
inputSpec, ok := spec.Inputs[k]
if !ok {
// ignore unknown "with:" keys
continue
}
converted, err := parseWorkflowCallInput(k, inputSpec.Type, raw)
if err != nil {
return nil, err
}
resolved[k] = converted
}
for name, in := range spec.Inputs {
if !in.Required {
continue
}
// resolved[name] is set when caller provided it OR when spec has a non-zero default - both satisfy "required".
if _, ok := resolved[name]; ok {
continue
}
return nil, fmt.Errorf("workflow_call input %q is required", name)
}
return resolved, nil
}
func parseWorkflowCallInput(name string, typ InputType, v any) (any, error) {
switch typ {
case InputTypeString:
return toString(v), nil
case InputTypeBoolean:
// strict type matching: a boolean input only accepts a native bool, not a "true"/"false" string
if b, ok := v.(bool); ok {
return b, nil
}
return false, fmt.Errorf("workflow_call input %q expects boolean", name)
case InputTypeNumber:
// strict type matching: a number input rejects "123"/"3.14" strings.
if _, isString := v.(string); isString {
return 0.0, fmt.Errorf("workflow_call input %q expects number", name)
}
return util.ToFloat64(v)
default:
return nil, fmt.Errorf("workflow_call input %q has unsupported type %q", name, typ)
}
}
// SecretsInherit is the literal keyword used in a caller's `secrets: inherit` directive
const SecretsInherit = "inherit"
// callerSecretValueRegexp matches the `${{ secrets.NAME }}` form expected for each value in a caller's `secrets:` mapping.
var callerSecretValueRegexp = regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
// ParseCallerSecrets decodes a caller's "secrets:" YAML node into one of two forms:
// - inherit == true: the caller wrote `secrets: inherit`; mapping is nil
// - inherit == false, mapping == {alias: source_name}: explicit mapping. Each value must be of the form `${{ secrets.NAME }}`.
//
// Both alias and source name are upper-cased: secret names are case-insensitive (matching GitHub),
// and Gitea stores secrets upper-cased, so this keeps lookups and schema validation consistent.
func ParseCallerSecrets(node yaml.Node) (inherit bool, mapping map[string]string, err error) {
if node.IsZero() {
return false, nil, nil
}
if node.Kind == yaml.ScalarNode && strings.TrimSpace(node.Value) == SecretsInherit {
return true, nil, nil
}
if node.Kind != yaml.MappingNode {
return false, nil, errors.New("invalid secrets: section, expected mapping or 'inherit'")
}
out := make(map[string]string, len(node.Content)/2)
for i := 0; i+1 < len(node.Content); i += 2 {
k := node.Content[i]
v := node.Content[i+1]
var sv string
if err := v.Decode(&sv); err != nil {
return false, nil, fmt.Errorf("decode secret %q: %w", k.Value, err)
}
matches := callerSecretValueRegexp.FindStringSubmatch(sv)
if len(matches) != 2 {
return false, nil, fmt.Errorf("caller secret %q value must be of the form ${{ secrets.NAME }}", k.Value)
}
out[strings.ToUpper(k.Value)] = strings.ToUpper(matches[1])
}
return false, out, nil
}
// ValidateCallerSecrets checks a caller's parsed explicit-mapping `secrets:` against the called workflow's declared `on.workflow_call.secrets` schema.
func ValidateCallerSecrets(spec *WorkflowCallSpec, mapping map[string]string) error {
if spec == nil {
return errors.New("ValidateCallerSecrets: nil workflow_call spec")
}
// Secret names are case-insensitive, so compare declared names and caller aliases upper-cased.
declaredNames := make(container.Set[string], len(spec.Secrets))
for name := range spec.Secrets {
declaredNames.Add(strings.ToUpper(name))
}
provided := make(container.Set[string], len(mapping))
for alias := range mapping {
up := strings.ToUpper(alias)
provided.Add(up)
if !declaredNames.Contains(up) {
return fmt.Errorf("caller secret %q is not declared in the called workflow's on.workflow_call.secrets", alias)
}
}
for name, sec := range spec.Secrets {
if sec.Required && !provided.Contains(strings.ToUpper(name)) {
return fmt.Errorf("required secret %q is not provided by the caller", name)
}
}
return nil
}
// EvaluateWorkflowCallOutputs evaluates a called workflow's "on.workflow_call.outputs.<name>.value" expressions against the provided contexts.
func EvaluateWorkflowCallOutputs(spec *WorkflowCallSpec, gitCtx *model.GithubContext, vars map[string]string, inputs map[string]any, jobOutputs JobOutputs) (map[string]string, error) {
if spec == nil || len(spec.Outputs) == 0 {
return map[string]string{}, nil
}
jobsCtx := make(map[string]*model.WorkflowCallResult, len(jobOutputs))
for jobID, outputs := range jobOutputs {
jobsCtx[jobID] = &model.WorkflowCallResult{Outputs: outputs}
}
// See `on.workflow_call.outputs.<output_id>.value` in https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability
env := &exprparser.EvaluationEnvironment{
Github: gitCtx,
Jobs: &jobsCtx,
Vars: vars,
Inputs: inputs,
}
interpreter := exprparser.NewInterpeter(env, exprparser.Config{})
out := make(map[string]string, len(spec.Outputs))
for name, o := range spec.Outputs {
v, err := evaluateWorkflowCallOutputValue(interpreter, o.Value)
if err != nil {
return nil, fmt.Errorf("workflow_call output %q: %w", name, err)
}
out[name] = v
}
return out, nil
}
func evaluateWorkflowCallOutputValue(interpreter exprparser.Interpreter, value string) (string, error) {
if !strings.Contains(value, "${{") || !strings.Contains(value, "}}") {
return value, nil
}
expr, err := rewriteSubExpression(value, true)
if err != nil {
return "", err
}
evaluated, err := interpreter.Evaluate(expr, exprparser.DefaultStatusCheckNone)
if err != nil {
return "", err
}
return toString(evaluated), nil
}
func toString(v any) string {
switch s := v.(type) {
case string:
return s
case nil:
return ""
default:
return fmt.Sprintf("%v", s)
}
}
@@ -0,0 +1,471 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package jobparser
import (
"maps"
"testing"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v4"
)
func TestParseWorkflowCallSpec(t *testing.T) {
t.Run("malformed YAML surfaces a parse error", func(t *testing.T) {
// Mismatched flow-sequence brackets — yaml.Unmarshal must reject this.
_, err := ParseWorkflowCallSpec([]byte(`name: bad
on: [workflow_call
jobs:
noop: { }
`))
require.Error(t, err)
})
t.Run("workflow without on.workflow_call is rejected", func(t *testing.T) {
notCallable := []byte(`name: ordinary
on: push
jobs:
noop:
runs-on: ubuntu-latest
steps:
- run: echo
`)
_, err := ParseWorkflowCallSpec(notCallable)
require.Error(t, err)
assert.Contains(t, err.Error(), "does not declare on.workflow_call")
})
t.Run("input missing the required type field is rejected", func(t *testing.T) {
content := callableWorkflow(t, `inputs:
x:
description: missing type
`)
_, err := ParseWorkflowCallSpec(content)
require.Error(t, err)
assert.Contains(t, err.Error(), `missing required field "type"`)
})
t.Run("inputs/secrets/outputs are decoded", func(t *testing.T) {
content := callableWorkflow(t, `inputs:
env:
type: string
required: true
secrets:
DEPLOY_KEY:
required: true
outputs:
sha:
value: ${{ jobs.build.outputs.commit }}
`)
spec, err := ParseWorkflowCallSpec(content)
require.NoError(t, err)
assert.Equal(t, InputTypeString, spec.Inputs["env"].Type)
assert.True(t, spec.Inputs["env"].Required)
assert.True(t, spec.Secrets["DEPLOY_KEY"].Required)
assert.Equal(t, "${{ jobs.build.outputs.commit }}", spec.Outputs["sha"].Value)
})
}
func TestEvaluateCallerWith(t *testing.T) {
t.Run("empty with: returns empty map", func(t *testing.T) {
out, err := EvaluateCallerWith("caller", &Job{}, nil, callerResults("caller", nil, nil), nil, nil)
require.NoError(t, err)
assert.Empty(t, out)
})
t.Run("non-string raw values pass through unchanged", func(t *testing.T) {
job := &Job{With: map[string]any{
"already_bool": true,
"already_int": 42,
"already_slice": []any{"a", "b"},
}}
out, err := EvaluateCallerWith("caller", job, nil, callerResults("caller", nil, nil), nil, nil)
require.NoError(t, err)
assert.Equal(t, true, out["already_bool"])
assert.Equal(t, 42, out["already_int"])
assert.Equal(t, []any{"a", "b"}, out["already_slice"])
})
t.Run("expressions resolve against vars/inputs/results", func(t *testing.T) {
job := &Job{With: map[string]any{
"env_name": "${{ vars.ENV }}",
"from_inputs": "${{ inputs.PARENT_VAR }}",
"from_needs": "${{ needs.upstream.outputs.commit }}",
}}
gitCtx := map[string]any{"event": map[string]any{}}
results := callerResults("caller", []string{"upstream"}, map[string]*JobResult{
"upstream": {Result: "success", Outputs: map[string]string{"commit": "abc123"}},
})
vars := map[string]string{"ENV": "staging"}
inputs := map[string]any{"PARENT_VAR": "from-parent"}
out, err := EvaluateCallerWith("caller", job, gitCtx, results, vars, inputs)
require.NoError(t, err)
assert.Equal(t, "staging", out["env_name"])
assert.Equal(t, "from-parent", out["from_inputs"])
assert.Equal(t, "abc123", out["from_needs"])
})
t.Run("matrix.X resolves to this caller row's matrix instance", func(t *testing.T) {
var rawMatrix yaml.Node
require.NoError(t, rawMatrix.Encode(map[string][]any{"target": {"staging"}}))
job := &Job{
With: map[string]any{"env": "${{ matrix.target }}"},
Strategy: Strategy{RawMatrix: rawMatrix},
}
out, err := EvaluateCallerWith("caller", job, nil, callerResults("caller", nil, nil), nil, nil)
require.NoError(t, err)
assert.Equal(t, "staging", out["env"])
})
}
func TestMatchCallerInputsAgainstSpec(t *testing.T) {
// mustParseSpec wraps ParseWorkflowCallSpec for test brevity.
mustParseSpec := func(t *testing.T, content []byte) *WorkflowCallSpec {
t.Helper()
spec, err := ParseWorkflowCallSpec(content)
require.NoError(t, err)
return spec
}
t.Run("default is filled when caller does not provide the input", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
greeting:
type: string
default: hi
`))
out, err := MatchCallerInputsAgainstSpec(spec, nil)
require.NoError(t, err)
assert.Equal(t, map[string]any{"greeting": "hi"}, out)
})
t.Run("caller-provided value wins over default", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
greeting:
type: string
default: hi
`))
out, err := MatchCallerInputsAgainstSpec(spec, map[string]any{"greeting": "hello"})
require.NoError(t, err)
assert.Equal(t, map[string]any{"greeting": "hello"}, out)
})
t.Run("required input must be provided", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
target:
type: string
required: true
`))
_, err := MatchCallerInputsAgainstSpec(spec, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), `"target" is required`)
})
t.Run("required input is satisfied by a default value", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
target:
type: string
required: true
default: prod
`))
out, err := MatchCallerInputsAgainstSpec(spec, nil)
require.NoError(t, err)
assert.Equal(t, map[string]any{"target": "prod"}, out)
})
t.Run("boolean inputs accept native bool values and bool defaults", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
flag1:
type: boolean
flag2:
type: boolean
default: true
flag3:
type: boolean
`))
out, err := MatchCallerInputsAgainstSpec(spec, map[string]any{
"flag1": true,
"flag3": false,
})
require.NoError(t, err)
assert.Equal(t, true, out["flag1"])
assert.Equal(t, true, out["flag2"]) // from default
assert.Equal(t, false, out["flag3"])
})
t.Run("boolean input rejects strings", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
flag:
type: boolean
`))
_, err := MatchCallerInputsAgainstSpec(spec, map[string]any{"flag": "true"})
require.Error(t, err)
assert.Contains(t, err.Error(), "expects boolean")
})
t.Run("number inputs accept native numeric values and number defaults", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
count:
type: number
ratio:
type: number
default: 0.5
`))
out, err := MatchCallerInputsAgainstSpec(spec, map[string]any{"count": 42})
require.NoError(t, err)
assert.InDelta(t, 42.0, out["count"], 0)
assert.InDelta(t, 0.5, out["ratio"], 0)
})
t.Run("number input rejects strings", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
count:
type: number
`))
_, err := MatchCallerInputsAgainstSpec(spec, map[string]any{"count": "42"})
require.Error(t, err)
assert.Contains(t, err.Error(), "expects number")
})
t.Run("unknown caller-with key is silently dropped", func(t *testing.T) {
spec := mustParseSpec(t, callableWorkflow(t, `inputs:
known:
type: string
default: ok
`))
out, err := MatchCallerInputsAgainstSpec(spec, map[string]any{
"known": "yes",
"unknown": "ignored",
})
require.NoError(t, err)
assert.Equal(t, map[string]any{"known": "yes"}, out)
})
}
func TestParseCallerSecrets(t *testing.T) {
// secretYAMLNode unmarshals raw YAML text into a yaml.Node so tests can hand it to ParseCallerSecrets.
secretYAMLNode := func(t *testing.T, s string) yaml.Node {
t.Helper()
var node yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(s), &node))
// yaml.Unmarshal wraps content in a DocumentNode; the meaningful node is the first child.
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
return *node.Content[0]
}
return node
}
t.Run("zero node returns no inherit, no mapping", func(t *testing.T) {
inherit, mapping, err := ParseCallerSecrets(yaml.Node{})
require.NoError(t, err)
assert.False(t, inherit)
assert.Nil(t, mapping)
})
t.Run("\"inherit\" scalar sets inherit=true", func(t *testing.T) {
inherit, mapping, err := ParseCallerSecrets(secretYAMLNode(t, `inherit`))
require.NoError(t, err)
assert.True(t, inherit)
assert.Nil(t, mapping)
})
t.Run("non-inherit scalar is rejected", func(t *testing.T) {
_, _, err := ParseCallerSecrets(secretYAMLNode(t, `something-else`))
require.Error(t, err)
assert.Contains(t, err.Error(), "expected mapping or 'inherit'")
})
t.Run("mapping of secrets-style references is parsed", func(t *testing.T) {
inherit, mapping, err := ParseCallerSecrets(secretYAMLNode(t, `
DEPLOY_KEY: ${{ secrets.GITEA_DEPLOY_KEY }}
DB_PASS: ${{ secrets.PROD_DB_PASS }}
`))
require.NoError(t, err)
assert.False(t, inherit)
assert.Equal(t, map[string]string{
"DEPLOY_KEY": "GITEA_DEPLOY_KEY",
"DB_PASS": "PROD_DB_PASS",
}, mapping)
})
t.Run("alias and source names are upper-cased", func(t *testing.T) {
inherit, mapping, err := ParseCallerSecrets(secretYAMLNode(t, `
deploy_key: ${{ secrets.gitea_deploy_key }}
`))
require.NoError(t, err)
assert.False(t, inherit)
assert.Equal(t, map[string]string{"DEPLOY_KEY": "GITEA_DEPLOY_KEY"}, mapping)
})
t.Run("mapping value not in ${{ secrets.NAME }} form is rejected", func(t *testing.T) {
// plain string
_, _, err := ParseCallerSecrets(secretYAMLNode(t, `KEY: not-an-expression`))
require.Error(t, err)
assert.Contains(t, err.Error(), `must be of the form ${{ secrets.NAME }}`)
// expression but referencing the wrong context (vars instead of secrets)
_, _, err = ParseCallerSecrets(secretYAMLNode(t, `KEY: ${{ vars.NAME }}`))
require.Error(t, err)
assert.Contains(t, err.Error(), `must be of the form ${{ secrets.NAME }}`)
})
}
func TestValidateCallerSecrets(t *testing.T) {
specWith := func(secrets map[string]SecretSpec) *WorkflowCallSpec {
return &WorkflowCallSpec{Secrets: secrets}
}
t.Run("explicit mapping with all required + only declared aliases is accepted", func(t *testing.T) {
spec := specWith(map[string]SecretSpec{
"DEPLOY_KEY": {Required: true},
"OPTIONAL": {},
})
mapping := map[string]string{
"DEPLOY_KEY": "PROD_DEPLOY_KEY",
"OPTIONAL": "SOMETHING_ELSE",
}
require.NoError(t, ValidateCallerSecrets(spec, mapping))
})
t.Run("alias not in callee schema is rejected", func(t *testing.T) {
spec := specWith(map[string]SecretSpec{"DEPLOY_KEY": {}})
mapping := map[string]string{
"DEPLOY_KEY": "PROD_DEPLOY_KEY",
"EXTRA": "SOMETHING_NOT_DECLARED",
}
err := ValidateCallerSecrets(spec, mapping)
require.Error(t, err)
assert.Contains(t, err.Error(), `caller secret "EXTRA"`)
assert.Contains(t, err.Error(), `not declared`)
})
t.Run("missing required secret is rejected", func(t *testing.T) {
spec := specWith(map[string]SecretSpec{
"MUST_HAVE": {Required: true},
"OPTIONAL": {},
})
mapping := map[string]string{"OPTIONAL": "X"}
err := ValidateCallerSecrets(spec, mapping)
require.Error(t, err)
assert.Contains(t, err.Error(), `required secret "MUST_HAVE"`)
assert.Contains(t, err.Error(), `not provided`)
})
t.Run("callee with no secrets schema accepts an empty mapping", func(t *testing.T) {
spec := specWith(map[string]SecretSpec{})
require.NoError(t, ValidateCallerSecrets(spec, nil))
require.NoError(t, ValidateCallerSecrets(spec, map[string]string{}))
})
t.Run("callee with no secrets schema rejects a non-empty mapping", func(t *testing.T) {
spec := specWith(map[string]SecretSpec{})
err := ValidateCallerSecrets(spec, map[string]string{"X": "Y"})
require.Error(t, err)
assert.Contains(t, err.Error(), `caller secret "X"`)
})
t.Run("name matching is case-insensitive", func(t *testing.T) {
// declared name and caller alias differ only in case; both should match.
spec := specWith(map[string]SecretSpec{"deploy_key": {Required: true}})
mapping := map[string]string{"DEPLOY_KEY": "PROD_DEPLOY_KEY"}
require.NoError(t, ValidateCallerSecrets(spec, mapping))
})
t.Run("nil spec is rejected", func(t *testing.T) {
err := ValidateCallerSecrets(nil, map[string]string{"X": "Y"})
require.Error(t, err)
assert.Contains(t, err.Error(), "nil workflow_call spec")
})
}
func TestEvaluateWorkflowCallOutputs(t *testing.T) {
t.Run("nil spec returns empty map", func(t *testing.T) {
out, err := EvaluateWorkflowCallOutputs(nil, &model.GithubContext{}, nil, nil, nil)
require.NoError(t, err)
assert.Empty(t, out)
})
t.Run("spec with no outputs returns empty map", func(t *testing.T) {
spec := &WorkflowCallSpec{Outputs: map[string]OutputSpec{}}
out, err := EvaluateWorkflowCallOutputs(spec, &model.GithubContext{}, nil, nil, nil)
require.NoError(t, err)
assert.Empty(t, out)
})
t.Run("plain string value passes through unchanged", func(t *testing.T) {
spec := &WorkflowCallSpec{Outputs: map[string]OutputSpec{
"name": {Value: "static-value"},
}}
out, err := EvaluateWorkflowCallOutputs(spec, &model.GithubContext{}, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, map[string]string{"name": "static-value"}, out)
})
t.Run("output references jobs.<id>.outputs.<name>", func(t *testing.T) {
spec := &WorkflowCallSpec{Outputs: map[string]OutputSpec{
"sha": {Value: "${{ jobs.build.outputs.commit }}"},
}}
jobOutputs := JobOutputs{
"build": {"commit": "deadbeef"},
}
out, err := EvaluateWorkflowCallOutputs(spec, &model.GithubContext{}, nil, nil, jobOutputs)
require.NoError(t, err)
assert.Equal(t, "deadbeef", out["sha"])
})
t.Run("output references inputs.<name>", func(t *testing.T) {
spec := &WorkflowCallSpec{Outputs: map[string]OutputSpec{
"target": {Value: "${{ inputs.env_name }}"},
}}
inputs := map[string]any{"env_name": "staging"}
out, err := EvaluateWorkflowCallOutputs(spec, &model.GithubContext{}, nil, inputs, nil)
require.NoError(t, err)
assert.Equal(t, "staging", out["target"])
})
t.Run("multiple outputs are all evaluated", func(t *testing.T) {
spec := &WorkflowCallSpec{Outputs: map[string]OutputSpec{
"static": {Value: "static-value"},
"dynamic": {Value: "${{ vars.SUFFIX }}"},
}}
vars := map[string]string{"SUFFIX": "abc"}
out, err := EvaluateWorkflowCallOutputs(spec, &model.GithubContext{}, vars, nil, nil)
require.NoError(t, err)
assert.Equal(t, "static-value", out["static"])
assert.Equal(t, "abc", out["dynamic"])
})
t.Run("expression referencing an undefined symbol surfaces an error", func(t *testing.T) {
spec := &WorkflowCallSpec{Outputs: map[string]OutputSpec{
"bad": {Value: "${{ this.is.not.valid() }}"},
}}
_, err := EvaluateWorkflowCallOutputs(spec, &model.GithubContext{}, nil, nil, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), `output "bad"`)
})
}
// callableWorkflow returns a minimal valid called-workflow YAML with on.workflow_call.
func callableWorkflow(t *testing.T, body string) []byte {
t.Helper()
return []byte(`name: callable
on:
workflow_call:
` + body + `
jobs:
noop:
runs-on: ubuntu-latest
steps:
- run: "echo"
`)
}
// callerResults returns the minimum results map shape that NewInterpeter expects
func callerResults(callerJobID string, callerNeeds []string, deps map[string]*JobResult) map[string]*JobResult {
out := make(map[string]*JobResult, len(deps)+1)
maps.Copy(out, deps)
out[callerJobID] = &JobResult{Needs: callerNeeds}
return out
}
+2
View File
@@ -74,6 +74,7 @@ func TestHashing(t *testing.T) {
runTests := func(password, salt string, shouldPass bool) {
for _, algorithmName := range hashAlgorithmsToTest {
t.Run(algorithmName, func(t *testing.T) {
t.Parallel() // CPU-bound; these subtests only read the shared hasher registry
output, err := Parse(algorithmName).Hash(password, salt)
if shouldPass {
assert.NoError(t, err)
@@ -182,6 +183,7 @@ func TestVectors(t *testing.T) {
for i, vector := range vectors {
for _, algorithm := range vector.algorithms {
t.Run(strconv.Itoa(i)+": "+algorithm, func(t *testing.T) {
t.Parallel() // CPU-bound; these subtests only read the shared hasher registry
pa := Parse(algorithm)
assert.Equal(t, !vector.shouldfail, pa.VerifyPassword(vector.password, vector.output, vector.salt))
})
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package consts
const (
AsymKeyMinBitsRsa = 3071 // 3072-1 to tolerate the leading zero
AsymKeyMinBitsEC = 256
AsymKeyDefaultBitsRsa = 4096 // ssh-keygen command defaults to 3072
AsymKeyDefaultBitsEcdsa = 256
)
+80
View File
@@ -5,15 +5,23 @@
package generate
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"time"
"gitea.dev/modules/consts"
"gitea.dev/modules/util"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/ssh"
)
// NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN.
@@ -67,3 +75,75 @@ func NewJwtSecretWithBase64() ([]byte, string) {
func NewSecretKey() (string, error) {
return util.CryptoRandomString(64), nil
}
type SSHKeyType string
const (
SSHKeyRSA SSHKeyType = "rsa"
SSHKeyECDSA SSHKeyType = "ecdsa"
SSHKeyED25519 SSHKeyType = "ed25519"
)
func NewSSHKey(keyType SSHKeyType, bits int) (ssh.PublicKey, *pem.Block, error) {
pub, priv, err := commonKeyGen(keyType, bits)
if err != nil {
return nil, nil, err
}
pemPriv, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
return nil, nil, err
}
sshPub, err := ssh.NewPublicKey(pub)
if err != nil {
return nil, nil, err
}
return sshPub, pemPriv, nil
}
// commonKeyGen is an abstraction over rsa, ecdsa, and ed25519 generating functions
func commonKeyGen(keyType SSHKeyType, bits int) (crypto.PublicKey, crypto.PrivateKey, error) {
switch keyType {
case SSHKeyRSA:
bits = util.IfZero(bits, consts.AsymKeyDefaultBitsRsa)
if bits < consts.AsymKeyMinBitsRsa {
return nil, nil, util.NewInvalidArgumentErrorf("invalid rsa bits: %d", bits)
}
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return &privateKey.PublicKey, privateKey, nil
case SSHKeyED25519:
return ed25519.GenerateKey(rand.Reader)
case SSHKeyECDSA:
bits = util.IfZero(bits, consts.AsymKeyDefaultBitsEcdsa)
if bits < consts.AsymKeyMinBitsEC {
return nil, nil, util.NewInvalidArgumentErrorf("invalid elliptic-curve bits: %d", bits)
}
curve, err := getEllipticCurve(bits)
if err != nil {
return nil, nil, err
}
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
return &privateKey.PublicKey, privateKey, nil
default:
return nil, nil, util.NewInvalidArgumentErrorf("unknown key type: %s", keyType)
}
}
func getEllipticCurve(bits int) (elliptic.Curve, error) {
switch bits {
case 256:
return elliptic.P256(), nil
case 384:
return elliptic.P384(), nil
case 521:
return elliptic.P521(), nil
default:
return nil, util.NewInvalidArgumentErrorf("unsupported elliptic-curve bits: %d", bits)
}
}
+8 -40
View File
@@ -11,18 +11,10 @@ import (
"os/exec"
"strings"
"gitea.dev/modules/charset"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/util"
)
type CommitMessage struct {
MessageRaw string
messageUTF8 *string
messageTitle *string
messageBody *string
}
// Commit represents a git commit.
type Commit struct {
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache"
@@ -44,30 +36,6 @@ type CommitSignature struct {
Payload string
}
func (c *CommitMessage) MessageUTF8() string {
if c.messageUTF8 == nil {
bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}})
c.messageUTF8 = new(util.UnsafeBytesToString(bs))
}
return *c.messageUTF8
}
func (c *CommitMessage) MessageTitle() string {
if c.messageTitle == nil {
s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n")
c.messageTitle = new(strings.TrimSpace(s))
}
return *c.messageTitle
}
func (c *CommitMessage) MessageBody() string {
if c.messageBody == nil {
_, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n")
c.messageBody = new(strings.TrimSpace(s))
}
return *c.messageBody
}
// ParentID returns oid of n-th parent (0-based index).
// It returns nil if no such parent exists.
func (c *Commit) ParentID(n int) (ObjectID, error) {
@@ -162,13 +130,9 @@ func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) {
return c.repo.getCommitsBeforeLimit(c.ID, num)
}
// CommitsBeforeUntil returns the commits between commitID to current revision
func (c *Commit) CommitsBeforeUntil(commitID string) ([]*Commit, error) {
endCommit, err := c.repo.GetCommit(commitID)
if err != nil {
return nil, err
}
return c.repo.CommitsBetween(c, endCommit)
// CommitsBeforeUntil returns the commits in range "[cur, ref)"
func (c *Commit) CommitsBeforeUntil(ref RefName) ([]*Commit, error) {
return c.repo.CommitsBetween(c.ID.RefName(), ref, -1)
}
// SearchCommitsOptions specify the parameters for SearchCommits
@@ -289,11 +253,15 @@ func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) boo
if len(s) < minLen || len(s) > maxLen {
return false
}
return isStringLowerHex(s)
}
func isStringLowerHex(s string) bool {
for _, c := range s {
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
if !isHex {
return false
}
}
return true
return len(s) > 0 // it accepts odd length because "shorten commit id" can be 7-chars
}
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"net/mail"
"regexp"
"strings"
"sync"
"gitea.dev/modules/charset"
"gitea.dev/modules/container"
"gitea.dev/modules/util"
)
// CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer.
const CoAuthoredByTrailer = "Co-authored-by"
type CommitIdentity struct {
Name string
Email string
}
// CommitMessageTrailerValues keys are all in lower-case
type CommitMessageTrailerValues map[string][]string
type CommitMessage struct {
MessageRaw string
messageUTF8 *string
messageTitle *string
messageBody *string
trailerValues CommitMessageTrailerValues
allParticipants []*CommitIdentity
}
func (c *CommitMessage) MessageUTF8() string {
if c.messageUTF8 == nil {
bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}})
c.messageUTF8 = new(util.UnsafeBytesToString(bs))
}
return *c.messageUTF8
}
func (c *CommitMessage) MessageTitle() string {
if c.messageTitle == nil {
s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n")
c.messageTitle = new(strings.TrimSpace(s))
}
return *c.messageTitle
}
func (c *CommitMessage) MessageBody() string {
if c.messageBody == nil {
_, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n")
c.messageBody = new(strings.TrimSpace(s))
}
return *c.messageBody
}
func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues {
if c.trailerValues == nil {
_, _, trailer := CommitMessageSplitTrailer(c.MessageUTF8())
c.trailerValues = CommitMessageParseTrailer(trailer)
}
return c.trailerValues
}
var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp {
// the sep is either something like "\n---\n" or "\n\n" in the body, or at the start of the body like "---\n"
return regexp.MustCompile(`(?s)^(?P<content>.*?)(?P<sep>^|^\n|^-{3,}\n+|\n-{3,}\n+|\n\n)(?P<trailer>(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*\n*)$`)
})
// CommitMessageSplitTrailer tries to split the message by the trailer separator
// content + sep + trailer will reconstruct the original message
func CommitMessageSplitTrailer(s string) (content, sep, trailer string) {
s = util.NormalizeStringEOL(s)
re := commitMessageTrailerSplit()
v := re.FindStringSubmatch(s)
if v == nil {
return s, "", ""
}
return v[re.SubexpIndex("content")], v[re.SubexpIndex("sep")], v[re.SubexpIndex("trailer")]
}
func CommitMessageParseTrailer(s string) CommitMessageTrailerValues {
ret := CommitMessageTrailerValues{}
for line := range strings.SplitSeq(util.NormalizeStringEOL(s), "\n") {
k, v, ok := strings.Cut(line, ":")
if !ok {
continue
}
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
kLower := strings.ToLower(k)
ret[kLower] = append(ret[kLower], v)
}
return ret
}
// AllParticipantIdentities returns all the participants in the commit, the first one is the commit's author
func (c *Commit) AllParticipantIdentities() []*CommitIdentity {
if c.allParticipants != nil {
return c.allParticipants
}
exclude := container.Set[string]{}
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: c.Author.Name, Email: c.Author.Email})
exclude.Add(strings.ToLower(c.Author.Email))
addParticipant := func(name, email string) {
if name == "" && email == "" {
return
}
emailLower := strings.ToLower(email)
if emailLower != "" && exclude.Contains(emailLower) {
return
}
c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email})
exclude.Add(emailLower)
}
addParticipant(c.Committer.Name, c.Committer.Email)
for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] {
addr, err := mail.ParseAddress(coAuthorValue)
if err == nil {
addParticipant(addr.Name, addr.Address)
} else {
addParticipant(coAuthorValue, "")
}
}
return c.allParticipants
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) {
commit := &Commit{
CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"},
}
assert.Equal(t, "title ÿ", commit.MessageTitle())
assert.Equal(t, "body ÿ", commit.MessageBody())
assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8())
}
func TestCommitMessageTrailer(t *testing.T) {
cases := []struct {
msg, body, sep, trailer string
}{
{"", "", "", ""},
{"a", "a", "", ""},
{"a\n\nk", "a\n\nk", "", ""},
{"a\n\nk:v", "a", "\n\n", "k:v"},
{"a\n\nk:v\n\n", "a", "\n\n", "k:v\n\n"},
{"a\n--\nk:v", "a\n--\nk:v", "", ""},
{"a\n---\nk:v", "a", "\n---\n", "k:v"},
{"a\n\n---\n\nk:v", "a\n", "\n---\n\n", "k:v"},
{"k: v", "", "", "k: v"},
{"\nk:v", "", "\n", "k:v"},
{"\n\nk:v", "", "\n\n", "k:v"},
{"---\nk:v", "", "---\n", "k:v"},
{"\n---\nk:v", "", "\n---\n", "k:v"},
{"a:b\n---\nk:v", "a:b", "\n---\n", "k:v"},
}
for _, c := range cases {
body, sep, trailer := CommitMessageSplitTrailer(c.msg)
assert.Equal(t, c.body, body, "input=%q", c.msg)
assert.Equal(t, c.sep, sep, "input=%q", c.msg)
assert.Equal(t, c.trailer, trailer, "input=%q", c.msg)
}
}
func TestCommitMessageAllParticipantIdentities(t *testing.T) {
sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} }
idt := func(n, e string) *CommitIdentity { return &CommitIdentity{Name: n, Email: e} }
cases := []struct {
commit *Commit
participant []*CommitIdentity
}{
{
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"),
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"},
},
[]*CommitIdentity{idt("a", "a@m.com"), idt("c", "c@m.com"), idt("", "x@m.com")},
},
{
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"),
CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"},
},
[]*CommitIdentity{idt("a", "a@m.com")},
},
{
&Commit{
Author: sig("a", "a@m.com"), Committer: sig("", ""),
CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name <X@M.com>"},
},
[]*CommitIdentity{idt("a", "a@m.com"), idt("Full Name", "X@M.com")},
},
}
for _, c := range cases {
assert.Equal(t, c.participant, c.commit.AllParticipantIdentities())
}
}
+7 -9
View File
@@ -159,15 +159,6 @@ ISO-8859-1`, commitFromReader.Signature.Payload)
assert.Equal(t, commitFromReader, commitFromReader2)
}
func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) {
commit := &Commit{
CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"},
}
assert.Equal(t, "title ÿ", commit.MessageTitle())
assert.Equal(t, "body ÿ", commit.MessageBody())
assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8())
}
func TestHasPreviousCommit(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
@@ -208,3 +199,10 @@ func Test_GetCommitBranchStart(t *testing.T) {
assert.NotEmpty(t, startCommitID)
assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
}
func TestIsStringLikelyCommitID(t *testing.T) {
assert.True(t, IsStringLikelyCommitID(nil, "abc", 3))
assert.False(t, IsStringLikelyCommitID(nil, "abc", 4))
assert.True(t, IsStringLikelyCommitID(nil, strings.Repeat("a", 64), 4))
assert.False(t, IsStringLikelyCommitID(nil, strings.Repeat("a", 65), 4))
}
+2 -4
View File
@@ -46,10 +46,8 @@ func syncGitConfig(ctx context.Context) (err error) {
return err
}
if DefaultFeatures().CheckVersionAtLeast("2.10") {
if err := configSet(ctx, "receive.advertisePushOptions", "true"); err != nil {
return err
}
if err := configSet(ctx, "receive.advertisePushOptions", "true"); err != nil {
return err
}
if DefaultFeatures().CheckVersionAtLeast("2.18") {
+1 -8
View File
@@ -22,7 +22,7 @@ import (
"github.com/hashicorp/go-version"
)
const RequiredVersion = "2.6.0" // the minimum Git version required
const RequiredVersion = "2.13.0" // the minimum Git version required
type Features struct {
gitVersion *version.Version
@@ -173,13 +173,6 @@ func InitFull() (err error) {
if err = InitSimple(); err != nil {
return err
}
if setting.LFS.StartServer {
if !DefaultFeatures().CheckVersionAtLeast("2.1.2") {
return errors.New("LFS server support requires Git >= 2.1.2")
}
}
return syncGitConfig(context.Background())
}

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