mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-10 02:31:39 +02:00
Merge branch 'main' into main
This commit is contained in:
714 files changed
+12752
-7192
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Gitea DevContainer",
|
"name": "Gitea DevContainer",
|
||||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
"image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
// override "local" from packaged version
|
// override "local" from packaged version
|
||||||
"GOTOOLCHAIN": "auto"
|
"GOTOOLCHAIN": "auto"
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: go-caches
|
||||||
|
description: Restore and save go module, build, and golangci-lint caches
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
cache-name:
|
||||||
|
description: Short identifier used in the per-caller build cache key
|
||||||
|
required: true
|
||||||
|
build-cache:
|
||||||
|
description: Whether to include ~/.cache/go-build
|
||||||
|
default: "true"
|
||||||
|
build-cache-rotate:
|
||||||
|
description: Whether to rotate the build cache key per run so Go's test result cache can accumulate across runs
|
||||||
|
default: "false"
|
||||||
|
lint-cache:
|
||||||
|
description: Whether to include ~/.cache/golangci-lint
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate == 'true' }}
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.run_id }}
|
||||||
|
restore-keys: |
|
||||||
|
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}
|
||||||
|
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
|
||||||
|
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
|
gobuild-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate != 'true' }}
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
|
||||||
|
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
- if: ${{ inputs.lint-cache == 'true' }}
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.cache/golangci-lint
|
||||||
|
key: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum', '.golangci.yml') }}
|
||||||
|
restore-keys: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
labels: [modifies/dependencies]
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
cooldown:
|
|
||||||
default-days: 5
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Populates the go module, build, and golangci-lint caches under the default
|
||||||
|
# branch's cache scope so that PR runs have a warm fallback to restore from.
|
||||||
|
#
|
||||||
|
# GitHub Actions caches are scoped per ref: a PR run can only write to its own
|
||||||
|
# branch's scope, but can read from the base branch's scope as a fallback.
|
||||||
|
# PRs therefore cannot seed main's scope themselves. Running the same cache
|
||||||
|
# steps on push-to-main is the only opportunity to populate that fallback
|
||||||
|
# scope so fresh PR branches start with a useful cache on first run.
|
||||||
|
|
||||||
|
# A PR job's exact key lives in its own PR-scope (empty on first run, filled
|
||||||
|
# by later runs of the same PR); on miss, actions/cache's restore-keys fall
|
||||||
|
# back to prefix matches against entries this seeder saves in main's scope.
|
||||||
|
|
||||||
|
name: cache-seeder
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "go.sum"
|
||||||
|
- ".golangci.yml"
|
||||||
|
- ".github/actions/go-cache/action.yml"
|
||||||
|
- ".github/workflows/cache-seeder.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cache-seeder
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gobuild:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: seed
|
||||||
|
- run: make deps-backend
|
||||||
|
- run: TAGS="bindata" make backend
|
||||||
|
- run: TAGS="bindata sqlite sqlite_unlock_notify" make backend
|
||||||
|
- run: TAGS="bindata gogit sqlite sqlite_unlock_notify" GOEXPERIMENT="" make backend
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { job: lint-backend, tags: "bindata sqlite sqlite_unlock_notify", target: "lint-backend" }
|
||||||
|
- { job: lint-go-windows, tags: "bindata sqlite sqlite_unlock_notify", target: "lint-go-windows" }
|
||||||
|
- { job: lint-go-gogit, tags: "bindata sqlite sqlite_unlock_notify gogit", target: "lint-go" }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: ${{ matrix.job }}
|
||||||
|
lint-cache: "true"
|
||||||
|
- run: make deps-backend deps-tools
|
||||||
|
- run: make ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
TAGS: ${{ matrix.tags }}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: cron-flake-updater
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * 0' # runs weekly on Sunday at 00:00
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nix-flake-update:
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: DeterminateSystems/determinate-nix-action@v3
|
|
||||||
- uses: DeterminateSystems/update-flake-lock@main
|
|
||||||
with:
|
|
||||||
pr-title: "Update Nix flake"
|
|
||||||
pr-labels: |
|
|
||||||
dependencies
|
|
||||||
@@ -12,15 +12,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- run: make generate-gitignore
|
- run: make generate-gitignore
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
- name: push translations to repo
|
- name: push translations to repo
|
||||||
uses: appleboy/git-push-action@v1.2.0
|
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
|
||||||
with:
|
with:
|
||||||
author_email: "teabot@gitea.io"
|
author_email: "teabot@gitea.io"
|
||||||
author_name: GiteaBot
|
author_name: GiteaBot
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: cron-renovate
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 1 * * *" # daily at 01:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cron-renovate
|
||||||
|
|
||||||
|
env:
|
||||||
|
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cron-renovate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'go-gitea/gitea' # prevent running on forks
|
||||||
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
- uses: renovatebot/github-action@83ec54fee49ab67d9cd201084c1ff325b4b462e4 # v46.1.10
|
||||||
|
with:
|
||||||
|
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||||
|
configurationFile: renovate.json5
|
||||||
|
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
env:
|
||||||
|
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
|
||||||
|
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]'
|
||||||
|
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
|
||||||
@@ -12,8 +12,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: crowdin/github-action@v2
|
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||||
with:
|
with:
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: update locales
|
- name: update locales
|
||||||
run: ./build/update-locales.sh
|
run: ./build/update-locales.sh
|
||||||
- name: push translations to repo
|
- name: push translations to repo
|
||||||
uses: appleboy/git-push-action@v1.2.0
|
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
|
||||||
with:
|
with:
|
||||||
author_email: "teabot@gitea.io"
|
author_email: "teabot@gitea.io"
|
||||||
author_name: GiteaBot
|
author_name: GiteaBot
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ jobs:
|
|||||||
json: ${{ steps.changes.outputs.json }}
|
json: ${{ steps.changes.outputs.json }}
|
||||||
e2e: ${{ steps.changes.outputs.e2e }}
|
e2e: ${{ steps.changes.outputs.e2e }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: dorny/paths-filter@v4
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: lint-backend
|
||||||
|
lint-cache: "true"
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
- run: make lint-backend
|
- run: make lint-backend
|
||||||
env:
|
env:
|
||||||
@@ -37,11 +42,11 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: astral-sh/setup-uv@v8.0.0
|
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
- run: uv python install 3.14
|
- run: uv python install 3.14
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -57,8 +62,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: astral-sh/setup-uv@v8.0.0
|
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
- run: uv python install 3.14
|
- run: uv python install 3.14
|
||||||
- run: make deps-py
|
- run: make deps-py
|
||||||
- run: make lint-yaml
|
- run: make lint-yaml
|
||||||
@@ -70,11 +75,13 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
- run: make deps-frontend
|
- run: make deps-frontend
|
||||||
- run: make lint-json
|
- run: make lint-json
|
||||||
|
|
||||||
@@ -85,9 +92,9 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -102,8 +109,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -116,11 +123,16 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: lint-go-windows
|
||||||
|
lint-cache: "true"
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
- run: make lint-go-windows
|
- run: make lint-go-windows
|
||||||
env:
|
env:
|
||||||
@@ -135,11 +147,16 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: lint-go-gogit
|
||||||
|
lint-cache: "true"
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
- run: make lint-go
|
- run: make lint-go
|
||||||
env:
|
env:
|
||||||
@@ -152,11 +169,16 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: checks-backend
|
||||||
|
build-cache: "false"
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
||||||
|
|
||||||
@@ -167,9 +189,9 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -187,11 +209,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: backend
|
||||||
# no frontend build here as backend should be able to build
|
# no frontend build here as backend should be able to build
|
||||||
# even without any frontend files
|
# even without any frontend files
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
@@ -221,9 +247,9 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -238,8 +264,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -42,11 +42,15 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: pgsql
|
||||||
- name: Add hosts to /etc/hosts
|
- name: Add hosts to /etc/hosts
|
||||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
@@ -60,7 +64,6 @@ jobs:
|
|||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit
|
TAGS: bindata gogit
|
||||||
RACE_ENABLED: true
|
|
||||||
TEST_TAGS: gogit
|
TEST_TAGS: gogit
|
||||||
TEST_LDAP: 1
|
TEST_LDAP: 1
|
||||||
|
|
||||||
@@ -71,11 +74,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: sqlite
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: GOEXPERIMENT='' make backend
|
- run: GOEXPERIMENT='' make backend
|
||||||
env:
|
env:
|
||||||
@@ -130,11 +137,16 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: unit
|
||||||
|
build-cache-rotate: "true"
|
||||||
- name: Add hosts to /etc/hosts
|
- name: Add hosts to /etc/hosts
|
||||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
@@ -142,16 +154,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- name: unit-tests
|
- name: unit-tests
|
||||||
run: make unit-test-coverage test-check
|
run: make test-backend test-check
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
|
GOTESTFLAGS: -timeout=20m
|
||||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||||
- name: unit-tests-gogit
|
- name: unit-tests-gogit
|
||||||
run: GOEXPERIMENT='' make unit-test-coverage test-check
|
run: GOEXPERIMENT='' make test-backend test-check
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit
|
TAGS: bindata gogit
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
|
GOTESTFLAGS: -timeout=20m
|
||||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||||
|
|
||||||
test-mysql:
|
test-mysql:
|
||||||
@@ -185,11 +199,15 @@ jobs:
|
|||||||
- "587:587"
|
- "587:587"
|
||||||
- "993:993"
|
- "993:993"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: mysql
|
||||||
- name: Add hosts to /etc/hosts
|
- name: Add hosts to /etc/hosts
|
||||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
@@ -203,7 +221,6 @@ jobs:
|
|||||||
run: make test-mysql
|
run: make test-mysql
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
RACE_ENABLED: true
|
|
||||||
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||||
|
|
||||||
test-mssql:
|
test-mssql:
|
||||||
@@ -226,11 +243,15 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
cache: false
|
||||||
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: mssql
|
||||||
- name: Add hosts to /etc/hosts
|
- name: Add hosts to /etc/hosts
|
||||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
|
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
|
||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Build regular container image
|
- name: Build regular container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
push: false
|
push: false
|
||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||||
- name: Build rootless container image
|
- name: Build rootless container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
|||||||
@@ -20,13 +20,18 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
cache: false
|
||||||
- uses: actions/setup-node@v6
|
- uses: ./.github/actions/go-cache
|
||||||
|
with:
|
||||||
|
cache-name: e2e
|
||||||
|
build-cache: "false"
|
||||||
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v6
|
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
@@ -14,16 +14,16 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata sqlite sqlite_unlock_notify
|
||||||
- name: import gpg key
|
- name: import gpg key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -67,18 +67,18 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Get cleaned branch name
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -102,18 +102,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular docker image
|
- name: build regular docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
||||||
- name: build rootless docker image
|
- name: build rootless docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata sqlite sqlite_unlock_notify
|
||||||
- name: import gpg key
|
- name: import gpg key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||||
- name: Install GH CLI
|
- name: Install GH CLI
|
||||||
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1
|
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||||
with:
|
with:
|
||||||
gh-cli-version: 2.39.1
|
gh-cli-version: 2.39.1
|
||||||
- name: create github release
|
- name: create github release
|
||||||
@@ -77,13 +77,13 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -112,18 +112,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular container image
|
- name: build regular container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
annotations: ${{ steps.meta.outputs.annotations }}
|
||||||
- name: build rootless container image
|
- name: build rootless container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: pnpm/action-setup@v5
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata sqlite sqlite_unlock_notify
|
||||||
- name: import gpg key
|
- name: import gpg key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
echo "Cleaned name is ${REF_NAME}"
|
echo "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||||
- name: Install GH CLI
|
- name: Install GH CLI
|
||||||
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1
|
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||||
with:
|
with:
|
||||||
gh-cli-version: 2.39.1
|
gh-cli-version: 2.39.1
|
||||||
- name: create github release
|
- name: create github release
|
||||||
@@ -80,13 +80,13 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- uses: docker/metadata-action@v6
|
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
id: meta_rootless
|
id: meta_rootless
|
||||||
with:
|
with:
|
||||||
images: |-
|
images: |-
|
||||||
@@ -124,18 +124,18 @@ jobs:
|
|||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR using PAT
|
- name: Login to GHCR using PAT
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build regular container image
|
- name: build regular container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
annotations: ${{ steps.meta.outputs.annotations }}
|
||||||
- name: build rootless container image
|
- name: build rootless container image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
|
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
|
||||||
- Run `make lint-js` to lint `.ts` files
|
- Run `make lint-js` to lint `.ts` files
|
||||||
- Run `make tidy` after any `go.mod` changes
|
- Run `make tidy` after any `go.mod` changes
|
||||||
|
- Run single go tests with `go test -tags 'sqlite sqlite_unlock_notify' -run '^TestName$' ./modulepath/`
|
||||||
|
- Run single js test files with `pnpm exec vitest <path-filter>`
|
||||||
|
- Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e`
|
||||||
- Add the current year into the copyright header of new `.go` files
|
- Add the current year into the copyright header of new `.go` files
|
||||||
- Ensure no trailing whitespace in edited files
|
- Ensure no trailing whitespace in edited files
|
||||||
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
|
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
|
||||||
|
|||||||
@@ -4,6 +4,28 @@ This changelog goes through the changes that have been made in each release
|
|||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
|
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Add event.schedule context for schedule actions task (#37320) (#37348)
|
||||||
|
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
|
||||||
|
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
|
||||||
|
* Fix URL related escaping for oauth2 (#37334) (#37340)
|
||||||
|
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
|
||||||
|
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
|
||||||
|
* Fix bug when accessing user badges (#37321) (#37329)
|
||||||
|
* Fix AppFullLink (#37325) (#37328)
|
||||||
|
* Fix container auth for public instance (#37290) (#37294)
|
||||||
|
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
|
||||||
|
* Fix vite manifest update masking build errors (#37279) (#37310)
|
||||||
|
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
|
||||||
|
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
|
||||||
|
* Add URL to Learn more about blocking a user. (#37355) #37367
|
||||||
|
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
|
||||||
|
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
|
||||||
|
* Fix repo init README EOL (#37388) #37399
|
||||||
|
* Fix: dump with default zip type produces uncompressed zip (#37401) #37402
|
||||||
|
|
||||||
## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17
|
## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17
|
||||||
|
|
||||||
* BREAKING
|
* BREAKING
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ COMMA := ,
|
|||||||
|
|
||||||
XGO_VERSION := go-1.25.x
|
XGO_VERSION := go-1.25.x
|
||||||
|
|
||||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go
|
||||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
|
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 # renovate: datasource=go
|
||||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 # renovate: datasource=go
|
||||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
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
|
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.1
|
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
|
||||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11
|
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 # renovate: datasource=go
|
||||||
|
|
||||||
DOCKER_IMAGE ?= gitea/gitea
|
DOCKER_IMAGE ?= gitea/gitea
|
||||||
DOCKER_TAG ?= latest
|
DOCKER_TAG ?= latest
|
||||||
@@ -518,7 +518,8 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
|
|||||||
|
|
||||||
.PHONY: playwright
|
.PHONY: playwright
|
||||||
playwright: deps-frontend
|
playwright: deps-frontend
|
||||||
@pnpm exec playwright install --with-deps chromium firefox webkit $(PLAYWRIGHT_FLAGS)
|
@# on GitHub Actions VMs, playwright's system deps are pre-installed
|
||||||
|
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS)
|
||||||
|
|
||||||
.PHONY: test-e2e
|
.PHONY: test-e2e
|
||||||
test-e2e: playwright $(EXECUTABLE_E2E)
|
test-e2e: playwright $(EXECUTABLE_E2E)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@@ -82,7 +83,9 @@ func TestChangePasswordCommand(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
err := microcmdUserChangePassword().Run(ctx, tc.args)
|
cmd := microcmdUserChangePassword()
|
||||||
|
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
|
||||||
|
err := cmd.Run(ctx, tc.args)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Contains(t, err.Error(), tc.expectedErr)
|
require.Contains(t, err.Error(), tc.expectedErr)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ func TestCertCommandFailures(t *testing.T) {
|
|||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
app := cmdCert()
|
app := cmdCert()
|
||||||
|
app.Writer, app.ErrWriter = io.Discard, io.Discard
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
certFile := filepath.Join(tempDir, "cert.pem")
|
certFile := filepath.Join(tempDir, "cert.pem")
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultCommand(t *testing.T) {
|
|
||||||
test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
|
|
||||||
called := false
|
|
||||||
cmd := &cli.Command{
|
|
||||||
DefaultCommand: "test",
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "test",
|
|
||||||
Action: func(ctx context.Context, command *cli.Command) error {
|
|
||||||
retName, retValid := isValidDefaultSubCommand(command)
|
|
||||||
assert.Equal(t, expectedRetName, retName)
|
|
||||||
assert.Equal(t, expectedRetValid, retValid)
|
|
||||||
called = true
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.NoError(t, cmd.Run(t.Context(), args))
|
|
||||||
assert.True(t, called)
|
|
||||||
}
|
|
||||||
test(t, []string{"./gitea"}, "", true)
|
|
||||||
test(t, []string{"./gitea", "test"}, "", true)
|
|
||||||
test(t, []string{"./gitea", "other"}, "other", false)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Tests here reload the config system multiple times with uncontrollable details.
|
||||||
|
// So they must be in a separate package, to avoid affecting other tests
|
||||||
|
|
||||||
|
package cmdtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/cmd"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
unittest.MainTest(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePathOutput(workPath, customPath, customConf string) string {
|
||||||
|
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestApp(testCmd cli.Command) *cli.Command {
|
||||||
|
app := cmd.NewMainApp(cmd.AppVersion{})
|
||||||
|
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
||||||
|
cmd.PrepareSubcommandWithGlobalFlags(&testCmd)
|
||||||
|
app.Commands = append(app.Commands, &testCmd)
|
||||||
|
app.DefaultCommand = testCmd.Name
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
type runResult struct {
|
||||||
|
Stdout string
|
||||||
|
Stderr string
|
||||||
|
ExitCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
|
||||||
|
outBuf := new(strings.Builder)
|
||||||
|
errBuf := new(strings.Builder)
|
||||||
|
app.Writer = outBuf
|
||||||
|
app.ErrWriter = errBuf
|
||||||
|
exitCode := -1
|
||||||
|
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
|
||||||
|
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
|
||||||
|
if exitCode == -1 {
|
||||||
|
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
|
||||||
|
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
err := cmd.RunMainApp(app, args...)
|
||||||
|
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCliCmd(t *testing.T) {
|
||||||
|
defaultWorkPath := filepath.FromSlash("/tmp/mocked-work-path")
|
||||||
|
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
|
||||||
|
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
|
||||||
|
defer setting.MockBuiltinPaths(defaultWorkPath, "", "")()
|
||||||
|
|
||||||
|
cli.CommandHelpTemplate = "(command help template)"
|
||||||
|
cli.RootCommandHelpTemplate = "(app help template)"
|
||||||
|
cli.SubcommandHelpTemplate = "(subcommand help template)"
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
env map[string]string
|
||||||
|
cmd string
|
||||||
|
exp string
|
||||||
|
}{
|
||||||
|
// help commands
|
||||||
|
{
|
||||||
|
cmd: "./gitea -h",
|
||||||
|
exp: "DEFAULT CONFIGURATION:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea help",
|
||||||
|
exp: "DEFAULT CONFIGURATION:",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cmd: "./gitea -c /dev/null -h",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cmd: "./gitea -c /dev/null help",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea help -c /dev/null",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cmd: "./gitea -c /dev/null test-cmd -h",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea test-cmd -c /dev/null -h",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea test-cmd -h -c /dev/null",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cmd: "./gitea -c /dev/null test-cmd help",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea test-cmd -c /dev/null help",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea test-cmd help -c /dev/null",
|
||||||
|
exp: "ConfigFile: /dev/null",
|
||||||
|
},
|
||||||
|
|
||||||
|
// parse paths
|
||||||
|
{
|
||||||
|
cmd: "./gitea test-cmd",
|
||||||
|
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea -c /tmp/app.ini test-cmd",
|
||||||
|
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "./gitea test-cmd -c /tmp/app.ini",
|
||||||
|
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||||
|
cmd: "./gitea test-cmd",
|
||||||
|
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||||
|
cmd: "./gitea test-cmd --work-path /tmp/other",
|
||||||
|
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||||
|
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
|
||||||
|
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.cmd, func(t *testing.T) {
|
||||||
|
app := newTestApp(cli.Command{
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
for k, v := range c.env {
|
||||||
|
t.Setenv(k, v)
|
||||||
|
}
|
||||||
|
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
||||||
|
r, err := runTestApp(app, args...)
|
||||||
|
assert.NoError(t, err, c.cmd)
|
||||||
|
assert.NotEmpty(t, c.exp, c.cmd)
|
||||||
|
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
|
||||||
|
t.Log("Full output:\n" + r.Stdout)
|
||||||
|
t.Log("Expected:\n" + c.exp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCliCmdError(t *testing.T) {
|
||||||
|
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
||||||
|
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, 1, r.ExitCode)
|
||||||
|
assert.Empty(t, r.Stdout)
|
||||||
|
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
||||||
|
|
||||||
|
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
|
||||||
|
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, 2, r.ExitCode)
|
||||||
|
assert.Empty(t, r.Stdout)
|
||||||
|
assert.Equal(t, "exit error\n", r.Stderr)
|
||||||
|
|
||||||
|
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||||
|
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, 1, r.ExitCode)
|
||||||
|
assert.Empty(t, r.Stdout)
|
||||||
|
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
||||||
|
|
||||||
|
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||||
|
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
||||||
|
assert.Empty(t, r.Stdout)
|
||||||
|
assert.Empty(t, r.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCliCmdBefore(t *testing.T) {
|
||||||
|
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
||||||
|
configValues := map[string]string{}
|
||||||
|
setting.CustomConf = "/tmp/any.ini"
|
||||||
|
var actionCtx context.Context
|
||||||
|
app := newTestApp(cli.Command{
|
||||||
|
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
||||||
|
configValues["before"] = setting.CustomConf
|
||||||
|
return ctxNew, nil
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
configValues["action"] = setting.CustomConf
|
||||||
|
actionCtx = ctx
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ctxNew, actionCtx)
|
||||||
|
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||||
|
assert.Equal(t, "/dev/null", configValues["action"])
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
|
|||||||
if setting.InstallLock {
|
if setting.InstallLock {
|
||||||
// During config loading, there might also be logs (for example: deprecation warnings).
|
// During config loading, there might also be logs (for example: deprecation warnings).
|
||||||
// It must make sure that console logger is set up before config is loaded.
|
// It must make sure that console logger is set up before config is loaded.
|
||||||
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.")
|
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it. CustomConf=%s", setting.CustomConf)
|
||||||
return nil, errors.New("console logger must be setup before config is loaded")
|
return nil, errors.New("console logger must be setup before config is loaded")
|
||||||
}
|
}
|
||||||
level := defaultLevel
|
level := defaultLevel
|
||||||
+2
-2
@@ -48,7 +48,7 @@ DEFAULT CONFIGURATION:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
func PrepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
||||||
originBefore := originCmd.Before
|
originBefore := originCmd.Before
|
||||||
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
|
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
|
||||||
ctx = ctxOrig
|
ctx = ctxOrig
|
||||||
@@ -145,7 +145,7 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
|||||||
|
|
||||||
app.Before = PrepareConsoleLoggerLevel(log.INFO)
|
app.Before = PrepareConsoleLoggerLevel(log.INFO)
|
||||||
for i := range subCmdWithConfig {
|
for i := range subCmdWithConfig {
|
||||||
prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
|
PrepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
|
||||||
}
|
}
|
||||||
app.Commands = append(app.Commands, subCmdWithConfig...)
|
app.Commands = append(app.Commands, subCmdWithConfig...)
|
||||||
app.Commands = append(app.Commands, subCmdStandalone...)
|
app.Commands = append(app.Commands, subCmdStandalone...)
|
||||||
|
|||||||
+22
-211
@@ -5,17 +5,9 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/test"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -25,209 +17,28 @@ func TestMain(m *testing.M) {
|
|||||||
unittest.MainTest(m)
|
unittest.MainTest(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePathOutput(workPath, customPath, customConf string) string {
|
func TestDefaultCommand(t *testing.T) {
|
||||||
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
|
||||||
}
|
called := false
|
||||||
|
cmd := &cli.Command{
|
||||||
func newTestApp(testCmd cli.Command) *cli.Command {
|
DefaultCommand: "test",
|
||||||
app := NewMainApp(AppVersion{})
|
Commands: []*cli.Command{
|
||||||
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
{
|
||||||
prepareSubcommandWithGlobalFlags(&testCmd)
|
Name: "test",
|
||||||
app.Commands = append(app.Commands, &testCmd)
|
Action: func(ctx context.Context, command *cli.Command) error {
|
||||||
app.DefaultCommand = testCmd.Name
|
retName, retValid := isValidDefaultSubCommand(command)
|
||||||
return app
|
assert.Equal(t, expectedRetName, retName)
|
||||||
}
|
assert.Equal(t, expectedRetValid, retValid)
|
||||||
|
called = true
|
||||||
type runResult struct {
|
return nil
|
||||||
Stdout string
|
},
|
||||||
Stderr string
|
|
||||||
ExitCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
|
|
||||||
outBuf := new(strings.Builder)
|
|
||||||
errBuf := new(strings.Builder)
|
|
||||||
app.Writer = outBuf
|
|
||||||
app.ErrWriter = errBuf
|
|
||||||
exitCode := -1
|
|
||||||
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
|
|
||||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
|
|
||||||
if exitCode == -1 {
|
|
||||||
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
|
|
||||||
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
err := RunMainApp(app, args...)
|
|
||||||
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCliCmd(t *testing.T) {
|
|
||||||
defaultWorkPath := filepath.Dir(setting.AppPath)
|
|
||||||
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
|
|
||||||
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
|
|
||||||
|
|
||||||
cli.CommandHelpTemplate = "(command help template)"
|
|
||||||
cli.RootCommandHelpTemplate = "(app help template)"
|
|
||||||
cli.SubcommandHelpTemplate = "(subcommand help template)"
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
env map[string]string
|
|
||||||
cmd string
|
|
||||||
exp string
|
|
||||||
}{
|
|
||||||
// help commands
|
|
||||||
{
|
|
||||||
cmd: "./gitea -h",
|
|
||||||
exp: "DEFAULT CONFIGURATION:",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea help",
|
|
||||||
exp: "DEFAULT CONFIGURATION:",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
cmd: "./gitea -c /dev/null -h",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
cmd: "./gitea -c /dev/null help",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea help -c /dev/null",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
cmd: "./gitea -c /dev/null test-cmd -h",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea test-cmd -c /dev/null -h",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea test-cmd -h -c /dev/null",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
cmd: "./gitea -c /dev/null test-cmd help",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea test-cmd -c /dev/null help",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea test-cmd help -c /dev/null",
|
|
||||||
exp: "ConfigFile: /dev/null",
|
|
||||||
},
|
|
||||||
|
|
||||||
// parse paths
|
|
||||||
{
|
|
||||||
cmd: "./gitea test-cmd",
|
|
||||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea -c /tmp/app.ini test-cmd",
|
|
||||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "./gitea test-cmd -c /tmp/app.ini",
|
|
||||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
|
||||||
cmd: "./gitea test-cmd",
|
|
||||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
|
||||||
cmd: "./gitea test-cmd --work-path /tmp/other",
|
|
||||||
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
|
||||||
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
|
|
||||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cases {
|
|
||||||
t.Run(c.cmd, func(t *testing.T) {
|
|
||||||
defer test.MockVariableValue(&setting.InstallLock, false)()
|
|
||||||
app := newTestApp(cli.Command{
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
for k, v := range c.env {
|
}
|
||||||
t.Setenv(k, v)
|
assert.NoError(t, cmd.Run(t.Context(), args))
|
||||||
}
|
assert.True(t, called)
|
||||||
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
|
||||||
r, err := runTestApp(app, args...)
|
|
||||||
assert.NoError(t, err, c.cmd)
|
|
||||||
assert.NotEmpty(t, c.exp, c.cmd)
|
|
||||||
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
|
|
||||||
t.Log("Full output:\n" + r.Stdout)
|
|
||||||
t.Log("Expected:\n" + c.exp)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
test(t, []string{"./gitea"}, "", true)
|
||||||
|
test(t, []string{"./gitea", "test"}, "", true)
|
||||||
func TestCliCmdError(t *testing.T) {
|
test(t, []string{"./gitea", "other"}, "other", false)
|
||||||
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
|
||||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, 1, r.ExitCode)
|
|
||||||
assert.Empty(t, r.Stdout)
|
|
||||||
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
|
||||||
|
|
||||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
|
|
||||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, 2, r.ExitCode)
|
|
||||||
assert.Empty(t, r.Stdout)
|
|
||||||
assert.Equal(t, "exit error\n", r.Stderr)
|
|
||||||
|
|
||||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
|
||||||
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, 1, r.ExitCode)
|
|
||||||
assert.Empty(t, r.Stdout)
|
|
||||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
|
||||||
|
|
||||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
|
||||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
|
||||||
assert.Empty(t, r.Stdout)
|
|
||||||
assert.Empty(t, r.Stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCliCmdBefore(t *testing.T) {
|
|
||||||
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
|
||||||
configValues := map[string]string{}
|
|
||||||
setting.CustomConf = "/tmp/any.ini"
|
|
||||||
var actionCtx context.Context
|
|
||||||
app := newTestApp(cli.Command{
|
|
||||||
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
|
||||||
configValues["before"] = setting.CustomConf
|
|
||||||
return ctxNew, nil
|
|
||||||
},
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
configValues["action"] = setting.CustomConf
|
|
||||||
actionCtx = ctx
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, ctxNew, actionCtx)
|
|
||||||
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
|
||||||
assert.Equal(t, "/dev/null", configValues["action"])
|
|
||||||
}
|
}
|
||||||
@@ -525,8 +525,11 @@ INTERNAL_TOKEN =
|
|||||||
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
|
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
|
||||||
;TWO_FACTOR_AUTH =
|
;TWO_FACTOR_AUTH =
|
||||||
;;
|
;;
|
||||||
;; The value of the X-Frame-Options HTTP header for HTML responses. Use "unset" to remove the header.
|
;; The value of the X-Frame-Options HTTP header for all responses. Use "unset" to remove the header.
|
||||||
;X_FRAME_OPTIONS = SAMEORIGIN
|
;X_FRAME_OPTIONS = SAMEORIGIN
|
||||||
|
;;
|
||||||
|
;; The value of the X-Content-Type-Options HTTP header for all responses. Use "unset" to remove the header.
|
||||||
|
;X_CONTENT_TYPE_OPTIONS = nosniff
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -592,6 +595,11 @@ ENABLED = true
|
|||||||
;; * https://github.com/git-ecosystem/git-credential-manager
|
;; * https://github.com/git-ecosystem/git-credential-manager
|
||||||
;; * https://gitea.com/gitea/tea
|
;; * https://gitea.com/gitea/tea
|
||||||
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
|
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
|
||||||
|
;;
|
||||||
|
;; By default, OAuth2 applications can only use "http" and "https" as their redirect URI schemes.
|
||||||
|
;; If you need to use other schemes (e.g. for desktop applications), you can specify them here as a comma-separated list.
|
||||||
|
;; For example: set "my-scheme, com.example.app" to support "my-scheme://..." and "com.example.app://..." redirect URIs.
|
||||||
|
;CUSTOM_SCHEMES =
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -2968,6 +2976,8 @@ LEVEL = Info
|
|||||||
;; Comma-separated list of workflow directories, the first one to exist
|
;; Comma-separated list of workflow directories, the first one to exist
|
||||||
;; in a repo is used to find Actions workflow files
|
;; in a repo is used to find Actions workflow files
|
||||||
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
|
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
|
||||||
|
;; Maximum number of attempts a single workflow run can have. Default value is 50.
|
||||||
|
;MAX_RERUN_ATTEMPTS = 50
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776169885,
|
"lastModified": 1776877367,
|
||||||
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
|
"narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
|
"rev": "0726a0ecb6d4e08f6adced58726b95db924cef57",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ require (
|
|||||||
github.com/PuerkitoBio/goquery v1.12.0
|
github.com/PuerkitoBio/goquery v1.12.0
|
||||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
|
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.15
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||||
github.com/blevesearch/bleve/v2 v2.5.7
|
github.com/blevesearch/bleve/v2 v2.5.7
|
||||||
github.com/bohde/codel v0.2.0
|
github.com/bohde/codel v0.2.0
|
||||||
@@ -37,7 +37,7 @@ require (
|
|||||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
|
||||||
github.com/chi-middleware/proxy v1.1.1
|
github.com/chi-middleware/proxy v1.1.1
|
||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
||||||
github.com/dlclark/regexp2 v1.11.5
|
github.com/dlclark/regexp2 v1.12.0
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
|
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
|
||||||
@@ -49,14 +49,14 @@ require (
|
|||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-co-op/gocron/v2 v2.20.0
|
github.com/go-co-op/gocron/v2 v2.21.0
|
||||||
github.com/go-enry/go-enry/v2 v2.9.6
|
github.com/go-enry/go-enry/v2 v2.9.6
|
||||||
github.com/go-git/go-billy/v5 v5.8.0
|
github.com/go-git/go-billy/v5 v5.8.0
|
||||||
github.com/go-git/go-git/v5 v5.18.0
|
github.com/go-git/go-git/v5 v5.18.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13
|
github.com/go-ldap/ldap/v3 v3.4.13
|
||||||
github.com/go-redsync/redsync/v4 v4.16.0
|
github.com/go-redsync/redsync/v4 v4.16.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/go-webauthn/webauthn v0.16.4
|
github.com/go-webauthn/webauthn v0.16.5
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
@@ -139,10 +139,10 @@ require (
|
|||||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.41.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect
|
||||||
github.com/aws/smithy-go v1.24.2 // indirect
|
github.com/aws/smithy-go v1.25.0 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
@@ -258,7 +258,7 @@ require (
|
|||||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.4 // indirect
|
||||||
github.com/unknwon/com v1.0.1 // indirect
|
github.com/unknwon/com v1.0.1 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
|
|||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12 h1:yv3mfWt/eiDTTry6fkN5hh8wHJfU5ygnw+DJp10C0/c=
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13 h1:IIW5QmNI9PrnDTBCPa75HcD0g+hoD/a+d388dbIAkEM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12/go.mod h1:voO3LP/dZ4CTERiNWCz3DFLbK/8hbfeC1OJkLW+sang=
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13/go.mod h1:B1FUSufCQp3d8VUzob1EY+AdSo2OuEWNEY6GQff7EHA=
|
||||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U=
|
||||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -239,8 +239,8 @@ github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2r
|
|||||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
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/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=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
@@ -288,8 +288,8 @@ 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.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
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-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-co-op/gocron/v2 v2.20.0 h1:9IMrnnVSWjfSh3E54gWmWCHbloQJLh6f9+nwyKfLNpc=
|
github.com/go-co-op/gocron/v2 v2.21.0 h1:e1nt9AEFglarRH9/9y9q0V5sblwxlknpHPjttEajrwQ=
|
||||||
github.com/go-co-op/gocron/v2 v2.20.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
github.com/go-co-op/gocron/v2 v2.21.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||||
github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUDbSs=
|
github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUDbSs=
|
||||||
github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
|
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 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
|
||||||
@@ -325,8 +325,8 @@ 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-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 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
|
github.com/go-webauthn/webauthn v0.16.5 h1:x+vADHlaiIjta23kGhtwyCIlB5mayKx6SBlpwQ5NF9A=
|
||||||
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
|
github.com/go-webauthn/webauthn v0.16.5/go.mod h1:mQC6L0lZ5Kiu35G70zeB2WnrW4+vbHjR8Koq4HdVaMg=
|
||||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
||||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
@@ -695,8 +695,8 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKN
|
|||||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
||||||
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
|
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 v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
|||||||
+38
-13
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
@@ -61,7 +62,8 @@ const (
|
|||||||
// ActionArtifact is a file that is stored in the artifact storage.
|
// ActionArtifact is a file that is stored in the artifact storage.
|
||||||
type ActionArtifact struct {
|
type ActionArtifact struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RunID int64 `xorm:"index unique(runid_name_path)"` // The run id of the artifact
|
RunID int64 `xorm:"index unique(runid_attempt_name_path)"` // The run id of the artifact
|
||||||
|
RunAttemptID int64 `xorm:"index unique(runid_attempt_name_path) NOT NULL DEFAULT 0"`
|
||||||
RunnerID int64
|
RunnerID int64
|
||||||
RepoID int64 `xorm:"index"`
|
RepoID int64 `xorm:"index"`
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
@@ -80,9 +82,9 @@ type ActionArtifact struct {
|
|||||||
// * "application/pdf", "text/html", etc.: real content type of the artifact
|
// * "application/pdf", "text/html", etc.: real content type of the artifact
|
||||||
ContentEncodingOrType string `xorm:"content_encoding"`
|
ContentEncodingOrType string `xorm:"content_encoding"`
|
||||||
|
|
||||||
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
|
ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"` // The path to the artifact when runner uploads it
|
||||||
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
|
ArtifactName string `xorm:"index unique(runid_attempt_name_path)"` // The name of the artifact when runner uploads it
|
||||||
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
|
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
||||||
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
|
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
|
||||||
@@ -92,12 +94,13 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
|
|||||||
if err := t.LoadJob(ctx); err != nil {
|
if err := t.LoadJob(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
artifact, err := getArtifactByNameAndPath(ctx, t.Job.RunID, artifactName, artifactPath)
|
artifact, err := getArtifactByNameAndPath(ctx, t.Job.RunID, t.Job.RunAttemptID, artifactName, artifactPath)
|
||||||
if errors.Is(err, util.ErrNotExist) {
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
artifact := &ActionArtifact{
|
artifact := &ActionArtifact{
|
||||||
ArtifactName: artifactName,
|
ArtifactName: artifactName,
|
||||||
ArtifactPath: artifactPath,
|
ArtifactPath: artifactPath,
|
||||||
RunID: t.Job.RunID,
|
RunID: t.Job.RunID,
|
||||||
|
RunAttemptID: t.Job.RunAttemptID,
|
||||||
RunnerID: t.RunnerID,
|
RunnerID: t.RunnerID,
|
||||||
RepoID: t.RepoID,
|
RepoID: t.RepoID,
|
||||||
OwnerID: t.OwnerID,
|
OwnerID: t.OwnerID,
|
||||||
@@ -122,9 +125,9 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
|
|||||||
return artifact, nil
|
return artifact, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getArtifactByNameAndPath(ctx context.Context, runID int64, name, fpath string) (*ActionArtifact, error) {
|
func getArtifactByNameAndPath(ctx context.Context, runID, runAttemptID int64, name, fpath string) (*ActionArtifact, error) {
|
||||||
var art ActionArtifact
|
var art ActionArtifact
|
||||||
has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, fpath).Get(&art)
|
has, err := db.GetEngine(ctx).Where("run_id = ? AND run_attempt_id = ? AND artifact_name = ? AND artifact_path = ?", runID, runAttemptID, name, fpath).Get(&art)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
@@ -144,6 +147,7 @@ type FindArtifactsOptions struct {
|
|||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
RunID int64
|
RunID int64
|
||||||
|
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy artifacts have run_attempt_id=0)
|
||||||
ArtifactName string
|
ArtifactName string
|
||||||
Status int
|
Status int
|
||||||
FinalizedArtifactsV4 bool
|
FinalizedArtifactsV4 bool
|
||||||
@@ -163,6 +167,9 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
|
|||||||
if opts.RunID > 0 {
|
if opts.RunID > 0 {
|
||||||
cond = cond.And(builder.Eq{"run_id": opts.RunID})
|
cond = cond.And(builder.Eq{"run_id": opts.RunID})
|
||||||
}
|
}
|
||||||
|
if opts.RunAttemptID.Has() {
|
||||||
|
cond = cond.And(builder.Eq{"run_attempt_id": opts.RunAttemptID.Value()})
|
||||||
|
}
|
||||||
if opts.ArtifactName != "" {
|
if opts.ArtifactName != "" {
|
||||||
cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName})
|
cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName})
|
||||||
}
|
}
|
||||||
@@ -186,11 +193,12 @@ type ActionArtifactMeta struct {
|
|||||||
ExpiredUnix timeutil.TimeStamp
|
ExpiredUnix timeutil.TimeStamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run
|
// ListUploadedArtifactsMetaByRunAttempt returns uploaded artifacts meta scoped to a specific run and attempt.
|
||||||
func ListUploadedArtifactsMeta(ctx context.Context, repoID, runID int64) ([]*ActionArtifactMeta, error) {
|
// Pass runAttemptID=0 to target legacy artifacts (pre-v331) belonging to the run.
|
||||||
|
func ListUploadedArtifactsMetaByRunAttempt(ctx context.Context, repoID, runID, runAttemptID int64) ([]*ActionArtifactMeta, error) {
|
||||||
arts := make([]*ActionArtifactMeta, 0, 10)
|
arts := make([]*ActionArtifactMeta, 0, 10)
|
||||||
return arts, db.GetEngine(ctx).Table("action_artifact").
|
return arts, db.GetEngine(ctx).Table("action_artifact").
|
||||||
Where("repo_id=? AND run_id=? AND (status=? OR status=?)", repoID, runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired).
|
Where("repo_id=? AND run_id=? AND run_attempt_id=? AND (status=? OR status=?)", repoID, runID, runAttemptID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired).
|
||||||
GroupBy("artifact_name").
|
GroupBy("artifact_name").
|
||||||
Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix").
|
Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix").
|
||||||
Find(&arts)
|
Find(&arts)
|
||||||
@@ -217,12 +225,29 @@ func SetArtifactExpired(ctx context.Context, artifactID int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
|
// SetArtifactNeedDeleteByID sets an artifact to need-delete by ID, cron job will delete it.
|
||||||
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
|
func SetArtifactNeedDeleteByID(ctx context.Context, artifactID int64) error {
|
||||||
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
|
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetArtifactNeedDeleteByRunAttempt sets an artifact to need-delete in a run attempt, cron job will delete it.
|
||||||
|
// runAttemptID may be 0 for legacy artifacts created before ActionRunAttempt existed.
|
||||||
|
func SetArtifactNeedDeleteByRunAttempt(ctx context.Context, runID, runAttemptID int64, name string) error {
|
||||||
|
_, err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=? AND artifact_name=? AND status = ?", runID, runAttemptID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArtifactsByRunAttemptAndName returns all artifacts with the given name in the specified run attempt.
|
||||||
|
// This supports both attempt-scoped data and legacy artifacts with run_attempt_id=0.
|
||||||
|
func GetArtifactsByRunAttemptAndName(ctx context.Context, runID, runAttemptID int64, artifactName string) ([]*ActionArtifact, error) {
|
||||||
|
arts := make([]*ActionArtifact, 0)
|
||||||
|
return arts, db.GetEngine(ctx).
|
||||||
|
Where("run_id = ? AND run_attempt_id = ? AND artifact_name = ?", runID, runAttemptID, artifactName).
|
||||||
|
OrderBy("id").
|
||||||
|
Find(&arts)
|
||||||
|
}
|
||||||
|
|
||||||
// SetArtifactDeleted sets an artifact to deleted
|
// SetArtifactDeleted sets an artifact to deleted
|
||||||
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
|
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
|
||||||
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
|
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
|
||||||
|
|||||||
+52
-28
@@ -30,7 +30,7 @@ import (
|
|||||||
type ActionRun struct {
|
type ActionRun struct {
|
||||||
ID int64
|
ID int64
|
||||||
Title string
|
Title string
|
||||||
RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"`
|
RepoID int64 `xorm:"unique(repo_index)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
OwnerID int64 `xorm:"index"`
|
OwnerID int64 `xorm:"index"`
|
||||||
WorkflowID string `xorm:"index"` // the name of workflow file
|
WorkflowID string `xorm:"index"` // the name of workflow file
|
||||||
@@ -50,15 +50,20 @@ type ActionRun struct {
|
|||||||
Status Status `xorm:"index"`
|
Status Status `xorm:"index"`
|
||||||
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
|
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
|
||||||
RawConcurrency string // raw concurrency
|
RawConcurrency string // raw concurrency
|
||||||
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
|
|
||||||
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
|
// Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced.
|
||||||
// Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0
|
// When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops.
|
||||||
Started timeutil.TimeStamp
|
Started timeutil.TimeStamp
|
||||||
Stopped timeutil.TimeStamp
|
Stopped timeutil.TimeStamp
|
||||||
// PreviousDuration is used for recording previous duration
|
|
||||||
|
// PreviousDuration is kept only for legacy runs created before ActionRunAttempt existed.
|
||||||
|
// New runs and reruns no longer update this field and use attempt-scoped durations instead.
|
||||||
PreviousDuration time.Duration
|
PreviousDuration time.Duration
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||||
|
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -115,7 +120,7 @@ func (run *ActionRun) RefTooltip() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadAttributes load Repo TriggerUser if not loaded
|
// LoadAttributes load Repo TriggerUser if not loaded
|
||||||
func (run *ActionRun) LoadAttributes(ctx context.Context) error {
|
func (run *ActionRun) LoadAttributes(ctx context.Context) (err error) {
|
||||||
if run == nil {
|
if run == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -129,11 +134,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if run.TriggerUser == nil {
|
if run.TriggerUser == nil {
|
||||||
u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID)
|
run.TriggerUserID, run.TriggerUser, err = user_model.GetPossibleUserByID(ctx, run.TriggerUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
run.TriggerUser = u
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -160,6 +164,31 @@ func (run *ActionRun) Duration() time.Duration {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLatestAttempt returns
|
||||||
|
// - the latest attempt of the run
|
||||||
|
// - (nil, false, nil) for legacy runs that have no attempt records
|
||||||
|
func (run *ActionRun) GetLatestAttempt(ctx context.Context) (*ActionRunAttempt, bool, error) {
|
||||||
|
if run.LatestAttemptID == 0 {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
attempt, err := GetRunAttemptByRepoAndID(ctx, run.RepoID, run.LatestAttemptID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return attempt, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (run *ActionRun) GetEffectiveConcurrency(ctx context.Context) (string, bool, error) {
|
||||||
|
attempt, has, err := run.GetLatestAttempt(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
return attempt.ConcurrencyGroup, attempt.ConcurrencyCancel, nil
|
||||||
|
}
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
|
func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
|
||||||
if run.Event == webhook_module.HookEventPush {
|
if run.Event == webhook_module.HookEventPush {
|
||||||
var payload api.PushPayload
|
var payload api.PushPayload
|
||||||
@@ -406,14 +435,11 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||||||
|
|
||||||
type ActionRunIndex db.ResourceIndex
|
type ActionRunIndex db.ResourceIndex
|
||||||
|
|
||||||
func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) {
|
// GetConcurrentRunAttemptsAndJobs returns run attempts and jobs in the same concurrency group by statuses.
|
||||||
runs, err := db.Find[ActionRun](ctx, &FindRunOptions{
|
func GetConcurrentRunAttemptsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRunAttempt, []*ActionRunJob, error) {
|
||||||
RepoID: repoID,
|
attempts, err := FindConcurrentRunAttempts(ctx, repoID, concurrencyGroup, status)
|
||||||
ConcurrencyGroup: concurrencyGroup,
|
|
||||||
Status: status,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("find runs: %w", err)
|
return nil, nil, fmt.Errorf("find run attempts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{
|
jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{
|
||||||
@@ -425,36 +451,34 @@ func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGrou
|
|||||||
return nil, nil, fmt.Errorf("find jobs: %w", err)
|
return nil, nil, fmt.Errorf("find jobs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runs, jobs, nil
|
return attempts, jobs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) {
|
func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunAttempt) ([]*ActionRunJob, error) {
|
||||||
if actionRun.ConcurrencyGroup == "" {
|
if attempt.ConcurrencyGroup == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobsToCancel []*ActionRunJob
|
var jobsToCancel []*ActionRunJob
|
||||||
|
|
||||||
statusFindOption := []Status{StatusWaiting, StatusBlocked}
|
statusFindOption := []Status{StatusWaiting, StatusBlocked}
|
||||||
if actionRun.ConcurrencyCancel {
|
if attempt.ConcurrencyCancel {
|
||||||
statusFindOption = append(statusFindOption, StatusRunning)
|
statusFindOption = append(statusFindOption, StatusRunning)
|
||||||
}
|
}
|
||||||
runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption)
|
attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, attempt.RepoID, attempt.ConcurrencyGroup, statusFindOption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||||
}
|
}
|
||||||
jobsToCancel = append(jobsToCancel, jobs...)
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
|
|
||||||
// cancel runs in the same concurrency group
|
// cancel runs in the same concurrency group
|
||||||
for _, run := range runs {
|
for _, concurrentAttempt := range attempts {
|
||||||
if run.ID == actionRun.ID {
|
if concurrentAttempt.RunID == attempt.RunID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
|
jobs, err := GetRunJobsByRunAndAttemptID(ctx, concurrentAttempt.RunID, concurrentAttempt.ID)
|
||||||
RunID: run.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err)
|
return nil, fmt.Errorf("find run %d attempt %d jobs: %w", concurrentAttempt.RunID, concurrentAttempt.ID, err)
|
||||||
}
|
}
|
||||||
jobsToCancel = append(jobsToCancel, jobs...)
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActionRunAttempt represents a single execution attempt of an ActionRun.
|
||||||
|
type ActionRunAttempt struct {
|
||||||
|
ID int64
|
||||||
|
RepoID int64 `xorm:"index(repo_concurrency_status)"`
|
||||||
|
RunID int64 `xorm:"UNIQUE(run_attempt)"`
|
||||||
|
Run *ActionRun `xorm:"-"`
|
||||||
|
Attempt int64 `xorm:"UNIQUE(run_attempt)"`
|
||||||
|
|
||||||
|
TriggerUserID int64
|
||||||
|
TriggerUser *user_model.User `xorm:"-"`
|
||||||
|
|
||||||
|
ConcurrencyGroup string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"`
|
||||||
|
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
|
|
||||||
|
Status Status `xorm:"index(repo_concurrency_status)"`
|
||||||
|
Started timeutil.TimeStamp
|
||||||
|
Stopped timeutil.TimeStamp
|
||||||
|
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ActionRunAttempt) TableName() string {
|
||||||
|
return "action_run_attempt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(ActionRunAttempt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attempt *ActionRunAttempt) Duration() time.Duration {
|
||||||
|
return calculateDuration(attempt.Started, attempt.Stopped, attempt.Status, attempt.Updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attempt *ActionRunAttempt) LoadAttributes(ctx context.Context) (err error) {
|
||||||
|
if attempt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt.Run == nil {
|
||||||
|
run, err := GetRunByRepoAndID(ctx, attempt.RepoID, attempt.RunID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
attempt.Run = run
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt.TriggerUser == nil {
|
||||||
|
attempt.TriggerUserID, attempt.TriggerUser, err = user_model.GetPossibleUserByID(ctx, attempt.TriggerUserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRunAttemptByRepoAndID(ctx context.Context, repoID, attemptID int64) (*ActionRunAttempt, error) {
|
||||||
|
var attempt ActionRunAttempt
|
||||||
|
has, err := db.GetEngine(ctx).Where("repo_id=? AND id=?", repoID, attemptID).Get(&attempt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, fmt.Errorf("run attempt %d in repo %d: %w", attemptID, repoID, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
return &attempt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRunAttemptByRunIDAndAttemptNum(ctx context.Context, runID, attemptNum int64) (*ActionRunAttempt, error) {
|
||||||
|
var attempt ActionRunAttempt
|
||||||
|
has, err := db.GetEngine(ctx).Where("run_id=? AND attempt=?", runID, attemptNum).Get(&attempt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, fmt.Errorf("run attempt %d for run %d: %w", attemptNum, runID, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
return &attempt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindConcurrentRunAttempts returns attempts in the given concurrency group and status set.
|
||||||
|
// Results are unordered; callers must not depend on any particular row order.
|
||||||
|
func FindConcurrentRunAttempts(ctx context.Context, repoID int64, concurrencyGroup string, statuses []Status) ([]*ActionRunAttempt, error) {
|
||||||
|
attempts := make([]*ActionRunAttempt, 0)
|
||||||
|
sess := db.GetEngine(ctx).Where("repo_id=? AND concurrency_group=?", repoID, concurrencyGroup)
|
||||||
|
if len(statuses) > 0 {
|
||||||
|
sess = sess.In("status", statuses)
|
||||||
|
}
|
||||||
|
return attempts, sess.Find(&attempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateRunAttempt(ctx context.Context, attempt *ActionRunAttempt, cols ...string) error {
|
||||||
|
if slices.Contains(cols, "status") && attempt.Started.IsZero() && attempt.Status.IsRunning() {
|
||||||
|
attempt.Started = timeutil.TimeStampNow()
|
||||||
|
cols = append(cols, "started")
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := db.GetEngine(ctx).ID(attempt.ID)
|
||||||
|
if len(cols) > 0 {
|
||||||
|
sess.Cols(cols...)
|
||||||
|
}
|
||||||
|
if _, err := sess.Update(attempt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only status/timing changes on an attempt need to update the latest run.
|
||||||
|
if len(cols) > 0 && !slices.Contains(cols, "status") && !slices.Contains(cols, "started") && !slices.Contains(cols, "stopped") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
run, err := GetRunByRepoAndID(ctx, attempt.RepoID, attempt.RunID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if run.LatestAttemptID != attempt.ID {
|
||||||
|
log.Warn("run %d cannot be updated by an old attempt %d", run.LatestAttemptID, attempt.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
run.Status = attempt.Status
|
||||||
|
run.Started = attempt.Started
|
||||||
|
run.Stopped = attempt.Stopped
|
||||||
|
return UpdateRun(ctx, run, "status", "started", "stopped")
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionRunAttemptList []*ActionRunAttempt
|
||||||
|
|
||||||
|
// GetUserIDs returns a slice of user's id
|
||||||
|
func (attempts ActionRunAttemptList) GetUserIDs() []int64 {
|
||||||
|
return container.FilterSlice(attempts, func(attempt *ActionRunAttempt) (int64, bool) {
|
||||||
|
return attempt.TriggerUserID, true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attempts ActionRunAttemptList) LoadTriggerUser(ctx context.Context) error {
|
||||||
|
userIDs := attempts.GetUserIDs()
|
||||||
|
users := make(map[int64]*user_model.User, len(userIDs))
|
||||||
|
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, attempt := range attempts {
|
||||||
|
if attempt.TriggerUserID == user_model.ActionsUserID {
|
||||||
|
attempt.TriggerUser = user_model.NewActionsUser()
|
||||||
|
} else {
|
||||||
|
attempt.TriggerUser = users[attempt.TriggerUserID]
|
||||||
|
if attempt.TriggerUser == nil {
|
||||||
|
attempt.TriggerUser = user_model.NewGhostUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRunAttemptsByRunID returns all attempts of a run, ordered by attempt number DESC (newest first).
|
||||||
|
func ListRunAttemptsByRunID(ctx context.Context, runID int64) (ActionRunAttemptList, error) {
|
||||||
|
var attempts ActionRunAttemptList
|
||||||
|
return attempts, db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("attempt DESC").Find(&attempts)
|
||||||
|
}
|
||||||
+119
-30
@@ -34,7 +34,10 @@ type ActionRunJob struct {
|
|||||||
CommitSHA string `xorm:"index"`
|
CommitSHA string `xorm:"index"`
|
||||||
IsForkPullRequest bool
|
IsForkPullRequest bool
|
||||||
Name string `xorm:"VARCHAR(255)"`
|
Name string `xorm:"VARCHAR(255)"`
|
||||||
Attempt int64
|
|
||||||
|
// for legacy jobs, this counts how many times the job has run;
|
||||||
|
// otherwise it matches the Attempt of the ActionRunAttempt identified by job.RunAttemptID
|
||||||
|
Attempt int64
|
||||||
|
|
||||||
// WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse
|
// WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse
|
||||||
// it should contain exactly one job with global workflow fields for this model
|
// it should contain exactly one job with global workflow fields for this model
|
||||||
@@ -43,8 +46,11 @@ type ActionRunJob struct {
|
|||||||
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
|
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
|
||||||
Needs []string `xorm:"JSON TEXT"`
|
Needs []string `xorm:"JSON TEXT"`
|
||||||
RunsOn []string `xorm:"JSON TEXT"`
|
RunsOn []string `xorm:"JSON TEXT"`
|
||||||
TaskID int64 // the latest task of the job
|
|
||||||
Status Status `xorm:"index"`
|
TaskID int64 // the task created by this job in its own attempt
|
||||||
|
SourceTaskID int64 `xorm:"NOT NULL DEFAULT 0"` // SourceTaskID points to a historical task when this job reuses an earlier attempt's result.
|
||||||
|
|
||||||
|
Status Status `xorm:"index"`
|
||||||
|
|
||||||
RawConcurrency string // raw concurrency from job YAML's "concurrency" section
|
RawConcurrency string // raw concurrency from job YAML's "concurrency" section
|
||||||
|
|
||||||
@@ -61,6 +67,14 @@ type ActionRunJob struct {
|
|||||||
// It is JSON-encoded repo_model.ActionsTokenPermissions and may be empty if not specified.
|
// It is JSON-encoded repo_model.ActionsTokenPermissions and may be empty if not specified.
|
||||||
TokenPermissions *repo_model.ActionsTokenPermissions `xorm:"JSON TEXT"`
|
TokenPermissions *repo_model.ActionsTokenPermissions `xorm:"JSON TEXT"`
|
||||||
|
|
||||||
|
// RunAttemptID identifies the ActionRunAttempt this job belongs to.
|
||||||
|
// A value of 0 indicates a legacy job created before ActionRunAttempt existed.
|
||||||
|
RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||||
|
// AttemptJobID is unique within a single attempt.
|
||||||
|
// For jobs created after ActionRunAttempt was introduced, the same logical job is expected to keep the same AttemptJobID across attempts.
|
||||||
|
// A value of 0 indicates a legacy job created before ActionRunAttempt existed.
|
||||||
|
AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||||
|
|
||||||
Started timeutil.TimeStamp
|
Started timeutil.TimeStamp
|
||||||
Stopped timeutil.TimeStamp
|
Stopped timeutil.TimeStamp
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
@@ -75,6 +89,13 @@ func (job *ActionRunJob) Duration() time.Duration {
|
|||||||
return calculateDuration(job.Started, job.Stopped, job.Status, job.Updated)
|
return calculateDuration(job.Started, job.Stopped, job.Status, job.Updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (job *ActionRunJob) EffectiveTaskID() int64 {
|
||||||
|
if job.TaskID > 0 {
|
||||||
|
return job.TaskID
|
||||||
|
}
|
||||||
|
return job.SourceTaskID
|
||||||
|
}
|
||||||
|
|
||||||
func (job *ActionRunJob) LoadRun(ctx context.Context) error {
|
func (job *ActionRunJob) LoadRun(ctx context.Context) error {
|
||||||
if job.Run == nil {
|
if job.Run == nil {
|
||||||
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
||||||
@@ -152,9 +173,50 @@ func GetRunJobByRunAndID(ctx context.Context, runID, jobID int64) (*ActionRunJob
|
|||||||
return &job, nil
|
return &job, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) {
|
func GetRunJobByAttemptJobID(ctx context.Context, runID, attemptID, attemptJobID int64) (*ActionRunJob, error) {
|
||||||
|
var job ActionRunJob
|
||||||
|
has, err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=? AND attempt_job_id=?", runID, attemptID, attemptJobID).Get(&job)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, fmt.Errorf("run job with attempt_job_id %d in run %d attempt %d: %w", attemptJobID, runID, attemptID, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &job, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestAttemptJobsByRepoAndRunID returns the jobs of the latest attempt for a run.
|
||||||
|
// It prefers the latest attempt when one exists, and falls back to legacy jobs with run_attempt_id=0 for runs created before ActionRunAttempt existed.
|
||||||
|
func GetLatestAttemptJobsByRepoAndRunID(ctx context.Context, repoID, runID int64) (ActionJobList, error) {
|
||||||
|
run, err := GetRunByRepoAndID(ctx, repoID, runID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if run.LatestAttemptID > 0 {
|
||||||
|
return GetRunJobsByRunAndAttemptID(ctx, runID, run.LatestAttemptID)
|
||||||
|
}
|
||||||
|
|
||||||
var jobs []*ActionRunJob
|
var jobs []*ActionRunJob
|
||||||
if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil {
|
if err := db.GetEngine(ctx).Where("repo_id=? AND run_id=? AND run_attempt_id=0", repoID, runID).OrderBy("id").Find(&jobs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllRunJobsByRepoAndRunID returns all jobs for a run across all attempts.
|
||||||
|
func GetAllRunJobsByRepoAndRunID(ctx context.Context, repoID, runID int64) (ActionJobList, error) {
|
||||||
|
var jobs []*ActionRunJob
|
||||||
|
if err := db.GetEngine(ctx).Where("repo_id=? AND run_id=?", repoID, runID).OrderBy("id").Find(&jobs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunJobsByRunAndAttemptID returns jobs for a run within a specific attempt.
|
||||||
|
// runAttemptID may be 0 to address legacy jobs that were created before ActionRunAttempt existed and therefore have no attempt association.
|
||||||
|
func GetRunJobsByRunAndAttemptID(ctx context.Context, runID, runAttemptID int64) (ActionJobList, error) {
|
||||||
|
var jobs []*ActionRunJob
|
||||||
|
if err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=?", runID, runAttemptID).OrderBy("id").Find(&jobs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return jobs, nil
|
return jobs, nil
|
||||||
@@ -196,25 +258,51 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Other goroutines may aggregate the status of the run and update it too.
|
// Other goroutines may aggregate the status of the attempt/run and update it too.
|
||||||
// So we need load the run and its jobs before updating the run.
|
// So we need to load the current jobs before updating the aggregate state.
|
||||||
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
if job.RunAttemptID > 0 {
|
||||||
if err != nil {
|
attempt, err := GetRunAttemptByRepoAndID(ctx, job.RepoID, job.RunAttemptID)
|
||||||
return 0, err
|
if err != nil {
|
||||||
}
|
return 0, err
|
||||||
jobs, err := GetRunJobsByRunID(ctx, job.RunID)
|
}
|
||||||
if err != nil {
|
jobs, err := GetRunJobsByRunAndAttemptID(ctx, job.RunID, job.RunAttemptID)
|
||||||
return 0, err
|
if err != nil {
|
||||||
}
|
return 0, err
|
||||||
run.Status = AggregateJobStatus(jobs)
|
}
|
||||||
if run.Started.IsZero() && run.Status.IsRunning() {
|
attempt.Status = AggregateJobStatus(jobs)
|
||||||
run.Started = timeutil.TimeStampNow()
|
if attempt.Started.IsZero() && attempt.Status.IsRunning() {
|
||||||
}
|
attempt.Started = timeutil.TimeStampNow()
|
||||||
if run.Stopped.IsZero() && run.Status.IsDone() {
|
}
|
||||||
run.Stopped = timeutil.TimeStampNow()
|
if attempt.Stopped.IsZero() && attempt.Status.IsDone() {
|
||||||
}
|
attempt.Stopped = timeutil.TimeStampNow()
|
||||||
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
|
}
|
||||||
return 0, fmt.Errorf("update run %d: %w", run.ID, err)
|
if err := UpdateRunAttempt(ctx, attempt, "status", "started", "stopped"); err != nil {
|
||||||
|
return 0, fmt.Errorf("update run attempt %d: %w", attempt.ID, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: Remove this fallback in the future.
|
||||||
|
// Legacy fallback: jobs created before migration v331 have RunAttemptID=0 and are NOT backfilled.
|
||||||
|
// This path keeps those runs' status consistent when their jobs finish, including:
|
||||||
|
// - jobs created before migration v331 and complete on the new version starts
|
||||||
|
// - zombie/abandoned cleanup cron tasks that call UpdateRunJob on legacy jobs
|
||||||
|
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
jobs, err := GetLatestAttemptJobsByRepoAndRunID(ctx, job.RepoID, job.RunID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
run.Status = AggregateJobStatus(jobs)
|
||||||
|
if run.Started.IsZero() && run.Status.IsRunning() {
|
||||||
|
run.Started = timeutil.TimeStampNow()
|
||||||
|
}
|
||||||
|
if run.Stopped.IsZero() && run.Status.IsDone() {
|
||||||
|
run.Stopped = timeutil.TimeStampNow()
|
||||||
|
}
|
||||||
|
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
|
||||||
|
return 0, fmt.Errorf("update run %d: %w", run.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +357,7 @@ func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob)
|
|||||||
if job.ConcurrencyCancel {
|
if job.ConcurrencyCancel {
|
||||||
statusFindOption = append(statusFindOption, StatusRunning)
|
statusFindOption = append(statusFindOption, StatusRunning)
|
||||||
}
|
}
|
||||||
runs, jobs, err := GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption)
|
attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||||
}
|
}
|
||||||
@@ -277,12 +365,13 @@ func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob)
|
|||||||
jobsToCancel = append(jobsToCancel, jobs...)
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
|
|
||||||
// cancel runs in the same concurrency group
|
// cancel runs in the same concurrency group
|
||||||
for _, run := range runs {
|
for _, attempt := range attempts {
|
||||||
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
|
if attempt.ID == job.RunAttemptID {
|
||||||
RunID: run.ID,
|
continue
|
||||||
})
|
}
|
||||||
|
jobs, err := GetRunJobsByRunAndAttemptID(ctx, attempt.RunID, attempt.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err)
|
return nil, fmt.Errorf("find run %d attempt %d jobs: %w", attempt.RunID, attempt.ID, err)
|
||||||
}
|
}
|
||||||
jobsToCancel = append(jobsToCancel, jobs...)
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@@ -70,6 +71,7 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err
|
|||||||
type FindRunJobOptions struct {
|
type FindRunJobOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RunID int64
|
RunID int64
|
||||||
|
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy jobs have run_attempt_id=0)
|
||||||
RepoID int64
|
RepoID int64
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
CommitSHA string
|
CommitSHA string
|
||||||
@@ -83,6 +85,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
|
|||||||
if opts.RunID > 0 {
|
if opts.RunID > 0 {
|
||||||
cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID})
|
cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID})
|
||||||
}
|
}
|
||||||
|
if opts.RunAttemptID.Has() {
|
||||||
|
cond = cond.And(builder.Eq{"`action_run_job`.run_attempt_id": opts.RunAttemptID.Value()})
|
||||||
|
}
|
||||||
if opts.RepoID > 0 {
|
if opts.RepoID > 0 {
|
||||||
cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
|
cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
@@ -25,12 +24,6 @@ func (runs RunList) GetUserIDs() []int64 {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (runs RunList) GetRepoIDs() []int64 {
|
|
||||||
return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
|
|
||||||
return run.RepoID, true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (runs RunList) LoadTriggerUser(ctx context.Context) error {
|
func (runs RunList) LoadTriggerUser(ctx context.Context) error {
|
||||||
userIDs := runs.GetUserIDs()
|
userIDs := runs.GetUserIDs()
|
||||||
users := make(map[int64]*user_model.User, len(userIDs))
|
users := make(map[int64]*user_model.User, len(userIDs))
|
||||||
@@ -50,18 +43,6 @@ func (runs RunList) LoadTriggerUser(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (runs RunList) LoadRepos(ctx context.Context) error {
|
|
||||||
repoIDs := runs.GetRepoIDs()
|
|
||||||
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, run := range runs {
|
|
||||||
run.Repo = repos[run.RepoID]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindRunOptions struct {
|
type FindRunOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
@@ -102,12 +83,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
|||||||
if opts.CommitSHA != "" {
|
if opts.CommitSHA != "" {
|
||||||
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
||||||
}
|
}
|
||||||
if len(opts.ConcurrencyGroup) > 0 {
|
|
||||||
if opts.RepoID == 0 {
|
|
||||||
panic("Invalid FindRunOptions: repo_id is required")
|
|
||||||
}
|
|
||||||
cond = cond.And(builder.Eq{"`action_run`.concurrency_group": opts.ConcurrencyGroup})
|
|
||||||
}
|
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,62 +4,13 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ScheduleList []*ActionSchedule
|
type ScheduleList []*ActionSchedule
|
||||||
|
|
||||||
// GetUserIDs returns a slice of user's id
|
|
||||||
func (schedules ScheduleList) GetUserIDs() []int64 {
|
|
||||||
return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
|
|
||||||
return schedule.TriggerUserID, true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (schedules ScheduleList) GetRepoIDs() []int64 {
|
|
||||||
return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
|
|
||||||
return schedule.RepoID, true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
|
|
||||||
userIDs := schedules.GetUserIDs()
|
|
||||||
users := make(map[int64]*user_model.User, len(userIDs))
|
|
||||||
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, schedule := range schedules {
|
|
||||||
if schedule.TriggerUserID == user_model.ActionsUserID {
|
|
||||||
schedule.TriggerUser = user_model.NewActionsUser()
|
|
||||||
} else {
|
|
||||||
schedule.TriggerUser = users[schedule.TriggerUserID]
|
|
||||||
if schedule.TriggerUser == nil {
|
|
||||||
schedule.TriggerUser = user_model.NewGhostUser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (schedules ScheduleList) LoadRepos(ctx context.Context) error {
|
|
||||||
repoIDs := schedules.GetRepoIDs()
|
|
||||||
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, schedule := range schedules {
|
|
||||||
schedule.Repo = repos[schedule.RepoID]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindScheduleOptions struct {
|
type FindScheduleOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
|
|||||||
@@ -272,7 +272,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := timeutil.TimeStampNow()
|
now := timeutil.TimeStampNow()
|
||||||
job.Attempt++
|
|
||||||
job.Started = now
|
job.Started = now
|
||||||
job.Status = StatusRunning
|
job.Status = StatusRunning
|
||||||
|
|
||||||
|
|||||||
@@ -186,15 +186,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
|
|||||||
if a.ActUser != nil {
|
if a.ActUser != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var err error
|
a.ActUserID, a.ActUser, _ = user_model.GetPossibleUserByID(ctx, a.ActUserID)
|
||||||
a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
} else if user_model.IsErrUserNotExist(err) {
|
|
||||||
a.ActUser = user_model.NewGhostUser()
|
|
||||||
} else {
|
|
||||||
log.Error("GetUserByID(%d): %v", a.ActUserID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Action) LoadRepo(ctx context.Context) error {
|
func (a *Action) LoadRepo(ctx context.Context) error {
|
||||||
|
|||||||
@@ -282,9 +282,3 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
|
|||||||
|
|
||||||
return actions, count, nil
|
return actions, count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountUserFeeds(ctx context.Context, userID int64) (int64, error) {
|
|
||||||
return db.GetEngine(ctx).Where("user_id = ?", userID).
|
|
||||||
And("is_deleted = ?", false).
|
|
||||||
Count(&Action{})
|
|
||||||
}
|
|
||||||
@@ -192,28 +192,6 @@ func (err ErrGPGKeyIDAlreadyUsed) Unwrap() error {
|
|||||||
return util.ErrAlreadyExist
|
return util.ErrAlreadyExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
|
|
||||||
type ErrGPGKeyAccessDenied struct {
|
|
||||||
UserID int64
|
|
||||||
KeyID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
|
|
||||||
func IsErrGPGKeyAccessDenied(err error) bool {
|
|
||||||
_, ok := err.(ErrGPGKeyAccessDenied)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
|
|
||||||
func (err ErrGPGKeyAccessDenied) Error() string {
|
|
||||||
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
|
|
||||||
err.UserID, err.KeyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrGPGKeyAccessDenied) Unwrap() error {
|
|
||||||
return util.ErrPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
|
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
|
||||||
type ErrKeyAccessDenied struct {
|
type ErrKeyAccessDenied struct {
|
||||||
UserID int64
|
UserID int64
|
||||||
|
|||||||
@@ -105,14 +105,6 @@ func addDeployKey(ctx context.Context, keyID, repoID int64, name, fingerprint st
|
|||||||
return key, db.Insert(ctx, key)
|
return key, db.Insert(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasDeployKey returns true if public key is a deploy key of given repository.
|
|
||||||
func HasDeployKey(ctx context.Context, keyID, repoID int64) bool {
|
|
||||||
has, _ := db.GetEngine(ctx).
|
|
||||||
Where("key_id = ? AND repo_id = ?", keyID, repoID).
|
|
||||||
Get(new(DeployKey))
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddDeployKey add new deploy key to database and authorized_keys file.
|
// AddDeployKey add new deploy key to database and authorized_keys file.
|
||||||
func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
|
func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
|
||||||
fingerprint, err := CalcFingerprint(content)
|
fingerprint, err := CalcFingerprint(content)
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64
|
|||||||
return grant, nil
|
return grant, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateGrant generates a grant for an user
|
// CreateGrant generates a grant for a user
|
||||||
func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) {
|
func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) {
|
||||||
grant := &OAuth2Grant{
|
grant := &OAuth2Grant{
|
||||||
ApplicationID: app.ID,
|
ApplicationID: app.ID,
|
||||||
@@ -461,7 +461,7 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth
|
|||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
|
|
||||||
// OAuth2Grant represents the permission of an user for a specific application to access resources
|
// OAuth2Grant represents the permission of a user for a specific application to access resources
|
||||||
type OAuth2Grant struct {
|
type OAuth2Grant struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"INDEX unique(user_application)"`
|
UserID int64 `xorm:"INDEX unique(user_application)"`
|
||||||
@@ -627,7 +627,7 @@ func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !has {
|
if !has {
|
||||||
return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
|
return nil, util.NewNotExistErrorf("oauth2 source not found, name: %q", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return authSource, nil
|
return authSource, nil
|
||||||
|
|||||||
+22
-30
@@ -12,19 +12,30 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
|
func TestOAuth2AuthorizationCode(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
t.Run("GenerateSetsValidUntil", func(t *testing.T) {
|
t.Run("GenerateSetsValidUntil", func(t *testing.T) {
|
||||||
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
|
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
|
||||||
expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600)
|
expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600)
|
||||||
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
|
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, expectedValidUntil, code.ValidUntil)
|
assert.Equal(t, expectedValidUntil, code.ValidUntil)
|
||||||
assert.False(t, code.IsExpired())
|
assert.False(t, code.IsExpired())
|
||||||
|
assert.Equal(t, int64(1), code.ID)
|
||||||
|
|
||||||
|
code2, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), code.Code)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, code.Code, code2.Code)
|
||||||
|
|
||||||
assert.NoError(t, code.Invalidate(t.Context()))
|
assert.NoError(t, code.Invalidate(t.Context()))
|
||||||
|
|
||||||
|
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, code)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Expired", func(t *testing.T) {
|
t.Run("Expired", func(t *testing.T) {
|
||||||
@@ -34,13 +45,14 @@ func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
|
|||||||
assert.True(t, code.IsExpired())
|
assert.True(t, code.IsExpired())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidateTwice", func(t *testing.T) {
|
t.Run("Invalidate", func(t *testing.T) {
|
||||||
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
|
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
|
||||||
assert.NoError(t, err)
|
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
|
||||||
if assert.NotNil(t, code) {
|
require.NoError(t, err)
|
||||||
assert.NoError(t, code.Invalidate(t.Context()))
|
require.NotNil(t, code)
|
||||||
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
|
require.NoError(t, code.Invalidate(t.Context()))
|
||||||
}
|
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: code.Code})
|
||||||
|
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,19 +236,6 @@ func TestRevokeOAuth2Grant(t *testing.T) {
|
|||||||
|
|
||||||
//////////////////// Authorization Code
|
//////////////////// Authorization Code
|
||||||
|
|
||||||
func TestGetOAuth2AuthorizationByCode(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, code)
|
|
||||||
assert.Equal(t, "authcode", code.Code)
|
|
||||||
assert.Equal(t, int64(1), code.ID)
|
|
||||||
|
|
||||||
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
|
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
|
||||||
// test plain
|
// test plain
|
||||||
code := &auth_model.OAuth2AuthorizationCode{
|
code := &auth_model.OAuth2AuthorizationCode{
|
||||||
@@ -284,13 +283,6 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
|
|||||||
assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String())
|
assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
|
|
||||||
assert.NoError(t, code.Invalidate(t.Context()))
|
|
||||||
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCode_TableName(t *testing.T) {
|
func TestOAuth2AuthorizationCode_TableName(t *testing.T) {
|
||||||
assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName())
|
assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName())
|
||||||
}
|
}
|
||||||
@@ -200,13 +200,3 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
|
|||||||
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
|
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
|
||||||
return had > 0, err
|
return had > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnCredentials implements the webauthn.User interface
|
|
||||||
func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) {
|
|
||||||
dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbCreds.ToCredentials(), nil
|
|
||||||
}
|
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
ref: "refs/heads/test"
|
ref: "refs/heads/test"
|
||||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||||
event: "push"
|
event: "push"
|
||||||
trigger_event: "push"
|
trigger_event: "schedule"
|
||||||
is_fork_pull_request: 0
|
is_fork_pull_request: 0
|
||||||
status: 1
|
status: 1
|
||||||
started: 1683636528
|
started: 1683636528
|
||||||
|
|||||||
@@ -1,39 +1,2 @@
|
|||||||
-
|
[]
|
||||||
id: 1
|
|
||||||
hook_id: 1
|
|
||||||
uuid: uuid1
|
|
||||||
is_delivered: true
|
|
||||||
is_succeed: false
|
|
||||||
request_content: >
|
|
||||||
{
|
|
||||||
"url": "/matrix-delivered",
|
|
||||||
"http_method":"PUT",
|
|
||||||
"headers": {
|
|
||||||
"X-Head": "42"
|
|
||||||
},
|
|
||||||
"body": "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 2
|
|
||||||
hook_id: 1
|
|
||||||
uuid: uuid2
|
|
||||||
is_delivered: true
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 3
|
|
||||||
hook_id: 1
|
|
||||||
uuid: uuid3
|
|
||||||
is_delivered: true
|
|
||||||
is_succeed: true
|
|
||||||
payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content)
|
|
||||||
request_content: >
|
|
||||||
{
|
|
||||||
"url": "/matrix-success",
|
|
||||||
"http_method":"PUT",
|
|
||||||
"headers": {
|
|
||||||
"X-Head": "42"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
name: "Test"
|
name: "Test"
|
||||||
client_id: "da7da3ba-9a13-4167-856f-3899de0b0138"
|
client_id: "da7da3ba-9a13-4167-856f-3899de0b0138"
|
||||||
client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=
|
client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=
|
||||||
redirect_uris: '["a", "https://example.com/xyzzy"]'
|
redirect_uris: '["https://example.com"]'
|
||||||
created_unix: 1546869730
|
created_unix: 1546869730
|
||||||
updated_unix: 1546869730
|
updated_unix: 1546869730
|
||||||
confidential_client: true
|
confidential_client: true
|
||||||
|
|||||||
@@ -1,17 +1,2 @@
|
|||||||
- id: 1
|
[]
|
||||||
grant_id: 1
|
|
||||||
code: "authcode"
|
|
||||||
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
|
|
||||||
code_challenge_method: "S256"
|
|
||||||
redirect_uri: "a"
|
|
||||||
valid_until: 3546869730
|
|
||||||
|
|
||||||
- id: 2
|
|
||||||
grant_id: 4
|
|
||||||
code: "authcodepublic"
|
|
||||||
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
|
|
||||||
code_challenge_method: "S256"
|
|
||||||
redirect_uri: "http://127.0.0.1/"
|
|
||||||
valid_until: 3546869730
|
|
||||||
|
|
||||||
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
||||||
@@ -1,54 +1,2 @@
|
|||||||
-
|
[]
|
||||||
id: 1
|
|
||||||
repo_id: 1
|
|
||||||
url: https://www.example.com/url1
|
|
||||||
content_type: 1 # json
|
|
||||||
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
|
|
||||||
is_active: true
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 2
|
|
||||||
repo_id: 1
|
|
||||||
url: https://www.example.com/url2
|
|
||||||
content_type: 1 # json
|
|
||||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
|
||||||
is_active: false
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 3
|
|
||||||
owner_id: 3
|
|
||||||
repo_id: 3
|
|
||||||
url: https://www.example.com/url3
|
|
||||||
content_type: 1 # json
|
|
||||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
|
||||||
is_active: true
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 4
|
|
||||||
repo_id: 2
|
|
||||||
url: https://www.example.com/url4
|
|
||||||
content_type: 1 # json
|
|
||||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
|
||||||
is_active: true
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 5
|
|
||||||
repo_id: 0
|
|
||||||
owner_id: 0
|
|
||||||
url: https://www.example.com/url5
|
|
||||||
content_type: 1 # json
|
|
||||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
|
||||||
is_active: true
|
|
||||||
is_system_webhook: true
|
|
||||||
|
|
||||||
-
|
|
||||||
id: 6
|
|
||||||
repo_id: 0
|
|
||||||
owner_id: 0
|
|
||||||
url: https://www.example.com/url6
|
|
||||||
content_type: 1 # json
|
|
||||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
|
||||||
is_active: true
|
|
||||||
is_system_webhook: false
|
|
||||||
|
|
||||||
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
@@ -60,24 +59,6 @@ func (branches BranchList) LoadPusher(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (branches BranchList) LoadRepo(ctx context.Context) error {
|
|
||||||
ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
|
|
||||||
return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil
|
|
||||||
})
|
|
||||||
|
|
||||||
reposMap := make(map[int64]*repo_model.Repository, len(ids))
|
|
||||||
if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, branch := range branches {
|
|
||||||
if branch.RepoID <= 0 || branch.Repo != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
branch.Repo = reposMap[branch.RepoID]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindBranchOptions struct {
|
type FindBranchOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
|
|||||||
@@ -400,16 +400,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) {
|
|||||||
if c.Poster != nil {
|
if c.Poster != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
c.PosterID, c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
|
||||||
c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
|
|
||||||
if err != nil {
|
|
||||||
if user_model.IsErrUserNotExist(err) {
|
|
||||||
c.PosterID = user_model.GhostUserID
|
|
||||||
c.Poster = user_model.NewGhostUser()
|
|
||||||
} else {
|
|
||||||
log.Error("getUserByID[%d]: %v", c.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,12 +89,6 @@ type ErrUnknownDependencyType struct {
|
|||||||
Type DependencyType
|
Type DependencyType
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
|
|
||||||
func IsErrUnknownDependencyType(err error) bool {
|
|
||||||
_, ok := err.(ErrUnknownDependencyType)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrUnknownDependencyType) Error() string {
|
func (err ErrUnknownDependencyType) Error() string {
|
||||||
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
|
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-25
@@ -48,21 +48,6 @@ func (err ErrIssueNotExist) Unwrap() error {
|
|||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
|
|
||||||
type ErrNewIssueInsert struct {
|
|
||||||
OriginalError error
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
|
|
||||||
func IsErrNewIssueInsert(err error) bool {
|
|
||||||
_, ok := err.(ErrNewIssueInsert)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrNewIssueInsert) Error() string {
|
|
||||||
return err.OriginalError.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
|
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
|
||||||
|
|
||||||
// Issue represents an issue or pull request of repository.
|
// Issue represents an issue or pull request of repository.
|
||||||
@@ -190,17 +175,10 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
|||||||
|
|
||||||
// LoadPoster loads poster
|
// LoadPoster loads poster
|
||||||
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
||||||
if issue.Poster == nil && issue.PosterID != 0 {
|
if issue.Poster != nil {
|
||||||
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
return nil
|
||||||
if err != nil {
|
|
||||||
issue.PosterID = user_model.GhostUserID
|
|
||||||
issue.Poster = user_model.NewGhostUser()
|
|
||||||
if !user_model.IsErrUserNotExist(err) {
|
|
||||||
return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
issue.PosterID, issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,27 +165,6 @@ func MovePin(ctx context.Context, issue *Issue, newPosition int) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) {
|
|
||||||
var issuePins []IssuePin
|
|
||||||
if err := db.GetEngine(ctx).
|
|
||||||
Table("issue_pin").
|
|
||||||
Where("repo_id = ?", repoID).
|
|
||||||
And("is_pull = ?", isPull).
|
|
||||||
Find(&issuePins); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(issuePins, func(i, j int) bool {
|
|
||||||
return issuePins[i].PinOrder < issuePins[j].PinOrder
|
|
||||||
})
|
|
||||||
|
|
||||||
var ids []int64
|
|
||||||
for _, pin := range issuePins {
|
|
||||||
ids = append(ids, pin.IssueID)
|
|
||||||
}
|
|
||||||
return ids, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) {
|
func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) {
|
||||||
var pins []*IssuePin
|
var pins []*IssuePin
|
||||||
if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil {
|
if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil {
|
||||||
|
|||||||
@@ -93,12 +93,6 @@ type ErrIssueIsOpen struct {
|
|||||||
Index int64
|
Index int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen.
|
|
||||||
func IsErrIssueIsOpen(err error) bool {
|
|
||||||
_, ok := err.(ErrIssueIsOpen)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrIssueIsOpen) Error() string {
|
func (err ErrIssueIsOpen) Error() string {
|
||||||
return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
|
return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
|
||||||
}
|
}
|
||||||
@@ -441,7 +435,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
|
|||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
Attachments: uuids,
|
Attachments: uuids,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return fmt.Errorf("newIssue: %w", err)
|
return fmt.Errorf("newIssue: %w", err)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch,
|
|||||||
return iw, exists, err
|
return iw, exists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckIssueWatch check if an user is watching an issue
|
// CheckIssueWatch check if a user is watching an issue
|
||||||
// it takes participants and repo watch into account
|
// it takes participants and repo watch into account
|
||||||
func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) {
|
func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) {
|
||||||
iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID)
|
iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID)
|
||||||
|
|||||||
@@ -437,8 +437,8 @@ func (pr *PullRequest) IsChecking() bool {
|
|||||||
return pr.Status == PullRequestStatusChecking
|
return pr.Status == PullRequestStatusChecking
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanAutoMerge returns true if this pull request can be merged automatically.
|
// IsStatusMergeable returns true if this pull request is mergeable to its base
|
||||||
func (pr *PullRequest) CanAutoMerge() bool {
|
func (pr *PullRequest) IsStatusMergeable() bool {
|
||||||
return pr.Status == PullRequestStatusMergeable
|
return pr.Status == PullRequestStatusMergeable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +475,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
|
|||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
Attachments: uuids,
|
Attachments: uuids,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return fmt.Errorf("newIssue: %w", err)
|
return fmt.Errorf("newIssue: %w", err)
|
||||||
|
|||||||
+4
-12
@@ -67,7 +67,7 @@ func (err ErrNotValidReviewRequest) Unwrap() error {
|
|||||||
return util.ErrInvalidArgument
|
return util.ErrInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR.
|
// ErrReviewRequestOnClosedPR represents an error when a user tries to request a re-review on a closed or merged PR.
|
||||||
type ErrReviewRequestOnClosedPR struct{}
|
type ErrReviewRequestOnClosedPR struct{}
|
||||||
|
|
||||||
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
|
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
|
||||||
@@ -176,15 +176,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
|
|||||||
if r.ReviewerID == 0 || r.Reviewer != nil {
|
if r.ReviewerID == 0 || r.Reviewer != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
|
r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
|
||||||
if err != nil {
|
|
||||||
if !user_model.IsErrUserNotExist(err) {
|
|
||||||
return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
|
|
||||||
}
|
|
||||||
r.ReviewerID = user_model.GhostUserID
|
|
||||||
r.Reviewer = user_model.NewGhostUser()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,8 +900,8 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us
|
|||||||
// CanMarkConversation Add or remove Conversation mark for a code comment permission check
|
// CanMarkConversation Add or remove Conversation mark for a code comment permission check
|
||||||
// the PR writer , official reviewer and poster can do it
|
// the PR writer , official reviewer and poster can do it
|
||||||
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
|
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
|
||||||
if doer == nil || issue == nil {
|
if doer == nil {
|
||||||
return false, errors.New("issue or doer is nil")
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.LoadRepo(ctx); err != nil {
|
if err = issue.LoadRepo(ctx); err != nil {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func deleteDB() error {
|
|||||||
}
|
}
|
||||||
db.Close()
|
db.Close()
|
||||||
|
|
||||||
// Check if we need to setup a specific schema
|
// Check if we need to set up a specific schema
|
||||||
if len(setting.Database.Schema) != 0 {
|
if len(setting.Database.Schema) != 0 {
|
||||||
db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
|
db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
|
||||||
setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
|
setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
|
||||||
@@ -91,7 +91,7 @@ func deleteDB() error {
|
|||||||
defer schrows.Close()
|
defer schrows.Close()
|
||||||
|
|
||||||
if !schrows.Next() {
|
if !schrows.Next() {
|
||||||
// Create and setup a DB schema
|
// Create and set up a DB schema
|
||||||
_, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema)
|
_, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -134,7 +134,8 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
|||||||
ourSkip := 2
|
ourSkip := 2
|
||||||
ourSkip += skip
|
ourSkip += skip
|
||||||
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
|
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
|
||||||
require.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
|
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||||
|
require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath))
|
||||||
|
|
||||||
if err := deleteDB(); err != nil {
|
if err := deleteDB(); err != nil {
|
||||||
t.Fatalf("unable to reset database: %v", err)
|
t.Fatalf("unable to reset database: %v", err)
|
||||||
@@ -166,7 +167,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fixturesDir := filepath.Join(filepath.Dir(setting.AppPath), "models", "migrations", "fixtures", t.Name())
|
fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name())
|
||||||
|
|
||||||
if _, err := os.Stat(fixturesDir); err == nil {
|
if _, err := os.Stat(fixturesDir); err == nil {
|
||||||
t.Logf("initializing fixtures from: %s", fixturesDir)
|
t.Logf("initializing fixtures from: %s", fixturesDir)
|
||||||
@@ -203,17 +204,17 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table
|
|||||||
func mainTest(m *testing.M) int {
|
func mainTest(m *testing.M) int {
|
||||||
testlogger.Init()
|
testlogger.Init()
|
||||||
|
|
||||||
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
|
tempWorkPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("migration-test-data-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
testlogger.Panicf("Unable to create temporary data path %v\n", err)
|
return testlogger.MainErrorf("Unable to create temporary dir for migration test: %v", err)
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
setting.AppDataPath = tmpDataPath
|
setting.MockBuiltinPaths(tempWorkPath, "", "")
|
||||||
|
setting.SetupGiteaTestEnv()
|
||||||
|
|
||||||
unittest.InitSettingsForTesting()
|
|
||||||
if err = git.InitFull(); err != nil {
|
if err = git.InitFull(); err != nil {
|
||||||
testlogger.Panicf("Unable to InitFull: %v\n", err)
|
return testlogger.MainErrorf("Unable to InitFull: %v", err)
|
||||||
}
|
}
|
||||||
setting.LoadDBSetting()
|
setting.LoadDBSetting()
|
||||||
setting.InitLoggersForTest()
|
setting.InitLoggersForTest()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/migrations/v1_24"
|
"code.gitea.io/gitea/models/migrations/v1_24"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_25"
|
"code.gitea.io/gitea/models/migrations/v1_25"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_26"
|
"code.gitea.io/gitea/models/migrations/v1_26"
|
||||||
|
"code.gitea.io/gitea/models/migrations/v1_27"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_6"
|
"code.gitea.io/gitea/models/migrations/v1_6"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_7"
|
"code.gitea.io/gitea/models/migrations/v1_7"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_8"
|
"code.gitea.io/gitea/models/migrations/v1_8"
|
||||||
@@ -405,6 +406,9 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
|
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
|
||||||
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
|
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
|
||||||
newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook),
|
newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook),
|
||||||
|
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
|
||||||
|
|
||||||
|
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
base.MainTest(m)
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionRunAttempt struct {
|
||||||
|
ID int64
|
||||||
|
RepoID int64 `xorm:"index(repo_concurrency_status)"`
|
||||||
|
RunID int64 `xorm:"UNIQUE(run_attempt)"`
|
||||||
|
Attempt int64 `xorm:"UNIQUE(run_attempt)"`
|
||||||
|
TriggerUserID int64
|
||||||
|
ConcurrencyGroup string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"`
|
||||||
|
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
|
Status int `xorm:"index(repo_concurrency_status)"`
|
||||||
|
Started timeutil.TimeStamp
|
||||||
|
Stopped timeutil.TimeStamp
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (actionRunAttempt) TableName() string {
|
||||||
|
return "action_run_attempt"
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionArtifact struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RunID int64 `xorm:"index unique(runid_attempt_name_path)"`
|
||||||
|
RunAttemptID int64 `xorm:"index unique(runid_attempt_name_path) NOT NULL DEFAULT 0"`
|
||||||
|
RunnerID int64
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
OwnerID int64
|
||||||
|
CommitSHA string
|
||||||
|
StoragePath string
|
||||||
|
FileSize int64
|
||||||
|
FileCompressedSize int64
|
||||||
|
ContentEncoding string `xorm:"content_encoding"`
|
||||||
|
ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"`
|
||||||
|
ArtifactName string `xorm:"index unique(runid_attempt_name_path)"`
|
||||||
|
Status int `xorm:"index"`
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
||||||
|
ExpiredUnix timeutil.TimeStamp `xorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (actionArtifact) TableName() string {
|
||||||
|
return "action_artifact"
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionRun mirrors the post-migration action_run schema.
|
||||||
|
type actionRun struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
RepoID int64 `xorm:"unique(repo_index)"`
|
||||||
|
OwnerID int64 `xorm:"index"`
|
||||||
|
WorkflowID string `xorm:"index"`
|
||||||
|
Index int64 `xorm:"index unique(repo_index)"`
|
||||||
|
TriggerUserID int64 `xorm:"index"`
|
||||||
|
ScheduleID int64
|
||||||
|
Ref string `xorm:"index"`
|
||||||
|
CommitSHA string
|
||||||
|
IsForkPullRequest bool
|
||||||
|
NeedApproval bool
|
||||||
|
ApprovedBy int64 `xorm:"index"`
|
||||||
|
Event string
|
||||||
|
EventPayload string `xorm:"LONGTEXT"`
|
||||||
|
TriggerEvent string
|
||||||
|
Status int `xorm:"index"`
|
||||||
|
Version int `xorm:"version default 0"`
|
||||||
|
RawConcurrency string
|
||||||
|
Started timeutil.TimeStamp
|
||||||
|
Stopped timeutil.TimeStamp
|
||||||
|
PreviousDuration time.Duration
|
||||||
|
LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (actionRun) TableName() string {
|
||||||
|
return "action_run"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddActionRunAttemptModel adds the ActionRunAttempt table and the supporting ActionRun/ActionRunJob fields.
|
||||||
|
func AddActionRunAttemptModel(x *xorm.Engine) error {
|
||||||
|
// add "action_run_attempt"
|
||||||
|
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(actionRunAttempt)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update "action_run_job"
|
||||||
|
type ActionRunJob struct {
|
||||||
|
RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||||
|
AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||||
|
SourceTaskID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
}
|
||||||
|
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(ActionRunJob)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update "action_artifact": let xorm sync add the new 4-column unique index (runid_attempt_name_path) and drop the old 3-column unique (runid_name_path)
|
||||||
|
if err := x.Sync(new(actionArtifact)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update "action_run"
|
||||||
|
//
|
||||||
|
// This migration intentionally removes the legacy run-level concurrency columns after
|
||||||
|
// introducing attempt-level concurrency on action_run_attempt.
|
||||||
|
//
|
||||||
|
// Existing values from action_run.concurrency_group / action_run.concurrency_cancel are
|
||||||
|
// not backfilled into action_run_attempt:
|
||||||
|
// - the old fields are only meaningful while a run is actively participating in
|
||||||
|
// concurrency scheduling
|
||||||
|
// - for completed legacy runs, keeping or backfilling those values has no practical
|
||||||
|
// effect on future scheduling behavior
|
||||||
|
// - scanning and backfilling old runs would add significant migration cost for little value
|
||||||
|
//
|
||||||
|
// This means the schema change is destructive for those two legacy columns by design.
|
||||||
|
//
|
||||||
|
// Let xorm sync add the latest_attempt_id column and drop the now-orphan (repo_id, concurrency_group) index.
|
||||||
|
if err := x.Sync(new(actionRun)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
concurrencyColumns := make([]string, 0, 2)
|
||||||
|
for _, col := range []string{"concurrency_group", "concurrency_cancel"} {
|
||||||
|
exist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "action_run", col)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exist {
|
||||||
|
concurrencyColumns = append(concurrencyColumns, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(concurrencyColumns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
if err := base.DropTableColumns(sess, "action_run", concurrencyColumns...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// DropTableColumns rebuilds the table on SQLite, which drops all existing indexes.
|
||||||
|
// Re-sync to restore the indexes defined on actionRun.
|
||||||
|
return x.Sync(new(actionRun))
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionRunBeforeV331 struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
ConcurrencyGroup string
|
||||||
|
ConcurrencyCancel bool
|
||||||
|
LatestAttemptID int64 `xorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (actionRunBeforeV331) TableName() string {
|
||||||
|
return "action_run"
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionRunJobBeforeV331 struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RunID int64 `xorm:"index"`
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (actionRunJobBeforeV331) TableName() string {
|
||||||
|
return "action_run_job"
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionArtifactBeforeV331 struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RunID int64 `xorm:"index unique(runid_name_path)"`
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
ArtifactPath string `xorm:"index unique(runid_name_path)"`
|
||||||
|
ArtifactName string `xorm:"index unique(runid_name_path)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (actionArtifactBeforeV331) TableName() string {
|
||||||
|
return "action_artifact"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_AddActionRunAttemptModel(t *testing.T) {
|
||||||
|
x, deferable := base.PrepareTestEnv(t, 0,
|
||||||
|
new(actionRunBeforeV331),
|
||||||
|
new(actionRunJobBeforeV331),
|
||||||
|
new(actionArtifactBeforeV331),
|
||||||
|
)
|
||||||
|
defer deferable()
|
||||||
|
if x == nil || t.Failed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := x.Insert(&actionArtifactBeforeV331{
|
||||||
|
RunID: 1,
|
||||||
|
RepoID: 1,
|
||||||
|
ArtifactPath: "artifact/path",
|
||||||
|
ArtifactName: "artifact-name",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, AddActionRunAttemptModel(x))
|
||||||
|
|
||||||
|
tableMap := base.LoadTableSchemasMap(t, x)
|
||||||
|
|
||||||
|
attemptTable := tableMap["action_run_attempt"]
|
||||||
|
require.NotNil(t, attemptTable)
|
||||||
|
attemptTablCols := []string{"id", "repo_id", "run_id", "attempt", "trigger_user_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "created", "updated"}
|
||||||
|
require.ElementsMatch(t, attemptTable.ColumnsSeq(), attemptTablCols)
|
||||||
|
|
||||||
|
runTable := tableMap["action_run"]
|
||||||
|
require.NotNil(t, runTable)
|
||||||
|
require.Contains(t, runTable.ColumnsSeq(), "latest_attempt_id")
|
||||||
|
require.NotContains(t, runTable.ColumnsSeq(), "concurrency_group")
|
||||||
|
require.NotContains(t, runTable.ColumnsSeq(), "concurrency_cancel")
|
||||||
|
|
||||||
|
jobTable := tableMap["action_run_job"]
|
||||||
|
require.NotNil(t, jobTable)
|
||||||
|
require.Contains(t, jobTable.ColumnsSeq(), "run_attempt_id")
|
||||||
|
require.Contains(t, jobTable.ColumnsSeq(), "attempt_job_id")
|
||||||
|
require.Contains(t, jobTable.ColumnsSeq(), "source_task_id")
|
||||||
|
|
||||||
|
attemptIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run_attempt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, hasIndexWithColumns(attemptIndexes, []string{"run_id", "attempt"}, true))
|
||||||
|
assert.True(t, hasIndexWithColumns(attemptIndexes, []string{"repo_id", "concurrency_group", "status"}, false))
|
||||||
|
|
||||||
|
runIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, hasIndexWithColumns(runIndexes, []string{"latest_attempt_id"}, false))
|
||||||
|
assert.False(t, hasIndexWithColumns(runIndexes, []string{"repo_id", "concurrency_group"}, false))
|
||||||
|
|
||||||
|
jobIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run_job")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, hasIndexWithColumns(jobIndexes, []string{"run_attempt_id"}, false))
|
||||||
|
assert.True(t, hasIndexWithColumns(jobIndexes, []string{"attempt_job_id"}, false))
|
||||||
|
|
||||||
|
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_artifact")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, hasIndexWithColumns(indexes, []string{"run_id", "artifact_path", "artifact_name"}, true))
|
||||||
|
assert.True(t, hasIndexWithColumns(indexes, []string{"run_id", "run_attempt_id", "artifact_path", "artifact_name"}, true))
|
||||||
|
|
||||||
|
_, err = x.Insert(&actionArtifact{
|
||||||
|
RunID: 1,
|
||||||
|
RunAttemptID: 2,
|
||||||
|
RepoID: 1,
|
||||||
|
ArtifactPath: "artifact/path",
|
||||||
|
ArtifactName: "artifact-name",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = x.Insert(&actionArtifact{
|
||||||
|
RunID: 1,
|
||||||
|
RunAttemptID: 2,
|
||||||
|
RepoID: 1,
|
||||||
|
ArtifactPath: "artifact/path",
|
||||||
|
ArtifactName: "artifact-name",
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = x.Insert(&actionRunAttempt{
|
||||||
|
RepoID: 1,
|
||||||
|
RunID: 1,
|
||||||
|
Attempt: 2,
|
||||||
|
TriggerUserID: 1,
|
||||||
|
Status: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = x.Insert(&actionRunAttempt{
|
||||||
|
RepoID: 1,
|
||||||
|
RunID: 1,
|
||||||
|
Attempt: 2,
|
||||||
|
TriggerUserID: 2,
|
||||||
|
Status: 1,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasIndexWithColumns(indexes map[string]*schemas.Index, cols []string, isUnique bool) bool {
|
||||||
|
for _, index := range indexes {
|
||||||
|
if isUnique && index.Type != schemas.UniqueType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slices.Equal(index.Cols, cols) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -8,10 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/perm"
|
|
||||||
"code.gitea.io/gitea/models/unit"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@@ -129,49 +126,6 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64)
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrgAssignees returns all users that have write access and can be assigned to issues
|
|
||||||
// of the any repository in the organization.
|
|
||||||
func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) {
|
|
||||||
e := db.GetEngine(ctx)
|
|
||||||
userIDs := make([]int64, 0, 10)
|
|
||||||
if err = e.Table("access").
|
|
||||||
Join("INNER", "repository", "`repository`.id = `access`.repo_id").
|
|
||||||
Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite).
|
|
||||||
Select("user_id").
|
|
||||||
Find(&userIDs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
additionalUserIDs := make([]int64, 0, 10)
|
|
||||||
if err = e.Table("team_user").
|
|
||||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
|
||||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
|
||||||
Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id").
|
|
||||||
Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
|
||||||
orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
|
|
||||||
Distinct("`team_user`.uid").
|
|
||||||
Select("`team_user`.uid").
|
|
||||||
Find(&additionalUserIDs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqueUserIDs := make(container.Set[int64])
|
|
||||||
uniqueUserIDs.AddMultiple(userIDs...)
|
|
||||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
|
||||||
|
|
||||||
users := make([]*user_model.User, 0, len(uniqueUserIDs))
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
if err = e.In("id", uniqueUserIDs.Values()).
|
|
||||||
Where(builder.Eq{"`user`.is_active": true}).
|
|
||||||
OrderBy(user_model.GetOrderByName()).
|
|
||||||
Find(&users); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
|
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
return nil, nil //nolint:nilnil // return nil when there are no users
|
return nil, nil //nolint:nilnil // return nil when there are no users
|
||||||
|
|||||||
@@ -28,19 +28,3 @@ func (t *TeamUnit) Unit() unit.Unit {
|
|||||||
func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) {
|
func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) {
|
||||||
return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units)
|
return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTeamUnits updates a teams's units
|
|
||||||
func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(units) > 0 {
|
|
||||||
if err = db.Insert(ctx, units); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -36,14 +36,6 @@ type SearchMembersOptions struct {
|
|||||||
TeamID int64
|
TeamID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts SearchMembersOptions) ToConds() builder.Cond {
|
|
||||||
cond := builder.NewCond()
|
|
||||||
if opts.TeamID > 0 {
|
|
||||||
cond = cond.And(builder.Eq{"": opts.TeamID})
|
|
||||||
}
|
|
||||||
return cond
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTeamMembers returns all members in given team of organization.
|
// GetTeamMembers returns all members in given team of organization.
|
||||||
func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) {
|
func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) {
|
||||||
var members []*user_model.User
|
var members []*user_model.User
|
||||||
|
|||||||
@@ -337,20 +337,6 @@ func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateColumnSorting update project column sorting
|
|
||||||
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
for i := range cl {
|
|
||||||
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
|
|
||||||
"sorting",
|
|
||||||
).Update(cl[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
|
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
|
||||||
columns := make([]*Column, 0, 5)
|
columns := make([]*Column, 0, 5)
|
||||||
if len(columnsIDs) == 0 {
|
if len(columnsIDs) == 0 {
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ package pull
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
||||||
@@ -78,16 +76,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
|||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
scheduledPRM.DoerID, scheduledPRM.Doer, err = user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||||
if errors.Is(err, util.ErrNotExist) {
|
return true, scheduledPRM, err
|
||||||
doer, err = user_model.NewGhostUser(), nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduledPRM.Doer = doer
|
|
||||||
return true, scheduledPRM, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteScheduledAutoMerge delete a scheduled pull request
|
// DeleteScheduledAutoMerge delete a scheduled pull request
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Star represents a starred repo by an user.
|
// Star represents a starred repo by a user.
|
||||||
type Star struct {
|
type Star struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UID int64 `xorm:"UNIQUE(s)"`
|
UID int64 `xorm:"UNIQUE(s)"`
|
||||||
|
|||||||
@@ -44,12 +44,6 @@ type ErrTopicNotExist struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
|
|
||||||
func IsErrTopicNotExist(err error) bool {
|
|
||||||
_, ok := err.(ErrTopicNotExist)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements error interface
|
// Error implements error interface
|
||||||
func (err ErrTopicNotExist) Error() string {
|
func (err ErrTopicNotExist) Error() string {
|
||||||
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
|
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@@ -94,8 +95,7 @@ func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Reposit
|
|||||||
return db.FindAndCount[Repository](ctx, opts)
|
return db.FindAndCount[Repository](ctx, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoAssignees returns all users that have write access and can be assigned to issues
|
// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository,
|
||||||
// of the repository,
|
|
||||||
func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) {
|
func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) {
|
||||||
if err = repo.LoadOwner(ctx); err != nil {
|
if err = repo.LoadOwner(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -114,15 +114,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
|||||||
uniqueUserIDs.AddMultiple(userIDs...)
|
uniqueUserIDs.AddMultiple(userIDs...)
|
||||||
|
|
||||||
if repo.Owner.IsOrganization() {
|
if repo.Owner.IsOrganization() {
|
||||||
additionalUserIDs := make([]int64, 0, 10)
|
// issues and pull requests both need "assignee list"
|
||||||
if err = e.Table("team_user").
|
additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests)
|
||||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
if err != nil {
|
||||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
|
||||||
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
|
||||||
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
|
|
||||||
Distinct("`team_user`.uid").
|
|
||||||
Select("`team_user`.uid").
|
|
||||||
Find(&additionalUserIDs); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ package repo_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
|
perm_model "code.gitea.io/gitea/models/perm"
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
@@ -14,9 +19,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRepoAssignees(t *testing.T) {
|
func TestUserRepo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
t.Run("GetIssuePostersWithSearch", testUserRepoGetIssuePostersWithSearch)
|
||||||
|
t.Run("Assignees", testUserRepoAssignees)
|
||||||
|
t.Run("AssigneesNoTeamUnit", testRepoAssigneesNoTeamUnit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUserRepoAssignees(t *testing.T) {
|
||||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
users, err := repo_model.GetRepoAssignees(t.Context(), repo2)
|
users, err := repo_model.GetRepoAssignees(t.Context(), repo2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -39,9 +49,29 @@ func TestRepoAssignees(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetIssuePostersWithSearch(t *testing.T) {
|
func testRepoAssigneesNoTeamUnit(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
ctx := t.Context()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
|
||||||
|
require.NoError(t, repo.LoadOwner(ctx))
|
||||||
|
require.True(t, repo.Owner.IsOrganization())
|
||||||
|
|
||||||
|
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}, &access_model.Access{}))
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
team := &organization.Team{OrgID: repo.OwnerID, LowerName: "admin-team", AccessMode: perm_model.AccessModeAdmin}
|
||||||
|
require.NoError(t, db.Insert(ctx, team))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: repo.OwnerID, TeamID: team.ID, UID: user.ID}))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: repo.OwnerID, TeamID: team.ID, RepoID: repo.ID}))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: repo.OwnerID, TeamID: team.ID, Type: unit.TypePullRequests, AccessMode: perm_model.AccessModeNone}))
|
||||||
|
|
||||||
|
users, err := repo_model.GetRepoAssignees(ctx, repo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, users, 1)
|
||||||
|
assert.ElementsMatch(t, []int64{4}, []int64{users[0].ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUserRepoGetIssuePostersWithSearch(t *testing.T) {
|
||||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
|
||||||
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")
|
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NoticeType describes the notice type
|
// NoticeType describes the notice type
|
||||||
@@ -60,18 +59,6 @@ func CreateRepositoryNotice(desc string, args ...any) error {
|
|||||||
return CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc, args...)
|
return CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveAllWithNotice removes all directories in given path and
|
|
||||||
// creates a system notice when error occurs.
|
|
||||||
func RemoveAllWithNotice(ctx context.Context, title, path string) {
|
|
||||||
if err := util.RemoveAll(path); err != nil {
|
|
||||||
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
|
|
||||||
log.Warn(title+" [%s]: %v", path, err)
|
|
||||||
if err = CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc); err != nil {
|
|
||||||
log.Error("CreateRepositoryNotice: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveStorageWithNotice removes a file from the storage and
|
// RemoveStorageWithNotice removes a file from the storage and
|
||||||
// creates a system notice when error occurs.
|
// creates a system notice when error occurs.
|
||||||
func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) {
|
func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package unittest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockServerOptions tweaks NewMockWebServer behavior.
|
||||||
|
type MockServerOptions struct {
|
||||||
|
// Routes installs extra handlers on the mux before the fixture fallback;
|
||||||
|
// more specific patterns win.
|
||||||
|
Routes func(mux *http.ServeMux)
|
||||||
|
// StripPrefix is trimmed from the request path before forwarding upstream,
|
||||||
|
// useful when the client prepends a prefix the real upstream does not use
|
||||||
|
// (e.g. go-github prepends "/api/v3").
|
||||||
|
StripPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockWebServer returns a test HTTP server that records upstream responses on demand
|
||||||
|
// and replays them from disk on subsequent runs.
|
||||||
|
//
|
||||||
|
// - liveMode=true: requests are forwarded to liveServerBaseURL and responses written as
|
||||||
|
// fixture files under testDataDir.
|
||||||
|
// - liveMode=false: responses come from existing fixture files.
|
||||||
|
//
|
||||||
|
// Fixture format: header lines ("Name: value"), a blank line, then the body. Before
|
||||||
|
// replay, occurrences of liveServerBaseURL in the body are swapped for the mock URL.
|
||||||
|
//
|
||||||
|
// The typical switch is an env var holding an API token; fixtures ship committed so the
|
||||||
|
// default run (no token) works offline.
|
||||||
|
//
|
||||||
|
// token := os.Getenv("GITEA_TOKEN")
|
||||||
|
// mock := NewMockWebServer(t, "https://gitea.com", fixtureDir, token != "")
|
||||||
|
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool, opts ...MockServerOptions) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var opt MockServerOptions
|
||||||
|
if len(opts) > 0 {
|
||||||
|
opt = opts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id", "set-cookie"}
|
||||||
|
|
||||||
|
var mockURL string
|
||||||
|
|
||||||
|
fallback := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqPath := r.URL.EscapedPath()
|
||||||
|
if r.URL.RawQuery != "" {
|
||||||
|
reqPath += "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
log.Info("mock server: %s %s", r.Method, reqPath)
|
||||||
|
|
||||||
|
fixturePath := fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(reqPath))
|
||||||
|
if strings.Contains(r.URL.Path, ".git/") {
|
||||||
|
fixturePath = fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(r.URL.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if liveMode {
|
||||||
|
require.NoError(t, os.MkdirAll(testDataDir, 0o755))
|
||||||
|
|
||||||
|
liveURL := liveServerBaseURL + strings.TrimPrefix(reqPath, opt.StripPrefix)
|
||||||
|
req, err := http.NewRequest(r.Method, liveURL, r.Body)
|
||||||
|
require.NoError(t, err, "building upstream request to %s", liveURL)
|
||||||
|
for name, values := range r.Header {
|
||||||
|
if strings.EqualFold(name, "accept-encoding") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err, "upstream request to %s failed", liveURL)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Less(t, resp.StatusCode, 400, "upstream %s returned status %d", liveURL, resp.StatusCode)
|
||||||
|
|
||||||
|
out, err := os.Create(fixturePath)
|
||||||
|
require.NoError(t, err, "creating fixture %s", fixturePath)
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(resp.Header)) {
|
||||||
|
if slices.Contains(ignoredHeaders, strings.ToLower(name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range resp.Header[name] {
|
||||||
|
_, err := fmt.Fprintf(out, "%s: %s\n", name, value)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = out.WriteString("\n")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
require.NoError(t, err, "writing fixture body for %s", liveURL)
|
||||||
|
require.NoError(t, out.Sync())
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := os.ReadFile(fixturePath)
|
||||||
|
require.NoError(t, err, "missing fixture: %s", fixturePath)
|
||||||
|
|
||||||
|
replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL)
|
||||||
|
headers, body, _ := strings.Cut(replayed, "\n\n")
|
||||||
|
for line := range strings.SplitSeq(headers, "\n") {
|
||||||
|
name, value, ok := strings.Cut(line, ": ")
|
||||||
|
if !ok || strings.EqualFold(name, "Content-Length") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.Header().Set(name, value)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = w.Write([]byte(body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
if opt.Routes != nil {
|
||||||
|
opt.Routes(mux)
|
||||||
|
}
|
||||||
|
mux.Handle("/", fallback)
|
||||||
|
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
mockURL = server.URL
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
return server
|
||||||
|
}
|
||||||
+17
-72
@@ -13,10 +13,8 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/system"
|
"code.gitea.io/gitea/models/system"
|
||||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
|
||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/setting/config"
|
"code.gitea.io/gitea/modules/setting/config"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
@@ -29,37 +27,6 @@ import (
|
|||||||
"xorm.io/xorm/names"
|
"xorm.io/xorm/names"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitSettingsForTesting initializes config provider and load common settings for tests
|
|
||||||
func InitSettingsForTesting() {
|
|
||||||
setting.SetupGiteaTestEnv()
|
|
||||||
|
|
||||||
log.OsExiter = func(code int) {
|
|
||||||
if code != 0 {
|
|
||||||
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
|
|
||||||
panic(fmt.Errorf("non-zero exit code during testing: %d", code))
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
if setting.CustomConf == "" {
|
|
||||||
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
|
|
||||||
_ = os.Remove(setting.CustomConf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// init paths and config system for testing
|
|
||||||
getTestEnv := func(key string) string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf})
|
|
||||||
|
|
||||||
if err := setting.PrepareAppDataPath(); err != nil {
|
|
||||||
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
|
|
||||||
}
|
|
||||||
// register the dummy hash algorithm function used in the test fixtures
|
|
||||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
|
||||||
|
|
||||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestOptions represents test options
|
// TestOptions represents test options
|
||||||
type TestOptions struct {
|
type TestOptions struct {
|
||||||
FixtureFiles []string
|
FixtureFiles []string
|
||||||
@@ -75,11 +42,20 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
|||||||
|
|
||||||
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
||||||
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||||
InitSettingsForTesting()
|
|
||||||
|
tempWorkPath, tempCleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("unit-test-dir-")
|
||||||
|
if err != nil {
|
||||||
|
return testlogger.MainErrorf("Failed to create temp dir for unit test: %v", err)
|
||||||
|
}
|
||||||
|
defer tempCleanup()
|
||||||
|
|
||||||
|
defer setting.MockBuiltinPaths(tempWorkPath, "", "")()
|
||||||
|
setting.SetupGiteaTestEnv()
|
||||||
|
|
||||||
giteaRoot := setting.GetGiteaTestSourceRoot()
|
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||||
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
||||||
if err := CreateTestEngine(fixturesOpts); err != nil {
|
if err := CreateTestEngine(fixturesOpts); err != nil {
|
||||||
testlogger.Panicf("Error creating test engine: %v\n", err)
|
return testlogger.MainErrorf("Error creating test database engine: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
setting.AppURL = "https://try.gitea.io/"
|
setting.AppURL = "https://try.gitea.io/"
|
||||||
@@ -91,59 +67,28 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
|||||||
setting.SSH.Domain = "try.gitea.io"
|
setting.SSH.Domain = "try.gitea.io"
|
||||||
setting.Database.Type = "sqlite3"
|
setting.Database.Type = "sqlite3"
|
||||||
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
||||||
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
|
|
||||||
if err != nil {
|
|
||||||
testlogger.Panicf("TempDir: %v\n", err)
|
|
||||||
}
|
|
||||||
defer cleanup1()
|
|
||||||
|
|
||||||
setting.RepoRootPath = repoRootPath
|
|
||||||
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
|
|
||||||
if err != nil {
|
|
||||||
testlogger.Panicf("TempDir: %v\n", err)
|
|
||||||
}
|
|
||||||
defer cleanup2()
|
|
||||||
|
|
||||||
setting.AppDataPath = appDataPath
|
|
||||||
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
|
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
|
||||||
|
|
||||||
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
|
|
||||||
|
|
||||||
setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs")
|
|
||||||
|
|
||||||
setting.Avatar.Storage.Path = filepath.Join(setting.AppDataPath, "avatars")
|
|
||||||
|
|
||||||
setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars")
|
|
||||||
|
|
||||||
setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive")
|
|
||||||
|
|
||||||
setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
|
|
||||||
|
|
||||||
setting.Actions.LogStorage.Path = filepath.Join(setting.AppDataPath, "actions_log")
|
|
||||||
|
|
||||||
setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home")
|
|
||||||
|
|
||||||
setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"
|
setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"
|
||||||
|
|
||||||
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
||||||
|
|
||||||
if err = cache.Init(); err != nil {
|
if err = cache.Init(); err != nil {
|
||||||
testlogger.Panicf("cache.Init: %v\n", err)
|
return testlogger.MainErrorf("cache.Init: %v", err)
|
||||||
}
|
}
|
||||||
if err = storage.Init(); err != nil {
|
if err = storage.Init(); err != nil {
|
||||||
testlogger.Panicf("storage.Init: %v\n", err)
|
return testlogger.MainErrorf("storage.Init: %v", err)
|
||||||
}
|
}
|
||||||
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
||||||
testlogger.Panicf("util.SyncDirs: %v\n", err)
|
return testlogger.MainErrorf("util.SyncDirs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = git.InitFull(); err != nil {
|
if err = git.InitFull(); err != nil {
|
||||||
testlogger.Panicf("git.Init: %v\n", err)
|
return testlogger.MainErrorf("git.Init: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if testOpts.SetUp != nil {
|
if testOpts.SetUp != nil {
|
||||||
if err := testOpts.SetUp(); err != nil {
|
if err := testOpts.SetUp(); err != nil {
|
||||||
testlogger.Panicf("set up failed: %v\n", err)
|
return testlogger.MainErrorf("set up failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +96,7 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
|||||||
|
|
||||||
if testOpts.TearDown != nil {
|
if testOpts.TearDown != nil {
|
||||||
if err := testOpts.TearDown(); err != nil {
|
if err := testOpts.TearDown(); err != nil {
|
||||||
testlogger.Panicf("tear down failed: %v\n", err)
|
return testlogger.MainErrorf("tear down failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exitStatus
|
return exitStatus
|
||||||
|
|||||||
+1
-20
@@ -64,7 +64,7 @@ type GetBadgeUsersOptions struct {
|
|||||||
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
|
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
|
||||||
sess := db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
Select("`user`.*").
|
Select("`user`.*").
|
||||||
Join("INNER", "user_badge", "`user_badge`.user_id=user.id").
|
Join("INNER", "user_badge", "`user_badge`.user_id=`user`.id").
|
||||||
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
|
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
|
||||||
Where("badge.slug=?", opts.BadgeSlug)
|
Where("badge.slug=?", opts.BadgeSlug)
|
||||||
|
|
||||||
@@ -212,12 +212,6 @@ func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveAllUserBadges removes all badges from a user.
|
|
||||||
func RemoveAllUserBadges(ctx context.Context, u *User) error {
|
|
||||||
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchBadgeOptions represents the options when finding badges
|
// SearchBadgeOptions represents the options when finding badges
|
||||||
type SearchBadgeOptions struct {
|
type SearchBadgeOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
@@ -258,16 +252,3 @@ func (opts *SearchBadgeOptions) ToOrders() string {
|
|||||||
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
|
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
|
||||||
return db.FindAndCount[Badge](ctx, opts)
|
return db.FindAndCount[Badge](ctx, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBadgeByID returns a specific badge by ID
|
|
||||||
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
|
|
||||||
badge := new(Badge)
|
|
||||||
has, err := db.GetEngine(ctx).ID(id).Get(badge)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !has {
|
|
||||||
return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id)
|
|
||||||
}
|
|
||||||
return badge, nil
|
|
||||||
}
|
|
||||||
@@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(blocks) == 0 {
|
if len(blocks) == 0 {
|
||||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
return nil, util.NewNotExistErrorf("blocking record doesn't exist")
|
||||||
}
|
}
|
||||||
return blocks[0], nil
|
return blocks[0], nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,11 +147,6 @@ func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress
|
|||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
|
|
||||||
_, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateEmail check if email is a valid & allowed address
|
// ValidateEmail check if email is a valid & allowed address
|
||||||
func ValidateEmail(email string) error {
|
func ValidateEmail(email string) error {
|
||||||
if err := validateEmailBasic(email); err != nil {
|
if err := validateEmailBasic(email); err != nil {
|
||||||
|
|||||||
@@ -71,27 +71,6 @@ func (err ErrUserProhibitLogin) Unwrap() error {
|
|||||||
return util.ErrPermissionDenied
|
return util.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrUserInactive represents a "ErrUserInactive" kind of error.
|
|
||||||
type ErrUserInactive struct {
|
|
||||||
UID int64
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrUserInactive checks if an error is a ErrUserInactive
|
|
||||||
func IsErrUserInactive(err error) bool {
|
|
||||||
_, ok := err.(ErrUserInactive)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrUserInactive) Error() string {
|
|
||||||
return fmt.Sprintf("user is inactive [uid: %d, name: %s]", err.UID, err.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap unwraps this error as a ErrPermission error
|
|
||||||
func (err ErrUserInactive) Unwrap() error {
|
|
||||||
return util.ErrPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error.
|
// ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error.
|
||||||
type ErrUserIsNotLocal struct {
|
type ErrUserIsNotLocal struct {
|
||||||
UID int64
|
UID int64
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ type ErrExternalLoginUserAlreadyExist struct {
|
|||||||
LoginSourceID int64
|
LoginSourceID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist.
|
|
||||||
func IsErrExternalLoginUserAlreadyExist(err error) bool {
|
|
||||||
_, ok := err.(ErrExternalLoginUserAlreadyExist)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrExternalLoginUserAlreadyExist) Error() string {
|
func (err ErrExternalLoginUserAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID)
|
return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID)
|
||||||
}
|
}
|
||||||
@@ -41,12 +35,6 @@ type ErrExternalLoginUserNotExist struct {
|
|||||||
LoginSourceID int64
|
LoginSourceID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist.
|
|
||||||
func IsErrExternalLoginUserNotExist(err error) bool {
|
|
||||||
_, ok := err.(ErrExternalLoginUserNotExist)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrExternalLoginUserNotExist) Error() string {
|
func (err ErrExternalLoginUserNotExist) Error() string {
|
||||||
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
|
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,6 @@ func (err ErrUserSettingIsNotExist) Unwrap() error {
|
|||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrUserSettingIsNotExist return true if err is ErrSettingIsNotExist
|
|
||||||
func IsErrUserSettingIsNotExist(err error) bool {
|
|
||||||
_, ok := err.(ErrUserSettingIsNotExist)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// genSettingCacheKey returns the cache key for some configuration
|
// genSettingCacheKey returns the cache key for some configuration
|
||||||
func genSettingCacheKey(userID int64, key string) string {
|
func genSettingCacheKey(userID int64, key string) string {
|
||||||
return fmt.Sprintf("user_%d.setting.%s", userID, key)
|
return fmt.Sprintf("user_%d.setting.%s", userID, key)
|
||||||
|
|||||||
+13
-7
@@ -7,6 +7,7 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -1013,17 +1014,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
|||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
|
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
|
||||||
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
|
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
|
||||||
if id < 0 {
|
if id < 0 {
|
||||||
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
|
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
|
||||||
return newFunc(), nil
|
u = newFunc()
|
||||||
}
|
}
|
||||||
return nil, ErrUserNotExist{UID: id}
|
|
||||||
} else if id == 0 {
|
|
||||||
return nil, ErrUserNotExist{}
|
|
||||||
}
|
}
|
||||||
return GetUserByID(ctx, id)
|
if u == nil {
|
||||||
|
u, err = GetUserByID(ctx, id)
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
u = NewGhostUser()
|
||||||
|
} else if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return u.ID, u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
|
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSystemUser(t *testing.T) {
|
func TestSystemUser(t *testing.T) {
|
||||||
u, err := GetPossibleUserByID(t.Context(), -1)
|
uid, u, err := GetPossibleUserByID(t.Context(), -1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(-1), uid)
|
||||||
assert.Equal(t, "Ghost", u.Name)
|
assert.Equal(t, "Ghost", u.Name)
|
||||||
assert.Equal(t, "ghost", u.LowerName)
|
assert.Equal(t, "ghost", u.LowerName)
|
||||||
assert.True(t, u.IsGhost())
|
assert.True(t, u.IsGhost())
|
||||||
@@ -21,8 +22,9 @@ func TestSystemUser(t *testing.T) {
|
|||||||
require.NotNil(t, u)
|
require.NotNil(t, u)
|
||||||
assert.Equal(t, "Ghost", u.Name)
|
assert.Equal(t, "Ghost", u.Name)
|
||||||
|
|
||||||
u, err = GetPossibleUserByID(t.Context(), -2)
|
uid, u, err = GetPossibleUserByID(t.Context(), -2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(-2), uid)
|
||||||
assert.Equal(t, "gitea-actions", u.Name)
|
assert.Equal(t, "gitea-actions", u.Name)
|
||||||
assert.Equal(t, "gitea-actions", u.LowerName)
|
assert.Equal(t, "gitea-actions", u.LowerName)
|
||||||
assert.True(t, u.IsGiteaActions())
|
assert.True(t, u.IsGiteaActions())
|
||||||
@@ -31,6 +33,8 @@ func TestSystemUser(t *testing.T) {
|
|||||||
require.NotNil(t, u)
|
require.NotNil(t, u)
|
||||||
assert.Equal(t, "Gitea Actions", u.FullName)
|
assert.Equal(t, "Gitea Actions", u.FullName)
|
||||||
|
|
||||||
_, err = GetPossibleUserByID(t.Context(), -3)
|
uid, u, err = GetPossibleUserByID(t.Context(), 999999)
|
||||||
require.Error(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(-1), uid)
|
||||||
|
assert.Equal(t, "Ghost", u.Name)
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@ func TestMain(m *testing.M) {
|
|||||||
"webhook.yml",
|
"webhook.yml",
|
||||||
"hook_task.yml",
|
"hook_task.yml",
|
||||||
},
|
},
|
||||||
|
SetUp: prepareWebhookTestData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -10,28 +10,28 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestListSystemWebhookOptions(t *testing.T) {
|
func TestListSystemWebhookOptions(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hookSystem := unittest.AssertExistsAndLoadBean(t, &Webhook{URL: "https://www.example.com/system"})
|
||||||
|
hookDefault := unittest.AssertExistsAndLoadBean(t, &Webhook{URL: "https://www.example.com/default"})
|
||||||
opts := ListSystemWebhookOptions{IsSystem: optional.None[bool]()}
|
opts := ListSystemWebhookOptions{IsSystem: optional.None[bool]()}
|
||||||
hooks, _, err := GetGlobalWebhooks(t.Context(), &opts)
|
hooks, _, err := GetGlobalWebhooks(t.Context(), &opts)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 2) {
|
require.Len(t, hooks, 2)
|
||||||
assert.Equal(t, int64(5), hooks[0].ID)
|
assert.Equal(t, hookSystem.ID, hooks[0].ID)
|
||||||
assert.Equal(t, int64(6), hooks[1].ID)
|
assert.Equal(t, hookDefault.ID, hooks[1].ID)
|
||||||
}
|
|
||||||
opts.IsSystem = optional.Some(true)
|
opts.IsSystem = optional.Some(true)
|
||||||
hooks, _, err = GetGlobalWebhooks(t.Context(), &opts)
|
hooks, _, err = GetGlobalWebhooks(t.Context(), &opts)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 1) {
|
require.Len(t, hooks, 1)
|
||||||
assert.Equal(t, int64(5), hooks[0].ID)
|
assert.Equal(t, hookSystem.ID, hooks[0].ID)
|
||||||
}
|
|
||||||
|
|
||||||
opts.IsSystem = optional.Some(false)
|
opts.IsSystem = optional.Some(false)
|
||||||
hooks, _, err = GetGlobalWebhooks(t.Context(), &opts)
|
hooks, _, err = GetGlobalWebhooks(t.Context(), &opts)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 1) {
|
require.Len(t, hooks, 1)
|
||||||
assert.Equal(t, int64(6), hooks[0].ID)
|
assert.Equal(t, hookDefault.ID, hooks[0].ID)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+151
-92
@@ -4,6 +4,7 @@
|
|||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,40 +15,105 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHookContentType_Name(t *testing.T) {
|
func prepareWebhookTestData() error {
|
||||||
assert.Equal(t, "json", ContentTypeJSON.Name())
|
if err := unittest.PrepareTestDatabase(); err != nil {
|
||||||
assert.Equal(t, "form", ContentTypeForm.Name())
|
return err
|
||||||
|
}
|
||||||
|
var hooks []*Webhook
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
RepoID: 1,
|
||||||
|
URL: "https://www.example.com/url1",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}`,
|
||||||
|
IsActive: true,
|
||||||
|
})
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
RepoID: 1,
|
||||||
|
URL: "https://www.example.com/url2",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{}`,
|
||||||
|
IsActive: false,
|
||||||
|
})
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
OwnerID: 3,
|
||||||
|
RepoID: 3,
|
||||||
|
URL: "https://www.example.com/url3",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
|
||||||
|
IsActive: true,
|
||||||
|
})
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
OwnerID: 3,
|
||||||
|
RepoID: 3,
|
||||||
|
URL: "https://www.example.com/url3",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{}`,
|
||||||
|
})
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
RepoID: 2,
|
||||||
|
URL: "https://www.example.com/url4",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
|
||||||
|
IsActive: true,
|
||||||
|
})
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
URL: "https://www.example.com/system",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
|
||||||
|
IsSystemWebhook: true,
|
||||||
|
})
|
||||||
|
hooks = append(hooks, &Webhook{
|
||||||
|
URL: "https://www.example.com/default",
|
||||||
|
ContentType: ContentTypeJSON,
|
||||||
|
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
|
||||||
|
})
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := db.TruncateBeans(ctx, &Webhook{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Insert(ctx, hooks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hook, _, _ := db.Get[Webhook](ctx, builder.Eq{"repo_id": 1, "is_active": true})
|
||||||
|
var tasks []*HookTask
|
||||||
|
tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()})
|
||||||
|
tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()})
|
||||||
|
tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()})
|
||||||
|
if err := db.TruncateBeans(ctx, &HookTask{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.Insert(ctx, tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidHookContentType(t *testing.T) {
|
func TestWebHookContentType(t *testing.T) {
|
||||||
|
assert.Equal(t, "json", ContentTypeJSON.Name())
|
||||||
|
assert.Equal(t, "form", ContentTypeForm.Name())
|
||||||
assert.True(t, IsValidHookContentType("json"))
|
assert.True(t, IsValidHookContentType("json"))
|
||||||
assert.True(t, IsValidHookContentType("form"))
|
assert.True(t, IsValidHookContentType("form"))
|
||||||
assert.False(t, IsValidHookContentType("invalid"))
|
assert.False(t, IsValidHookContentType("invalid"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhook_History(t *testing.T) {
|
func TestWebhook_History(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
|
||||||
webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
|
tasks, err := hook.History(t.Context(), 0)
|
||||||
tasks, err := webhook.History(t.Context(), 0)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, err)
|
require.Len(t, tasks, 3)
|
||||||
if assert.Len(t, tasks, 3) {
|
|
||||||
assert.Equal(t, int64(3), tasks[0].ID)
|
|
||||||
assert.Equal(t, int64(2), tasks[1].ID)
|
|
||||||
assert.Equal(t, int64(1), tasks[2].ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
|
hook = unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: "{}"})
|
||||||
tasks, err = webhook.History(t.Context(), 0)
|
tasks, err = hook.History(t.Context(), 0)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, tasks)
|
assert.Empty(t, tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhook_UpdateEvent(t *testing.T) {
|
func TestWebhook_UpdateEvent(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
|
||||||
webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
|
|
||||||
hookEvent := &webhook_module.HookEvent{
|
hookEvent := &webhook_module.HookEvent{
|
||||||
PushOnly: true,
|
PushOnly: true,
|
||||||
SendEverything: false,
|
SendEverything: false,
|
||||||
@@ -100,10 +166,10 @@ func TestCreateWebhook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetWebhookByRepoID(t *testing.T) {
|
func TestGetWebhookByRepoID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
|
||||||
hook, err := GetWebhookByRepoID(t.Context(), 1, 1)
|
loaded, err := GetWebhookByRepoID(t.Context(), 1, hook.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(1), hook.ID)
|
assert.Equal(t, hook.ID, loaded.ID)
|
||||||
|
|
||||||
_, err = GetWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
_, err = GetWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -111,10 +177,10 @@ func TestGetWebhookByRepoID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetWebhookByOwnerID(t *testing.T) {
|
func TestGetWebhookByOwnerID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3})
|
||||||
hook, err := GetWebhookByOwnerID(t.Context(), 3, 3)
|
loaded, err := GetWebhookByOwnerID(t.Context(), 3, hook.ID)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, int64(3), hook.ID)
|
require.Equal(t, hook.ID, loaded.ID)
|
||||||
|
|
||||||
_, err = GetWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
_, err = GetWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -122,48 +188,45 @@ func TestGetWebhookByOwnerID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetActiveWebhooksByRepoID(t *testing.T) {
|
func TestGetActiveWebhooksByRepoID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
|
||||||
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
|
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 1) {
|
require.Len(t, hooks, 1)
|
||||||
assert.Equal(t, int64(1), hooks[0].ID)
|
assert.Equal(t, hook.ID, hooks[0].ID)
|
||||||
assert.True(t, hooks[0].IsActive)
|
assert.True(t, hooks[0].IsActive)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetWebhooksByRepoID(t *testing.T) {
|
func TestGetWebhooksByRepoID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1})
|
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 2) {
|
require.Len(t, hooks, 2)
|
||||||
assert.Equal(t, int64(1), hooks[0].ID)
|
assert.Equal(t, int64(1), hooks[0].RepoID)
|
||||||
assert.Equal(t, int64(2), hooks[1].ID)
|
assert.True(t, hooks[0].IsActive)
|
||||||
}
|
assert.Equal(t, int64(1), hooks[1].RepoID)
|
||||||
|
assert.False(t, hooks[1].IsActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetActiveWebhooksByOwnerID(t *testing.T) {
|
func TestGetActiveWebhooksByOwnerID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
|
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 1) {
|
require.Len(t, hooks, 1)
|
||||||
assert.Equal(t, int64(3), hooks[0].ID)
|
assert.Equal(t, int64(3), hooks[0].OwnerID)
|
||||||
assert.True(t, hooks[0].IsActive)
|
assert.True(t, hooks[0].IsActive)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetWebhooksByOwnerID(t *testing.T) {
|
func TestGetWebhooksByOwnerID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3})
|
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if assert.Len(t, hooks, 1) {
|
require.Len(t, hooks, 2)
|
||||||
assert.Equal(t, int64(3), hooks[0].ID)
|
assert.Equal(t, int64(3), hooks[0].OwnerID)
|
||||||
assert.True(t, hooks[0].IsActive)
|
assert.True(t, hooks[0].IsActive)
|
||||||
}
|
assert.Equal(t, int64(3), hooks[1].OwnerID)
|
||||||
|
assert.False(t, hooks[1].IsActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateWebhook(t *testing.T) {
|
func TestUpdateWebhook(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`})
|
||||||
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
|
require.False(t, hook.IsActive)
|
||||||
hook.IsActive = true
|
hook.IsActive = true
|
||||||
hook.ContentType = ContentTypeForm
|
hook.ContentType = ContentTypeForm
|
||||||
unittest.AssertNotExistsBean(t, hook)
|
unittest.AssertNotExistsBean(t, hook)
|
||||||
@@ -172,48 +235,36 @@ func TestUpdateWebhook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteWebhookByRepoID(t *testing.T) {
|
func TestDeleteWebhookByRepoID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`})
|
||||||
unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1})
|
assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, hook.ID))
|
||||||
assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, 2))
|
unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, RepoID: 1})
|
||||||
unittest.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1})
|
|
||||||
|
|
||||||
err := DeleteWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
err := DeleteWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, IsErrWebhookNotExist(err))
|
assert.True(t, IsErrWebhookNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteWebhookByOwnerID(t *testing.T) {
|
func TestDeleteWebhookByOwnerID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, Events: `{}`})
|
||||||
unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3})
|
assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, hook.ID))
|
||||||
assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, 3))
|
unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, OwnerID: 3})
|
||||||
unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3})
|
|
||||||
|
|
||||||
err := DeleteWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
err := DeleteWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, IsErrWebhookNotExist(err))
|
assert.True(t, IsErrWebhookNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHookTasks(t *testing.T) {
|
func TestHookTasks(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
|
||||||
hookTasks, err := HookTasks(t.Context(), 1, 1)
|
hookTasks, err := HookTasks(t.Context(), hook.ID, 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.Len(t, hookTasks, 3) {
|
assert.Len(t, hookTasks, 3)
|
||||||
assert.Equal(t, int64(3), hookTasks[0].ID)
|
|
||||||
assert.Equal(t, int64(2), hookTasks[1].ID)
|
|
||||||
assert.Equal(t, int64(1), hookTasks[2].ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1)
|
hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, hookTasks)
|
assert.Empty(t, hookTasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateHookTask(t *testing.T) {
|
func TestCreateHookTask(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true})
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2}
|
||||||
HookID: 3,
|
|
||||||
PayloadVersion: 2,
|
|
||||||
}
|
|
||||||
unittest.AssertNotExistsBean(t, hookTask)
|
unittest.AssertNotExistsBean(t, hookTask)
|
||||||
_, err := CreateHookTask(t.Context(), hookTask)
|
_, err := CreateHookTask(t.Context(), hookTask)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -221,20 +272,23 @@ func TestCreateHookTask(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateHookTask(t *testing.T) {
|
func TestUpdateHookTask(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true})
|
||||||
|
hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2}
|
||||||
|
_, err := CreateHookTask(t.Context(), hookTask)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
hook := unittest.AssertExistsAndLoadBean(t, &HookTask{ID: 1})
|
hookTask.PayloadContent = "new payload content"
|
||||||
hook.PayloadContent = "new payload content"
|
hookTask.IsDelivered = true
|
||||||
hook.IsDelivered = true
|
unittest.AssertNotExistsBean(t, hookTask)
|
||||||
unittest.AssertNotExistsBean(t, hook)
|
assert.NoError(t, UpdateHookTask(t.Context(), hookTask))
|
||||||
assert.NoError(t, UpdateHookTask(t.Context(), hook))
|
unittest.AssertExistsAndLoadBean(t, hookTask)
|
||||||
unittest.AssertExistsAndLoadBean(t, hook)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
|
func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup1", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), hook))
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{
|
||||||
HookID: 3,
|
HookID: hook.ID,
|
||||||
IsDelivered: true,
|
IsDelivered: true,
|
||||||
Delivered: timeutil.TimeStampNanoNow(),
|
Delivered: timeutil.TimeStampNanoNow(),
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
@@ -249,9 +303,10 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
|
func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup2", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), hook))
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{
|
||||||
HookID: 4,
|
HookID: hook.ID,
|
||||||
IsDelivered: false,
|
IsDelivered: false,
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
@@ -265,9 +320,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
|
func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup3", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), hook))
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{
|
||||||
HookID: 4,
|
HookID: hook.ID,
|
||||||
IsDelivered: true,
|
IsDelivered: true,
|
||||||
Delivered: timeutil.TimeStampNanoNow(),
|
Delivered: timeutil.TimeStampNanoNow(),
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
@@ -282,9 +338,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
|
func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup4", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), hook))
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{
|
||||||
HookID: 3,
|
HookID: hook.ID,
|
||||||
IsDelivered: true,
|
IsDelivered: true,
|
||||||
Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
|
Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
@@ -299,9 +356,10 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
|
func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup5", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), hook))
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{
|
||||||
HookID: 4,
|
HookID: hook.ID,
|
||||||
IsDelivered: false,
|
IsDelivered: false,
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
@@ -315,9 +373,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
|
func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup6", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), hook))
|
||||||
hookTask := &HookTask{
|
hookTask := &HookTask{
|
||||||
HookID: 4,
|
HookID: hook.ID,
|
||||||
IsDelivered: true,
|
IsDelivered: true,
|
||||||
Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
|
Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithJobResults(results map[string]string) ParseOption {
|
|
||||||
return func(c *parseContext) {
|
|
||||||
c.jobResults = results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithGitContext(context *model.GithubContext) ParseOption {
|
func WithGitContext(context *model.GithubContext) ParseOption {
|
||||||
return func(c *parseContext) {
|
return func(c *parseContext) {
|
||||||
c.gitContext = context
|
c.gitContext = context
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package dump
|
package dump
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -85,7 +86,7 @@ func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, e
|
|||||||
var comp archives.ArchiverAsync
|
var comp archives.ArchiverAsync
|
||||||
switch format {
|
switch format {
|
||||||
case "zip":
|
case "zip":
|
||||||
comp = archives.Zip{}
|
comp = archives.Zip{Compression: zip.Deflate}
|
||||||
case "tar":
|
case "tar":
|
||||||
comp = archives.Tar{}
|
comp = archives.Tar{}
|
||||||
case "tar.sz":
|
case "tar.sz":
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
@@ -110,9 +109,3 @@ func (e *Event) WriteTo(w io.Writer) (int64, error) {
|
|||||||
|
|
||||||
return sum, err
|
return sum, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Event) String() string {
|
|
||||||
buf := new(strings.Builder)
|
|
||||||
_, _ = e.WriteTo(buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
+2
-2
@@ -192,13 +192,13 @@ func RunGitTests(m interface{ Run() int }) {
|
|||||||
func runGitTests(m interface{ Run() int }) int {
|
func runGitTests(m interface{ Run() int }) int {
|
||||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
testlogger.Panicf("unable to create temp dir: %s", err.Error())
|
return testlogger.MainErrorf("unable to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
setting.Git.HomePath = gitHomePath
|
setting.Git.HomePath = gitHomePath
|
||||||
if err = InitFull(); err != nil {
|
if err = InitFull(); err != nil {
|
||||||
testlogger.Panicf("failed to call Init: %s", err.Error())
|
return testlogger.MainErrorf("failed to call Init: %v", err)
|
||||||
}
|
}
|
||||||
return m.Run()
|
return m.Run()
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ func testMain(m *testing.M) int {
|
|||||||
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
|
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
|
||||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
testlogger.Panicf("failed to create temp dir: %v", err)
|
return testlogger.MainErrorf("failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ func StderrHasPrefix(err error, prefix string) bool {
|
|||||||
return strings.HasPrefix(stderr, prefix)
|
return strings.HasPrefix(stderr, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StderrContains(err error, sub string) bool {
|
||||||
|
stderr, ok := ErrorAsStderr(err)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(stderr, sub)
|
||||||
|
}
|
||||||
|
|
||||||
func IsErrorExitCode(err error, code int) bool {
|
func IsErrorExitCode(err error, code int) bool {
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
if errors.As(err, &exitError) {
|
if errors.As(err, &exitError) {
|
||||||
|
|||||||
@@ -44,23 +44,6 @@ func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions
|
|||||||
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitsCountBetween return numbers of commits between two commits
|
|
||||||
func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) {
|
|
||||||
count, err := CommitsCount(ctx, repo, CommitsCountOptions{
|
|
||||||
Revision: []string{start + ".." + end},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil && strings.Contains(err.Error(), "no merge base") {
|
|
||||||
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
|
|
||||||
// previously it would return the results of git rev-list before last so let's try that...
|
|
||||||
return CommitsCount(ctx, repo, CommitsCountOptions{
|
|
||||||
Revision: []string{start, end},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileCommitsCount return the number of files at a revision
|
// FileCommitsCount return the number of files at a revision
|
||||||
func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) {
|
func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) {
|
||||||
return CommitsCount(ctx, repo,
|
return CommitsCount(ctx, repo,
|
||||||
|
|||||||
@@ -5,21 +5,11 @@ package gitrepo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/globallock"
|
"code.gitea.io/gitea/modules/globallock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) {
|
|
||||||
result, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--get").
|
|
||||||
AddDynamicArguments(key))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRepoConfigLockKey(repoStoragePath string) string {
|
func getRepoConfigLockKey(repoStoragePath string) string {
|
||||||
return "repo-config:" + repoStoragePath
|
return "repo-config:" + repoStoragePath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,11 +78,6 @@ func (hl *HostMatchList) AppendBuiltin(builtin string) {
|
|||||||
hl.builtins = append(hl.builtins, builtin)
|
hl.builtins = append(hl.builtins, builtin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendPattern appends more pattern to match
|
|
||||||
func (hl *HostMatchList) AppendPattern(pattern string) {
|
|
||||||
hl.patterns = append(hl.patterns, pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmpty checks if the checklist is empty
|
// IsEmpty checks if the checklist is empty
|
||||||
func (hl *HostMatchList) IsEmpty() bool {
|
func (hl *HostMatchList) IsEmpty() bool {
|
||||||
return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0)
|
return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0)
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ func TestRender_email(t *testing.T) {
|
|||||||
|
|
||||||
func TestRender_emoji(t *testing.T) {
|
func TestRender_emoji(t *testing.T) {
|
||||||
setting.AppURL = markup.TestAppURL
|
setting.AppURL = markup.TestAppURL
|
||||||
setting.StaticURLPrefix = markup.TestAppURL
|
setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/")
|
||||||
|
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
expected = strings.ReplaceAll(expected, "&", "&")
|
expected = strings.ReplaceAll(expected, "&", "&")
|
||||||
@@ -500,7 +500,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPostProcess(t *testing.T) {
|
func TestPostProcess(t *testing.T) {
|
||||||
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
|
setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/") // can't run standalone
|
||||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||||
|
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser
|
|||||||
return r.goldmarkMarkdown.Convert(source, writer, opts...)
|
return r.goldmarkMarkdown.Convert(source, writer, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *GlodmarkRender) Renderer() renderer.Renderer {
|
|
||||||
return r.goldmarkMarkdown.Renderer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||||
if entering {
|
if entering {
|
||||||
languageBytes, _ := c.Language()
|
languageBytes, _ := c.Language()
|
||||||
|
|||||||
Loaded 100 of 714 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user