Merge branch 'main' into bun

This commit is contained in:
pat-s 2024-05-02 22:10:43 +02:00
commit 9586c6adcb
No known key found for this signature in database
GPG key ID: 3C6318841EF78925
432 changed files with 21207 additions and 8257 deletions

View file

@ -80,7 +80,22 @@
"devx",
"gomod",
"laszlocph",
"lockb"
"lockb",
"charmbracelet",
"GODEBUG",
"netdns",
"BUILDPLATFORM",
"repology",
"WORKDIR",
"corepack",
"binutils",
"nocolor",
"logfile",
"Keyfunc",
"protoc",
"PROTOC",
"GOBIN",
"GOPATH"
],
"ignorePaths": [
"**/node_modules/**/*",
@ -105,10 +120,8 @@
"agent/",
"cli/",
"cmd/",
"docker/",
"docs/",
"pipeline/",
"shared/",
"server/"
],
"enableFiletypes": ["dockercompose"]

16
.github/renovate.json vendored
View file

@ -2,18 +2,28 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>woodpecker-ci/renovate-config"],
"packageRules": [
{
"matchCurrentVersion": "<1.0.0",
"matchPackageNames": ["github.com/distribution/reference"],
"matchUpdateTypes": ["major", "minor"],
"dependencyDashboardApproval": true
},
{
"matchPackageNames": ["github.com/charmbracelet/huh/spinner"],
"enabled": false
},
{
"matchManagers": ["docker-compose"],
"matchFileNames": ["docker-compose.gitpod.yml"],
"matchFileNames": ["docker-compose.gitpod.yaml"],
"addLabels": ["devx"]
},
{
"groupName": "golang (lang)",
"groupName": "golang-lang",
"matchPackagePatterns": ["^golang$", "xgo"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "golang (packages)",
"groupName": "golang-packages",
"matchManagers": ["gomod"],
"matchUpdateTypes": ["minor", "patch"]
},

3
.gitignore vendored
View file

@ -13,7 +13,7 @@
*.so
*.dylib
vendor/
__debug_bin
__debug_bin*
# Test binary, built with `go test -c`
*.test
@ -41,6 +41,7 @@ extras/
/build/
/dist/
/data/
datastore/migration/testfiles/
docs/venv

View file

@ -166,6 +166,13 @@ linters:
- contextcheck
- forcetypeassert
- gci
- gomnd
issues:
exclude-rules:
- path: 'fixtures|cmd/agent/flags.go|cmd/server/flags.go|pipeline/backend/kubernetes/flags.go|_test.go'
linters:
- gomnd
run:
timeout: 15m

2
.mockery.yaml Normal file
View file

@ -0,0 +1,2 @@
---
disable-version-string: true

View file

@ -5,12 +5,12 @@ repos:
- id: check-hooks-apply
- id: check-useless-excludes
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/golangci/golangci-lint
rev: v1.55.2
rev: v1.57.2
hooks:
- id: golangci-lint
- repo: https://github.com/igorshubovych/markdownlint-cli
@ -24,7 +24,7 @@ repos:
- id: checkmake
exclude: '^docker/Dockerfile.make$' # actually a Dockerfile and not a makefile
- repo: https://github.com/hadolint/hadolint
rev: v2.12.1-beta
rev: v2.12.0
hooks:
- id: hadolint
- repo: https://github.com/pre-commit/mirrors-prettier
@ -32,7 +32,7 @@ repos:
hooks:
- id: prettier
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.33.0
rev: v1.35.1
hooks:
- id: yamllint
args: [--strict, -c=.yamllint.yaml]
@ -53,7 +53,7 @@ ci:
autofix_prs: true
autoupdate_branch: ''
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
autoupdate_schedule: monthly
autoupdate_schedule: quarterly
# NB: hadolint not included in pre-commit.ci
skip: [check-hooks-apply, check-useless-excludes, hadolint, prettier]
submodules: false

View file

@ -2,9 +2,10 @@ when:
event: tag
variables:
- &golang_image 'docker.io/golang:1.22.0'
- &golang_image 'docker.io/golang:1.22.2'
- &bun_image 'docker.io/oven/bun:1.0-alpine'
- &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.0'
# TODO: switch back to upstream image after https://github.com/techknowlogick/xgo/pull/224 got merged
- &xgo_image 'docker.io/pats22/xgo:go-1.22.1'
- &xgo_version 'go-1.21.2'
steps:
@ -89,11 +90,10 @@ steps:
release:
depends_on:
- checksums
image: docker.io/plugins/github-release
secrets:
- source: github_token
target: github_release_api_key
image: woodpeckerci/plugin-github-release:1.1.2
settings:
api_key:
from_secret: github_token
files:
- dist/*.tar.gz
- dist/*.deb

View file

@ -1,9 +1,10 @@
variables:
- &golang_image 'docker.io/golang:1.22.0'
- &golang_image 'docker.io/golang:1.22.2'
- &bun_image 'docker.io/oven/bun:1.0-alpine'
- &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.0'
# TODO: switch back to upstream image after https://github.com/techknowlogick/xgo/pull/224 got merged
- &xgo_image 'docker.io/pats22/xgo:go-1.22.1'
- &xgo_version 'go-1.21.2'
- &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:3.1.0'
- &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:3.2.1'
- &platforms_release 'linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/386,linux/amd64,linux/ppc64le,linux/riscv64,linux/s390x,freebsd/arm64,freebsd/amd64,openbsd/arm64,openbsd/amd64'
- &platforms_server 'linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le,linux/riscv64'
- &platforms_preview 'linux/amd64'

View file

@ -1,6 +1,6 @@
steps:
release-helper:
image: woodpeckerci/plugin-ready-release-go:1.0.3
image: woodpeckerci/plugin-ready-release-go:1.1.1
pull: true
settings:
release_branch: ${CI_REPO_DEFAULT_BRANCH}

25
.woodpecker/social.yaml Normal file
View file

@ -0,0 +1,25 @@
depends_on:
- docker
- binaries
when:
- event: tag
steps:
mastodon-toot:
image: docker.io/woodpeckerci/plugin-mastodon-post
settings:
server: https://floss.social
access_token:
from_secret: mastodon_token
visibility: public
ai_token:
from_secret: openai_token
ai_prompt: |
We want to present the next version of our app on Twitter.
Therefore we want to post a catching text, so users will know why they should
update to the newest version. If there is no special feature included
just summarize the changes in a few sentences. Use #WoodpeckerCI, #release and
additional fitting hashtags and emojis to make the post more appealing.
The changelog entry: {{ changelog }}

View file

@ -13,7 +13,7 @@ steps:
branch: renovate/*
- name: spellcheck
image: docker.io/node:21-alpine
image: docker.io/node:22-alpine
depends_on: []
commands:
- corepack enable
@ -23,7 +23,7 @@ steps:
image: docker.io/woodpeckerci/plugin-prettier:0.1.0
depends_on: []
settings:
version: 3.2.4
version: 3.2.5
- name: links
image: lycheeverse/lychee:0.14.3

View file

@ -38,6 +38,8 @@ steps:
image: *golang_image
commands:
- go run go.woodpecker-ci.org/woodpecker/v2/cmd/cli lint
environment:
WOODPECKER_DISABLE_UPDATE_CHECK: true
when:
- event: pull_request
path:
@ -93,7 +95,7 @@ steps:
- vendor
image: *golang_image
environment:
- WOODPECKER_DATABASE_DRIVER=sqlite3
WOODPECKER_DATABASE_DRIVER: sqlite3
commands:
- make test-server-datastore-coverage
when:
@ -104,8 +106,8 @@ steps:
- vendor
image: *golang_image
environment:
- WOODPECKER_DATABASE_DRIVER=postgres
- WOODPECKER_DATABASE_DATASOURCE=host=postgres user=postgres dbname=postgres sslmode=disable
WOODPECKER_DATABASE_DRIVER: postgres
WOODPECKER_DATABASE_DATASOURCE: 'host=postgres user=postgres dbname=postgres sslmode=disable'
commands:
- make test-server-datastore
when: *when
@ -115,8 +117,8 @@ steps:
- vendor
image: *golang_image
environment:
- WOODPECKER_DATABASE_DRIVER=mysql
- WOODPECKER_DATABASE_DATASOURCE=root@tcp(mysql:3306)/test?parseTime=true
WOODPECKER_DATABASE_DRIVER: mysql
WOODPECKER_DATABASE_DATASOURCE: root@tcp(mysql:3306)/test?parseTime=true
commands:
- make test-server-datastore
when: *when
@ -145,14 +147,14 @@ services:
image: docker.io/postgres:16
ports: ['5432']
environment:
- POSTGRES_USER=postgres
- POSTGRES_HOST_AUTH_METHOD=trust
POSTGRES_USER: postgres
POSTGRES_HOST_AUTH_METHOD: trust
when: *when
mysql:
image: docker.io/mysql:8.2.0
ports: ['3306']
environment:
- MYSQL_DATABASE=test
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
MYSQL_DATABASE: test
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
when: *when

View file

@ -1,6 +1,131 @@
# Changelog
## [2.3.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/2.3.0) - 2024-01-31
## [2.4.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.4.1) - 2024-03-20
### ❤️ Thanks to all contributors! ❤️
@manuelluis, @qwerty287, @xoxys
### 🔒 Security
- Only allow to deploy from push, tag and release [[#3522](https://github.com/woodpecker-ci/woodpecker/pull/3522)]
### 🐛 Bug Fixes
- Exclude setup from cli command exec. [[#3523](https://github.com/woodpecker-ci/woodpecker/pull/3523)]
- Fix uppercased env [[#3516](https://github.com/woodpecker-ci/woodpecker/pull/3516)]
- Fix env schema [[#3514](https://github.com/woodpecker-ci/woodpecker/pull/3514)]
### Misc
- Temp pin golangci version in makefile [[#3520](https://github.com/woodpecker-ci/woodpecker/pull/3520)]
## [2.4.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.4.0) - 2024-03-19
### ❤️ Thanks to all contributors! ❤️
@6543, @Ray-D-Song, @anbraten, @eliasscosta, @fernandrone, @kjuulh, @kytta, @langecode, @lukashass, @qwerty287, @rockdrilla, @sinlov, @smainz, @xoxys, @zc-devs, @zowhoey
### 🔒 Security
- Improve security context handling [[#3482](https://github.com/woodpecker-ci/woodpecker/pull/3482)]
- fix(deps): update module github.com/moby/moby to v24.0.9+incompatible [[#3323](https://github.com/woodpecker-ci/woodpecker/pull/3323)]
### ✨ Features
- Cli setup command [[#3384](https://github.com/woodpecker-ci/woodpecker/pull/3384)]
- Add bitbucket datacenter (server) support [[#2503](https://github.com/woodpecker-ci/woodpecker/pull/2503)]
- Cli updater [[#3382](https://github.com/woodpecker-ci/woodpecker/pull/3382)]
### 📚 Documentation
- Delete docs for v0.15.x [[#3508](https://github.com/woodpecker-ci/woodpecker/pull/3508)]
- Add deployment plugin [[#3495](https://github.com/woodpecker-ci/woodpecker/pull/3495)]
- Bump follow-redirects and fix broken anchors [[#3488](https://github.com/woodpecker-ci/woodpecker/pull/3488)]
- fix: plugin doc page not found [[#3480](https://github.com/woodpecker-ci/woodpecker/pull/3480)]
- Documentation improvements [[#3376](https://github.com/woodpecker-ci/woodpecker/pull/3376)]
- fix(deps): update docs npm deps non-major [[#3455](https://github.com/woodpecker-ci/woodpecker/pull/3455)]
- Add "Sonatype Nexus" plugin [[#3446](https://github.com/woodpecker-ci/woodpecker/pull/3446)]
- Add blog post [[#3439](https://github.com/woodpecker-ci/woodpecker/pull/3439)]
- Add "Gradle Wrapper Validation" plugin [[#3435](https://github.com/woodpecker-ci/woodpecker/pull/3435)]
- Add blog post [[#3410](https://github.com/woodpecker-ci/woodpecker/pull/3410)]
- Extend core ideas documentation [[#3405](https://github.com/woodpecker-ci/woodpecker/pull/3405)]
- docs: fix contributions link [[#3363](https://github.com/woodpecker-ci/woodpecker/pull/3363)]
- Update/fix some docs [[#3359](https://github.com/woodpecker-ci/woodpecker/pull/3359)]
- chore(deps): update dependency marked to v12 [[#3325](https://github.com/woodpecker-ci/woodpecker/pull/3325)]
### 🐛 Bug Fixes
- Fix skip setup for some general cli commands [[#3498](https://github.com/woodpecker-ci/woodpecker/pull/3498)]
- Move generic agent flags to cmd/agent/core [[#3484](https://github.com/woodpecker-ci/woodpecker/pull/3484)]
- Fix usage of WOODPECKER_DATABASE_DATASOURCE_FILE [[#3404](https://github.com/woodpecker-ci/woodpecker/pull/3404)]
- Set pull-request id and labels on pr-closed event [[#3442](https://github.com/woodpecker-ci/woodpecker/pull/3442)]
- Update org name on login [[#3409](https://github.com/woodpecker-ci/woodpecker/pull/3409)]
- Do not alter secret key upper-/lowercase [[#3375](https://github.com/woodpecker-ci/woodpecker/pull/3375)]
- fix: can't run multiple services on k8s [[#3395](https://github.com/woodpecker-ci/woodpecker/pull/3395)]
- Fix agent polling [[#3378](https://github.com/woodpecker-ci/woodpecker/pull/3378)]
- Remove empty strings from slice before parsing agent config [[#3387](https://github.com/woodpecker-ci/woodpecker/pull/3387)]
- Set correct link for commit [[#3368](https://github.com/woodpecker-ci/woodpecker/pull/3368)]
- Fix schema links [[#3369](https://github.com/woodpecker-ci/woodpecker/pull/3369)]
- Fix correctly handle gitlab pr closed events [[#3362](https://github.com/woodpecker-ci/woodpecker/pull/3362)]
- fix: update schema event_enum to remove error warning when.event [[#3357](https://github.com/woodpecker-ci/woodpecker/pull/3357)]
- Fix version check on next [[#3340](https://github.com/woodpecker-ci/woodpecker/pull/3340)]
- Ignore gitlab merge request events without code changes [[#3338](https://github.com/woodpecker-ci/woodpecker/pull/3338)]
- Ignore gitlab push events without commits [[#3339](https://github.com/woodpecker-ci/woodpecker/pull/3339)]
- Consider gitlab inherited permissions [[#3308](https://github.com/woodpecker-ci/woodpecker/pull/3308)]
- fix: agent panic when node is terminated during step execution [[#3331](https://github.com/woodpecker-ci/woodpecker/pull/3331)]
### 📈 Enhancement
- Enable golangci linter gomnd [[#3171](https://github.com/woodpecker-ci/woodpecker/pull/3171)]
- Apply "grpcnotrace" go build tag [[#3448](https://github.com/woodpecker-ci/woodpecker/pull/3448)]
- Simplify store interfaces [[#3437](https://github.com/woodpecker-ci/woodpecker/pull/3437)]
- Deprecate alternative names on secrets [[#3406](https://github.com/woodpecker-ci/woodpecker/pull/3406)]
- Store workflows/steps for blocked pipeline [[#2757](https://github.com/woodpecker-ci/woodpecker/pull/2757)]
- Parse email from Gitea webhook [[#3420](https://github.com/woodpecker-ci/woodpecker/pull/3420)]
- Replace http types on forge interface [[#3374](https://github.com/woodpecker-ci/woodpecker/pull/3374)]
- Prevent agent deletion when it's still running tasks [[#3377](https://github.com/woodpecker-ci/woodpecker/pull/3377)]
- Refactor internal services [[#915](https://github.com/woodpecker-ci/woodpecker/pull/915)]
- Lint for event filter and deprecate `exclude` [[#3222](https://github.com/woodpecker-ci/woodpecker/pull/3222)]
- Allow editing all environment variables in pipeline popups [[#3314](https://github.com/woodpecker-ci/woodpecker/pull/3314)]
- Parse backend options in backend [[#3227](https://github.com/woodpecker-ci/woodpecker/pull/3227)]
- Make agent usable for external backends [[#3270](https://github.com/woodpecker-ci/woodpecker/pull/3270)]
- Add no branches text [[#3312](https://github.com/woodpecker-ci/woodpecker/pull/3312)]
- Add loading spinner to repo list [[#3310](https://github.com/woodpecker-ci/woodpecker/pull/3310)]
### Misc
- Post on mastodon when releasing a new version [[#3509](https://github.com/woodpecker-ci/woodpecker/pull/3509)]
- chore(deps): update dependency alpine_3_18/ca-certificates to v20240226 [[#3501](https://github.com/woodpecker-ci/woodpecker/pull/3501)]
- fix(deps): update module github.com/google/go-github/v59 to v60 [[#3493](https://github.com/woodpecker-ci/woodpecker/pull/3493)]
- fix(deps): update dependency @intlify/unplugin-vue-i18n to v3 [[#3492](https://github.com/woodpecker-ci/woodpecker/pull/3492)]
- chore(deps): update dependency vue-tsc to v2 [[#3491](https://github.com/woodpecker-ci/woodpecker/pull/3491)]
- chore(deps): update dependency eslint-config-airbnb-typescript to v18 [[#3490](https://github.com/woodpecker-ci/woodpecker/pull/3490)]
- chore(deps): update web npm deps non-major [[#3489](https://github.com/woodpecker-ci/woodpecker/pull/3489)]
- fix(deps): update golang (packages) [[#3486](https://github.com/woodpecker-ci/woodpecker/pull/3486)]
- fix(deps): update module google.golang.org/protobuf to v1.33.0 [security] [[#3487](https://github.com/woodpecker-ci/woodpecker/pull/3487)]
- chore(deps): update docker.io/techknowlogick/xgo docker tag to go-1.22.1 [[#3476](https://github.com/woodpecker-ci/woodpecker/pull/3476)]
- chore(deps): update docker.io/golang docker tag to v1.22.1 [[#3475](https://github.com/woodpecker-ci/woodpecker/pull/3475)]
- Update prettier version [[#3471](https://github.com/woodpecker-ci/woodpecker/pull/3471)]
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v1.1.0 [[#3464](https://github.com/woodpecker-ci/woodpecker/pull/3464)]
- chore(deps): lock file maintenance [[#3465](https://github.com/woodpecker-ci/woodpecker/pull/3465)]
- chore(deps): update postgres docker tag to v16.2 [[#3461](https://github.com/woodpecker-ci/woodpecker/pull/3461)]
- chore(deps): update lycheeverse/lychee docker tag to v0.14.3 [[#3429](https://github.com/woodpecker-ci/woodpecker/pull/3429)]
- fix(deps): update golang (packages) [[#3430](https://github.com/woodpecker-ci/woodpecker/pull/3430)]
- More `when` filters [[#3407](https://github.com/woodpecker-ci/woodpecker/pull/3407)]
- Apply `documentation`/`ui` label to corresponding renovate updates [[#3400](https://github.com/woodpecker-ci/woodpecker/pull/3400)]
- chore(deps): update dependency eslint-plugin-simple-import-sort to v12 [[#3396](https://github.com/woodpecker-ci/woodpecker/pull/3396)]
- chore(deps): update typescript-eslint monorepo to v7 (major) [[#3397](https://github.com/woodpecker-ci/woodpecker/pull/3397)]
- fix(deps): update module github.com/google/go-github/v58 to v59 [[#3398](https://github.com/woodpecker-ci/woodpecker/pull/3398)]
- chore(deps): update docker.io/techknowlogick/xgo docker tag to go-1.22.0 [[#3392](https://github.com/woodpecker-ci/woodpecker/pull/3392)]
- chore(deps): update docker.io/golang docker tag [[#3391](https://github.com/woodpecker-ci/woodpecker/pull/3391)]
- fix(deps): update golang (packages) [[#3393](https://github.com/woodpecker-ci/woodpecker/pull/3393)]
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v3.1.0 [[#3394](https://github.com/woodpecker-ci/woodpecker/pull/3394)]
- Add link checking [[#3371](https://github.com/woodpecker-ci/woodpecker/pull/3371)]
- Apply `dependencies` label to all PRs [[#3358](https://github.com/woodpecker-ci/woodpecker/pull/3358)]
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v3.0.1 [[#3324](https://github.com/woodpecker-ci/woodpecker/pull/3324)]
## [2.3.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.3.0) - 2024-01-31
### ❤️ Thanks to all contributors! ❤️
@ -57,7 +182,7 @@
- build: fix nfpm path for server binary [[#3246](https://github.com/woodpecker-ci/woodpecker/pull/3246)]
## [2.2.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/2.2.1) - 2024-01-21
## [2.2.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.2.1) - 2024-01-21
### ❤️ Thanks to all contributors! ❤️
@ -71,7 +196,7 @@
- Build tarball for distribution packages [[#3244](https://github.com/woodpecker-ci/woodpecker/pull/3244)]
## [2.2.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/2.2.0) - 2024-01-21
## [2.2.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.2.0) - 2024-01-21
### ❤️ Thanks to all contributors! ❤️
@ -191,7 +316,7 @@
- Use `yamllint` [[#3066](https://github.com/woodpecker-ci/woodpecker/pull/3066)]
- Use dag in ci config [[#3010](https://github.com/woodpecker-ci/woodpecker/pull/3010)]
## [2.1.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/2.1.1) - 2023-12-27
## [2.1.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.1.1) - 2023-12-27
### ❤️ Thanks to all contributors! ❤️
@ -212,7 +337,7 @@
- Add some tests [[#3030](https://github.com/woodpecker-ci/woodpecker/pull/3030)]
## [2.1.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/2.1.0) - 2023-12-26
## [2.1.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.1.0) - 2023-12-26
### ❤️ Thanks to all contributors! ❤️
@ -317,7 +442,7 @@
- Update web npm deps non-major [[#2884](https://github.com/woodpecker-ci/woodpecker/pull/2884)]
- Update docker.io/woodpeckerci/plugin-docker-buildx Docker tag to v2.2.1 [[#2883](https://github.com/woodpecker-ci/woodpecker/pull/2883)]
## [2.0.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/2.0.0) - 2023-11-23
## [2.0.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.0.0) - 2023-11-23
### ❤️ Thanks to all contributors! ❤️

View file

@ -58,8 +58,6 @@ ifeq (in_docker,$(firstword $(MAKECMDGOALS)))
-e TARGETOS="$(TARGETOS)" \
-e TARGETARCH="$(TARGETARCH)" \
-e CGO_ENABLED="$(CGO_ENABLED)" \
-e GOPATH=/tmp/go \
-e HOME=/tmp/home \
-v $(PWD):/build --rm woodpecker/make:local make $(MAKE_ARGS)
else
@ -110,7 +108,7 @@ clean-all: clean ## Clean all artifacts
rm -rf docs/docs/40-cli.md docs/swagger.json
.PHONY: generate
generate: generate-swagger ## Run all code generations
generate: install-tools generate-swagger ## Run all code generations
go generate ./...
generate-swagger: install-tools ## Run swagger code generation
@ -127,7 +125,7 @@ check-xgo: ## Check if xgo is installed
install-tools: ## Install development tools
@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest ; \
fi ; \
hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install mvdan.cc/gofumpt@latest; \
@ -137,6 +135,15 @@ install-tools: ## Install development tools
fi ; \
hash addlicense > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install github.com/google/addlicense@latest; \
fi ; \
hash mockery > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install github.com/vektra/mockery/v2@latest; \
fi ; \
hash protoc-gen-go > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest; \
fi ; \
hash protoc-gen-go-grpc > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest; \
fi
ui-dependencies: ## Install UI dependencies
@ -226,7 +233,7 @@ release-server-xgo: check-xgo ## Create server binaries for release using xgo
@echo "arch (xgo):$(TARGETARCH_XGO)"
@echo "arch (buildx):$(TARGETARCH_BUILDX)"
CGO_CFLAGS="$(CGO_CFLAGS)" xgo -go $(XGO_VERSION) -dest ./dist/server/$(TARGETOS)_$(TARGETARCH_BUILDX) -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external $(LDFLAGS)' -targets '$(TARGETOS)/$(TARGETARCH_XGO)' -out woodpecker-server -pkg cmd/server .
CGO_CFLAGS="$(CGO_CFLAGS)" xgo -go $(XGO_VERSION) -dest ./dist/server/$(TARGETOS)_$(TARGETARCH_BUILDX) -tags 'netgo osusergo grpcnotrace $(TAGS)' -ldflags '-linkmode external $(LDFLAGS)' -targets '$(TARGETOS)/$(TARGETARCH_XGO)' -out woodpecker-server -pkg cmd/server .
@if [ "$${XGO_IN_XGO:-0}" -eq "1" ]; then echo "inside xgo image"; \
mkdir -p ./dist/server/$(TARGETOS)_$(TARGETARCH_BUILDX); \
mv -vf /build/woodpecker-server* ./dist/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \
@ -238,18 +245,18 @@ release-server-xgo: check-xgo ## Create server binaries for release using xgo
release-server: ## Create server binaries for release
# compile
GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) CGO_ENABLED=${CGO_ENABLED} go build -ldflags '${LDFLAGS}' -o dist/server/$(TARGETOS)_$(TARGETARCH)/woodpecker-server$(BIN_SUFFIX) go.woodpecker-ci.org/woodpecker/v2/cmd/server
GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) CGO_ENABLED=${CGO_ENABLED} go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/server/$(TARGETOS)_$(TARGETARCH)/woodpecker-server$(BIN_SUFFIX) go.woodpecker-ci.org/woodpecker/v2/cmd/server
# tar binary files
tar -cvzf dist/woodpecker-server_$(TARGETOS)_$(TARGETARCH).tar.gz -C dist/server/$(TARGETOS)_$(TARGETARCH) woodpecker-server$(BIN_SUFFIX)
release-agent: ## Create agent binaries for release
# compile
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o dist/agent/linux_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o dist/agent/linux_arm64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o dist/agent/linux_arm/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o dist/agent/windows_amd64/woodpecker-agent.exe go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o dist/agent/darwin_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o dist/agent/darwin_arm64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/agent/linux_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/agent/linux_arm64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/agent/linux_arm/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/agent/windows_amd64/woodpecker-agent.exe go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/agent/darwin_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o dist/agent/darwin_arm64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent
# tar binary files
tar -cvzf dist/woodpecker-agent_linux_amd64.tar.gz -C dist/agent/linux_amd64 woodpecker-agent
tar -cvzf dist/woodpecker-agent_linux_arm64.tar.gz -C dist/agent/linux_arm64 woodpecker-agent
@ -299,6 +306,10 @@ bundle-cli: bundle-prepare ## Create bundles for cli
.PHONY: bundle
bundle: bundle-agent bundle-server bundle-cli ## Create all bundles
.PHONY: spellcheck
spellcheck:
pnpx cspell lint --no-progress --gitignore '{**,.*}/{*,.*}'
##@ Docs
.PHONY: docs
docs: ## Generate docs (currently only for the cli)

View file

@ -7,8 +7,8 @@
</p>
<br/>
<p align="center">
<a href="https://ci.woodpecker-ci.org/repos/3780" title="Build Status">
<img src="https://ci.woodpecker-ci.org/api/badges/3780/status.svg" alt="Build Status">
<a href="https://ci.woodpecker-ci.org/repos/3780" title="Pipeline Status">
<img src="https://ci.woodpecker-ci.org/api/badges/3780/status.svg" alt="Pipeline Status">
</a>
<a href="https://codecov.io/gh/woodpecker-ci/woodpecker">
<img src="https://codecov.io/gh/woodpecker-ci/woodpecker/branch/main/graph/badge.svg" alt="Code coverage">

View file

@ -40,7 +40,7 @@ func NewAuthGrpcClient(conn *grpc.ClientConn, agentToken string, agentID int64)
}
func (c *AuthClient) Auth() (string, int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //nolint: gomnd
defer cancel()
req := &proto.AuthRequest{

View file

@ -53,8 +53,9 @@ func (c *client) Close() error {
func (c *client) newBackOff() backoff.BackOff {
b := backoff.NewExponentialBackOff()
b.MaxInterval = 10 * time.Second
b.InitialInterval = 10 * time.Millisecond
b.MaxElapsedTime = 0
b.MaxInterval = 10 * time.Second //nolint: gomnd
b.InitialInterval = 10 * time.Millisecond //nolint: gomnd
return b
}

View file

@ -50,7 +50,7 @@ func NewRunner(workEngine rpc.Peer, f rpc.Filter, h string, state *State, backen
}
}
func (r *Runner) Run(runnerCtx context.Context) error {
func (r *Runner) Run(runnerCtx context.Context) error { //nolint:contextcheck
log.Debug().Msg("request next execution")
meta, _ := metadata.FromOutgoingContext(runnerCtx)
@ -158,7 +158,7 @@ func (r *Runner) Run(runnerCtx context.Context) error {
if canceled.IsSet() {
state.Error = ""
state.ExitCode = 137
state.ExitCode = pipeline.ExitCodeKilled
} else if err != nil {
pExitError := &pipeline.ExitError{}
switch {
@ -166,7 +166,7 @@ func (r *Runner) Run(runnerCtx context.Context) error {
state.ExitCode = pExitError.Code
case errors.Is(err, pipeline.ErrCancel):
state.Error = ""
state.ExitCode = 137
state.ExitCode = pipeline.ExitCodeKilled
canceled.SetTo(true)
default:
state.ExitCode = 1

View file

@ -21,6 +21,12 @@ import (
)
var GlobalFlags = append([]cli.Flag{
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CONFIG"},
Name: "config",
Aliases: []string{"c"},
Usage: "path to config file",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_TOKEN"},
Name: "token",
@ -69,14 +75,29 @@ func FormatFlag(tmpl string, hidden ...bool) *cli.StringFlag {
}
}
// OutputFlags returns a slice of cli.Flag containing output format options.
func OutputFlags(def string) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "output",
Usage: "output format",
Value: def,
},
&cli.BoolFlag{
Name: "output-no-headers",
Usage: "don't print headers",
},
}
}
var RepoFlag = &cli.StringFlag{
Name: "repository",
Aliases: []string{"repo"},
Usage: "repository id or full-name (e.g. 134 or octocat/hello-world)",
Usage: "repository id or full name (e.g. 134 or octocat/hello-world)",
}
var OrgFlag = &cli.StringFlag{
Name: "organization",
Aliases: []string{"org"},
Usage: "organization id or full-name (e.g. 123 or octocat)",
Usage: "organization id or full name (e.g. 123 or octocat)",
}

View file

@ -8,6 +8,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal/config"
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
)
@ -17,7 +18,7 @@ var (
)
func Before(c *cli.Context) error {
if err := SetupGlobalLogger(c); err != nil {
if err := setupGlobalLogger(c); err != nil {
return err
}
@ -36,7 +37,7 @@ func Before(c *cli.Context) error {
log.Debug().Msg("Checking for updates ...")
newVersion, err := update.CheckForUpdate(waitForUpdateCheck, true)
newVersion, err := update.CheckForUpdate(waitForUpdateCheck, false)
if err != nil {
log.Error().Err(err).Msgf("Failed to check for updates")
return
@ -49,15 +50,15 @@ func Before(c *cli.Context) error {
}
}()
return nil
return config.Load(c)
}
func After(_ *cli.Context) error {
if waitForUpdateCheck != nil {
select {
case <-waitForUpdateCheck.Done():
// When the actual command already finished, we still wait 250ms for the update check to finish
case <-time.After(time.Millisecond * 250):
// When the actual command already finished, we still wait 500ms for the update check to finish
case <-time.After(time.Millisecond * 500):
log.Debug().Msg("Update check stopped due to timeout")
cancelWaitForUpdate(errors.New("update check timeout"))
}

View file

@ -20,6 +20,6 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/shared/logger"
)
func SetupGlobalLogger(c *cli.Context) error {
func setupGlobalLogger(c *cli.Context) error {
return logger.SetupGlobalLogger(c, false)
}

View file

@ -106,7 +106,8 @@ func deploy(c *cli.Context) error {
}
}
env := c.Args().Get(2)
envArgIndex := 2
env := c.Args().Get(envArgIndex)
if env == "" {
return fmt.Errorf("please specify the target environment (i.e. production)")
}

View file

@ -57,7 +57,12 @@ func run(c *cli.Context) error {
func execDir(c *cli.Context, dir string) error {
// TODO: respect pipeline dependency
repoPath, _ := filepath.Abs(filepath.Dir(dir))
repoPath := c.String("repo-path")
if repoPath != "" {
repoPath, _ = filepath.Abs(repoPath)
} else {
repoPath, _ = filepath.Abs(filepath.Dir(dir))
}
if runtime.GOOS == "windows" {
repoPath = convertPathForWindows(repoPath)
}
@ -79,7 +84,12 @@ func execDir(c *cli.Context, dir string) error {
}
func execFile(c *cli.Context, file string) error {
repoPath, _ := filepath.Abs(filepath.Dir(file))
repoPath := c.String("repo-path")
if repoPath != "" {
repoPath, _ = filepath.Abs(repoPath)
} else {
repoPath, _ = filepath.Abs(filepath.Dir(file))
}
if runtime.GOOS == "windows" {
repoPath = convertPathForWindows(repoPath)
}
@ -123,13 +133,13 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
pipelineEnv := make(map[string]string)
for _, env := range c.StringSlice("env") {
envs := strings.SplitN(env, "=", 2)
pipelineEnv[envs[0]] = envs[1]
if oldVar, exists := environ[envs[0]]; exists {
before, after, _ := strings.Cut(env, "=")
pipelineEnv[before] = after
if oldVar, exists := environ[before]; exists {
// override existing values, but print a warning
log.Warn().Msgf("environment variable '%s' had value '%s', but got overwritten", envs[0], oldVar)
log.Warn().Msgf("environment variable '%s' had value '%s', but got overwritten", before, oldVar)
}
environ[envs[0]] = envs[1]
environ[before] = after
}
tmpl, err := envsubst.ParseFile(file)
@ -244,8 +254,16 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
).Run(c.Context)
}
// convertPathForWindows converts a path to use slash separators
// for Windows. If the path is a Windows volume name like C:, it
// converts it to an absolute root path starting with slash (e.g.
// C: -> /c). Otherwise it just converts backslash separators to
// slashes.
func convertPathForWindows(path string) string {
base := filepath.VolumeName(path)
// Check if path is volume name like C:
//nolint: gomnd
if len(base) == 2 {
path = strings.TrimPrefix(path, base)
base = strings.ToLower(strings.TrimSuffix(base, ":"))

View file

@ -29,6 +29,11 @@ var flags = []cli.Flag{
Usage: "run from local directory",
Value: true,
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_REPO_PATH"},
Name: "repo-path",
Usage: "path to local repository",
},
&cli.DurationFlag{
EnvVars: []string{"WOODPECKER_TIMEOUT"},
Name: "timeout",
@ -199,6 +204,10 @@ var flags = []cli.Flag{
EnvVars: []string{"CI_PIPELINE_TARGET"},
Name: "pipeline-target",
},
&cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_TASK"},
Name: "pipeline-task",
},
&cli.StringFlag{
EnvVars: []string{"CI_COMMIT_SHA"},
Name: "commit-sha",

View file

@ -61,6 +61,7 @@ func metadataFromContext(c *cli.Context, axis matrix.Axis) metadata.Metadata {
Event: c.String("pipeline-event"),
ForgeURL: c.String("pipeline-url"),
Target: c.String("pipeline-target"),
Task: c.String("pipeline-task"),
Commit: metadata.Commit{
Sha: c.String("commit-sha"),
Ref: c.String("commit-ref"),

View file

@ -0,0 +1,141 @@
package config
import (
"encoding/json"
"errors"
"os"
"slices"
"github.com/adrg/xdg"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"github.com/zalando/go-keyring"
)
type Config struct {
ServerURL string `json:"server_url"`
Token string `json:"-"`
LogLevel string `json:"log_level"`
}
var skipSetupForCommands = []string{"setup", "help", "h", "version", "update", "lint", "exec", ""}
func Load(c *cli.Context) error {
if firstArg := c.Args().First(); slices.Contains(skipSetupForCommands, firstArg) {
return nil
}
config, err := Get(c, c.String("config"))
if err != nil {
return err
}
if config == nil {
config = &Config{
LogLevel: "info",
ServerURL: c.String("server-url"),
Token: c.String("token"),
}
}
if !c.IsSet("server") {
err = c.Set("server", config.ServerURL)
if err != nil {
return err
}
}
if !c.IsSet("token") {
err = c.Set("token", config.Token)
if err != nil {
return err
}
}
if !c.IsSet("log-level") {
err = c.Set("log-level", config.LogLevel)
if err != nil {
return err
}
}
if config.ServerURL == "" || config.Token == "" {
log.Info().Msg("The woodpecker-cli is not yet set up. Please run `woodpecker-cli setup` or provide the required environment variables / flags.")
return errors.New("woodpecker-cli is not configured")
}
return nil
}
func getConfigPath(configPath string) (string, error) {
if configPath != "" {
return configPath, nil
}
configPath, err := xdg.ConfigFile("woodpecker/config.json")
if err != nil {
return "", err
}
return configPath, nil
}
func Get(ctx *cli.Context, _configPath string) (*Config, error) {
configPath, err := getConfigPath(_configPath)
if err != nil {
return nil, err
}
content, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
log.Debug().Err(err).Msg("Failed to read the config file")
return nil, err
} else if err != nil && os.IsNotExist(err) {
log.Debug().Msg("The config file does not exist")
return nil, nil
}
c := &Config{}
err = json.Unmarshal(content, c)
if err != nil {
return nil, err
}
// load token from keyring
service := ctx.App.Name
secret, err := keyring.Get(service, c.ServerURL)
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
return nil, err
}
if err == nil {
c.Token = secret
}
return c, nil
}
func Save(ctx *cli.Context, _configPath string, c *Config) error {
config, err := json.Marshal(c)
if err != nil {
return err
}
configPath, err := getConfigPath(_configPath)
if err != nil {
return err
}
// save token to keyring
service := ctx.App.Name
err = keyring.Set(service, c.ServerURL, c.Token)
if err != nil {
return err
}
err = os.WriteFile(configPath, config, 0o600)
if err != nil {
return err
}
return nil
}

View file

@ -107,11 +107,11 @@ func ParseRepo(client woodpecker.Client, str string) (repoID int64, err error) {
func ParseKeyPair(p []string) map[string]string {
params := map[string]string{}
for _, i := range p {
parts := strings.SplitN(i, "=", 2)
if len(parts) != 2 {
before, after, ok := strings.Cut(i, "=")
if !ok || before == "" {
continue
}
params[parts[0]] = parts[1]
params[before] = after
}
return params
}

View file

@ -114,7 +114,7 @@ func lintFile(_ *cli.Context, file string) error {
hasErrors = true
}
if data := err.GetLinterData(); data != nil {
if data := pipeline_errors.GetLinterData(err); data != nil {
line = fmt.Sprintf("%s %s\t%s", line, output.String(data.Field).Bold(), err.Message)
} else {
line = fmt.Sprintf("%s %s", line, err.Message)

View file

@ -26,7 +26,7 @@ import (
var logPurgeCmd = &cli.Command{
Name: "purge",
Usage: "purge a log",
ArgsUsage: "<repo-id|repo-full-name> <pipeline>",
ArgsUsage: "<repo-id|repo-full-name> <pipeline> [step]",
Action: logPurge,
}
@ -45,7 +45,21 @@ func logPurge(c *cli.Context) (err error) {
return err
}
err = client.LogsPurge(repoID, number)
stepArg := c.Args().Get(2) //nolint: gomnd
// TODO: Add lookup by name: stepID, err := internal.ParseStep(client, repoID, stepIDOrName)
var stepID int64
if len(stepArg) != 0 {
stepID, err = strconv.ParseInt(stepArg, 10, 64)
if err != nil {
return err
}
}
if stepID > 0 {
err = client.StepLogsPurge(repoID, number, stepID)
} else {
err = client.LogsPurge(repoID, number)
}
if err != nil {
return err
}

24
cli/output/output.go Normal file
View file

@ -0,0 +1,24 @@
package output
import (
"errors"
"strings"
)
var ErrOutputOptionRequired = errors.New("output option required")
func ParseOutputOptions(out string) (string, []string) {
out, opt, found := strings.Cut(out, "=")
if !found {
return out, nil
}
var optList []string
if opt != "" {
optList = strings.Split(opt, ",")
}
return out, optList
}

203
cli/output/table.go Normal file
View file

@ -0,0 +1,203 @@
package output
import (
"fmt"
"io"
"reflect"
"sort"
"strings"
"text/tabwriter"
"unicode"
"github.com/mitchellh/mapstructure"
)
// NewTable creates a new Table.
func NewTable(out io.Writer) *Table {
padding := 2
return &Table{
w: tabwriter.NewWriter(out, 0, 0, padding, ' ', 0),
columns: map[string]bool{},
fieldMapping: map[string]FieldFn{},
fieldAlias: map[string]string{},
allowedFields: map[string]bool{},
}
}
type FieldFn func(obj any) string
type writerFlusher interface {
io.Writer
Flush() error
}
// Table is a generic way to format object as a table.
type Table struct {
w writerFlusher
columns map[string]bool
fieldMapping map[string]FieldFn
fieldAlias map[string]string
allowedFields map[string]bool
}
// Columns returns a list of known output columns.
func (o *Table) Columns() (cols []string) {
for c := range o.columns {
cols = append(cols, c)
}
sort.Strings(cols)
return
}
// AddFieldAlias overrides the field name to allow custom column headers.
func (o *Table) AddFieldAlias(field, alias string) *Table {
o.fieldAlias[field] = alias
return o
}
// AddFieldFn adds a function which handles the output of the specified field.
func (o *Table) AddFieldFn(field string, fn FieldFn) *Table {
o.fieldMapping[field] = fn
o.allowedFields[field] = true
o.columns[field] = true
return o
}
// AddAllowedFields reads all first level fieldnames of the struct and allows them to be used.
func (o *Table) AddAllowedFields(obj any) (*Table, error) {
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Struct {
return o, fmt.Errorf("AddAllowedFields input must be a struct")
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
k := t.Field(i).Type.Kind()
if k != reflect.Bool &&
k != reflect.Float32 &&
k != reflect.Float64 &&
k != reflect.String &&
k != reflect.Int &&
k != reflect.Int64 {
// only allow simple values
// complex values need to be mapped via a FieldFn
continue
}
o.allowedFields[strings.ToLower(t.Field(i).Name)] = true
o.allowedFields[fieldName(t.Field(i).Name)] = true
o.columns[fieldName(t.Field(i).Name)] = true
}
return o, nil
}
// RemoveAllowedField removes fields from the allowed list.
func (o *Table) RemoveAllowedField(fields ...string) *Table {
for _, field := range fields {
delete(o.allowedFields, field)
delete(o.columns, field)
}
return o
}
// ValidateColumns returns an error if invalid columns are specified.
func (o *Table) ValidateColumns(cols []string) error {
var invalidCols []string
for _, col := range cols {
if _, ok := o.allowedFields[strings.ToLower(col)]; !ok {
invalidCols = append(invalidCols, col)
}
}
if len(invalidCols) > 0 {
return fmt.Errorf("invalid table columns: %s", strings.Join(invalidCols, ","))
}
return nil
}
// WriteHeader writes the table header.
func (o *Table) WriteHeader(columns []string) {
var header []string
for _, col := range columns {
if alias, ok := o.fieldAlias[col]; ok {
col = alias
}
header = append(header, strings.ReplaceAll(strings.ToUpper(col), "_", " "))
}
_, _ = fmt.Fprintln(o.w, strings.Join(header, "\t"))
}
func (o *Table) Flush() error {
return o.w.Flush()
}
// Write writes a table line.
func (o *Table) Write(columns []string, obj any) error {
var data map[string]any
if err := mapstructure.Decode(obj, &data); err != nil {
return fmt.Errorf("failed to decode object: %w", err)
}
dataL := map[string]any{}
for key, value := range data {
dataL[strings.ToLower(key)] = value
}
var out []string
for _, col := range columns {
colName := strings.ToLower(col)
if alias, ok := o.fieldAlias[colName]; ok {
if fn, ok := o.fieldMapping[alias]; ok {
out = append(out, fn(obj))
continue
}
}
if fn, ok := o.fieldMapping[colName]; ok {
out = append(out, fn(obj))
continue
}
if value, ok := dataL[strings.ReplaceAll(colName, "_", "")]; ok {
if value == nil {
out = append(out, NA(""))
continue
}
if b, ok := value.(bool); ok {
out = append(out, YesNo(b))
continue
}
if s, ok := value.(string); ok {
out = append(out, NA(s))
continue
}
out = append(out, fmt.Sprintf("%v", value))
}
}
_, _ = fmt.Fprintln(o.w, strings.Join(out, "\t"))
return nil
}
func NA(s string) string {
if s == "" {
return "-"
}
return s
}
func YesNo(b bool) string {
if b {
return "yes"
}
return "no"
}
func fieldName(name string) string {
r := []rune(name)
var out []rune
for i := range r {
if i > 0 && (unicode.IsUpper(r[i])) && (i+1 < len(r) && unicode.IsLower(r[i+1]) || unicode.IsLower(r[i-1])) {
out = append(out, '_')
}
out = append(out, unicode.ToLower(r[i]))
}
return string(out)
}

75
cli/output/table_test.go Normal file
View file

@ -0,0 +1,75 @@
package output
import (
"bytes"
"os"
"strings"
"testing"
)
type writerFlusherStub struct {
bytes.Buffer
}
func (s writerFlusherStub) Flush() error {
return nil
}
type testFieldsStruct struct {
Name string
Number int
}
func TestTableOutput(t *testing.T) {
var wfs writerFlusherStub
to := NewTable(os.Stdout)
to.w = &wfs
t.Run("AddAllowedFields", func(t *testing.T) {
_, _ = to.AddAllowedFields(testFieldsStruct{})
if _, ok := to.allowedFields["name"]; !ok {
t.Error("name should be a allowed field")
}
})
t.Run("AddFieldAlias", func(t *testing.T) {
to.AddFieldAlias("woodpecker_ci", "woodpecker ci")
if alias, ok := to.fieldAlias["woodpecker_ci"]; !ok || alias != "woodpecker ci" {
t.Errorf("woodpecker_ci alias should be 'woodpecker ci', is: %v", alias)
}
})
t.Run("AddFieldOutputFn", func(t *testing.T) {
to.AddFieldFn("woodpecker ci", FieldFn(func(_ any) string {
return "WOODPECKER CI!!!"
}))
if _, ok := to.fieldMapping["woodpecker ci"]; !ok {
t.Errorf("'woodpecker ci' field output fn should be set")
}
})
t.Run("ValidateColumns", func(t *testing.T) {
err := to.ValidateColumns([]string{"non-existent", "NAME"})
if err == nil ||
strings.Contains(err.Error(), "name") ||
!strings.Contains(err.Error(), "non-existent") {
t.Errorf("error should contain 'non-existent' but not 'name': %v", err)
}
})
t.Run("WriteHeader", func(t *testing.T) {
to.WriteHeader([]string{"woodpecker_ci", "name"})
if wfs.String() != "WOODPECKER CI\tNAME\n" {
t.Errorf("written header should be 'WOODPECKER CI\\tNAME\\n', is: %q", wfs.String())
}
wfs.Reset()
})
t.Run("WriteLine", func(t *testing.T) {
_ = to.Write([]string{"woodpecker_ci", "name", "number"}, &testFieldsStruct{"test123", 1000000000})
if wfs.String() != "WOODPECKER CI!!!\ttest123\t1000000000\n" {
t.Errorf("written line should be 'WOODPECKER CI!!!\\ttest123\\t1000000000\\n', is: %q", wfs.String())
}
wfs.Reset()
})
t.Run("Columns", func(t *testing.T) {
if len(to.Columns()) != 3 {
t.Errorf("unexpected number of columns: %v", to.Columns())
}
})
}

View file

@ -15,9 +15,7 @@
package pipeline
import (
"os"
"strings"
"text/template"
"github.com/urfave/cli/v2"
@ -31,8 +29,7 @@ var pipelineCreateCmd = &cli.Command{
Usage: "create new pipeline",
ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineCreate,
Flags: []cli.Flag{
common.FormatFlag(tmplPipelineList),
Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{
Name: "branch",
Usage: "branch to create pipeline from",
@ -42,7 +39,7 @@ var pipelineCreateCmd = &cli.Command{
Name: "var",
Usage: "key=value",
},
},
}...),
}
func pipelineCreate(c *cli.Context) error {
@ -60,9 +57,9 @@ func pipelineCreate(c *cli.Context) error {
variables := make(map[string]string)
for _, vaz := range c.StringSlice("var") {
sp := strings.SplitN(vaz, "=", 2)
if len(sp) == 2 {
variables[sp[0]] = sp[1]
before, after, _ := strings.Cut(vaz, "=")
if before != "" && after != "" {
variables[before] = after
}
}
@ -76,10 +73,5 @@ func pipelineCreate(c *cli.Context) error {
return err
}
tmpl, err := template.New("_").Parse(c.String("format") + "\n")
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, pipeline)
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline})
}

View file

@ -15,14 +15,13 @@
package pipeline
import (
"os"
"strconv"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var pipelineInfoCmd = &cli.Command{
@ -30,7 +29,7 @@ var pipelineInfoCmd = &cli.Command{
Usage: "show pipeline details",
ArgsUsage: "<repo-id|repo-full-name> [pipeline]",
Action: pipelineInfo,
Flags: []cli.Flag{common.FormatFlag(tmplPipelineInfo)},
Flags: common.OutputFlags("table"),
}
func pipelineInfo(c *cli.Context) error {
@ -65,20 +64,5 @@ func pipelineInfo(c *cli.Context) error {
return err
}
tmpl, err := template.New("_").Parse(c.String("format"))
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, pipeline)
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline})
}
// template for pipeline information
var tmplPipelineInfo = `Number: {{ .Number }}
Status: {{ .Status }}
Event: {{ .Event }}
Commit: {{ .Commit }}
Branch: {{ .Branch }}
Ref: {{ .Ref }}
Message: {{ .Message }}
Author: {{ .Author }}
`

View file

@ -15,13 +15,11 @@
package pipeline
import (
"os"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var pipelineLastCmd = &cli.Command{
@ -29,14 +27,13 @@ var pipelineLastCmd = &cli.Command{
Usage: "show latest pipeline details",
ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineLast,
Flags: []cli.Flag{
common.FormatFlag(tmplPipelineInfo),
Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{
Name: "branch",
Usage: "branch name",
Value: "main",
},
},
}...),
}
func pipelineLast(c *cli.Context) error {
@ -55,9 +52,5 @@ func pipelineLast(c *cli.Context) error {
return err
}
tmpl, err := template.New("_").Parse(c.String("format"))
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, pipeline)
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline})
}

View file

@ -15,22 +15,20 @@
package pipeline
import (
"os"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
//nolint:gomnd
var pipelineListCmd = &cli.Command{
Name: "ls",
Usage: "show pipeline history",
ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineList,
Flags: []cli.Flag{
common.FormatFlag(tmplPipelineList),
Action: List,
Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{
Name: "branch",
Usage: "branch filter",
@ -48,28 +46,33 @@ var pipelineListCmd = &cli.Command{
Usage: "limit the list size",
Value: 25,
},
},
}...),
}
func pipelineList(c *cli.Context) error {
repoIDOrFullName := c.Args().First()
func List(c *cli.Context) error {
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
resources, err := pipelineList(c, client)
if err != nil {
return err
}
return pipelineOutput(c, resources)
}
func pipelineList(c *cli.Context, client woodpecker.Client) ([]woodpecker.Pipeline, error) {
resources := make([]woodpecker.Pipeline, 0)
repoIDOrFullName := c.Args().First()
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return resources, err
}
pipelines, err := client.PipelineList(repoID)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(c.String("format") + "\n")
if err != nil {
return err
return resources, err
}
branch := c.String("branch")
@ -91,21 +94,9 @@ func pipelineList(c *cli.Context) error {
if status != "" && pipeline.Status != status {
continue
}
if err := tmpl.Execute(os.Stdout, pipeline); err != nil {
return err
}
resources = append(resources, *pipeline)
count++
}
return nil
}
// template for pipeline list information
var tmplPipelineList = "\x1b[33mPipeline #{{ .Number }} \x1b[0m" + `
Status: {{ .Status }}
Event: {{ .Event }}
Commit: {{ .Commit }}
Branch: {{ .Branch }}
Ref: {{ .Ref }}
Author: {{ .Author }} {{ if .Email }}<{{.Email}}>{{ end }}
Message: {{ .Message }}
`
return resources, nil
}

132
cli/pipeline/list_test.go Normal file
View file

@ -0,0 +1,132 @@
package pipeline
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker/mocks"
)
func TestPipelineList(t *testing.T) {
testtases := []struct {
name string
repoID int64
repoErr error
pipelines []*woodpecker.Pipeline
pipelineErr error
args []string
expected []woodpecker.Pipeline
wantErr error
}{
{
name: "success",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
},
{
name: "filter by branch",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--branch", "main", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
},
{
name: "filter by event",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--event", "push", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
},
{
name: "filter by status",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--status", "success", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
},
},
{
name: "limit results",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--limit", "2", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
},
},
{
name: "pipeline list error",
repoID: 1,
pipelineErr: errors.New("pipeline error"),
args: []string{"ls", "repo/name"},
wantErr: errors.New("pipeline error"),
},
}
for _, tt := range testtases {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks.NewClient(t)
mockClient.On("PipelineList", mock.Anything).Return(tt.pipelines, tt.pipelineErr)
mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil)
app := &cli.App{Writer: io.Discard}
c := cli.NewContext(app, nil, nil)
command := pipelineListCmd
command.Action = func(c *cli.Context) error {
pipelines, err := pipelineList(c, mockClient)
if tt.wantErr != nil {
assert.EqualError(t, err, tt.wantErr.Error())
return nil
}
assert.NoError(t, err)
assert.EqualValues(t, tt.expected, pipelines)
return nil
}
_ = command.Run(c, tt.args...)
})
}
}

View file

@ -41,12 +41,14 @@ func pipelineLogs(c *cli.Context) error {
return err
}
number, err := strconv.ParseInt(c.Args().Get(1), 10, 64)
numberArgIndex := 1
number, err := strconv.ParseInt(c.Args().Get(numberArgIndex), 10, 64)
if err != nil {
return err
}
step, err := strconv.ParseInt(c.Args().Get(2), 10, 64)
stepArgIndex := 2
step, err := strconv.ParseInt(c.Args().Get(stepArgIndex), 10, 64)
if err != nil {
return err
}

View file

@ -15,7 +15,15 @@
package pipeline
import (
"fmt"
"io"
"os"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/output"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
// Command exports the pipeline command set.
@ -37,3 +45,53 @@ var Command = &cli.Command{
pipelineCreateCmd,
},
}
func pipelineOutput(c *cli.Context, resources []woodpecker.Pipeline, fd ...io.Writer) error {
outfmt, outopt := output.ParseOutputOptions(c.String("output"))
noHeader := c.Bool("output-no-headers")
var out io.Writer
switch len(fd) {
case 0:
out = os.Stdout
case 1:
out = fd[0]
default:
out = os.Stdout
}
switch outfmt {
case "go-template":
if len(outopt) < 1 {
return fmt.Errorf("%w: missing template", output.ErrOutputOptionRequired)
}
tmpl, err := template.New("_").Parse(outopt[0] + "\n")
if err != nil {
return err
}
if err := tmpl.Execute(out, resources); err != nil {
return err
}
case "table":
fallthrough
default:
table := output.NewTable(out)
cols := []string{"Number", "Status", "Event", "Branch", "Commit", "Author"}
if len(outopt) > 0 {
cols = outopt
}
if !noHeader {
table.WriteHeader(cols)
}
for _, resource := range resources {
if err := table.Write(cols, resource); err != nil {
return err
}
}
table.Flush()
}
return nil
}

View file

@ -0,0 +1,86 @@
package pipeline
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
func TestPipelineOutput(t *testing.T) {
tests := []struct {
name string
args []string
expected string
wantErr bool
}{
{
name: "table output with default columns",
args: []string{},
expected: "NUMBER STATUS EVENT BRANCH COMMIT AUTHOR\n1 success push main abcdef John Doe\n",
},
{
name: "table output with custom columns",
args: []string{"output", "--output", "table=Number,Status,Branch"},
expected: "NUMBER STATUS BRANCH\n1 success main\n",
},
{
name: "table output with no header",
args: []string{"output", "--output-no-headers"},
expected: "1 success push main abcdef John Doe\n",
},
{
name: "go-template output",
args: []string{"output", "--output", "go-template={{range . }}{{.Number}} {{.Status}} {{.Branch}}{{end}}"},
expected: "1 success main\n",
},
{
name: "invalid go-template",
args: []string{"output", "--output", "go-template={{.InvalidField}}"},
wantErr: true,
},
}
pipelines := []woodpecker.Pipeline{
{
Number: 1,
Status: "success",
Event: "push",
Branch: "main",
Commit: "abcdef",
Author: "John Doe",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{Writer: io.Discard}
c := cli.NewContext(app, nil, nil)
command := &cli.Command{}
command.Name = "output"
command.Flags = common.OutputFlags("table")
command.Action = func(c *cli.Context) error {
var buf bytes.Buffer
err := pipelineOutput(c, pipelines, &buf)
if tt.wantErr {
assert.Error(t, err)
return nil
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, buf.String())
return nil
}
_ = command.Run(c, tt.args...)
})
}
}

93
cli/setup/setup.go Normal file
View file

@ -0,0 +1,93 @@
package setup
import (
"errors"
"strings"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal/config"
"go.woodpecker-ci.org/woodpecker/v2/cli/setup/ui"
)
// Command exports the setup command.
var Command = &cli.Command{
Name: "setup",
Usage: "setup the woodpecker-cli for the first time",
Args: true,
ArgsUsage: "[server-url]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "server-url",
Usage: "The URL of the woodpecker server",
},
&cli.StringFlag{
Name: "token",
Usage: "The token to authenticate with the woodpecker server",
},
},
Action: setup,
}
func setup(c *cli.Context) error {
_config, err := config.Get(c, c.String("config"))
if err != nil {
return err
} else if _config != nil {
setupAgain, err := ui.Confirm("The woodpecker-cli was already configured. Do you want to configure it again?")
if err != nil {
return err
}
if !setupAgain {
log.Info().Msg("Configuration skipped")
return nil
}
}
serverURL := c.String("server-url")
if serverURL == "" {
serverURL = c.Args().First()
}
if serverURL == "" {
serverURL, err = ui.Ask("Enter the URL of the woodpecker server", "https://ci.woodpecker-ci.org", true)
if err != nil {
return err
}
if serverURL == "" {
return errors.New("server URL cannot be empty")
}
}
if !strings.Contains(serverURL, "://") {
serverURL = "https://" + serverURL
}
token := c.String("token")
if token == "" {
token, err = receiveTokenFromUI(c.Context, serverURL)
if err != nil {
return err
}
if token == "" {
return errors.New("no token received from the UI")
}
}
err = config.Save(c, c.String("config"), &config.Config{
ServerURL: serverURL,
Token: token,
LogLevel: "info",
})
if err != nil {
return err
}
log.Info().Msg("The woodpecker-cli has been successfully setup")
return nil
}

135
cli/setup/token_fetcher.go Normal file
View file

@ -0,0 +1,135 @@
package setup
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"os/exec"
"runtime"
"time"
"github.com/charmbracelet/huh/spinner"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
func receiveTokenFromUI(c context.Context, serverURL string) (string, error) {
port := randomPort()
tokenReceived := make(chan string)
srv := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port)}
srv.Handler = setupRouter(tokenReceived)
go func() {
log.Debug().Msgf("Listening for token response on :%d", port)
_ = srv.ListenAndServe()
}()
defer func() {
log.Debug().Msg("Shutting down server")
_ = srv.Shutdown(c)
}()
err := openBrowser(fmt.Sprintf("%s/cli/auth?port=%d", serverURL, port))
if err != nil {
return "", err
}
spinnerCtx, spinnerDone := context.WithCancelCause(c)
go func() {
err = spinner.New().
Title("Waiting for token ...").
Context(spinnerCtx).
Run()
if err != nil {
return
}
}()
// wait for token to be received or timeout
select {
case token := <-tokenReceived:
spinnerDone(nil)
return token, nil
case <-c.Done():
spinnerDone(nil)
return "", c.Err()
case <-time.After(5 * time.Minute):
spinnerDone(nil)
return "", errors.New("timed out waiting for token")
}
}
func setupRouter(tokenReceived chan string) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
e := gin.New()
e.UseRawPath = true
e.Use(gin.Recovery())
e.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
})
e.POST("/token", func(c *gin.Context) {
data := struct {
Token string `json:"token"`
}{}
err := c.BindJSON(&data)
if err != nil {
log.Debug().Err(err).Msg("Failed to bind JSON")
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid request",
})
return
}
tokenReceived <- data.Token
c.JSON(http.StatusOK, gin.H{
"ok": "true",
})
})
return e
}
func openBrowser(url string) error {
var err error
log.Debug().Msgf("Opening browser with URL: %s", url)
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}
func randomPort() int {
const minPort = 10000
const maxPort = 65535
source := rand.NewSource(time.Now().UnixNano())
rand := rand.New(source)
return rand.Intn(maxPort-minPort+1) + minPort
}

26
cli/setup/ui/ask.go Normal file
View file

@ -0,0 +1,26 @@
package ui
import (
"errors"
"strings"
"github.com/charmbracelet/huh"
)
func Ask(prompt, placeholder string, required bool) (string, error) {
var input string
err := huh.NewInput().
Title(prompt).
Value(&input).
Placeholder(placeholder).Validate(func(s string) error {
if required && strings.TrimSpace(s) == "" {
return errors.New("required")
}
return nil
}).Run()
if err != nil {
return "", err
}
return strings.TrimSpace(input), nil
}

19
cli/setup/ui/confirm.go Normal file
View file

@ -0,0 +1,19 @@
package ui
import (
"github.com/charmbracelet/huh"
)
func Confirm(prompt string) (bool, error) {
var confirm bool
err := huh.NewConfirm().
Title(prompt).
Affirmative("Yes!").
Negative("No.").
Value(&confirm).Run()
if err != nil {
return false, err
}
return confirm, err
}

View file

@ -1,11 +1,9 @@
package update
type GithubRelease struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
type VersionData struct {
Latest string `json:"latest"`
Next string `json:"next"`
RC string `json:"rc"`
}
type NewVersion struct {
@ -13,4 +11,7 @@ type NewVersion struct {
AssetURL string
}
const githubReleaseURL = "https://api.github.com/repos/woodpecker-ci/woodpecker/releases/latest"
const (
woodpeckerVersionURL = "https://woodpecker-ci.org/version.json"
githubBinaryURL = "https://github.com/woodpecker-ci/woodpecker/releases/download/v%s/woodpecker-cli_%s_%s.tar.gz"
)

View file

@ -10,6 +10,7 @@ import (
"os"
"path"
"runtime"
"strings"
"github.com/rs/zerolog/log"
@ -17,14 +18,18 @@ import (
)
func CheckForUpdate(ctx context.Context, force bool) (*NewVersion, error) {
return checkForUpdate(ctx, woodpeckerVersionURL, force)
}
func checkForUpdate(ctx context.Context, versionURL string, force bool) (*NewVersion, error) {
log.Debug().Msgf("Current version: %s", version.String())
if version.String() == "dev" && !force {
log.Debug().Msgf("Skipping update check for development version")
if (version.String() == "dev" || strings.HasPrefix(version.String(), "next-")) && !force {
log.Debug().Msgf("Skipping update check for development & next versions")
return nil, nil
}
req, err := http.NewRequestWithContext(ctx, "GET", githubReleaseURL, nil)
req, err := http.NewRequestWithContext(ctx, "GET", versionURL, nil)
if err != nil {
return nil, err
}
@ -39,34 +44,32 @@ func CheckForUpdate(ctx context.Context, force bool) (*NewVersion, error) {
return nil, errors.New("failed to fetch the latest release")
}
var release GithubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
var versionData VersionData
if err := json.NewDecoder(resp.Body).Decode(&versionData); err != nil {
return nil, err
}
upstreamVersion := versionData.Latest
if strings.HasPrefix(version.String(), "next-") {
upstreamVersion = versionData.Next
} else if strings.HasSuffix(version.String(), "rc-") {
upstreamVersion = versionData.RC
}
installedVersion := strings.TrimPrefix(version.Version, "v")
upstreamVersion = strings.TrimPrefix(upstreamVersion, "v")
// using the latest release
if release.TagName == version.String() && !force {
if installedVersion == upstreamVersion && !force {
log.Debug().Msgf("No new version available")
return nil, nil
}
log.Debug().Msgf("Latest version: %s", release.TagName)
assetURL := ""
fileName := fmt.Sprintf("woodpecker-cli_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH)
for _, asset := range release.Assets {
if fileName == asset.Name {
assetURL = asset.BrowserDownloadURL
log.Debug().Msgf("Found asset for the current OS and arch: %s", assetURL)
break
}
}
if assetURL == "" {
return nil, errors.New("no asset found for the current OS")
}
log.Debug().Msgf("New version available: %s", upstreamVersion)
assetURL := fmt.Sprintf(githubBinaryURL, upstreamVersion, runtime.GOOS, runtime.GOARCH)
return &NewVersion{
Version: release.TagName,
Version: upstreamVersion,
AssetURL: assetURL,
}, nil
}

View file

@ -0,0 +1,61 @@
package update
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"go.woodpecker-ci.org/woodpecker/v2/version"
)
func TestCheckForUpdate(t *testing.T) {
version.Version = "1.0.0"
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/version.json" {
http.NotFound(w, r)
return
}
_, _ = io.WriteString(w, `{"latest": "1.0.1", "next": "1.0.2", "rc": "1.0.3"}`)
}
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
defer ts.Close()
newVersion, err := checkForUpdate(context.Background(), ts.URL+"/version.json", false)
if err != nil {
t.Fatalf("Failed to check for updates: %v", err)
}
if newVersion == nil || newVersion.Version != "1.0.1" {
t.Fatalf("Expected a new version 1.0.1, got: %s", newVersion)
}
}
func TestDownloadNewVersion(t *testing.T) {
downloadFilePath := "/woodpecker-cli_linux_amd64.tar.gz"
fixtureHandler := func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != downloadFilePath {
http.NotFound(w, r)
return
}
_, _ = io.WriteString(w, `blob`)
}
ts := httptest.NewServer(http.HandlerFunc(fixtureHandler))
defer ts.Close()
file, err := downloadNewVersion(context.Background(), ts.URL+downloadFilePath)
if err != nil {
t.Fatalf("Failed to download new version: %v", err)
}
if file == "" {
t.Fatalf("Expected a file path, got: %s", file)
}
_ = os.Remove(file)
}

View file

@ -89,7 +89,7 @@ func run(c *cli.Context, backends []types.Backend) error {
agentToken := c.String("grpc-token")
authClient := agentRpc.NewAuthGrpcClient(authConn, agentToken, agentConfig.AgentID)
authInterceptor, err := agentRpc.NewAuthInterceptor(authClient, 30*time.Minute)
authInterceptor, err := agentRpc.NewAuthInterceptor(authClient, 30*time.Minute) //nolint: gomnd
if err != nil {
return err
}
@ -276,12 +276,12 @@ func stringSliceAddToMap(sl []string, m map[string]string) error {
m = make(map[string]string)
}
for _, v := range utils.StringSliceDeleteEmpty(sl) {
parts := strings.SplitN(v, "=", 2)
switch len(parts) {
case 2:
m[parts[0]] = parts[1]
case 1:
return fmt.Errorf("key '%s' does not have a value assigned", parts[0])
before, after, _ := strings.Cut(v, "=")
switch {
case before != "" && after != "":
m[before] = after
case before != "":
return fmt.Errorf("key '%s' does not have a value assigned", before)
default:
return fmt.Errorf("empty string in slice")
}

View file

@ -22,6 +22,7 @@ import (
"github.com/urfave/cli/v2"
)
//nolint:gomnd
var flags = []cli.Flag{
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_SERVER"},
@ -97,4 +98,16 @@ var flags = []cli.Flag{
Usage: "backend to run pipelines on",
Value: "auto-detect",
},
&cli.IntFlag{
EnvVars: []string{"WOODPECKER_CONNECT_RETRY_COUNT"},
Name: "connect-retry-count",
Usage: "number of times to retry connecting to the server",
Value: 5,
},
&cli.DurationFlag{
EnvVars: []string{"WOODPECKER_CONNECT_RETRY_DELAY"},
Name: "connect-retry-delay",
Usage: "duration to wait before retrying to connect to the server",
Value: time.Second * 2,
},
}

View file

@ -39,14 +39,14 @@ func initHealth() {
func handleHeartbeat(w http.ResponseWriter, _ *http.Request) {
if counter.Healthy() {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
}
}
func handleVersion(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "text/json")
err := json.NewEncoder(w).Encode(versionResp{
Source: "https://github.com/woodpecker-ci/woodpecker",
@ -59,9 +59,9 @@ func handleVersion(w http.ResponseWriter, _ *http.Request) {
func handleStats(w http.ResponseWriter, _ *http.Request) {
if counter.Healthy() {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Add("Content-Type", "text/json")
if _, err := counter.WriteTo(w); err != nil {
@ -92,8 +92,8 @@ func pinger(c *cli.Context) error {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("agent returned non-200 status code")
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("agent returned non-http.StatusOK status code")
}
return nil
}

View file

@ -29,6 +29,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/cli/registry"
"go.woodpecker-ci.org/woodpecker/v2/cli/repo"
"go.woodpecker-ci.org/woodpecker/v2/cli/secret"
"go.woodpecker-ci.org/woodpecker/v2/cli/setup"
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
"go.woodpecker-ci.org/woodpecker/v2/cli/user"
"go.woodpecker-ci.org/woodpecker/v2/version"
@ -58,6 +59,7 @@ func newApp() *cli.App {
lint.Command,
loglevel.Command,
cron.Command,
setup.Command,
update.Command,
}

View file

@ -26,7 +26,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get agent list",
"summary": "List agents",
"parameters": [
{
"type": "string",
@ -64,13 +64,14 @@ const docTemplate = `{
}
},
"post": {
"description": "Creates a new agent with a random token",
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Create a new agent with a random token so a new agent can connect to the server",
"summary": "Create a new agent",
"parameters": [
{
"type": "string",
@ -108,7 +109,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get agent information",
"summary": "Get an agent",
"parameters": [
{
"type": "string",
@ -173,7 +174,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Update agent information",
"summary": "Update an agent",
"parameters": [
{
"type": "string",
@ -218,7 +219,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get agent tasks",
"summary": "List agent tasks",
"parameters": [
{
"type": "string",
@ -283,7 +284,7 @@ const docTemplate = `{
"tags": [
"Badges"
],
"summary": "Get status badge, SVG format",
"summary": "Get status of pipeline as SVG badge",
"parameters": [
{
"type": "integer",
@ -744,7 +745,7 @@ const docTemplate = `{
"tags": [
"Organizations"
],
"summary": "Lookup organization by full-name",
"summary": "Lookup an organization by full name",
"parameters": [
{
"type": "string",
@ -756,7 +757,7 @@ const docTemplate = `{
},
{
"type": "string",
"description": "the organizations full-name / slug",
"description": "the organizations full name / slug",
"name": "org_full_name",
"in": "path",
"required": true
@ -781,7 +782,7 @@ const docTemplate = `{
"tags": [
"Orgs"
],
"summary": "Get all orgs",
"summary": "List organizations",
"parameters": [
{
"type": "string",
@ -828,7 +829,7 @@ const docTemplate = `{
"tags": [
"Orgs"
],
"summary": "Delete an org",
"summary": "Delete an organization",
"parameters": [
{
"type": "string",
@ -861,7 +862,7 @@ const docTemplate = `{
"tags": [
"Organization"
],
"summary": "Get organization by id",
"summary": "Get an organization",
"parameters": [
{
"type": "string",
@ -900,7 +901,7 @@ const docTemplate = `{
"tags": [
"Organization permissions"
],
"summary": "Get the permissions of the current user in the given organization",
"summary": "Get the permissions of the currently authenticated user for the given organization",
"parameters": [
{
"type": "string",
@ -939,7 +940,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Get the organization secret list",
"summary": "List organization secrets",
"parameters": [
{
"type": "string",
@ -990,7 +991,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Persist/create an organization secret",
"summary": "Create an organization secret",
"parameters": [
{
"type": "string",
@ -1035,7 +1036,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Get the named organization secret",
"summary": "Get a organization secret by name",
"parameters": [
{
"type": "string",
@ -1076,7 +1077,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Delete the named secret from an organization",
"summary": "Delete an organization secret by name",
"parameters": [
{
"type": "string",
@ -1114,7 +1115,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Update an organization secret",
"summary": "Update an organization secret by name",
"parameters": [
{
"type": "string",
@ -1166,7 +1167,7 @@ const docTemplate = `{
"tags": [
"Pipeline queues"
],
"summary": "List pipeline queues",
"summary": "List pipelines in queue",
"parameters": [
{
"type": "string",
@ -1257,7 +1258,7 @@ const docTemplate = `{
"tags": [
"Pipeline queues"
],
"summary": "Pause a pipeline queue",
"summary": "Pause the pipeline queue",
"parameters": [
{
"type": "string",
@ -1283,7 +1284,7 @@ const docTemplate = `{
"tags": [
"Pipeline queues"
],
"summary": "Resume a pipeline queue",
"summary": "Resume the pipeline queue",
"parameters": [
{
"type": "string",
@ -1303,13 +1304,14 @@ const docTemplate = `{
},
"/repos": {
"get": {
"description": "Returns a list of all repositories. Requires admin rights.",
"produces": [
"application/json"
],
"tags": [
"Repositories"
],
"summary": "List all repositories on the server. Requires admin rights.",
"summary": "List all repositories on the server",
"parameters": [
{
"type": "string",
@ -1395,7 +1397,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Get repository by full-name",
"summary": "Lookup a repository by full name",
"parameters": [
{
"type": "string",
@ -1407,7 +1409,7 @@ const docTemplate = `{
},
{
"type": "string",
"description": "the repository full-name / slug",
"description": "the repository full name / slug",
"name": "repo_full_name",
"in": "path",
"required": true
@ -1425,13 +1427,14 @@ const docTemplate = `{
},
"/repos/repair": {
"post": {
"description": "Executes a repair process on all repositories. Requires admin rights.",
"produces": [
"text/plain"
],
"tags": [
"Repositories"
],
"summary": "Repair all repositories on the server. Requires admin rights.",
"summary": "Repair all repositories on the server",
"parameters": [
{
"type": "string",
@ -1457,7 +1460,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Get repository information",
"summary": "Get a repository",
"parameters": [
{
"type": "string",
@ -1525,7 +1528,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Change a repository",
"summary": "Update a repository",
"parameters": [
{
"type": "string",
@ -1570,7 +1573,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Get repository branches",
"summary": "Get branches of a repository",
"parameters": [
{
"type": "string",
@ -1623,7 +1626,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Change a repository's owner, to the one holding the access token",
"summary": "Change a repository's owner to the currently authenticated user",
"parameters": [
{
"type": "string",
@ -1659,7 +1662,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Get the cron job list",
"summary": "List cron jobs",
"parameters": [
{
"type": "string",
@ -1710,7 +1713,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Persist/creat a cron job",
"summary": "Create a cron job",
"parameters": [
{
"type": "string",
@ -1755,7 +1758,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Get a cron job by id",
"summary": "Get a cron job",
"parameters": [
{
"type": "string",
@ -1837,7 +1840,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Delete a cron job by id",
"summary": "Delete a cron job",
"parameters": [
{
"type": "string",
@ -1927,7 +1930,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Deletes log",
"summary": "Deletes all logs of a pipeline",
"parameters": [
{
"type": "string",
@ -1967,7 +1970,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Log information",
"summary": "Get logs for a pipeline step",
"parameters": [
{
"type": "string",
@ -2012,6 +2015,53 @@ const docTemplate = `{
}
}
},
"/repos/{repo_id}/logs/{number}/{stepId}": {
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Pipeline logs"
],
"summary": "Delete step logs of a pipeline",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the number of the pipeline",
"name": "number",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the step id",
"name": "stepId",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/repos/{repo_id}/move": {
"post": {
"produces": [
@ -2061,7 +2111,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Repository permission information",
"summary": "Check current authenticated users access to the repository",
"parameters": [
{
"type": "string",
@ -2091,13 +2141,14 @@ const docTemplate = `{
},
"/repos/{repo_id}/pipelines": {
"get": {
"description": "Get a list of pipelines for a repository.",
"produces": [
"application/json"
],
"tags": [
"Pipelines"
],
"summary": "Get pipelines, current running and past ones",
"summary": "List repository pipelines",
"parameters": [
{
"type": "string",
@ -2127,6 +2178,18 @@ const docTemplate = `{
"description": "for response pagination, max items per page",
"name": "perPage",
"in": "query"
},
{
"type": "string",
"description": "only return pipelines before this RFC3339 date",
"name": "before",
"in": "query"
},
{
"type": "string",
"description": "only return pipelines after this RFC3339 date",
"name": "after",
"in": "query"
}
],
"responses": {
@ -2148,7 +2211,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Run/trigger a pipelines",
"summary": "Trigger a manual pipeline",
"parameters": [
{
"type": "string",
@ -2193,7 +2256,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Pipeline information by number",
"summary": "Get a repositories pipeline",
"parameters": [
{
"type": "string",
@ -2280,6 +2343,44 @@ const docTemplate = `{
}
}
}
},
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Pipelines"
],
"summary": "Delete a pipeline",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the number of the pipeline",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/repos/{repo_id}/pipelines/{number}/approve": {
@ -2290,7 +2391,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Start pipelines in gated repos",
"summary": "Approve and start a pipeline",
"parameters": [
{
"type": "string",
@ -2333,7 +2434,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Cancels a pipeline",
"summary": "Cancel a pipeline",
"parameters": [
{
"type": "string",
@ -2373,7 +2474,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Pipeline configuration",
"summary": "Get configuration files for a pipeline",
"parameters": [
{
"type": "string",
@ -2419,7 +2520,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Decline pipelines in gated repos",
"summary": "Decline a pipeline",
"parameters": [
{
"type": "string",
@ -2462,7 +2563,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "List active pull requests",
"summary": "List active pull requests of a repository",
"parameters": [
{
"type": "string",
@ -2515,7 +2616,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Get the registry list",
"summary": "List registries",
"parameters": [
{
"type": "string",
@ -2566,7 +2667,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Persist/create a registry",
"summary": "Create a registry",
"parameters": [
{
"type": "string",
@ -2611,7 +2712,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Get a named registry",
"summary": "Get a registry by name",
"parameters": [
{
"type": "string",
@ -2652,7 +2753,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Delete a named registry",
"summary": "Delete a registry by name",
"parameters": [
{
"type": "string",
@ -2690,7 +2791,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Update a named registry",
"summary": "Update a registry by name",
"parameters": [
{
"type": "string",
@ -2775,7 +2876,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Get the secret list",
"summary": "List repository secrets",
"parameters": [
{
"type": "string",
@ -2826,7 +2927,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Persist/create a secret",
"summary": "Create a repository secret",
"parameters": [
{
"type": "string",
@ -2871,7 +2972,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Get a named secret",
"summary": "Get a repository secret by name",
"parameters": [
{
"type": "string",
@ -2912,7 +3013,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Delete a named secret",
"summary": "Delete a repository secret by name",
"parameters": [
{
"type": "string",
@ -2950,7 +3051,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Update a named secret",
"summary": "Update a repository secret by name",
"parameters": [
{
"type": "string",
@ -3002,7 +3103,7 @@ const docTemplate = `{
"tags": [
"Secrets"
],
"summary": "Get the global secret list",
"summary": "List global secrets",
"parameters": [
{
"type": "string",
@ -3046,7 +3147,7 @@ const docTemplate = `{
"tags": [
"Secrets"
],
"summary": "Persist/create a global secret",
"summary": "Create a global secret",
"parameters": [
{
"type": "string",
@ -3214,14 +3315,14 @@ const docTemplate = `{
},
"/stream/events": {
"get": {
"description": "event source streaming for compatibility with quic and http2",
"description": "With quic and http2 support",
"produces": [
"text/plain"
],
"tags": [
"Events"
],
"summary": "Event stream",
"summary": "Stream events like pipeline updates",
"responses": {
"200": {
"description": "OK"
@ -3237,7 +3338,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Log stream",
"summary": "Stream logs of a pipeline step",
"parameters": [
{
"type": "integer",
@ -3276,7 +3377,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "Returns the currently authenticated user.",
"summary": "Get the currently authenticated user",
"parameters": [
{
"type": "string",
@ -3299,14 +3400,14 @@ const docTemplate = `{
},
"/user/feed": {
"get": {
"description": "Feed entries can be used to display information on the latest builds.",
"description": "The feed lists the most recent pipeline for the currently authenticated user.",
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "A feed entry for a build.",
"summary": "Get the currently authenticaed users pipeline feed",
"parameters": [
{
"type": "string",
@ -3321,7 +3422,10 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Feed"
"type": "array",
"items": {
"$ref": "#/definitions/Feed"
}
}
}
}
@ -3336,7 +3440,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "Get user's repos",
"summary": "Get user's repositories",
"parameters": [
{
"type": "string",
@ -3426,7 +3530,7 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Get all users",
"summary": "List users",
"parameters": [
{
"type": "string",
@ -3580,7 +3684,7 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Change a user",
"summary": "Update a user",
"parameters": [
{
"type": "string",
@ -3844,6 +3948,9 @@ const docTemplate = `{
"Org": {
"type": "object",
"properties": {
"forge_id": {
"type": "integer"
},
"id": {
"type": "integer"
},
@ -3916,13 +4023,16 @@ const docTemplate = `{
"created_at": {
"type": "integer"
},
"deploy_task": {
"type": "string"
},
"deploy_to": {
"type": "string"
},
"errors": {
"type": "array",
"items": {
"$ref": "#/definitions/errors.PipelineError"
"$ref": "#/definitions/types.PipelineError"
}
},
"event": {
@ -4048,6 +4158,9 @@ const docTemplate = `{
"active": {
"type": "boolean"
},
"allow_deploy": {
"type": "boolean"
},
"allow_pr": {
"type": "boolean"
},
@ -4072,6 +4185,9 @@ const docTemplate = `{
"default_branch": {
"type": "string"
},
"forge_id": {
"type": "integer"
},
"forge_remote_id": {
"description": "ForgeRemoteID is the unique identifier for the repository on the forge.",
"type": "string"
@ -4123,6 +4239,9 @@ const docTemplate = `{
"RepoPatch": {
"type": "object",
"properties": {
"allow_deploy": {
"type": "boolean"
},
"allow_pr": {
"type": "boolean"
},
@ -4365,6 +4484,9 @@ const docTemplate = `{
"description": "Email is the email address for this user.\n\nrequired: true",
"type": "string"
},
"forge_id": {
"type": "integer"
},
"id": {
"description": "the id for this user.\n\nrequired: true",
"type": "integer"
@ -4402,45 +4524,6 @@ const docTemplate = `{
"EventManual"
]
},
"errors.PipelineError": {
"type": "object",
"properties": {
"data": {},
"is_warning": {
"type": "boolean"
},
"message": {
"type": "string"
},
"type": {
"$ref": "#/definitions/errors.PipelineErrorType"
}
}
},
"errors.PipelineErrorType": {
"type": "string",
"enum": [
"linter",
"deprecation",
"compiler",
"generic",
"bad_habit"
],
"x-enum-comments": {
"PipelineErrorTypeBadHabit": "some bad-habit error",
"PipelineErrorTypeCompiler": "some error with the config semantics",
"PipelineErrorTypeDeprecation": "using some deprecated feature",
"PipelineErrorTypeGeneric": "some generic error",
"PipelineErrorTypeLinter": "some error with the config syntax"
},
"x-enum-varnames": [
"PipelineErrorTypeLinter",
"PipelineErrorTypeDeprecation",
"PipelineErrorTypeCompiler",
"PipelineErrorTypeGeneric",
"PipelineErrorTypeBadHabit"
]
},
"model.Workflow": {
"type": "object",
"properties": {
@ -4487,6 +4570,45 @@ const docTemplate = `{
"$ref": "#/definitions/StatusValue"
}
}
},
"types.PipelineError": {
"type": "object",
"properties": {
"data": {},
"is_warning": {
"type": "boolean"
},
"message": {
"type": "string"
},
"type": {
"$ref": "#/definitions/types.PipelineErrorType"
}
}
},
"types.PipelineErrorType": {
"type": "string",
"enum": [
"linter",
"deprecation",
"compiler",
"generic",
"bad_habit"
],
"x-enum-comments": {
"PipelineErrorTypeBadHabit": "some bad-habit error",
"PipelineErrorTypeCompiler": "some error with the config semantics",
"PipelineErrorTypeDeprecation": "using some deprecated feature",
"PipelineErrorTypeGeneric": "some generic error",
"PipelineErrorTypeLinter": "some error with the config syntax"
},
"x-enum-varnames": [
"PipelineErrorTypeLinter",
"PipelineErrorTypeDeprecation",
"PipelineErrorTypeCompiler",
"PipelineErrorTypeGeneric",
"PipelineErrorTypeBadHabit"
]
}
}
}`

View file

@ -194,16 +194,6 @@ var flags = append([]cli.Flag{
Name: "keepalive-min-time",
Usage: "server-side enforcement policy on the minimum amount of time a client should wait before sending a keepalive ping.",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_SECRET_ENDPOINT"},
Name: "secret-service",
Usage: "secret plugin endpoint",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_REGISTRY_ENDPOINT"},
Name: "registry-service",
Usage: "registry plugin endpoint",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CONFIG_SERVICE_ENDPOINT"},
Name: "config-service-endpoint",
@ -219,7 +209,7 @@ var flags = append([]cli.Flag{
EnvVars: []string{"WOODPECKER_DATABASE_DATASOURCE"},
Name: "datasource",
Usage: "database driver configuration string",
Value: "woodpecker.sqlite",
Value: datasourceDefaultValue(),
FilePath: os.Getenv("WOODPECKER_DATABASE_DATASOURCE_FILE"),
},
&cli.StringFlag{
@ -256,11 +246,6 @@ var flags = append([]cli.Flag{
Usage: "Disable version check in admin web ui.",
Name: "skip-version-check",
},
&cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_ADDONS"},
Name: "addons",
Usage: "list of addon files",
},
//
// backend options for pipeline compiler
//
@ -319,6 +304,35 @@ var flags = append([]cli.Flag{
Usage: "set the cpus allowed to execute containers",
},
//
&cli.StringFlag{
Name: "forge-url",
Usage: "url of the forge",
EnvVars: []string{"WOODPECKER_FORGE_URL", "WOODPECKER_GITHUB_URL", "WOODPECKER_GITLAB_URL", "WOODPECKER_GITEA_URL", "WOODPECKER_BITBUCKET_URL"},
},
&cli.StringFlag{
Name: "forge-oauth-client",
Usage: "oauth2 client id",
EnvVars: []string{"WOODPECKER_FORGE_CLIENT", "WOODPECKER_GITHUB_CLIENT", "WOODPECKER_GITLAB_CLIENT", "WOODPECKER_GITEA_CLIENT", "WOODPECKER_BITBUCKET_CLIENT", "WOODPECKER_BITBUCKET_DC_CLIENT_ID"},
},
&cli.StringFlag{
Name: "forge-oauth-secret",
Usage: "oauth2 client secret",
EnvVars: []string{"WOODPECKER_FORGE_SECRET", "WOODPECKER_GITHUB_SECRET", "WOODPECKER_GITLAB_SECRET", "WOODPECKER_GITEA_SECRET", "WOODPECKER_BITBUCKET_SECRET", "WOODPECKER_BITBUCKET_DC_CLIENT_SECRET"},
},
&cli.BoolFlag{
Name: "forge-skip-verify",
Usage: "skip ssl verification",
EnvVars: []string{"WOODPECKER_FORGE_SKIP_VERIFY", "WOODPECKER_GITHUB_SKIP_VERIFY", "WOODPECKER_GITLAB_SKIP_VERIFY", "WOODPECKER_GITEA_SKIP_VERIFY", "WOODPECKER_BITBUCKET_SKIP_VERIFY"},
},
//
// Addon
//
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ADDON_FORGE"},
Name: "addon-forge",
Usage: "path to forge addon executable",
},
//
// GitHub
//
&cli.BoolFlag{
@ -326,24 +340,6 @@ var flags = append([]cli.Flag{
Name: "github",
Usage: "github driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITHUB_URL"},
Name: "github-server",
Usage: "github server address",
Value: "https://github.com",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITHUB_CLIENT"},
Name: "github-client",
Usage: "github oauth2 client id",
FilePath: os.Getenv("WOODPECKER_GITHUB_CLIENT_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITHUB_SECRET"},
Name: "github-secret",
Usage: "github oauth2 client secret",
FilePath: os.Getenv("WOODPECKER_GITHUB_SECRET_FILE"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_GITHUB_MERGE_REF"},
Name: "github-merge-ref",
@ -351,9 +347,10 @@ var flags = append([]cli.Flag{
Value: true,
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_GITHUB_SKIP_VERIFY"},
Name: "github-skip-verify",
Usage: "github skip ssl verification",
EnvVars: []string{"WOODPECKER_GITHUB_PUBLIC_ONLY"},
Name: "github-public-only",
Usage: "github tokens should only get access to public repos",
Value: false,
},
//
// Gitea
@ -364,27 +361,9 @@ var flags = append([]cli.Flag{
Usage: "gitea driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITEA_URL"},
Name: "gitea-server",
Usage: "gitea server address",
Value: "https://try.gitea.io",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITEA_CLIENT"},
Name: "gitea-client",
Usage: "gitea oauth2 client id",
FilePath: os.Getenv("WOODPECKER_GITEA_CLIENT_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITEA_SECRET"},
Name: "gitea-secret",
Usage: "gitea oauth2 client secret",
FilePath: os.Getenv("WOODPECKER_GITEA_SECRET_FILE"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_GITEA_SKIP_VERIFY"},
Name: "gitea-skip-verify",
Usage: "gitea skip ssl verification",
EnvVars: []string{"WOODPECKER_DEV_GITEA_OAUTH_URL"},
Name: "gitea-oauth-server",
Usage: "user-facing gitea server url for oauth",
},
//
// Bitbucket
@ -394,18 +373,6 @@ var flags = append([]cli.Flag{
Name: "bitbucket",
Usage: "bitbucket driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BITBUCKET_CLIENT"},
Name: "bitbucket-client",
Usage: "bitbucket oauth2 client id",
FilePath: os.Getenv("WOODPECKER_BITBUCKET_CLIENT_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BITBUCKET_SECRET"},
Name: "bitbucket-secret",
Usage: "bitbucket oauth2 client secret",
FilePath: os.Getenv("WOODPECKER_BITBUCKET_SECRET_FILE"),
},
//
// Gitlab
//
@ -414,29 +381,6 @@ var flags = append([]cli.Flag{
Name: "gitlab",
Usage: "gitlab driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITLAB_URL"},
Name: "gitlab-server",
Usage: "gitlab server address",
Value: "https://gitlab.com",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITLAB_CLIENT"},
Name: "gitlab-client",
Usage: "gitlab oauth2 client id",
FilePath: os.Getenv("WOODPECKER_GITLAB_CLIENT_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITLAB_SECRET"},
Name: "gitlab-secret",
Usage: "gitlab oauth2 client secret",
FilePath: os.Getenv("WOODPECKER_GITLAB_SECRET_FILE"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_GITLAB_SKIP_VERIFY"},
Name: "gitlab-skip-verify",
Usage: "gitlab skip ssl verification",
},
//
// Bitbucket DataCenter/Server (previously Stash)
//
@ -445,23 +389,6 @@ var flags = append([]cli.Flag{
Name: "bitbucket-dc",
Usage: "Bitbucket DataCenter/Server driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BITBUCKET_DC_URL"},
Name: "bitbucket-dc-server",
Usage: "Bitbucket DataCenter/Server server address",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BITBUCKET_DC_CLIENT_ID"},
Name: "bitbucket-dc-client-id",
Usage: "Bitbucket DataCenter/Server OAuth 2.0 client id",
FilePath: os.Getenv("WOODPECKER_BITBUCKET_DC_CLIENT_ID_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BITBUCKET_DC_CLIENT_SECRET"},
Name: "bitbucket-dc-client-secret",
Usage: "Bitbucket DataCenter/Server OAuth 2.0 client secret",
FilePath: os.Getenv("WOODPECKER_BITBUCKET_DC_CLIENT_SECRET_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BITBUCKET_DC_GIT_USERNAME"},
Name: "bitbucket-dc-git-username",
@ -510,3 +437,13 @@ var flags = append([]cli.Flag{
Usage: "Flag to decrypt all encrypted data and disable encryption on server",
},
}, logger.GlobalLoggerFlags...)
// If woodpecker is running inside a container the default value for
// the datasource is different from running outside a container.
func datasourceDefaultValue() string {
_, found := os.LookupEnv("WOODPECKER_IN_CONTAINER")
if found {
return "/var/lib/woodpecker/woodpecker.sqlite"
}
return "woodpecker.sqlite"
}

View file

@ -38,7 +38,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto"
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/cron"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/setup"
woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc"
"go.woodpecker-ci.org/woodpecker/v2/server/logging"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
@ -82,11 +82,6 @@ func run(c *cli.Context) error {
)
}
_forge, err := setupForge(c)
if err != nil {
return fmt.Errorf("can't setup forge: %w", err)
}
_store, err := setupStore(c)
if err != nil {
return fmt.Errorf("can't setup store: %w", err)
@ -97,7 +92,7 @@ func run(c *cli.Context) error {
}
}()
err = setupEvilGlobals(c, _store, _forge)
err = setupEvilGlobals(c, _store)
if err != nil {
return fmt.Errorf("can't setup globals: %w", err)
}
@ -107,7 +102,7 @@ func run(c *cli.Context) error {
setupMetrics(&g, _store)
g.Go(func() error {
return cron.Start(c.Context, _store, _forge)
return cron.Start(c.Context, _store)
})
// start the grpc server
@ -130,7 +125,6 @@ func run(c *cli.Context) error {
)
woodpeckerServer := woodpeckerGrpcServer.NewWoodpeckerServer(
_forge,
server.Config.Services.Queue,
server.Config.Services.Logs,
server.Config.Services.Pubsub,
@ -270,17 +264,13 @@ func run(c *cli.Context) error {
return g.Wait()
}
func setupEvilGlobals(c *cli.Context, s store.Store, f forge.Forge) error {
// forge
server.Config.Services.Forge = f
func setupEvilGlobals(c *cli.Context, s store.Store) error {
// services
server.Config.Services.Queue = setupQueue(c, s)
server.Config.Services.Logs = logging.New()
server.Config.Services.Pubsub = pubsub.New()
server.Config.Services.Membership = setupMembershipService(c, f)
serviceMangager, err := services.NewManager(c, s)
server.Config.Services.Membership = setupMembershipService(c, s)
serviceMangager, err := services.NewManager(c, s, setup.Forge)
if err != nil {
return fmt.Errorf("could not setup service manager: %w", err)
}

View file

@ -18,9 +18,7 @@ package main
import (
"context"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
@ -31,17 +29,9 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/cache"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucket"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucketdatacenter"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/github"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab"
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
"go.woodpecker-ci.org/woodpecker/v2/shared/addon"
addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types"
)
func setupStore(c *cli.Context) (store.Store, error) {
@ -101,99 +91,8 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue {
return queue.WithTaskStore(queue.New(c.Context), s)
}
func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService {
return cache.NewMembershipService(r)
}
// setupForge helper function to set up the forge from the CLI arguments.
func setupForge(c *cli.Context) (forge.Forge, error) {
addonForge, err := addon.Load[forge.Forge](c.StringSlice("addons"), addonTypes.TypeForge)
if err != nil {
return nil, err
}
if addonForge != nil {
return addonForge.Value, nil
}
switch {
case c.Bool("github"):
return setupGitHub(c)
case c.Bool("gitlab"):
return setupGitLab(c)
case c.Bool("bitbucket"):
return setupBitbucket(c)
case c.Bool("bitbucket-dc"):
return setupBitbucketDatacenter(c)
case c.Bool("gitea"):
return setupGitea(c)
default:
return nil, fmt.Errorf("version control system not configured")
}
}
// setupBitbucket helper function to setup the Bitbucket forge from the CLI arguments.
func setupBitbucket(c *cli.Context) (forge.Forge, error) {
opts := &bitbucket.Opts{
Client: c.String("bitbucket-client"),
Secret: c.String("bitbucket-secret"),
}
log.Trace().Msgf("forge (bitbucket) opts: %#v", opts)
return bitbucket.New(opts)
}
// setupGitea helper function to setup the Gitea forge from the CLI arguments.
func setupGitea(c *cli.Context) (forge.Forge, error) {
server, err := url.Parse(c.String("gitea-server"))
if err != nil {
return nil, err
}
opts := gitea.Opts{
URL: strings.TrimRight(server.String(), "/"),
Client: c.String("gitea-client"),
Secret: c.String("gitea-secret"),
SkipVerify: c.Bool("gitea-skip-verify"),
}
if len(opts.URL) == 0 {
return nil, fmt.Errorf("WOODPECKER_GITEA_URL must be set")
}
log.Trace().Msgf("forge (gitea) opts: %#v", opts)
return gitea.New(opts)
}
// setupBitbucketDatacenter helper function to setup the Bitbucket DataCenter/Server forge from the CLI arguments.
func setupBitbucketDatacenter(c *cli.Context) (forge.Forge, error) {
opts := bitbucketdatacenter.Opts{
URL: c.String("bitbucket-dc-server"),
Username: c.String("bitbucket-dc-git-username"),
Password: c.String("bitbucket-dc-git-password"),
ClientID: c.String("bitbucket-dc-client-id"),
ClientSecret: c.String("bitbucket-dc-client-secret"),
}
log.Trace().Msgf("Forge (bitbucketdatacenter) opts: %#v", opts)
return bitbucketdatacenter.New(opts)
}
// setupGitLab helper function to setup the GitLab forge from the CLI arguments.
func setupGitLab(c *cli.Context) (forge.Forge, error) {
return gitlab.New(gitlab.Opts{
URL: c.String("gitlab-server"),
ClientID: c.String("gitlab-client"),
ClientSecret: c.String("gitlab-secret"),
SkipVerify: c.Bool("gitlab-skip-verify"),
})
}
// setupGitHub helper function to setup the GitHub forge from the CLI arguments.
func setupGitHub(c *cli.Context) (forge.Forge, error) {
opts := github.Opts{
URL: c.String("github-server"),
Client: c.String("github-client"),
Secret: c.String("github-secret"),
SkipVerify: c.Bool("github-skip-verify"),
MergeRef: c.Bool("github-merge-ref"),
}
log.Trace().Msgf("forge (github) opts: %#v", opts)
return github.New(opts)
func setupMembershipService(_ *cli.Context, _store store.Store) cache.MembershipService {
return cache.NewMembershipService(_store)
}
func setupMetrics(g *errgroup.Group, _store store.Store) {

View file

@ -3,7 +3,7 @@ version: '3'
services:
gitea-database:
image: postgres:16.1-alpine
image: postgres:16.2-alpine
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: 123456

View file

@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
FROM docker.io/alpine:3.19
# renovate: datasource=repology depName=alpine_3_18/ca-certificates versioning=loose
ENV CA_CERTIFICATES_VERSION="20230506-r0"
ENV CA_CERTIFICATES_VERSION="20240226-r0"
RUN apk add -U --no-cache ca-certificates=${CA_CERTIFICATES_VERSION}
ENV GODEBUG=netdns=go
EXPOSE 3000

View file

@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
FROM docker.io/alpine:3.19
# renovate: datasource=repology depName=alpine_3_18/ca-certificates versioning=loose
ENV CA_CERTIFICATES_VERSION="20230506-r0"
ENV CA_CERTIFICATES_VERSION="20240226-r0"
RUN apk add -U --no-cache ca-certificates=${CA_CERTIFICATES_VERSION}
ENV GODEBUG=netdns=go

View file

@ -1,28 +1,34 @@
# docker build --rm -f docker/Dockerfile.make -t woodpecker/make:local .
FROM docker.io/golang:1.22-alpine3.18 as golang_image
FROM docker.io/node:21-alpine3.18
FROM docker.io/golang:1.22-alpine3.19 as golang_image
FROM docker.io/node:22-alpine3.19
# renovate: datasource=repology depName=alpine_3_18/make versioning=loose
ENV MAKE_VERSION="4.4.1-r1"
# renovate: datasource=repology depName=alpine_3_18/gcc versioning=loose
ENV GCC_VERSION="12.2.1_git20220924-r10"
# renovate: datasource=repology depName=alpine_3_18/binutils-gold versioning=loose
ENV BINUTILS_GOLD_VERSION="2.40-r7"
# renovate: datasource=repology depName=alpine_3_18/musl-dev versioning=loose
ENV MUSL_DEV_VERSION="1.2.4-r2"
# renovate: datasource=repology depName=alpine_3_19/make versioning=loose
ENV MAKE_VERSION="4.4.1-r2"
# renovate: datasource=repology depName=alpine_3_19/gcc versioning=loose
ENV GCC_VERSION="13.2.1_git20231014-r0"
# renovate: datasource=repology depName=alpine_3_19/binutils-gold versioning=loose
ENV BINUTILS_GOLD_VERSION="2.41-r0"
# renovate: datasource=repology depName=alpine_3_19/musl-dev versioning=loose
ENV MUSL_DEV_VERSION="1.2.4_git20230717-r4"
# renovate: datasource=repology depName=alpine_3_19/protoc versioning=loose
ENV PROTOC_VERSION="24.4-r0"
RUN apk add --no-cache --update make=${MAKE_VERSION} gcc=${GCC_VERSION} binutils-gold=${BINUTILS_GOLD_VERSION} musl-dev=${MUSL_DEV_VERSION} && \
RUN apk add --no-cache --update make=${MAKE_VERSION} gcc=${GCC_VERSION} binutils-gold=${BINUTILS_GOLD_VERSION} musl-dev=${MUSL_DEV_VERSION} protoc=${PROTOC_VERSION} && \
corepack enable
# Build packages.
COPY --from=golang_image /usr/local/go /usr/local/go
COPY Makefile /
ENV PATH=$PATH:/usr/local/go/bin
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# Cache tools
RUN make install-tools && \
mv /root/go/bin/* /usr/local/go/bin/ && \
chmod 755 /usr/local/go/bin/*
RUN GOBIN=/usr/local/go/bin make install-tools && \
rm -rf /Makefile
ENV GOPATH=/tmp/go
ENV HOME=/tmp/home
ENV PATH=$PATH:/usr/local/go/bin:/tmp/go/bin
WORKDIR /build
RUN chmod -R 777 /root

View file

@ -1,13 +1,13 @@
FROM docker.io/alpine:3.19
# renovate: datasource=repology depName=alpine_3_18/ca-certificates versioning=loose
ENV CA_CERTIFICATES_VERSION="20230506-r0"
ENV CA_CERTIFICATES_VERSION="20240226-r0"
ARG TARGETOS TARGETARCH
RUN apk add -U --no-cache ca-certificates=${CA_CERTIFICATES_VERSION}
ENV GODEBUG=netdns=go
ENV WOODPECKER_DATABASE_DATASOURCE=/var/lib/woodpecker/woodpecker.sqlite
ENV WOODPECKER_DATABASE_DRIVER=sqlite3
# Internal setting do NOT change! Signals that woodpecker is running inside a container
ENV WOODPECKER_IN_CONTAINER=true
ENV XDG_CACHE_HOME=/var/lib/woodpecker
ENV XDG_DATA_HOME=/var/lib/woodpecker
EXPOSE 8000 9000 80 443

View file

@ -3,8 +3,8 @@ FROM --platform=$BUILDPLATFORM docker.io/golang:1.22 AS certs
FROM scratch
ARG TARGETOS TARGETARCH
ENV GODEBUG=netdns=go
ENV WOODPECKER_DATABASE_DATASOURCE=/var/lib/woodpecker/woodpecker.sqlite
ENV WOODPECKER_DATABASE_DRIVER=sqlite3
# Internal setting do NOT change! Signals that woodpecker is running inside a container
ENV WOODPECKER_IN_CONTAINER=true
ENV XDG_CACHE_HOME=/var/lib/woodpecker
ENV XDG_DATA_HOME=/var/lib/woodpecker
EXPOSE 8000 9000 80 443

View file

@ -75,13 +75,13 @@ kubectl apply -f $PLUGIN_TEMPLATE
```yaml title=".woodpecker.yaml"
steps:
deploy-to-k8s:
- name: deploy-to-k8s
image: laszlocloud/my-k8s-plugin
settings:
template: config/k8s/service.yaml
```
See [plugin docs](./20-usage/51-plugins/10-overview.md).
See [plugin docs](./20-usage/51-plugins/51-overview.md).
## Continue reading

View file

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,63 @@
# Terminology
## Woodpecker architecture
![Woodpecker architecture](architecture.svg)
## Pipeline, workflow & step
![Relation between pipelines, workflows and steps](pipeline-workflow-step.svg)
## Glossary
- **Woodpecker CI**: The project name around Woodpecker.
- **Woodpecker**: An open-source tool that executes [pipelines][Pipeline] on your code.
- **Server**: The component of Woodpecker that handles webhooks from forges, orchestrates agents, and sends status back. It also serves the API and web UI for administration and configuration.
- **Agent**: A component of Woodpecker that executes [pipelines][Pipeline] (specifically one or more [workflows][Workflow]) with a specific backend (e.g. [Docker][], Kubernetes, [local][Local]). It connects to the server via GRPC.
- **CLI**: The Woodpecker command-line interface (CLI) is a terminal tool used to administer the server, to execute pipelines locally for debugging / testing purposes, and to perform tasks like linting pipelines.
- **Pipeline**: A sequence of [workflows][Workflow] that are executed on the code. [Pipelines][Pipeline] are triggered by events.
- **Workflow**: A sequence of steps and services that are executed as part of a [pipeline][Pipeline]. Workflows are represented by YAML files. Each [workflow][Workflow] has its own isolated [workspace][Workspace], and often additional resources like a shared network (docker).
- **Steps**: Individual commands, actions or tasks within a [workflow][Workflow].
- **Code**: Refers to the files tracked by the version control system used by the [forge][Forge].
- **Repos**: Short for repositories, these are storage locations where code is stored.
- **Forge**: The hosting platform or service where the repositories are hosted.
- **Workspace**: A folder shared between all steps of a [workflow][Workflow] containing the repository and all the generated data from previous steps.
- **Event**: Triggers the execution of a [pipeline][Pipeline], such as a [forge][Forge] event like `push`, or `manual` triggered manually from the UI.
- **Commit**: A defined state of the code, usually associated with a version control system like Git.
- **Matrix**: A configuration option that allows the execution of [workflows][Workflow] for each value in the [matrix][Matrix].
- **Service**: A service is a step that is executed from the start of a [workflow][Workflow] until its end. It can be accessed by name via the network from other steps within the same [workflow][Workflow].
- **Plugins**: [Plugins][Plugin] are extensions that provide pre-defined actions or commands for a step in a [workflow][Workflow]. They can be configured via settings.
- **Container**: A lightweight and isolated environment where commands are executed.
- **YAML File**: A file format used to define and configure [workflows][Workflow].
- **Dependency**: [Workflows][Workflow] can depend on each other, and if possible, they are executed in parallel.
- **Status**: Status refers to the outcome of a step or [workflow][Workflow] after it has been executed, determined by the internal command exit code. At the end of a [workflow][Workflow], its status is sent to the [forge][Forge].
- **Service extension**: Some parts of Woodpecker internal services like secrets storage or config fetcher can be replaced through service extensions.
## Pipeline events
- `push`: A push event is triggered when a commit is pushed to a branch.
- `pull_request`: A pull request event is triggered when a pull request is opened or a new commit is pushed to it.
- `pull_request_closed`: A pull request closed event is triggered when a pull request is closed or merged.
- `tag`: A tag event is triggered when a tag is pushed.
- `release`: A release event is triggered when a release, pre-release or draft is created. (You can apply further filters using [evaluate](../20-workflow-syntax.md#evaluate) with [environment variables](../50-environment.md#built-in-environment-variables).)
- `manual`: A manual event is triggered when a user manually triggers a pipeline.
- `cron`: A cron event is triggered when a cron job is executed.
## Conventions
Sometimes there are multiple terms that can be used to describe something. This section lists the preferred terms to use in Woodpecker:
- Environment variables `*_LINK` should be called `*_URL`. In the code use `URL()` instead of `Link()`
- Use the term **pipelines** instead of the previous **builds**
- Use the term **steps** instead of the previous **jobs**
<!-- References -->
[Pipeline]: ../20-workflow-syntax.md
[Workflow]: ../25-workflows.md
[Forge]: ../../30-administration/11-forges/11-overview.md
[Plugin]: ../51-plugins/51-overview.md
[Workspace]: ../20-workflow-syntax.md#workspace
[Matrix]: ../30-matrix-workflows.md
[Docker]: ../../30-administration/22-backends/10-docker.md
[Local]: ../../30-administration/22-backends/20-local.md

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -50,7 +50,8 @@ git commit -m "updated README [CI SKIP]"
## Steps
Every step of your workflow executes commands inside a specified container. The defined commands are executed serially.
Every step of your workflow executes commands inside a specified container.<br>
The defined steps are executed in sequence by default, if they should run in parallel you can use [`depends_on`](./20-workflow-syntax.md#depends_on).<br>
The associated commit is checked out with git to a workspace which is mounted to every step of the workflow as the working directory.
```diff
@ -160,6 +161,9 @@ Only build steps can define commands. You cannot use commands with plugins or se
Allows you to specify the entrypoint for containers. Note that this must be a list of the command and its arguments (e.g. `["/bin/sh", "-c"]`).
If you define [`commands`](#commands), the default entrypoint will be `["/bin/sh", "-c", "echo $CI_SCRIPT | base64 -d | /bin/sh -e"]`.
You can also use a custom shell with `CI_SCRIPT` (Base64-encoded) if you set `commands`.
### `environment`
Woodpecker provides the ability to pass environment variables to individual steps.
@ -188,7 +192,8 @@ Some of the steps may be allowed to fail without causing the whole workflow and
### `when` - Conditional Execution
Woodpecker supports defining a list of conditions for a step by using a `when` block. If at least one of the conditions in the `when` block evaluate to true the step is executed, otherwise it is skipped. A condition can be a check like:
Woodpecker supports defining a list of conditions for a step by using a `when` block. If at least one of the conditions in the `when` block evaluate to true the step is executed, otherwise it is skipped. A condition is evaluated to true if _all_ subconditions are true.
A condition can be a check like:
```diff
steps:
@ -203,6 +208,11 @@ Woodpecker supports defining a list of conditions for a step by using a `when` b
+ branch: main
```
The `slack` step is executed if one of these conditions is met:
1. The pipeline is executed from a pull request in the repo `test/test`
2. The pipeline is executed from a push to `maiǹ`
#### `repo`
Example conditional execution by repository:
@ -352,20 +362,6 @@ when:
- platform: [linux/*, windows/amd64]
```
<!-- markdownlint-disable no-duplicate-heading -->
#### `environment`
<!-- markdownlint-enable no-duplicate-heading -->
Execute a step for deployment events matching the target deployment environment:
```yaml
when:
- environment: production
- event: deployment
```
#### `matrix`
Execute a step for a single matrix permutation:
@ -478,6 +474,19 @@ Normally steps of a workflow are executed serially in the order in which they ar
- go test
```
:::note
You can define a step to start immediately without dependencies by adding an empty `depends_on: []`. By setting `depends_on` on a single step all other steps will be immediately executed as well if no further dependencies are specified.
```yaml
steps:
- name: check code format
image: mstruebing/editorconfig-checker
depends_on: [] # enable parallel steps
...
```
:::
### `volumes`
Woodpecker gives the ability to define Docker volumes in the YAML. You can use this parameter to mount files or folders on the host machine into your containers.
@ -652,7 +661,7 @@ Example configuration to use a custom clone plugin:
```diff
clone:
git:
- name: git
+ image: octocat/custom-git-plugin
```
@ -702,7 +711,7 @@ skip_clone: true
## `when` - Global workflow conditions
Woodpecker gives the ability to skip whole workflows (not just steps #when---conditional-execution-1) based on certain conditions by a `when` block. If all conditions in the `when` block evaluate to true the workflow is executed, otherwise it is skipped, but treated as successful and other workflows depending on it will still continue.
Woodpecker gives the ability to skip whole workflows ([not just steps](#when---conditional-execution)) based on certain conditions by a `when` block. If all conditions in the `when` block evaluate to true the workflow is executed, otherwise it is skipped, but treated as successful and other workflows depending on it will still continue.
For more information about the specific filters, take a look at the [step-specific `when` filters](#when---conditional-execution).
@ -738,7 +747,7 @@ Workflows that should run even on failure should set the `runs_on` tag. See [her
Woodpecker gives the ability to configure privileged mode in the YAML. You can use this parameter to launch containers with escalated capabilities.
:::info
Privileged mode is only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./71-project-settings.md#trusted) to enable trusted mode.
Privileged mode is only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./75-project-settings.md#trusted) to enable trusted mode.
:::
```diff

View file

@ -6,7 +6,7 @@ In case there is a single configuration in `.woodpecker.yaml` Woodpecker will cr
By placing the configurations in a folder which is by default named `.woodpecker/` Woodpecker will create a pipeline with multiple workflows each named by the file they are defined in. Only `.yml` and `.yaml` files will be used and files in any subfolders like `.woodpecker/sub-folder/test.yaml` will be ignored.
You can also set some custom path like `.my-ci/pipelines/` instead of `.woodpecker/` in the [project settings](./71-project-settings.md).
You can also set some custom path like `.my-ci/pipelines/` instead of `.woodpecker/` in the [project settings](./75-project-settings.md).
## Benefits of using workflows
@ -18,7 +18,7 @@ You can also set some custom path like `.my-ci/pipelines/` instead of `.woodpeck
:::warning
Please note that files are only shared between steps of the same workflow (see [File changes are incremental](./20-workflow-syntax.md#file-changes-are-incremental)). That means you cannot access artifacts e.g. from the `build` workflow in the `deploy` workflow.
If you still need to pass artifacts between the workflows you need use some storage [plugin](./51-plugins/10-overview.md) (e.g. one which stores files in an Amazon S3 bucket).
If you still need to pass artifacts between the workflows you need use some storage [plugin](./51-plugins/51-overview.md) (e.g. one which stores files in an Amazon S3 bucket).
:::
```bash

View file

@ -139,5 +139,5 @@ steps:
```
:::note
If you want to control the architecture of a pipeline on a Kubernetes runner, see [the nodeSelector documentation of the Kubernetes backend](../30-administration/22-backends/40-kubernetes.md#nodeSelector).
If you want to control the architecture of a pipeline on a Kubernetes runner, see [the nodeSelector documentation of the Kubernetes backend](../30-administration/22-backends/40-kubernetes.md#node-selector).
:::

View file

@ -35,6 +35,10 @@ Example registry hostname matching logic:
- Hostname `docker.io` matches `bradyrydzewski/golang`
- Hostname `docker.io` matches `bradyrydzewski/golang:latest`
:::note
The flow above doesn't work in Kubernetes. There is [workaround](../30-administration/22-backends/40-kubernetes.md#images-from-private-registries).
:::
## Global registry support
To make a private registry globally available, check the [server configuration docs](../30-administration/10-server-config.md#global-registry-setting).

View file

@ -19,7 +19,7 @@ To configure cron jobs you need at least push access to the repository.
+ cron: "name of the cron job" # if you only want to execute this step by a specific cron job
```
1. Create a new cron job in the repository settings:
2. Create a new cron job in the repository settings:
![cron settings](./cron-settings.png)

View file

@ -81,10 +81,11 @@ This is the reference list of all environment variables available to your pipeli
| | **Current pipeline** |
| `CI_PIPELINE_NUMBER` | pipeline number |
| `CI_PIPELINE_PARENT` | number of parent pipeline |
| `CI_PIPELINE_EVENT` | pipeline event (see [pipeline events](../20-usage/15-terminiology/index.md#pipeline-events)) |
| `CI_PIPELINE_EVENT` | pipeline event (see [pipeline events](../20-usage/15-terminology/index.md#pipeline-events)) |
| `CI_PIPELINE_URL` | link to the web UI for the pipeline |
| `CI_PIPELINE_FORGE_URL` | link to the forge's web UI for the commit(s) or tag that triggered the pipeline |
| `CI_PIPELINE_DEPLOY_TARGET` | pipeline deploy target for `deployment` events (i.e. production) |
| `CI_PIPELINE_DEPLOY_TASK` | pipeline deploy task for `deployment` events (i.e. migration) |
| `CI_PIPELINE_STATUS` | pipeline status (success, failure) |
| `CI_PIPELINE_CREATED` | pipeline created UNIX timestamp |
| `CI_PIPELINE_STARTED` | pipeline started UNIX timestamp |
@ -114,10 +115,11 @@ This is the reference list of all environment variables available to your pipeli
| | **Previous pipeline** |
| `CI_PREV_PIPELINE_NUMBER` | previous pipeline number |
| `CI_PREV_PIPELINE_PARENT` | previous pipeline number of parent pipeline |
| `CI_PREV_PIPELINE_EVENT` | previous pipeline event (see [pipeline events](../20-usage/15-terminiology/index.md#pipeline-events)) |
| `CI_PREV_PIPELINE_EVENT` | previous pipeline event (see [pipeline events](../20-usage/15-terminology/index.md#pipeline-events)) |
| `CI_PREV_PIPELINE_URL` | previous pipeline link in CI |
| `CI_PREV_PIPELINE_FORGE_URL` | previous pipeline link to event in forge |
| `CI_PREV_PIPELINE_DEPLOY_TARGET` | previous pipeline deploy target for `deployment` events (ie production) |
| `CI_PREV_PIPELINE_DEPLOY_TASK` | previous pipeline deploy task for `deployment` events (ie migration) |
| `CI_PREV_PIPELINE_STATUS` | previous pipeline status (success, failure) |
| `CI_PREV_PIPELINE_CREATED` | previous pipeline created UNIX timestamp |
| `CI_PREV_PIPELINE_STARTED` | previous pipeline started UNIX timestamp |

View file

@ -42,12 +42,29 @@ Values like this are converted to JSON and then passed to your plugin. In the ex
### Secrets
Secrets should be passed as settings too. Therefore, users should use [`from_secret`](../40-secrets.md#use-secrets-in-settings).
Secrets should be passed as settings too. Therefore, users should use [`from_secret`](../40-secrets.md#use-secrets-in-settings-and-environment).
## Plugin library
For Go, we provide a plugin library you can use to get easy access to internal env vars and your settings. See <https://codeberg.org/woodpecker-plugins/go-plugin>.
## Metadata
In your documentation, you can use a Markdown header to define metadata for your plugin. This data is used by [our plugin index](/plugins).
Supported metadata:
- `name`: The plugin's full name
- `icon`: URL to your plugin's icon
- `description`: A short description of what it's doing
- `author`: Your name
- `tags`: List of keywords (e.g. `[git, clone]` for the clone plugin)
- `containerImage`: name of the container image
- `containerImageUrl`: link to the container image
- `url`: homepage or repository of your plugin
If you want your plugin to be listed in the index, you should add as many fields as possible, but only `name` is required.
## Example plugin
This provides a brief tutorial for creating a Woodpecker webhook plugin, using simple shell scripting, to make HTTP requests during the build pipeline.
@ -118,5 +135,5 @@ docker run --rm \
These should also be built for different OS/architectures.
- Use [built-in env vars](../50-environment.md#built-in-environment-variables) where possible.
- Do not use any configuration except settings (and internal env vars). This means: Don't require using [`environment`](../50-environment.md) and don't require specific secret names.
- Add a `docs.md` file, listing all your settings and plugin metadata ([example](https://codeberg.org/woodpecker-plugins/plugin-docker-buildx/src/branch/main/docs.md)).
- Add your plugin to the [plugin index](/plugins) using your `docs.md` ([the example above in the index](https://woodpecker-ci.org/plugins/Docker%20Buildx)).
- Add a `docs.md` file, listing all your settings and plugin metadata ([example](https://github.com/woodpecker-ci/plugin-git/blob/main/docs.md)).
- Add your plugin to the [plugin index](/plugins) using your `docs.md` ([the example above in the index](https://woodpecker-ci.org/plugins/Git%20Clone)).

View file

@ -3,7 +3,7 @@
Woodpecker gives the ability to define Docker volumes in the YAML. You can use this parameter to mount files or folders on the host machine into your containers.
:::note
Volumes are only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./71-project-settings.md#trusted) to enable trusted mode.
Volumes are only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./75-project-settings.md#trusted) to enable trusted mode.
:::
```diff

View file

@ -0,0 +1,62 @@
# Linter
Woodpecker automatically lints your workflow files for errors, deprecations and bad habits. Errors and warnings are shown in the UI for any pipelines.
![errors and warnings in UI](./linter-warnings-errors.png)
## Running the linter from CLI
You can run the linter also manually from the CLI:
```shell
woodpecker-cli lint <workflow files>
```
## Bad habit warnings
Woodpecker warns you if your configuration contains some bad habits.
### Event filter for all steps
All your items in `when` blocks should have an `event` filter, so no step runs on all events. This is recommended because if new events are added, your steps probably shouldn't run on those as well.
Examples of an **incorrect** config for this rule:
```yaml
when:
- branch: main
- event: tag
```
This will trigger the warning because the first item (`branch: main`) does not filter with an event.
```yaml
steps:
- name: test
when:
branch: main
- name: deploy
when:
event: tag
```
Examples of a **correct** config for this rule:
```yaml
when:
- branch: main
event: push
- event: tag
```
```yaml
steps:
- name: test
when:
event: [tag, push]
- name: deploy
when:
- event: tag
```

View file

@ -0,0 +1,61 @@
# Project settings
As the owner of a project in Woodpecker you can change project related settings via the web interface.
![project settings](./project-settings.png)
## Pipeline path
The path to the pipeline config file or folder. By default it is left empty which will use the following configuration resolution `.woodpecker/*.{yaml,yml}` -> `.woodpecker.yaml` -> `.woodpecker.yml`. If you set a custom path Woodpecker tries to load your configuration or fails if no configuration could be found at the specified location. To use a [multiple workflows](./25-workflows.md) with a custom path you have to change it to a folder path ending with a `/` like `.woodpecker/`.
## Repository hooks
Your Version-Control-System will notify Woodpecker about events via webhooks. If you want your pipeline to only run on specific webhooks, you can check them with this setting.
## Allow pull requests
Enables handling webhook's pull request event. If disabled, then pipeline won't run for pull requests.
## Allow deployments
Enables a pipeline to be started with the `deploy` event from a successful pipeline.
:::danger
Only activate this option if you trust all users who have push access to your repository.
Otherwise, these users will be able to steal secrets that are only available for `deploy` events.
:::
## Protected
Every pipeline initiated by an webhook event needs to be approved by a project members with push permissions before being executed.
The protected option can be used as an additional review process before running potentially harmful pipelines. Especially if pipelines can be executed by third-parties through pull-requests.
## Trusted
If you set your project to trusted, a pipeline step and by this the underlying containers gets access to escalated capabilities like mounting volumes.
:::note
Only server admins can set this option. If you are not a server admin this option won't be shown in your project settings.
:::
## Only inject netrc credentials into trusted containers
Cloning pipeline step may need git credentials. They are injected via netrc. By default, they're only injected if this option is enabled, the repo is trusted ([see above](#trusted)) or the image is a trusted clone image. If you uncheck the option, git credentials will be injected into any container in clone step.
## Project visibility
You can change the visibility of your project by this setting. If a user has access to a project they can see all builds and their logs and artifacts. Settings, Secrets and Registries can only be accessed by owners.
- `Public` Every user can see your project without being logged in.
- `Internal` Only authenticated users of the Woodpecker instance can see this project.
- `Private` Only you and other owners of the repository can see this project.
## Timeout
After this timeout a pipeline has to finish or will be treated as timed out.
## Cancel previous pipelines
By enabling this option for a pipeline event previous pipelines of the same event and context will be canceled before starting the newly triggered one.

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -61,7 +61,7 @@ You can install Woodpecker on multiple ways:
Authentication is done using OAuth and is delegated to your forge which is configured using environment variables.
See the complete reference for all supported forges [here](../11-forges/10-overview.md).
See the complete reference for all supported forges [here](../11-forges/11-overview.md).
## Database

View file

@ -1,7 +1,7 @@
# NixOS
:::info
Note that this module is not maintained by the woodpecker-developers.
Note that this module is not maintained by the Woodpecker developers.
If you experience issues please open a bug report in the [nixpkgs repo](https://github.com/NixOS/nixpkgs/issues/new/choose) where the module is maintained.
:::
@ -85,4 +85,4 @@ All configuration options can be found via [NixOS Search](https://search.nixos.o
## Tips and tricks
There are some resources on how to utilize Woodpecker more effectively with NixOS on the [Awesome Woodpecker](../../92-awesome.md) page, like using the runners nix-store in the pipeline
There are some resources on how to utilize Woodpecker more effectively with NixOS on the [Awesome Woodpecker](../../92-awesome.md) page, like using the runners nix-store in the pipeline.

View file

@ -6,7 +6,7 @@ toc_max_heading_level: 2
## User registration
Woodpecker does not have its own user registry; users are provided from your [forge](./11-forges/10-overview.md) (using OAuth2).
Woodpecker does not have its own user registry; users are provided from your [forge](./11-forges/11-overview.md) (using OAuth2).
Registration is closed by default (`WOODPECKER_OPEN=false`). If registration is open (`WOODPECKER_OPEN=true`) then every user with an account at the configured forge can login to Woodpecker.
@ -69,7 +69,7 @@ To handle sensitive data in docker-compose or docker-swarm configurations there
For docker-compose you can use a `.env` file next to your compose configuration to store the secrets outside of the compose file. While this separates configuration from secrets it is still not very secure.
Alternatively use docker-secrets. As it may be difficult to use docker secrets for environment variables woodpecker allows to read sensible data from files by providing a `*_FILE` option of all sensible configuration variables. Woodpecker will try to read the value directly from this file. Keep in mind that when the original environment variable gets specified at the same time it will override the value read from the file.
Alternatively use docker-secrets. As it may be difficult to use docker secrets for environment variables Woodpecker allows to read sensible data from files by providing a `*_FILE` option of all sensible configuration variables. Woodpecker will try to read the value directly from this file. Keep in mind that when the original environment variable gets specified at the same time it will override the value read from the file.
```diff title="docker-compose.yaml"
version: '3'
@ -419,7 +419,7 @@ The database driver name. Possible values are `sqlite3`, `mysql` or `postgres`.
### `WOODPECKER_DATABASE_DATASOURCE`
> Default: `woodpecker.sqlite`
> Default: `woodpecker.sqlite` if not running inside a container, `/var/lib/woodpecker/woodpecker.sqlite` if running inside a container
The database connection string. The default value is the path of the embedded SQLite database file.
@ -441,30 +441,6 @@ WOODPECKER_DATABASE_DATASOURCE=postgres://root:password@1.2.3.4:5432/woodpecker?
Read the value for `WOODPECKER_DATABASE_DATASOURCE` from the specified filepath
### `WOODPECKER_ENCRYPTION_KEY`
> Default: empty
Encryption key used to encrypt secrets in DB. See [secrets encryption](./40-encryption.md)
### `WOODPECKER_ENCRYPTION_KEY_FILE`
> Default: empty
Read the value for `WOODPECKER_ENCRYPTION_KEY` from the specified filepath
### `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE`
> Default: empty
Filepath to encryption keyset used to encrypt secrets in DB. See [secrets encryption](./40-encryption.md)
### `WOODPECKER_ENCRYPTION_DISABLE`
> Default: empty
Boolean flag to decrypt secrets in DB and disable server encryption. See [secrets encryption](./40-encryption.md)
### `WOODPECKER_PROMETHEUS_AUTH_TOKEN`
> Default: empty
@ -497,12 +473,6 @@ Supported variables:
- `owner`: the repo's owner
- `repo`: the repo's name
### `WOODPECKER_ADDONS`
> Default: empty
List of addon files. See [addons](./75-addons/00-overview.md).
---
### `WOODPECKER_LIMIT_MEM_SWAP`
@ -583,4 +553,8 @@ See [Bitbucket configuration](./11-forges/50-bitbucket.md#configuration)
### `WOODPECKER_GITLAB_...`
See [Gitlab configuration](./11-forges/40-gitlab.md#configuration)
See [GitLab configuration](./11-forges/40-gitlab.md#configuration)
### `WOODPECKER_ADDON_FORGE`
See [addon forges](./11-forges/100-addon.md).

View file

@ -0,0 +1,68 @@
# Addon forges
If the forge you're using does not comply with [Woodpecker's requirements](../../92-development/02-core-ideas.md#forges) or your setup is too specific to be added to Woodpecker's core, you can write your own forge using an addon forge.
:::warning
Addon forges are still experimental. Their implementation can change and break at any time.
:::
:::danger
You need to trust the author of the addon forge you use. It can access authentication codes and other possibly sensitive information.
:::
## Usage
To use an addon forge, download the correct addon version. Then, you can add the following to your configuration:
```ini
WOODPECKER_ADDON_FORGE=/path/to/your/addon/forge/file
```
In case you run Woodpecker as container, you probably want to mount the addon binary to `/opt/addons/`.
### Bug reports
If you experience bugs, please check which component has the issue. If it's the addon, **do not raise an issue in the main repository**, but rather use the separate addon repositories. To check which component is responsible for the bug, look at the logs. Logs from addons are marked with a special field `addon` containing their addon file name.
## List of addon forges
If you wrote or found an addon forge, please add it here so others can find it!
_Be the first one to add your addon forge!_
## Creating addon forges
Addons use RPC to communicate to the server and are implemented using the [`go-plugin` library](https://github.com/hashicorp/go-plugin).
### Writing your code
This example will use the Go language.
Directly import Woodpecker's Go packages (`go.woodpecker-ci.org/woodpecker/woodpecker/v2`) and use the interfaces and types defined there.
In the `main` function, just call `"go.woodpecker-ci.org/woodpecker/v2/server/forge/addon".Serve` with a `"go.woodpecker-ci.org/woodpecker/v2/server/forge".Forge` as argument.
This will take care of connecting the addon forge to the server.
### Example structure
```go
package main
import (
"context"
"net/http"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/addon"
forgeTypes "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
func main() {
addon.Serve(config{})
}
type config struct {
}
// `config` must implement `"go.woodpecker-ci.org/woodpecker/v2/server/forge".Forge`. You must directly use Woodpecker's packages - see imports above.
```

View file

@ -81,3 +81,9 @@ Read the value for `WOODPECKER_GITHUB_SECRET` from the specified filepath.
> Default: `false`
Configure if SSL verification should be skipped.
### `WOODPECKER_GITHUB_PUBLIC_ONLY`
> Default: `false`
Configures the GitHub OAuth client to only obtain a token that can manage public repositories.

View file

@ -16,7 +16,7 @@ WOODPECKER_GITEA_SECRET=YOUR_GITEA_CLIENT_SECRET
## Gitea on the same host with containers
If you have Gitea also running on the same host within a container, make sure the agent does have access to it.
The agent tries to clone using the URL which Gitea reports through its API. For simplified connectivity, you should add the woodpecker agent to the same docker network as Gitea is in.
The agent tries to clone using the URL which Gitea reports through its API. For simplified connectivity, you should add the Woodpecker agent to the same docker network as Gitea is in.
Otherwise, the communication should go via the `docker0` gateway (usually 172.17.0.1).
To configure the Docker network if the network's name is `gitea`, configure it like this:
@ -93,3 +93,11 @@ Read the value for `WOODPECKER_GITEA_SECRET` from the specified filepath
> Default: `false`
Configure if SSL verification should be skipped.
## Advanced options
### `WOODPECKER_DEV_GITEA_OAUTH_URL`
> Default: value of `WOODPECKER_GITEA_URL`
Configures the user-facing Gitea server address. Should be used if `WOODPECKER_GITEA_URL` points to an internal URL used for API requests.

View file

@ -14,7 +14,7 @@ WOODPECKER_BITBUCKET_SECRET=...
## Registration
You must register an OAuth application at Bitbucket in order to get a key and secret combination for woodpecker. Navigate to your workspace settings and choose `OAuth consumers` from the menu, and finally click `Add Consumer` (the url should be like: `https://bitbucket.org/[your-project-name]/workspace/settings/api`).
You must register an OAuth application at Bitbucket in order to get a key and secret combination for Woodpecker. Navigate to your workspace settings and choose `OAuth consumers` from the menu, and finally click `Add Consumer` (the url should be like: `https://bitbucket.org/[your-project-name]/workspace/settings/api`).
Please set a name and set the `Callback URL` like this:

View file

@ -10,24 +10,23 @@ Woodpecker comes with experimental support for Bitbucket Datacenter / Server, fo
To enable Bitbucket Server you should configure the Woodpecker container using the following environment variables:
```diff
# docker-compose.yml
version: '3'
```diff title="docker-compose.yaml"
version: '3'
services:
woodpecker-server:
[...]
environment:
- [...]
+ - WOODPECKER_BITBUCKET_DC=true
+ - WOODPECKER_BITBUCKET_DC_GIT_USERNAME=foo
+ - WOODPECKER_BITBUCKET_DC_GIT_PASSWORD=bar
+ - WOODPECKER_BITBUCKET_DC_CLIENT_ID=xxx
+ - WOODPECKER_BITBUCKET_DC_CLIENT_SECRET=yyy
+ - WOODPECKER_BITBUCKET_DC_URL=http://stash.mycompany.com
services:
woodpecker-server:
[...]
environment:
- [...]
+ - WOODPECKER_BITBUCKET_DC=true
+ - WOODPECKER_BITBUCKET_DC_GIT_USERNAME=foo
+ - WOODPECKER_BITBUCKET_DC_GIT_PASSWORD=bar
+ - WOODPECKER_BITBUCKET_DC_CLIENT_ID=xxx
+ - WOODPECKER_BITBUCKET_DC_CLIENT_SECRET=yyy
+ - WOODPECKER_BITBUCKET_DC_URL=http://stash.mycompany.com
woodpecker-agent:
[...]
woodpecker-agent:
[...]
```
## Service Account

View file

@ -178,7 +178,7 @@ See [Kubernetes backend configuration](./22-backends/40-kubernetes.md#configurat
### `WOODPECKER_BACKEND_LOCAL_*`
See [Local backend configuration](./22-backends/20-local.md#further-configuration)
See [Local backend configuration](./22-backends/20-local.md#options)
## Advanced Settings

View file

@ -6,6 +6,12 @@ toc_max_heading_level: 2
The kubernetes backend executes steps inside standalone pods. A temporary PVC is created for the lifetime of the pipeline to transfer files between steps.
## Images from private registries
In order to pull private container images defined in your pipeline YAML you must provide [registry credentials in Kubernetes secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/).
As the secret is Agent-wide, it has to be placed in namespace defined by `WOODPECKER_BACKEND_K8S_NAMESPACE`.
Besides, you need to provide the secret name to Agent via `WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES`.
## Job specific configuration
### Resources
@ -32,14 +38,21 @@ steps:
cpu: 1000m
```
### `serviceAccountName`
You can use [Limit Ranges](https://kubernetes.io/docs/concepts/policy/limit-range/) if you want to set the limits by per-namespace basis.
Specify the name of the ServiceAccount which the build pod will mount. This serviceAccount must be created externally.
See the [kubernetes documentation](https://kubernetes.io/docs/concepts/security/service-accounts/) for more information on using serviceAccounts.
### Runtime class
### `nodeSelector`
`runtimeClassName` specifies the name of the RuntimeClass which will be used to run this pod. If no `runtimeClassName` is specified, the default RuntimeHandler will be used.
See the [kubernetes documentation](https://kubernetes.io/docs/concepts/containers/runtime-class/) for more information on specifying runtime classes.
Specifies the label which is used to select the node on which the job will be executed.
### Service account
`serviceAccountName` specifies the name of the ServiceAccount which the pod will mount. This service account must be created externally.
See the [kubernetes documentation](https://kubernetes.io/docs/concepts/security/service-accounts/) for more information on using service accounts.
### Node selector
`nodeSelector` specifies the labels which are used to select the node on which the job will be executed.
Labels defined here will be appended to a list which already contains `"kubernetes.io/arch"`.
By default `"kubernetes.io/arch"` is inferred from the agents' platform. One can override it by setting that label in the `nodeSelector` section of the `backend_options`.
@ -66,9 +79,11 @@ And then overwrite the `nodeSelector` in the `backend_options` section of the st
kubernetes.io/arch: "${ARCH}"
```
### `tolerations`
You can use [PodNodeSelector](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podnodeselector) admission controller if you want to set the node selector by per-namespace basis.
When you use nodeSelector and the node pool is configured with Taints, you need to specify the Tolerations. Tolerations allow the scheduler to schedule pods with matching taints.
### Tolerations
When you use `nodeSelector` and the node pool is configured with Taints, you need to specify the Tolerations. Tolerations allow the scheduler to schedule pods with matching taints.
See the [kubernetes documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information on using tolerations.
Example pipeline configuration:
@ -102,8 +117,8 @@ steps:
### Volumes
To mount volumes a persistent volume (PV) and persistent volume claim (PVC) are needed on the cluster which can be referenced in steps via the `volume:` option.
Assuming a PVC named "woodpecker-cache" exists, it can be referenced as follows in a step:
To mount volumes a persistent volume (PV) and persistent volume claim (PVC) are needed on the cluster which can be referenced in steps via the `volumes` option.
Assuming a PVC named `woodpecker-cache` exists, it can be referenced as follows in a step:
```yaml
steps:
@ -117,9 +132,9 @@ steps:
[...]
```
### `securityContext`
### Security context
Use the following configuration to set the `securityContext` for the pod/container running a given pipeline step:
Use the following configuration to set the [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) for the pod/container running a given pipeline step:
```yaml
steps:
@ -154,7 +169,31 @@ spec:
[...]
```
See the [kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) for more information on using `securityContext`.
You can also restrict a container's syscalls with [seccomp](https://kubernetes.io/docs/tutorials/security/seccomp/) profile
```yaml
backend_options:
kubernetes:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/audit.json
```
or restrict a container's access to resources by specifying [AppArmor](https://kubernetes.io/docs/tutorials/security/apparmor/) profile
```yaml
backend_options:
kubernetes:
securityContext:
apparmorProfile:
type: Localhost
localhostProfile: k8s-apparmor-example-deny-write
```
:::note
AppArmor syntax follows [KEP-24](https://github.com/kubernetes/enhancements/blob/fddcbb9cbf3df39ded03bad71228265ac6e5215f/keps/sig-node/24-apparmor/README.md).
:::
## Tips and tricks
@ -215,3 +254,9 @@ Additional annotations to apply to worker pods. Must be a YAML object, e.g. `{"e
> Default: `false`
Determines if containers must be required to run as non-root users.
### `WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES`
> Default: empty
Secret names to pull images from private repositories. See, how to [Pull an Image from a Private Registry](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/).

View file

@ -1,87 +0,0 @@
# Secrets encryption
:::danger
Secrets encryption is currently broken and therefore disabled by default. It will be fixed in an upcoming release.
Check:
- <https://github.com/woodpecker-ci/woodpecker/issues/1541> and
- <https://github.com/woodpecker-ci/woodpecker/pull/2300>
:::
By default, Woodpecker does not encrypt secrets in its database. You can enable encryption
using simple AES key or more advanced [Google TINK](https://developers.google.com/tink) encryption.
## Common
### Enabling secrets encryption
To enable secrets encryption and encrypt all existing secrets in database set
`WOODPECKER_ENCRYPTION_KEY`, `WOODPECKER_ENCRYPTION_KEY_FILE` or `WOODPECKER_ENCRYPTION_TINK_KEYSET_PATH` environment
variable depending on encryption method of your choice.
After encryption is enabled you will be unable to start Woodpecker server without providing valid encryption key!
### Disabling encryption and decrypting all secrets
To disable secrets encryption and decrypt database you need to start server with valid
`WOODPECKER_ENCRYPTION_KEY` or `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE` environment variable set depending on
enabled encryption method, and `WOODPECKER_ENCRYPTION_DISABLE` set to true.
After secrets was decrypted server will proceed working in unencrypted mode. You will not need to use "disable encryption"
variable or encryption keys to start server anymore.
## AES
Simple AES encryption.
### Configuration
You can manage encryption on server using these environment variables:
- `WOODPECKER_ENCRYPTION_KEY` - encryption key
- `WOODPECKER_ENCRYPTION_KEY_FILE` - file to read encryption key from
- `WOODPECKER_ENCRYPTION_DISABLE` - disable encryption flag used to decrypt all data on server
## TINK
TINK uses AEAD encryption instead of simple AES and supports key rotation.
<!-- markdownlint-disable no-duplicate-heading -->
### Configuration
<!-- markdownlint-enable no-duplicate-heading -->
You can manage encryption on server using these two environment variables:
- `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE` - keyset filepath
- `WOODPECKER_ENCRYPTION_DISABLE` - disable encryption flag used to decrypt all data on server
### Encryption keys
You will need plaintext AEAD-compatible Google TINK keyset to encrypt your data.
To generate it and then rotate keys if needed, install `tinkey`([installation guide](https://developers.google.com/tink/install-tinkey))
Keyset contains one or more keys, used to encrypt or decrypt your data, and primary key ID, used to determine which key
to use while encrypting new data.
Keyset generation example:
```bash
tinkey create-keyset --key-template AES256_GCM --out-format json --out keyset.json
```
### Key rotation
Use `tinkey` to rotate encryption keys in your existing keyset:
```bash
tinkey rotate-keyset --in keyset_v1.json --out keyset_v2.json --key-template AES256_GCM
```
Then you just need to replace server keyset file with the new one. At the moment server detects new encryption
keyset it will re-encrypt all existing secrets with the new key, so you will be unable to start server with previous
keyset anymore.

View file

@ -11,6 +11,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Deprecated uppercasing all secret env vars, instead, the value of the `secrets` property is used. [Read more](./20-usage/40-secrets.md#use-secrets-in-commands)
- Deprecated alternative names for secrets, use `environment` with `from_secret`
- Deprecated slice definition for env vars
- Deprecated `environment` filter, use `when.evaluate`
## 2.0.0
@ -66,7 +67,7 @@ Some versions need some changes to the server configuration or the pipeline conf
Only projects created after updating will have an empty value by default. Existing projects will stick to the current pipeline path which is `.drone.yml` in most cases.
Read more about it at the [Project Settings](./20-usage/71-project-settings.md#pipeline-path)
Read more about it at the [Project Settings](./20-usage/75-project-settings.md#pipeline-path)
- From version `0.15.0` ongoing there will be three types of docker images: `latest`, `next` and `x.x.x` with an alpine variant for each type like `latest-alpine`.
If you used `latest` before to try pre-release features you should switch to `next` after this release.

View file

@ -1,6 +1,6 @@
# Awesome Woodpecker
A curated list of awesome things related to Woodpecker-CI.
A curated list of awesome things related to Woodpecker CI.
If you have some missing resources, please feel free to [open a pull-request](https://github.com/woodpecker-ci/woodpecker/edit/main/docs/docs/92-awesome.md) and add them.
@ -14,7 +14,7 @@ If you have some missing resources, please feel free to [open a pull-request](ht
## Projects using Woodpecker
- [Woodpecker-CI](https://github.com/woodpecker-ci/woodpecker/tree/main/.woodpecker) itself
- [Woodpecker CI](https://github.com/woodpecker-ci/woodpecker/tree/main/.woodpecker) itself
- [All official plugins](https://github.com/woodpecker-ci?q=plugin&type=all)
- [dessalines/thumb-key](https://github.com/dessalines/thumb-key/blob/main/.woodpecker.yml) - Android Jetpack compose linting and building
- [Vieter](https://git.rustybever.be/vieter-v/vieter) - Archlinux/Pacman repository server & automated package build system
@ -24,12 +24,12 @@ If you have some missing resources, please feel free to [open a pull-request](ht
## Tools
- [Convert Drone CI pipelines to Woodpecker CI](https://codeberg.org/lafriks/woodpecker-pipeline-transform)
- [Ansible NAS](https://github.com/davestephens/ansible-nas/) - a homelab Ansible playbook that can set up Woodpecker-CI and Gitea
- [Ansible NAS](https://github.com/davestephens/ansible-nas/) - a homelab Ansible playbook that can set up Woodpecker CI and Gitea
- [picus](https://github.com/windsource/picus) - Picus connects to a Woodpecker CI server and creates an agent in the cloud when there are pending workflows.
- [Hetzner cloud](https://www.hetzner.com/cloud) based [Woodpecker compatible autoscaler](https://git.ljoonal.xyz/ljoonal/hetzner-ci-autoscaler) - Creates and destroys VPS instances based on the count of pending & running jobs.
- [woodpecker-lint](https://git.schmidl.dev/schtobia/woodpecker-lint) - A repository for linting a woodpecker config file via pre-commit hook
- [Grafana Dashboard](https://github.com/Janik-Haag/woodpecker-grafana-dashboard) - A dashboard visualizing information exposed by the woodpecker prometheus endpoint.
- [woodpecker-autoscaler](https://github.com/Lerentis/woodpecker-autoscaler) - Yet another woodpecker autoscaler currently targeting [Hetzner cloud](https://www.hetzner.com/cloud) that works in parallel to other autoscaler implementations.
- [woodpecker-lint](https://git.schmidl.dev/schtobia/woodpecker-lint) - A repository for linting a Woodpecker config file via pre-commit hook
- [Grafana Dashboard](https://github.com/Janik-Haag/woodpecker-grafana-dashboard) - A dashboard visualizing information exposed by the Woodpecker prometheus endpoint.
- [woodpecker-autoscaler](https://github.com/Lerentis/woodpecker-autoscaler) - Yet another Woodpecker autoscaler currently targeting [Hetzner cloud](https://www.hetzner.com/cloud) that works in parallel to other autoscaler implementations.
## Configuration Services
@ -50,8 +50,10 @@ If you have some missing resources, please feel free to [open a pull-request](ht
- [Locally Cached Nix CI with Woodpecker](https://blog.kotatsu.dev/posts/2023-04-21-woodpecker-nix-caching/)
- [How to run Cypress auto-tests on Woodpecker CI and report results to Slack](https://devforth.io/blog/how-to-run-cypress-auto-tests-on-woodpecker-ci-and-report-results-to-slack/)
- [Quest For CICD - WoodpeckerCI](https://omaramin.me/posts/woodpecker/)
- [Getting started with Woodpecker CI](https://blog.lutra-it.eu/getting-started-with-woodpecker-ci.html)
- [Getting started with Woodpecker CI](https://systeemkabouter.eu/getting-started-with-woodpecker-ci.html)
- [Installing gitea and woodpecker using binary packages](https://neelex.com/2023/03/26/Installing-gitea-using-binary-packages/)
- [Deploying mdbook to codeberg pages using woodpecker CI](https://www.markpitblado.me/blog/ci-deployment-of-mdbook)
- [Deploy a Fly app with Woodpecker CI](https://joeroe.io/2024/01/09/deploy-fly-woodpecker-ci.html)
## Videos

View file

@ -82,7 +82,7 @@ WOODPECKER_HEALTHCHECK=false
### Setup OAuth
Create an OAuth app for your forge as described in the [forges documentation](../30-administration/11-forges/10-overview.md). If you set `WOODPECKER_DEV_OAUTH_HOST=http://localhost:8000` you can use that address with the path as explained for the specific forge to login without the need for a public address. For example for GitHub you would use `http://localhost:8000/authorize` as authorization callback URL.
Create an OAuth app for your forge as described in the [forges documentation](../30-administration/11-forges/11-overview.md). If you set `WOODPECKER_DEV_OAUTH_HOST=http://localhost:8000` you can use that address with the path as explained for the specific forge to login without the need for a public address. For example for GitHub you would use `http://localhost:8000/authorize` as authorization callback URL.
## Developing with VS Code

View file

@ -8,7 +8,7 @@
## Addons and extensions
If you are wondering whether your contribution will be accepted to be merged in the Woodpecker core, or whether it's better to write an
[addon](../30-administration/75-addons/00-overview.md), [extension](../30-administration/100-external-configuration-api.md) or an
[addon forge](../30-administration/11-forges/100-addon.md), [extension](../30-administration/100-external-configuration-api.md) or an
[external custom backend](../30-administration/22-backends/50-custom-backends.md), please check these points:
- Is your change very specific to your setup and unlikely to be used by anyone else?

View file

@ -10,8 +10,8 @@ Testing UI changes would require us to rebuild the UI after each adjustment to t
![UI Proxy architecture](./ui-proxy.svg)
Start the UI server locally with [hot-reloading](https://stackoverflow.com/a/41429055/8461267) by running: `bun start`. To enable the forwarding of requests to the UI server you have to enable the dev-proxy inside the Woodpecker server by adding `WOODPECKER_DEV_WWW_PROXY=http://localhost:8010` to your `.env` file.
After starting the Woodpecker server as explained in the [debugging](./01-getting-started.md#debugging) section, you should now be able to access the UI under [http://localhost:8000](http://localhost:8000).
Start the UI server locally with [hot-reloading](https://stackoverflow.com/a/41429055/8461267) by running: `pnpm start`. To enable the forwarding of requests to the UI server you have to enable the dev-proxy inside the Woodpecker server by adding `WOODPECKER_DEV_WWW_PROXY=http://localhost:8010` to your `.env` file.
After starting the Woodpecker server as explained in the [debugging](./01-getting-started.md#debugging-woodpecker) section, you should now be able to access the UI under [http://localhost:8000](http://localhost:8000).
## Tools and frameworks

View file

@ -10,6 +10,7 @@ const config: Config = {
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'throw',
onBrokenAnchors: 'throw',
onDuplicateRoutes: 'throw',
organizationName: 'woodpecker-ci',
projectName: 'woodpecker-ci.github.io',
@ -246,14 +247,18 @@ const config: Config = {
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/woodpecker-ci/woodpecker/edit/main/docs/',
includeCurrentVersion: true,
lastVersion: '2.3',
lastVersion: '2.4',
versions: {
current: {
label: 'Next',
banner: 'unreleased',
},
'2.4': {
label: '2.4.x',
},
'2.3': {
label: '2.3.x',
banner: 'unmaintained',
},
'2.2': {
label: '2.2.x',
@ -271,10 +276,6 @@ const config: Config = {
label: '1.0.x',
banner: 'unmaintained',
},
'0.15': {
label: '0.15.x',
banner: 'unmaintained',
},
},
},
blog: {
@ -312,6 +313,7 @@ const config: Config = {
options: {
loader: 'tsx',
target: isServer ? 'node12' : 'es2017',
supported: { 'dynamic-import': false },
},
}),
},

View file

@ -21,12 +21,12 @@
"@mdx-js/react": "^3.0.0",
"@svgr/webpack": "^8.1.0",
"clsx": "^2.1.0",
"esbuild-loader": "^4.0.2",
"esbuild-loader": "^4.1.0",
"file-loader": "^6.2.0",
"prism-react-renderer": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"redocusaurus": "^2.0.0",
"redocusaurus": "^2.0.2",
"url-loader": "^4.1.1"
},
"browserslist": {
@ -43,13 +43,13 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.1.0",
"@docusaurus/tsconfig": "3.1.1",
"@docusaurus/tsconfig": "3.2.1",
"@docusaurus/types": "^3.1.0",
"@types/node": "^20.11.2",
"@types/react": "^18.2.48",
"@types/node": "^20.11.30",
"@types/react": "^18.2.67",
"@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3",
"typescript": "^5.3.3"
"typescript": "^5.4.3"
},
"workspaces": [
"plugins/*"

View file

@ -14,14 +14,14 @@
"@docusaurus/theme-classic": "^3.0.0",
"@docusaurus/types": "^3.0.0",
"@tsconfig/docusaurus": "^2.0.0",
"@types/marked": "^5.0.0",
"@types/node": "^20.0.0",
"axios": "^1.6.0",
"@types/marked": "^6.0.0",
"@types/node": "^20.11.30",
"axios": "^1.6.8",
"concurrently": "^8.0.0",
"isomorphic-dompurify": "^2.0.0",
"marked": "^12.0.0",
"isomorphic-dompurify": "^2.5.0",
"marked": "^12.0.1",
"tslib": "^2.6.1",
"typescript": "^5.0.0"
"typescript": "^5.4.3"
},
"peerDependencies": {
"react": "^17.0.2 || ^18.0.0",
@ -29,6 +29,6 @@
},
"dependencies": {
"fuse.js": "^7.0.0",
"yaml": "^2.3.1"
"yaml": "^2.4.1"
}
}

View file

@ -70,11 +70,6 @@
"docs": "https://raw.githubusercontent.com/ViViDboarder/drone-webdav/master/docs.md",
"verified": false
},
{
"name": "Chart releaser",
"docs": "https://raw.githubusercontent.com/woodpecker-ci/plugin-chart-releaser/master/docs.md",
"verified": true
},
{
"name": "Aptly publish",
"docs": "https://gitea.zionetrix.net/bn8/aptly-publish/raw/branch/master/docs.md",
@ -184,6 +179,31 @@
"name": "Gradle Wrapper Validation",
"docs": "https://codeberg.org/beaks/gradle-wrapper-validation/raw/branch/main/docs.md",
"verified": false
},
{
"name": "Sonatype Nexus",
"docs": "https://raw.githubusercontent.com/rockdrilla/woodpecker-sonatype-nexus/main/docs.md",
"verified": false
},
{
"name": "Mastodon Post",
"docs": "https://codeberg.org/woodpecker-plugins/mastodon-post/raw/branch/main/docs.md",
"verified": true
},
{
"name": "Discord",
"docs": "https://raw.githubusercontent.com/appleboy/drone-discord/master/DOCS.md",
"verified": false
},
{
"name": "Forge deployments",
"docs": "https://raw.githubusercontent.com/woodpecker-ci/plugin-deployments/main/docs.md",
"verified": true
},
{
"name": "Twine",
"docs": "https://gitea.elara.ws/music-kraken/plugin-twine/raw/branch/master/docs.md",
"verified": false
}
]
}

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