Merge branch 'main' into pr/anbraten/3849

This commit is contained in:
pat-s 2025-01-11 00:09:49 +01:00
commit b804585b88
No known key found for this signature in database
GPG key ID: 3C6318841EF78925
1043 changed files with 37686 additions and 25984 deletions

View file

@ -6,39 +6,51 @@
"en_us", "en_us",
// code // code
"go", "go",
"node", "node"
// package names
"npm"
], ],
"words": [ "words": [
"abool", "abool",
"addgroup",
"adduser",
"anbraten", "anbraten",
"antfu", "antfu",
"apimachinery", "apimachinery",
"appleboy",
"Archlinux",
"autoincr", "autoincr",
"automerge",
"autoscaler", "autoscaler",
"backporting",
"backports",
"binutils", "binutils",
"bitbucketdatacenter", "bitbucketdatacenter",
"Boguslawski", "Boguslawski",
"bradrydzewski", "bradrydzewski",
"BUILDPLATFORM", "BUILDPLATFORM",
"buildx", "buildx",
"caddyfile",
"ccmenu", "ccmenu",
"certmagic", "certmagic",
"charmbracelet", "charmbracelet",
"cicd",
"ciphertext", "ciphertext",
"Cloudron",
"Codeberg", "Codeberg",
"compatiblelicenses", "compatiblelicenses",
"corepack", "corepack",
"cpuset", "cpuset",
"creativecommons", "creativecommons",
"Curr", "Curr",
"CERTDIR",
"datacenter", "datacenter",
"DATASOURCE", "DATASOURCE",
"Debugf", "Debugf",
"desaturate", "desaturate",
"devx", "devx",
"dind",
"Dockle",
"doublestar", "doublestar",
"emojify",
"envsubst", "envsubst",
"errgroup", "errgroup",
"estree", "estree",
@ -46,64 +58,90 @@
"excalidraw", "excalidraw",
"favicons", "favicons",
"Fediverse", "Fediverse",
"Feishu",
"Fogas", "Fogas",
"forbidigo", "forbidigo",
"Forgejo", "Forgejo",
"fsnotify", "fsnotify",
"Geeklab",
"Georgiana", "Georgiana",
"gitea", "gitea",
"gitmodules",
"GOARCH", "GOARCH",
"GOBIN", "GOBIN",
"gocritic", "gocritic",
"GODEBUG", "GODEBUG",
"godoc",
"Gogs",
"golangci", "golangci",
"gomod", "gomod",
"gonic", "gonic",
"GOPATH", "GOPATH",
"Gource",
"handlebargh",
"HEALTHCHECK", "HEALTHCHECK",
"healthz", "healthz",
"Hetzner", "Hetzner",
"HETZNERCLOUD",
"homelab",
"HTMLURL", "HTMLURL",
"HTTPFS", "HTTPFS",
"httpsig", "httpsign",
"HTTPURL", "HTTPURL",
"httputil", "httputil",
"ianvs", "ianvs",
"iconify", "iconify",
"inetutils",
"Infima",
"Infof", "Infof",
"Informatyka", "Informatyka",
"intlify", "intlify",
"Ionescu", "Ionescu",
"Jetpack",
"Kaniko", "Kaniko",
"Keyfunc", "Keyfunc",
"kyvg", "kyvg",
"LASTEXITCODE", "LASTEXITCODE",
"Laszlo", "Laszlo",
"laszlocph", "laszlocph",
"letsencrypt",
"loadbalancer",
"logfile", "logfile",
"loglevel", "loglevel",
"LONGBLOB", "LONGBLOB",
"LONGTEXT", "LONGTEXT",
"lonix1",
"mapstructure", "mapstructure",
"markdownlint", "markdownlint",
"mdbook",
"memswap", "memswap",
"Metas", "Metas",
"mhmxs", "mhmxs",
"moby", "moby",
"Msgf", "Msgf",
"mstruebing",
"multiarch", "multiarch",
"multierr", "multierr",
"netdns", "netdns",
"Netrc", "Netrc",
"Nextcloud",
"nfpm", "nfpm",
"nixos", "nixos",
"nixpkgs",
"nocolor", "nocolor",
"nolint", "nolint",
"norunningpipelines", "norunningpipelines",
"nosniff", "nosniff",
"ntfy",
"octocat", "octocat",
"openapi",
"opensource",
"Pacman",
"picus",
"Pinia", "Pinia",
"pkce", "pkce",
"pnpx",
"Polyform",
"posix", "posix",
"ppid", "ppid",
"Println", "Println",
@ -123,19 +161,26 @@
"repology", "repology",
"reslimit", "reslimit",
"Reviewdog", "Reviewdog",
"Rieter",
"riscv", "riscv",
"rundll32", "rundll32",
"Rydzewski", "Rydzewski",
"seccomp", "seccomp",
"secprofile", "secprofile",
"securecookie", "securecookie",
"selfhosted",
"sess", "sess",
"shellescape", "shellescape",
"sigstore",
"Sonatype",
"SSHURL", "SSHURL",
"sslmode",
"stepbuilder", "stepbuilder",
"stretchr", "stretchr",
"structs",
"sublicensable", "sublicensable",
"swaggo", "swaggo",
"syscalls",
"TARGETARCH", "TARGETARCH",
"TARGETOS", "TARGETOS",
"techknowlogick", "techknowlogick",
@ -143,38 +188,49 @@
"threadcreate", "threadcreate",
"tink", "tink",
"tinycolor", "tinycolor",
"tmole",
"tmpfs", "tmpfs",
"tmpl", "tmpl",
"tolerations", "tolerations",
"Traefik",
"tseslint", "tseslint",
"ttlcache", "ttlcache",
"TUNEIT",
"Tunnelmole",
"typecheck", "typecheck",
"Typeflag", "Typeflag",
"unplugin", "unplugin",
"Upsert", "Upsert",
"urfave", "urfave",
"usecase",
"varchar", "varchar",
"varz", "varz",
"Vieter",
"virtualisation",
"visualisation",
"vite",
"vueuse", "vueuse",
"waivable", "waivable",
"Warnf", "Warnf",
"webhookd",
"Weblate", "Weblate",
"windi", "windi",
"windicss", "windicss",
"woodpeckerci", "woodpeckerci",
"WORKDIR", "WORKDIR",
"Wrapf", "Wrapf",
"x-enum-varnames",
"xlink", "xlink",
"xlog", "xlog",
"xorm", "xorm",
"xormigrate", "xormigrate",
"xyaml", "xyaml",
"yamls", "yamls",
"Yuno",
"zerolog", "zerolog",
"zerologger" "zerologger"
], ],
"ignorePaths": [ "ignorePaths": [
"**/node_modules/**/*",
"*.excalidraw", "*.excalidraw",
"*.svg", "*.svg",
"*_test.go", "*_test.go",
@ -185,19 +241,26 @@
".vscode/extensions.json", ".vscode/extensions.json",
"CHANGELOG.md", "CHANGELOG.md",
"Makefile", "Makefile",
"flake.lock",
"flake.nix", "flake.nix",
"go.mod", "go.mod",
"go.sum", "**/*.pb.go",
"pipeline/rpc/proto/woodpecker.pb.go",
"pnpm-lock.yaml",
"server/store/datastore/migration/**/*", "server/store/datastore/migration/**/*",
"web/components.d.ts", "web/components.d.ts",
"web/src/assets/locales/**/*", "web/src/assets/locales/**/*",
"**/fixtures/**", "**/fixtures/**",
"**/testdata/**", "**/testdata/**",
"docs/versioned_docs/",
"package.json",
// generated
"go.sum",
"flake.lock",
"pnpm-lock.yaml",
"**/node_modules/**/*",
"cmd/server/openapi/docs.go",
"renovate.json",
// TODO: remove the following // TODO: remove the following
"docs/" "docs/**/*.js",
"docs/**/*.ts"
], ],
// Exclude imports, because they are also strings. // Exclude imports, because they are also strings.
"ignoreRegExpList": [ "ignoreRegExpList": [
@ -210,7 +273,9 @@
// ignore nolint directive // ignore nolint directive
"//\\s*nolint:.*", "//\\s*nolint:.*",
// ignore docker image names // ignore docker image names
"\\s*docker\\.io/.*" "\\s*docker\\.io/.*",
// ignore inline svg in css
"\\s*url\\(\"data:image/svg\\+xml.*"
], ],
"enableFiletypes": ["dockercompose"] "enableFiletypes": ["dockercompose"]
} }

5
.ecrc
View file

@ -1,14 +1,15 @@
{ {
"Exclude": [ "Exclude": [
".git", ".git",
"go.mod", "go.sum", "go.mod",
"go.sum",
"vendor", "vendor",
"fixtures", "fixtures",
"LICENSE", "LICENSE",
"node_modules", "node_modules",
"server/store/datastore/migration/test-files/sqlite.db", "server/store/datastore/migration/test-files/sqlite.db",
"server/store/datastore/feed.go", "server/store/datastore/feed.go",
"cmd/server/docs/docs.go", "cmd/server/openapi/docs.go",
"_test.go", "_test.go",
"Makefile" "Makefile"
] ]

11
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,11 @@
<!--
Please check the following tips:
1. Avoid using force-push and commands that require it (such as `commit --amend` and `rebase origin/main`). This makes it more difficult for the maintainers to review your work. Add new commits on top of the current branch, and merge the new state of `main` into your branch with plain `merge`.
2. Provide a meaningful title for this pull request. It will be used as the commit message when this pull request is merged. Add as many commits as you like with any messages you like, they will be squashed into one commit.
3. If this pull request fixes an issue, refer to the issue with messages like `Closes #1234`, or `Fixes #1234` in the pull description.
4. Please check that you are targeting the `main` branch. Pull requests on release branches are only allowed for backports.
5. Make sure you have read contribution guidelines: https://woodpecker-ci.org/docs/development/getting-started
6. It is recommended to enable "Allow edits by maintainers", so maintainers can help you more easily.
-->

12
.github/renovate.json vendored
View file

@ -1,12 +1,13 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>woodpecker-ci/renovate-config"], "extends": ["github>woodpecker-ci/renovate-config"],
"automergeType": "pr",
"customManagers": [ "customManagers": [
{ {
"customType": "regex", "customType": "regex",
"fileMatch": ["shared/constant/constant.go"], "fileMatch": ["shared/constant/constant.go"],
"matchStrings": [ "matchStrings": [
"//\\s*renovate:\\s*datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s+DefaultCloneImage = \"docker.io/woodpeckerci/plugin-git:(?<currentValue>.*)\"" "//\\s*renovate:\\s*datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s+DefaultClonePlugin = \"docker.io/woodpeckerci/plugin-git:(?<currentValue>.*)\""
], ],
"versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}"
} }
@ -29,8 +30,8 @@
}, },
{ {
"groupName": "golang-lang", "groupName": "golang-lang",
"matchPackagePatterns": ["^golang$", "xgo"], "matchUpdateTypes": ["minor", "patch"],
"matchUpdateTypes": ["minor", "patch"] "matchPackageNames": ["/^golang$/", "/xgo/"]
}, },
{ {
"groupName": "golang-packages", "groupName": "golang-packages",
@ -60,9 +61,10 @@
"matchFileNames": ["docs/**/package.json"] "matchFileNames": ["docs/**/package.json"]
}, },
{ {
"description": "Extract version from xgo container tags",
"matchDatasources": ["docker"], "matchDatasources": ["docker"],
"matchPackagePatterns": ["xgo"], "versioning": "regex:^go-(?<major>\\d+)\\.(?<minor>\\d+)\\.x$",
"versioning": "regex:^go-(?<major>\\d+)?(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))$" "matchPackageNames": ["/techknowlogick/xgo/"]
} }
] ]
} }

3
.gitignore vendored
View file

@ -51,4 +51,7 @@ docs/venv
### Generated by CI ### ### Generated by CI ###
docs/docs/40-cli.md docs/docs/40-cli.md
docs/openapi.json
# Removed once v3.0.x is minimum version to be touched
docs/swagger.json docs/swagger.json

View file

@ -112,9 +112,10 @@ vscode:
- 'EditorConfig.EditorConfig' - 'EditorConfig.EditorConfig'
- 'dbaeumer.vscode-eslint' - 'dbaeumer.vscode-eslint'
- 'esbenp.prettier-vscode' - 'esbenp.prettier-vscode'
- 'voorjaar.windicss-intellisense' - 'bradlc.vscode-tailwindcss'
- 'Vue.volar' - 'Vue.volar'
- 'redhat.vscode-yaml' - 'redhat.vscode-yaml'
- 'davidanson.vscode-markdownlint' - 'davidanson.vscode-markdownlint'
- 'streetsidesoftware.code-spell-checker' - 'streetsidesoftware.code-spell-checker'
- 'stivo.tailwind-fold'
# cSpell:enable # cSpell:enable

1
.lycheeignore Normal file
View file

@ -0,0 +1 @@
https://stackoverflow.com/*

View file

@ -5,16 +5,16 @@ repos:
- id: check-hooks-apply - id: check-hooks-apply
- id: check-useless-excludes - id: check-useless-excludes
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v5.0.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v1.59.1 rev: v1.63.4
hooks: hooks:
- id: golangci-lint - id: golangci-lint
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.41.0 rev: v0.43.0
hooks: hooks:
- id: markdownlint - id: markdownlint
exclude: '^(docs/versioned_docs/.*|CHANGELOG.md)$' exclude: '^(docs/versioned_docs/.*|CHANGELOG.md)$'
@ -24,11 +24,11 @@ repos:
- id: checkmake - id: checkmake
exclude: '^docker/Dockerfile.make$' # actually a Dockerfile and not a makefile exclude: '^docker/Dockerfile.make$' # actually a Dockerfile and not a makefile
- repo: https://github.com/hadolint/hadolint - repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta rev: v2.13.1-beta
hooks: hooks:
- id: hadolint - id: hadolint
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: v4.0.0-alpha.8 rev: v3.4.2
hooks: hooks:
- id: prettier - id: prettier
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git

View file

@ -1,10 +1,8 @@
build/ build/
docs/versioned_docs/
docs/.docusaurus/
docs/pnpm-lock.yaml
dist/ dist/
CHANGELOG.md CHANGELOG.md
# web/ must be directly formatted from there # web/ and docs/ must be directly formatted from there
# to prevent conflicts with different prettier version # to prevent conflicts with different prettier version
web/ web/
docs/

View file

@ -5,10 +5,11 @@
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"voorjaar.windicss-intellisense", "bradlc.vscode-tailwindcss",
"Vue.volar", "Vue.volar",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"davidanson.vscode-markdownlint" "davidanson.vscode-markdownlint",
"stivo.tailwind-fold"
], ],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace. // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [] "unwantedRecommendations": []

View file

@ -1,12 +1,17 @@
when: when:
event: tag - event: tag
- event: pull_request
branch: ${CI_REPO_DEFAULT_BRANCH}
path:
- Makefile
- .woodpecker/binaries.yaml
variables: variables:
- &golang_image 'docker.io/golang:1.22' - &golang_image 'docker.io/golang:1.23'
- &node_image 'docker.io/node:22-alpine' - &node_image 'docker.io/node:23-alpine'
- &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.x' - &xgo_image 'docker.io/techknowlogick/xgo:go-1.23.x'
# cspell:words bindata netgo TARGZ # cspell:words bindata netgo
steps: steps:
build-web: build-web:
@ -35,7 +40,7 @@ steps:
environment: environment:
PLATFORMS: linux|arm64/v8;linux|amd64;windows|amd64 PLATFORMS: linux|arm64/v8;linux|amd64;windows|amd64
TAGS: bindata sqlite sqlite_unlock_notify netgo TAGS: bindata sqlite sqlite_unlock_notify netgo
TARGZ: '1' ARCHIVE_IT: '1'
build-tarball: build-tarball:
depends_on: depends_on:
@ -50,6 +55,8 @@ steps:
- vendor - vendor
image: *golang_image image: *golang_image
commands: commands:
- apt update
- apt install -y zip
- make release-agent - make release-agent
build-cli: build-cli:
@ -57,6 +64,8 @@ steps:
- vendor - vendor
image: *golang_image image: *golang_image
commands: commands:
- apt update
- apt install -y zip
- make release-cli - make release-cli
build-deb-rpm: build-deb-rpm:
@ -90,13 +99,16 @@ steps:
release: release:
depends_on: depends_on:
- checksums - checksums
image: woodpeckerci/plugin-release:0.1.0 image: woodpeckerci/plugin-release:0.2.2
settings: settings:
api_key: api_key:
from_secret: github_token from_secret: github_token
files: files:
- dist/*.tar.gz - dist/*.tar.gz
- dist/*.zip
- dist/*.deb - dist/*.deb
- dist/*.rpm - dist/*.rpm
- dist/checksums.txt - dist/checksums.txt
title: ${CI_COMMIT_TAG##v} title: ${CI_COMMIT_TAG##v}
when:
event: tag

View file

@ -1,15 +1,15 @@
variables: variables:
- &golang_image 'docker.io/golang:1.22' - &golang_image 'docker.io/golang:1.23'
- &node_image 'docker.io/node:22-alpine' - &node_image 'docker.io/node:23-alpine'
- &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.x' - &xgo_image 'docker.io/techknowlogick/xgo:go-1.23.x'
- &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:4.0.0' - &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:5.1.0'
- &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_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_server 'linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le,linux/riscv64'
- &platforms_preview 'linux/amd64' - &platforms_preview 'linux/amd64'
- &platforms_alpine 'linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le' - &platforms_alpine 'linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le'
- &build_args 'CI_COMMIT_SHA=${CI_COMMIT_SHA},CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH},CI_COMMIT_TAG=${CI_COMMIT_TAG}' - &build_args 'CI_COMMIT_SHA=${CI_COMMIT_SHA},CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH},CI_COMMIT_TAG=${CI_COMMIT_TAG}'
# cspell:words woodpeckerbot netgo TARGZ # cspell:words woodpeckerbot netgo
# vars used on push / tag events only # vars used on push / tag events only
- publish_logins: &publish_logins # Default DockerHub login - publish_logins: &publish_logins # Default DockerHub login
@ -41,9 +41,6 @@ variables:
when: when:
- event: [pull_request, tag] - event: [pull_request, tag]
- event: push
branch:
- renovate/*
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: *when_path path: *when_path
@ -61,7 +58,6 @@ steps:
path: *when_path path: *when_path
- branch: - branch:
- ${CI_REPO_DEFAULT_BRANCH} - ${CI_REPO_DEFAULT_BRANCH}
- renovate/*
event: [push, tag] event: [push, tag]
path: *when_path path: *when_path
@ -82,7 +78,6 @@ steps:
path: *when_path path: *when_path
- branch: - branch:
- ${CI_REPO_DEFAULT_BRANCH} - ${CI_REPO_DEFAULT_BRANCH}
- renovate/*
event: [push, tag] event: [push, tag]
path: *when_path path: *when_path
@ -104,9 +99,6 @@ steps:
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"' evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
- event: pull_request - event: pull_request
path: *when_path path: *when_path
- event: push
path: *when_path
branch: renovate/*
cross-compile-server: cross-compile-server:
depends_on: depends_on:
@ -127,31 +119,19 @@ steps:
event: [push, tag] event: [push, tag]
path: *when_path path: *when_path
publish-server-preview:
depends_on:
- cross-compile-server-preview
image: *buildx_plugin
settings:
repo: woodpeckerci/woodpecker-server
dockerfile: docker/Dockerfile.server.multiarch
platforms: *platforms_preview
tag: pull_${CI_COMMIT_PULL_REQUEST}
logins: *publish_logins
when: &when-preview
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
event: pull_request
publish-server-alpine-preview: publish-server-alpine-preview:
depends_on: depends_on:
- cross-compile-server-preview - cross-compile-server-preview
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: woodpeckerci/woodpecker-server repo: woodpeckerci/woodpecker-server
dockerfile: docker/Dockerfile.server.alpine.multiarch dockerfile: docker/Dockerfile.server.alpine.multiarch.rootless
platforms: *platforms_preview platforms: *platforms_preview
tag: pull_${CI_COMMIT_PULL_REQUEST}-alpine tag: pull_${CI_COMMIT_PULL_REQUEST}-alpine
logins: *publish_logins logins: *publish_logins
when: *when-preview when: &when-preview
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
event: pull_request
build-server-dryrun: build-server-dryrun:
depends_on: depends_on:
@ -162,16 +142,13 @@ steps:
settings: settings:
dry_run: true dry_run: true
repo: woodpeckerci/woodpecker-server repo: woodpeckerci/woodpecker-server
dockerfile: docker/Dockerfile.server.multiarch dockerfile: docker/Dockerfile.server.multiarch.rootless
platforms: *platforms_preview platforms: *platforms_preview
tag: pull_${CI_COMMIT_PULL_REQUEST} tag: pull_${CI_COMMIT_PULL_REQUEST}
when: &when-dryrun when: &when-dryrun
- evaluate: 'not (CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images")' - evaluate: 'not (CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images")'
event: pull_request event: pull_request
path: *when_path path: *when_path
- event: push
path: *when_path
branch: renovate/*
publish-next-server: publish-next-server:
depends_on: depends_on:
@ -179,7 +156,7 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_server repo: *publish_repos_server
dockerfile: docker/Dockerfile.server.multiarch dockerfile: docker/Dockerfile.server.multiarch.rootless
platforms: *platforms_server platforms: *platforms_server
tag: [next, 'next-${CI_COMMIT_SHA:0:10}'] tag: [next, 'next-${CI_COMMIT_SHA:0:10}']
logins: *publish_logins logins: *publish_logins
@ -194,7 +171,7 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_server repo: *publish_repos_server
dockerfile: docker/Dockerfile.server.alpine.multiarch dockerfile: docker/Dockerfile.server.alpine.multiarch.rootless
platforms: *platforms_alpine platforms: *platforms_alpine
tag: [next-alpine, 'next-${CI_COMMIT_SHA:0:10}-alpine'] tag: [next-alpine, 'next-${CI_COMMIT_SHA:0:10}-alpine']
logins: *publish_logins logins: *publish_logins
@ -206,10 +183,9 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_server repo: *publish_repos_server
dockerfile: docker/Dockerfile.server.multiarch dockerfile: docker/Dockerfile.server.multiarch.rootless
platforms: *platforms_server platforms: *platforms_server
# remove 'latest' on older version branches to avoid accidental downgrade tag: ['${CI_COMMIT_TAG%%.*}', '${CI_COMMIT_TAG%.*}-alpine', '${CI_COMMIT_TAG}']
tag: [latest, '${CI_COMMIT_TAG}']
logins: *publish_logins logins: *publish_logins
when: &when-release when: &when-release
event: tag event: tag
@ -220,10 +196,9 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_server repo: *publish_repos_server
dockerfile: docker/Dockerfile.server.alpine.multiarch dockerfile: docker/Dockerfile.server.alpine.multiarch.rootless
platforms: *platforms_alpine platforms: *platforms_alpine
# remove 'latest-alpine' on older version branches to avoid accidental downgrade tag: ['${CI_COMMIT_TAG%%.*}-alpine', '${CI_COMMIT_TAG%.*}-alpine', '${CI_COMMIT_TAG}-alpine']
tag: [latest-alpine, '${CI_COMMIT_TAG}-alpine']
logins: *publish_logins logins: *publish_logins
when: *when-release when: *when-release
@ -231,15 +206,15 @@ steps:
# A g e n t # # A g e n t #
############# #############
publish-agent-preview: publish-agent-preview-alpine:
depends_on: depends_on:
- vendor - vendor
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: woodpeckerci/woodpecker-agent repo: woodpeckerci/woodpecker-agent
dockerfile: docker/Dockerfile.agent.multiarch dockerfile: docker/Dockerfile.agent.alpine.multiarch
platforms: *platforms_preview platforms: *platforms_preview
tag: pull_${CI_COMMIT_PULL_REQUEST} tag: pull_${CI_COMMIT_PULL_REQUEST}-alpine
build_args: *build_args build_args: *build_args
logins: *publish_logins logins: *publish_logins
when: *when-preview when: *when-preview
@ -303,8 +278,7 @@ steps:
repo: *publish_repos_agent repo: *publish_repos_agent
dockerfile: docker/Dockerfile.agent.multiarch dockerfile: docker/Dockerfile.agent.multiarch
platforms: *platforms_release platforms: *platforms_release
# remove 'latest' on older version branches to avoid accidental downgrade tag: ['${CI_COMMIT_TAG%%.*}', '${CI_COMMIT_TAG%.*}', '${CI_COMMIT_TAG}']
tag: [latest, '${CI_COMMIT_TAG}']
logins: *publish_logins logins: *publish_logins
build_args: *build_args build_args: *build_args
when: *when-release when: *when-release
@ -320,8 +294,7 @@ steps:
repo: *publish_repos_agent repo: *publish_repos_agent
dockerfile: docker/Dockerfile.agent.alpine.multiarch dockerfile: docker/Dockerfile.agent.alpine.multiarch
platforms: *platforms_alpine platforms: *platforms_alpine
# remove 'latest-alpine' on older version branches to avoid accidental downgrade tag: ['${CI_COMMIT_TAG%%.*}-alpine', '${CI_COMMIT_TAG%.*}-alpine', '${CI_COMMIT_TAG}-alpine']
tag: [latest-alpine, '${CI_COMMIT_TAG}-alpine']
logins: *publish_logins logins: *publish_logins
build_args: *build_args build_args: *build_args
when: *when-release when: *when-release
@ -330,19 +303,6 @@ steps:
# C L I # # C L I #
######### #########
publish-cli-preview:
depends_on:
- vendor
image: *buildx_plugin
settings:
repo: woodpeckerci/woodpecker-cli
dockerfile: docker/Dockerfile.cli.multiarch
platforms: *platforms_preview
tag: pull_${CI_COMMIT_PULL_REQUEST}
build_args: *build_args
logins: *publish_logins
when: *when-preview
build-cli-dryrun: build-cli-dryrun:
depends_on: depends_on:
- vendor - vendor
@ -350,7 +310,7 @@ steps:
settings: settings:
dry_run: true dry_run: true
repo: woodpeckerci/woodpecker-cli repo: woodpeckerci/woodpecker-cli
dockerfile: docker/Dockerfile.cli.multiarch dockerfile: docker/Dockerfile.cli.multiarch.rootless
platforms: *platforms_preview platforms: *platforms_preview
tag: pull_${CI_COMMIT_PULL_REQUEST} tag: pull_${CI_COMMIT_PULL_REQUEST}
build_args: *build_args build_args: *build_args
@ -365,7 +325,7 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_cli repo: *publish_repos_cli
dockerfile: docker/Dockerfile.cli.multiarch dockerfile: docker/Dockerfile.cli.multiarch.rootless
platforms: *platforms_release platforms: *platforms_release
tag: [next, 'next-${CI_COMMIT_SHA:0:10}'] tag: [next, 'next-${CI_COMMIT_SHA:0:10}']
logins: *publish_logins logins: *publish_logins
@ -381,7 +341,7 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_cli repo: *publish_repos_cli
dockerfile: docker/Dockerfile.cli.alpine.multiarch dockerfile: docker/Dockerfile.cli.alpine.multiarch.rootless
platforms: *platforms_alpine platforms: *platforms_alpine
tag: [next-alpine, 'next-${CI_COMMIT_SHA:0:10}-alpine'] tag: [next-alpine, 'next-${CI_COMMIT_SHA:0:10}-alpine']
logins: *publish_logins logins: *publish_logins
@ -397,10 +357,9 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_cli repo: *publish_repos_cli
dockerfile: docker/Dockerfile.cli.multiarch dockerfile: docker/Dockerfile.cli.multiarch.rootless
platforms: *platforms_release platforms: *platforms_release
# remove 'latest' on older version branches to avoid accidental downgrade tag: ['${CI_COMMIT_TAG%%.*}', '${CI_COMMIT_TAG%.*}', '${CI_COMMIT_TAG}']
tag: [latest, '${CI_COMMIT_TAG}']
logins: *publish_logins logins: *publish_logins
build_args: *build_args build_args: *build_args
when: *when-release when: *when-release
@ -414,10 +373,9 @@ steps:
image: *buildx_plugin image: *buildx_plugin
settings: settings:
repo: *publish_repos_cli repo: *publish_repos_cli
dockerfile: docker/Dockerfile.cli.alpine.multiarch dockerfile: docker/Dockerfile.cli.alpine.multiarch.rootless
platforms: *platforms_alpine platforms: *platforms_alpine
# remove 'latest-alpine' on older version branches to avoid accidental downgrade tag: ['${CI_COMMIT_TAG%%.*}-alpine', '${CI_COMMIT_TAG%.*}-alpine', '${CI_COMMIT_TAG}-alpine']
tag: [latest-alpine, '${CI_COMMIT_TAG}-alpine']
logins: *publish_logins logins: *publish_logins
build_args: *build_args build_args: *build_args
when: *when-release when: *when-release

View file

@ -1,7 +1,7 @@
variables: variables:
- &golang_image 'docker.io/golang:1.22' - &golang_image 'docker.io/golang:1.23'
- &node_image 'docker.io/node:21-alpine' - &node_image 'docker.io/node:23-alpine'
- &alpine_image 'docker.io/alpine:3.19' - &alpine_image 'docker.io/alpine:3.21'
- path: &when_path - path: &when_path
- 'docs/**' - 'docs/**'
- '.woodpecker/docs.yaml' - '.woodpecker/docs.yaml'
@ -31,13 +31,22 @@ when:
- <<: *docker_path - <<: *docker_path
branch: branch:
- ${CI_REPO_DEFAULT_BRANCH} - ${CI_REPO_DEFAULT_BRANCH}
- renovate/*
- event: pull_request_closed - event: pull_request_closed
path: *when_path path: *when_path
- event: manual - event: manual
evaluate: 'TASK == "docs"' evaluate: 'TASK == "docs"'
steps: steps:
prettier:
image: docker.io/woodpeckerci/plugin-prettier:next
settings:
version: 3.3.3
plugins:
- 'prettier-plugin-tailwindcss'
- '@ianvs/prettier-plugin-sort-imports'
when:
- event: pull_request
build-cli: build-cli:
image: *golang_image image: *golang_image
commands: commands:
@ -60,7 +69,7 @@ steps:
- event: manual - event: manual
deploy-preview: deploy-preview:
image: docker.io/woodpeckerci/plugin-surge-preview:1.3.0 image: docker.io/woodpeckerci/plugin-surge-preview:1.3.3
settings: settings:
path: 'docs/build/' path: 'docs/build/'
surge_token: surge_token:
@ -69,13 +78,14 @@ steps:
from_secret: GITHUB_TOKEN_SURGE from_secret: GITHUB_TOKEN_SURGE
failure: ignore failure: ignore
when: when:
event: [pull_request, pull_request_closed] - event: [pull_request, pull_request_closed]
path: *when_path path: *when_path
deploy-prepare: deploy-prepare:
image: *alpine_image image: *alpine_image
secrets: environment:
- BOT_PRIVATE_KEY BOT_PRIVATE_KEY:
from_secret: BOT_PRIVATE_KEY
commands: commands:
- apk add openssh-client git - apk add openssh-client git
- mkdir -p $HOME/.ssh - mkdir -p $HOME/.ssh
@ -127,8 +137,9 @@ steps:
deploy: deploy:
image: *alpine_image image: *alpine_image
secrets: environment:
- BOT_PRIVATE_KEY BOT_PRIVATE_KEY:
from_secret: BOT_PRIVATE_KEY
commands: commands:
- apk add openssh-client rsync git - apk add openssh-client rsync git
- mkdir -p $HOME/.ssh - mkdir -p $HOME/.ssh

31
.woodpecker/links.yaml Normal file
View file

@ -0,0 +1,31 @@
when:
- event: cron
cron: links
steps:
- name: links
image: docker.io/lycheeverse/lychee:0.15.1
failure: ignore
depends_on: []
commands:
- lychee pipeline/frontend/yaml/linter/schema/schema.json > links.md
- lychee --exclude localhost docs/docs/ >> links.md
- lychee --exclude localhost docs/src/pages/ >> links.md
- echo -e "\nLast checked:$(date)" >> links.md
- name: Update issue
image: docker.io/alpine:3.21
depends_on: links
environment:
GITHUB_TOKEN:
from_secret: github_token
commands:
- apk add -q --no-cache jq curl
- export ISSUE_NUMBER=4514
- export DESCRIPTION=$(cat links.md)
- |
curl -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/${CI_REPO}/issues/$ISSUE_NUMBER \
-d "$(jq -n --arg body "$DESCRIPTION" '{body: $body}')"

View file

@ -1,16 +1,15 @@
when:
- event: push
branch:
- ${CI_REPO_DEFAULT_BRANCH}
- release/*
steps: steps:
release-helper: - name: release-helper
image: woodpeckerci/plugin-ready-release-go:1.1.2 image: docker.io/woodpeckerci/plugin-ready-release-go:3.1.1
pull: true
settings: settings:
release_branch: ${CI_REPO_DEFAULT_BRANCH} release_branch: ${CI_COMMIT_BRANCH}
forge_type: github forge_type: github
git_email: woodpecker-bot@obermui.de git_email: woodpecker-bot@obermui.de
github_token: github_token:
from_secret: GITHUB_TOKEN from_secret: GITHUB_TOKEN
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
evaluate: 'TASK == "release-helper"'

View file

@ -1,24 +1,25 @@
when: when:
- event: [pull_request, cron] - event: [pull_request]
- event: push - event: push
branch: branch:
- ${CI_REPO_DEFAULT_BRANCH} - ${CI_REPO_DEFAULT_BRANCH}
- renovate/*
variables: variables:
- &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.1.0 - &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.3.0
steps: steps:
backend: backend:
depends_on: [] depends_on: []
image: *trivy_plugin image: *trivy_plugin
settings: settings:
server: server
skip-dirs: web/,docs/ skip-dirs: web/,docs/
docs: docs:
depends_on: [] depends_on: []
image: *trivy_plugin image: *trivy_plugin
settings: settings:
server: server
skip-dirs: node_modules/,plugins/woodpecker-plugins/node_modules/ skip-dirs: node_modules/,plugins/woodpecker-plugins/node_modules/
dir: docs/ dir: docs/
@ -26,5 +27,15 @@ steps:
depends_on: [] depends_on: []
image: *trivy_plugin image: *trivy_plugin
settings: settings:
server: server
skip-dirs: node_modules/ skip-dirs: node_modules/
dir: web/ dir: web/
services:
server:
image: *trivy_plugin
settings:
service: true
db-repository: docker.io/aquasec/trivy-db:2
ports:
- 10000

View file

@ -1,19 +1,15 @@
when: when:
- event: pull_request - event: pull_request
- event: push
branch: renovate/*
steps: steps:
- name: lint-editorconfig - name: lint-editorconfig
image: docker.io/mstruebing/editorconfig-checker:v3.0.3 image: docker.io/woodpeckerci/plugin-editorconfig-checker:0.2.0
depends_on: [] depends_on: []
when: when:
- event: pull_request - event: pull_request
- event: push
branch: renovate/*
- name: spellcheck - name: spellcheck
image: docker.io/node:22-alpine image: docker.io/node:23-alpine
depends_on: [] depends_on: []
commands: commands:
- corepack enable - corepack enable
@ -23,15 +19,11 @@ steps:
- tree --gitignore -I 012_columns_rename_procs_to_steps.go -I versioned_docs -I '*opensource.svg'| pnpx cspell lint --no-progress stdin - tree --gitignore -I 012_columns_rename_procs_to_steps.go -I versioned_docs -I '*opensource.svg'| pnpx cspell lint --no-progress stdin
- name: prettier - name: prettier
image: docker.io/woodpeckerci/plugin-prettier:0.1.0 image: docker.io/woodpeckerci/plugin-prettier:next
pull: true
depends_on: [] depends_on: []
settings: settings:
version: 3.2.5 version: 3.3.3
plugins:
- name: links - 'prettier-plugin-tailwindcss'
image: docker.io/lycheeverse/lychee:0.15.1 - '@ianvs/prettier-plugin-sort-imports'
depends_on: []
commands:
- lychee pipeline/frontend/yaml/linter/schema/schema.json
- lychee --exclude localhost docs/docs/
- lychee --exclude localhost docs/src/pages/

View file

@ -1,5 +1,5 @@
variables: variables:
- &golang_image 'docker.io/golang:1.22' - &golang_image 'docker.io/golang:1.23'
- &when - &when
- path: &when_path # related config files - path: &when_path # related config files
- '.woodpecker/test.yaml' - '.woodpecker/test.yaml'
@ -10,14 +10,9 @@ variables:
# schema changes # schema changes
- 'pipeline/schema/**' - 'pipeline/schema/**'
event: pull_request event: pull_request
- event: push
branch: renovate/*
path: *when_path
when: when:
- event: pull_request - event: pull_request
- event: push
branch: renovate/*
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: *when_path path: *when_path
@ -37,9 +32,11 @@ steps:
- vendor - vendor
image: *golang_image image: *golang_image
commands: commands:
- go run go.woodpecker-ci.org/woodpecker/v2/cmd/cli lint - go run go.woodpecker-ci.org/woodpecker/v3/cmd/cli lint
environment: environment:
WOODPECKER_DISABLE_UPDATE_CHECK: true WOODPECKER_DISABLE_UPDATE_CHECK: true
WOODPECKER_LINT_STRICT: true
WOODPECKER_PLUGINS_PRIVILEGED: 'docker.io/woodpeckerci/plugin-docker-buildx'
when: when:
- event: pull_request - event: pull_request
path: path:
@ -61,14 +58,14 @@ steps:
- make lint - make lint
when: *when when: *when
check-swagger: check-openapi:
depends_on: depends_on:
- vendor - vendor
image: *golang_image image: *golang_image
commands: commands:
- 'make generate-swagger' - 'make generate-openapi'
- 'DIFF=$(git diff | head)' - 'DIFF=$(git diff | head)'
- '[ -n "$DIFF" ] && { echo "swagger not up to date, exec `make generate-swagger` and commit"; exit 1; } || true' - '[ -n "$DIFF" ] && { echo "openapi not up to date, exec `make generate-openapi` and commit"; exit 1; } || true'
when: *when when: *when
lint-license-header: lint-license-header:
@ -128,7 +125,7 @@ steps:
- test - test
- sqlite - sqlite
pull: true pull: true
image: docker.io/woodpeckerci/plugin-codecov:2.1.2 image: docker.io/woodpeckerci/plugin-codecov:2.1.6
settings: settings:
files: files:
- agent-coverage.out - agent-coverage.out
@ -144,7 +141,7 @@ steps:
services: services:
postgres: postgres:
image: docker.io/postgres:16 image: docker.io/postgres:17
ports: ['5432'] ports: ['5432']
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
@ -152,7 +149,7 @@ services:
when: *when when: *when
mysql: mysql:
image: docker.io/mysql:8.2.0 image: docker.io/mysql:9.1.0
ports: ['3306'] ports: ['3306']
environment: environment:
MYSQL_DATABASE: test MYSQL_DATABASE: test

View file

@ -3,10 +3,9 @@ when:
- event: push - event: push
branch: branch:
- release/* - release/*
- renovate/*
variables: variables:
- &node_image 'docker.io/node:22-alpine' - &node_image 'docker.io/node:23-alpine'
- &when - &when
path: path:
# related config files # related config files
@ -25,6 +24,18 @@ steps:
- pnpm install --frozen-lockfile - pnpm install --frozen-lockfile
when: *when when: *when
prettier:
depends_on:
- install-dependencies
image: docker.io/woodpeckerci/plugin-prettier:next
pull: true
settings:
version: 3.3.3
plugins:
- 'prettier-plugin-tailwindcss'
- '@ianvs/prettier-plugin-sort-imports'
when: *when
lint: lint:
depends_on: depends_on:
- install-dependencies - install-dependencies
@ -58,6 +69,7 @@ steps:
test: test:
depends_on: depends_on:
- install-dependencies - install-dependencies
- format-check # wait for it else test artifacts are falsely detected as wrong
image: *node_image image: *node_image
directory: web/ directory: web/
commands: commands:

View file

@ -6,6 +6,7 @@ ignore-from-file:
- .gitignore - .gitignore
- server/store/datastore/migration/test-files/.gitignore - server/store/datastore/migration/test-files/.gitignore
- web/.gitignore - web/.gitignore
- web/.yamlignore
rules: rules:
line-length: disable line-length: disable

View file

@ -1,5 +1,490 @@
# Changelog # Changelog
## [3.0.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v3.0.0) - 2024-12-13
### ❤️ Thanks to all contributors! ❤️
@6543, @Fishbowler, @M0Rf30, @anbraten, @cduchenoy, @fernandrone, @gnowland, @greenaar, @hg, @j04n-f, @jenrik, @johanneskastl, @jolheiser, @lafriks, @lukashass, @meln5674, @not-my-profile, @pat-s, @qwerty287, @smainz, @tori-27, @tsufeki, @xoxys, @xtexChooser, @zc-devs
### 💥 Breaking changes
- Drop native Let's Encrypt support [[#4541](https://github.com/woodpecker-ci/woodpecker/pull/4541)]
- Set new default approval mode based on repo visibility [[#4456](https://github.com/woodpecker-ci/woodpecker/pull/4456)]
- Do not set empty environment variables [[#4193](https://github.com/woodpecker-ci/woodpecker/pull/4193)]
- Unify cli commands and flags [[#4481](https://github.com/woodpecker-ci/woodpecker/pull/4481)]
- Move pipeline logs command [[#4480](https://github.com/woodpecker-ci/woodpecker/pull/4480)]
- Fix woodpecker-go repo model to match server [[#4479](https://github.com/woodpecker-ci/woodpecker/pull/4479)]
- Restructure cli commands [[#4467](https://github.com/woodpecker-ci/woodpecker/pull/4467)]
- Add pagination options to all supported endpoints in sdk [[#4463](https://github.com/woodpecker-ci/woodpecker/pull/4463)]
- Allow to set custom trusted clone plugins [[#4352](https://github.com/woodpecker-ci/woodpecker/pull/4352)]
- Add PipelineListsOptions to woodpecker-go [[#3652](https://github.com/woodpecker-ci/woodpecker/pull/3652)]
- Remove `secrets` in favor of `from_secret` [[#4363](https://github.com/woodpecker-ci/woodpecker/pull/4363)]
- Kubernetes | Docker: Add support for rootless images [[#4151](https://github.com/woodpecker-ci/woodpecker/pull/4151)]
- Split repo trusted setting [[#4025](https://github.com/woodpecker-ci/woodpecker/pull/4025)]
- Move docker resource limit settings from server to agent [[#3174](https://github.com/woodpecker-ci/woodpecker/pull/3174)]
- Set `/woodpecker` as default workdir for the **woodpecker-cli** container [[#4130](https://github.com/woodpecker-ci/woodpecker/pull/4130)]
- Require upgrade from 2.x [[#4112](https://github.com/woodpecker-ci/woodpecker/pull/4112)]
- Don't expose task data via api [[#4108](https://github.com/woodpecker-ci/woodpecker/pull/4108)]
- Remove some ci environment variables [[#3846](https://github.com/woodpecker-ci/woodpecker/pull/3846)]
- Remove all default privileged plugins [[#4053](https://github.com/woodpecker-ci/woodpecker/pull/4053)]
- Add option to filter secrets by plugins with specific tags [[#4069](https://github.com/woodpecker-ci/woodpecker/pull/4069)]
- Remove old pipeline options [[#4016](https://github.com/woodpecker-ci/woodpecker/pull/4016)]
- Remove various deprecations [[#4017](https://github.com/woodpecker-ci/woodpecker/pull/4017)]
- Drop repo name fallback for hooks [[#4013](https://github.com/woodpecker-ci/woodpecker/pull/4013)]
- Improve local backend detection [[#4006](https://github.com/woodpecker-ci/woodpecker/pull/4006)]
- Refactor JSON and SDK fields [[#3968](https://github.com/woodpecker-ci/woodpecker/pull/3968)]
- Migrate to maintained cron lib and remove seconds [[#3785](https://github.com/woodpecker-ci/woodpecker/pull/3785)]
- Switch to profile-based AppArmor configuration [[#4008](https://github.com/woodpecker-ci/woodpecker/pull/4008)]
- Remove Kubernetes default image pull secret name `regcred` [[#4005](https://github.com/woodpecker-ci/woodpecker/pull/4005)]
- Drop "WOODPECKER_WEBHOOK_HOST" env var and adjust docs [[#3969](https://github.com/woodpecker-ci/woodpecker/pull/3969)]
- Drop version in schema [[#3970](https://github.com/woodpecker-ci/woodpecker/pull/3970)]
- Update docker to v27 [[#3972](https://github.com/woodpecker-ci/woodpecker/pull/3972)]
- Require gitlab 12.4 [[#3966](https://github.com/woodpecker-ci/woodpecker/pull/3966)]
- Migrate to maintained httpsign library [[#3839](https://github.com/woodpecker-ci/woodpecker/pull/3839)]
- Remove `WOODPECKER_DEV_OAUTH_HOST` and `WOODPECKER_DEV_GITEA_OAUTH_URL` [[#3961](https://github.com/woodpecker-ci/woodpecker/pull/3961)]
- Remove deprecated pipeline keywords: `pipeline:`, `platform:`, `branches:` [[#3916](https://github.com/woodpecker-ci/woodpecker/pull/3916)]
- server: remove old unused routes [[#3845](https://github.com/woodpecker-ci/woodpecker/pull/3845)]
- CLI: remove step-id and add step-number as option to logs [[#3927](https://github.com/woodpecker-ci/woodpecker/pull/3927)]
### 🔒 Security
- Add server config to disable user registered agents [[#4206](https://github.com/woodpecker-ci/woodpecker/pull/4206)]
- chore: fix `http-proxy-middleware` CVE [[#4257](https://github.com/woodpecker-ci/woodpecker/pull/4257)]
- Allow altering trusted clone plugins and filter them via tag [[#4074](https://github.com/woodpecker-ci/woodpecker/pull/4074)]
- Update gitea sdk [[#4012](https://github.com/woodpecker-ci/woodpecker/pull/4012)]
- Update Forgejo SDK [[#3948](https://github.com/woodpecker-ci/woodpecker/pull/3948)]
### ✨ Features
- Add user as docker backend_option [[#4526](https://github.com/woodpecker-ci/woodpecker/pull/4526)]
- Implement org/user agents [[#3539](https://github.com/woodpecker-ci/woodpecker/pull/3539)]
- Replay pipeline using `cli exec` by downloading metadata [[#4103](https://github.com/woodpecker-ci/woodpecker/pull/4103)]
- Update clone plugin to support sha256 [[#4136](https://github.com/woodpecker-ci/woodpecker/pull/4136)]
### 🐛 Bug Fixes
- Fix BB ambiguous commit status key [[#4544](https://github.com/woodpecker-ci/woodpecker/pull/4544)]
- fix: addon JSON pointers [[#4508](https://github.com/woodpecker-ci/woodpecker/pull/4508)]
- Fix apparmorProfile being ignored when it's the only field [[#4507](https://github.com/woodpecker-ci/woodpecker/pull/4507)]
- Sanitize strings in table output [[#4466](https://github.com/woodpecker-ci/woodpecker/pull/4466)]
- Cleanup openapi generation [[#4331](https://github.com/woodpecker-ci/woodpecker/pull/4331)]
- Support github refresh tokens [[#3811](https://github.com/woodpecker-ci/woodpecker/pull/3811)]
- Fix not working overflow on repo list message [[#4420](https://github.com/woodpecker-ci/woodpecker/pull/4420)]
- Fix avatar column type [[#4340](https://github.com/woodpecker-ci/woodpecker/pull/4340)]
- fix `error="io: read/write on closed pipe"` on k8s backend [[#4281](https://github.com/woodpecker-ci/woodpecker/pull/4281)]
- Move update notifier dot into settings button [[#4334](https://github.com/woodpecker-ci/woodpecker/pull/4334)]
- gitea: add check if pull_request webhook is missing pull info [[#4305](https://github.com/woodpecker-ci/woodpecker/pull/4305)]
- Refresh token before loading branches [[#4284](https://github.com/woodpecker-ci/woodpecker/pull/4284)]
- Delete GitLab webhooks with partial URL match [[#4259](https://github.com/woodpecker-ci/woodpecker/pull/4259)]
- Increase `WOODPECKER_FORGE_TIMEOUT` to fix config fetching for GitLab [[#4262](https://github.com/woodpecker-ci/woodpecker/pull/4262)]
- Ensure cli exec has by default not the same prefix [[#4132](https://github.com/woodpecker-ci/woodpecker/pull/4132)]
- Fix repo add loading spinner [[#4135](https://github.com/woodpecker-ci/woodpecker/pull/4135)]
- Fix migration registries table [[#4111](https://github.com/woodpecker-ci/woodpecker/pull/4111)]
- Wait for tracer to be done before finishing workflow [[#4068](https://github.com/woodpecker-ci/woodpecker/pull/4068)]
- Fix schema with detached steps [[#4066](https://github.com/woodpecker-ci/woodpecker/pull/4066)]
- Fix schema with commands and entrypoint [[#4065](https://github.com/woodpecker-ci/woodpecker/pull/4065)]
- Read long log lines from file storage correctly [[#4048](https://github.com/woodpecker-ci/woodpecker/pull/4048)]
- Set refspec for gitlab MR [[#4021](https://github.com/woodpecker-ci/woodpecker/pull/4021)]
- Set `CI_PREV_COMMIT_{SOURCE,TARGET}_BRANCH` as mentioned in the documentation [[#4001](https://github.com/woodpecker-ci/woodpecker/pull/4001)]
- [Bitbucket Datacenter] Return empty list instead of null [[#4010](https://github.com/woodpecker-ci/woodpecker/pull/4010)]
- Fix BB PR pipeline ref [[#3985](https://github.com/woodpecker-ci/woodpecker/pull/3985)]
- Change Bitbucket PR hook to point the source branch, commit & ref [[#3965](https://github.com/woodpecker-ci/woodpecker/pull/3965)]
- Add updated, merged and declined events to bb webhook activation [[#3963](https://github.com/woodpecker-ci/woodpecker/pull/3963)]
- Fix login via navbar [[#3962](https://github.com/woodpecker-ci/woodpecker/pull/3962)]
- Truncate creation in list [[#3952](https://github.com/woodpecker-ci/woodpecker/pull/3952)]
- Fix panic if forge is unreachable [[#3944](https://github.com/woodpecker-ci/woodpecker/pull/3944)]
### 📚 Documentation
- Show client flags [[#4542](https://github.com/woodpecker-ci/woodpecker/pull/4542)]
- chore(deps): update react monorepo to v19 (major) [[#4523](https://github.com/woodpecker-ci/woodpecker/pull/4523)]
- chore(deps): update docs npm deps non-major [[#4519](https://github.com/woodpecker-ci/woodpecker/pull/4519)]
- chore(deps): lock file maintenance [[#4502](https://github.com/woodpecker-ci/woodpecker/pull/4502)]
- chore(deps): lock file maintenance [[#4501](https://github.com/woodpecker-ci/woodpecker/pull/4501)]
- chore(deps): update dependency isomorphic-dompurify to v2.18.0 [[#4493](https://github.com/woodpecker-ci/woodpecker/pull/4493)]
- fix(deps): update docs npm deps non-major [[#4484](https://github.com/woodpecker-ci/woodpecker/pull/4484)]
- Add migration notes for restructured cli commands [[#4476](https://github.com/woodpecker-ci/woodpecker/pull/4476)]
- Various fixes for `awesome.md` [[#4448](https://github.com/woodpecker-ci/woodpecker/pull/4448)]
- chore(deps): lock file maintenance [[#4453](https://github.com/woodpecker-ci/woodpecker/pull/4453)]
- chore(deps): update dependency isomorphic-dompurify to v2.17.0 [[#4449](https://github.com/woodpecker-ci/woodpecker/pull/4449)]
- fix(deps): update docs npm deps non-major [[#4441](https://github.com/woodpecker-ci/woodpecker/pull/4441)]
- chore(deps): update dependency @docusaurus/tsconfig to v3.6.2 [[#4433](https://github.com/woodpecker-ci/woodpecker/pull/4433)]
- chore(deps): lock file maintenance [[#4435](https://github.com/woodpecker-ci/woodpecker/pull/4435)]
- Bump minimum nodejs to v20 [[#4417](https://github.com/woodpecker-ci/woodpecker/pull/4417)]
- chore(deps): lock file maintenance [[#4402](https://github.com/woodpecker-ci/woodpecker/pull/4402)]
- Add microsoft teams plugin [[#4400](https://github.com/woodpecker-ci/woodpecker/pull/4400)]
- fix(deps): update docs npm deps non-major [[#4394](https://github.com/woodpecker-ci/woodpecker/pull/4394)]
- chore(deps): update dependency @types/node to v22 [[#4395](https://github.com/woodpecker-ci/woodpecker/pull/4395)]
- chore(deps): update dependency marked to v15 [[#4396](https://github.com/woodpecker-ci/woodpecker/pull/4396)]
- Podman is not (official) supported [[#4367](https://github.com/woodpecker-ci/woodpecker/pull/4367)]
- Add EditorConfig-Checker Plugin to docs [[#4371](https://github.com/woodpecker-ci/woodpecker/pull/4371)]
- Update netrc option description [[#4342](https://github.com/woodpecker-ci/woodpecker/pull/4342)]
- Fix deployment event note [[#4283](https://github.com/woodpecker-ci/woodpecker/pull/4283)]
- Improve migration notes [[#4291](https://github.com/woodpecker-ci/woodpecker/pull/4291)]
- Add instructions how to build images locally [[#4277](https://github.com/woodpecker-ci/woodpecker/pull/4277)]
- chore(deps): update docs npm deps non-major [[#4238](https://github.com/woodpecker-ci/woodpecker/pull/4238)]
- Correct spelling [[#4279](https://github.com/woodpecker-ci/woodpecker/pull/4279)]
- Add Telegram plugin [[#4229](https://github.com/woodpecker-ci/woodpecker/pull/4229)]
- Remove archived plugin [[#4227](https://github.com/woodpecker-ci/woodpecker/pull/4227)]
- Use "Woodpecker Authors" as copyright on website [[#4225](https://github.com/woodpecker-ci/woodpecker/pull/4225)]
- chore(deps): update dependency cookie to v1 [[#4224](https://github.com/woodpecker-ci/woodpecker/pull/4224)]
- fix(deps): update docs npm deps non-major [[#4221](https://github.com/woodpecker-ci/woodpecker/pull/4221)]
- Fix errant apostrophe in docker-compose documentation [[#4203](https://github.com/woodpecker-ci/woodpecker/pull/4203)]
- chore(deps): lock file maintenance [[#4186](https://github.com/woodpecker-ci/woodpecker/pull/4186)]
- chore(deps): update dependency concurrently to v9 [[#4176](https://github.com/woodpecker-ci/woodpecker/pull/4176)]
- chore(deps): update docs npm deps non-major [[#4164](https://github.com/woodpecker-ci/woodpecker/pull/4164)]
- Update image filter error message [[#4143](https://github.com/woodpecker-ci/woodpecker/pull/4143)]
- Docs: reference to built-in docker compose and remove deprecated version from compose examples [[#4123](https://github.com/woodpecker-ci/woodpecker/pull/4123)]
- directory key is allowed for services [[#4127](https://github.com/woodpecker-ci/woodpecker/pull/4127)]
- [docs] Removes dot prefix from pipeline configuration filenames [[#4105](https://github.com/woodpecker-ci/woodpecker/pull/4105)]
- Use kaniko plugin in docs as example [[#4072](https://github.com/woodpecker-ci/woodpecker/pull/4072)]
- Add some posts and videos [[#4070](https://github.com/woodpecker-ci/woodpecker/pull/4070)]
- Move event type descriptions from Terminology to Workflow Syntax [[#4062](https://github.com/woodpecker-ci/woodpecker/pull/4062)]
- Add community posts from discussions [[#4058](https://github.com/woodpecker-ci/woodpecker/pull/4058)]
- Add a pull request template with some basic guidelines [[#4055](https://github.com/woodpecker-ci/woodpecker/pull/4055)]
- Add examples of CI environment variable values [[#4009](https://github.com/woodpecker-ci/woodpecker/pull/4009)]
- Fix inline author warning [[#4040](https://github.com/woodpecker-ci/woodpecker/pull/4040)]
- Updated Secrets image filter docs [[#4028](https://github.com/woodpecker-ci/woodpecker/pull/4028)]
- Update dependency marked to v14 [[#4036](https://github.com/woodpecker-ci/woodpecker/pull/4036)]
- Update docs npm deps non-major [[#4033](https://github.com/woodpecker-ci/woodpecker/pull/4033)]
- Overhaul README [[#3995](https://github.com/woodpecker-ci/woodpecker/pull/3995)]
- fix(deps): update docs npm deps non-major [[#3990](https://github.com/woodpecker-ci/woodpecker/pull/3990)]
- Add spellchecking for docs [[#3787](https://github.com/woodpecker-ci/woodpecker/pull/3787)]
### 📈 Enhancement
- Use docusaurus faster [[#4528](https://github.com/woodpecker-ci/woodpecker/pull/4528)]
- Use pagination helper to list pipelines in cli [[#4478](https://github.com/woodpecker-ci/woodpecker/pull/4478)]
- Some UI improvements [[#4497](https://github.com/woodpecker-ci/woodpecker/pull/4497)]
- Add status filter to list pipeline API [[#4494](https://github.com/woodpecker-ci/woodpecker/pull/4494)]
- Use JS-native date/time formatting [[#4488](https://github.com/woodpecker-ci/woodpecker/pull/4488)]
- Add pipeline purge command to cli [[#4470](https://github.com/woodpecker-ci/woodpecker/pull/4470)]
- Add option to limit the resultset returned by paginate helper [[#4475](https://github.com/woodpecker-ci/woodpecker/pull/4475)]
- Add filter to list repository pipelines API [[#4416](https://github.com/woodpecker-ci/woodpecker/pull/4416)]
- Increase log level when failing to fetch YAML [[#4107](https://github.com/woodpecker-ci/woodpecker/pull/4107)]
- Trim space to all config flags that allow to read value from file [[#4468](https://github.com/woodpecker-ci/woodpecker/pull/4468)]
- Change default icon size to 20 [[#4458](https://github.com/woodpecker-ci/woodpecker/pull/4458)]
- Add visibility icon to repo list [[#4460](https://github.com/woodpecker-ci/woodpecker/pull/4460)]
- Unify pipeline status icons [[#4414](https://github.com/woodpecker-ci/woodpecker/pull/4414)]
- Improve project settings descriptions [[#4410](https://github.com/woodpecker-ci/woodpecker/pull/4410)]
- Add count badge to visualize counters in tab title [[#4419](https://github.com/woodpecker-ci/woodpecker/pull/4419)]
- Redesign repo list and include last pipeline [[#4386](https://github.com/woodpecker-ci/woodpecker/pull/4386)]
- Use KeyValueEditor for DeployPipelinePopup too [[#4412](https://github.com/woodpecker-ci/woodpecker/pull/4412)]
- Use separate routes instead of anchors [[#4285](https://github.com/woodpecker-ci/woodpecker/pull/4285)]
- Untangle settings / header slots [[#4403](https://github.com/woodpecker-ci/woodpecker/pull/4403)]
- Fix responsiveness of the settings template [[#4383](https://github.com/woodpecker-ci/woodpecker/pull/4383)]
- Use squared spinner for active pipelines [[#4379](https://github.com/woodpecker-ci/woodpecker/pull/4379)]
- Add server configuration option to add default set of labels for workflows that has no labels specified [[#4326](https://github.com/woodpecker-ci/woodpecker/pull/4326)]
- Add `cli lint` option to treat warnings as errors [[#4373](https://github.com/woodpecker-ci/woodpecker/pull/4373)]
- Improve error message for wrong secrets / environment config [[#4359](https://github.com/woodpecker-ci/woodpecker/pull/4359)]
- Improve linter messages in UI [[#4351](https://github.com/woodpecker-ci/woodpecker/pull/4351)]
- Pass settings to services [[#4338](https://github.com/woodpecker-ci/woodpecker/pull/4338)]
- Inline model types for migrations [[#4293](https://github.com/woodpecker-ci/woodpecker/pull/4293)]
- Add options to control the database connections (open,idle,timeout) [[#4212](https://github.com/woodpecker-ci/woodpecker/pull/4212)]
- Move Queue creation behind new func that evaluates queue type [[#4252](https://github.com/woodpecker-ci/woodpecker/pull/4252)]
- Add additional error message on swagger v2 to v3 convert [[#4254](https://github.com/woodpecker-ci/woodpecker/pull/4254)]
- Deprecate `secrets` [[#4235](https://github.com/woodpecker-ci/woodpecker/pull/4235)]
- Agent edit/detail view: change the help url based on the backend [[#4219](https://github.com/woodpecker-ci/woodpecker/pull/4219)]
- Use middleware to load org [[#4208](https://github.com/woodpecker-ci/woodpecker/pull/4208)]
- Assign workflows to agents with the best label matches [[#4201](https://github.com/woodpecker-ci/woodpecker/pull/4201)]
- Report custom labels set by agent admins back [[#4141](https://github.com/woodpecker-ci/woodpecker/pull/4141)]
- Highlight invalid entries in manual pipeline trigger [[#4153](https://github.com/woodpecker-ci/woodpecker/pull/4153)]
- Print agent labels in debug mode [[#4155](https://github.com/woodpecker-ci/woodpecker/pull/4155)]
- Implement registries for Kubernetes backend [[#4092](https://github.com/woodpecker-ci/woodpecker/pull/4092)]
- Correct cli exec flags and remove ineffective ones [[#4129](https://github.com/woodpecker-ci/woodpecker/pull/4129)]
- Set repo user to repairing user when old user is missing [[#4128](https://github.com/woodpecker-ci/woodpecker/pull/4128)]
- Restart tasks on dead agents sooner [[#4114](https://github.com/woodpecker-ci/woodpecker/pull/4114)]
- Adjust cli exec metadata structure to equal server metadata [[#4119](https://github.com/woodpecker-ci/woodpecker/pull/4119)]
- Allow to restart declined pipelines [[#4109](https://github.com/woodpecker-ci/woodpecker/pull/4109)]
- Add indices to repo table [[#4087](https://github.com/woodpecker-ci/woodpecker/pull/4087)]
- Add systemd unit files to the RPM/DEB packages [[#3986](https://github.com/woodpecker-ci/woodpecker/pull/3986)]
- Duplicate key `workflow_id` in the agent logs [[#4046](https://github.com/woodpecker-ci/woodpecker/pull/4046)]
- Improve error on config loading [[#4024](https://github.com/woodpecker-ci/woodpecker/pull/4024)]
- Show error if secret name is missing [[#4014](https://github.com/woodpecker-ci/woodpecker/pull/4014)]
- Show error returned from API [[#3980](https://github.com/woodpecker-ci/woodpecker/pull/3980)]
- Move manual popup to own page [[#3981](https://github.com/woodpecker-ci/woodpecker/pull/3981)]
- Fail on InvalidImageName [[#4007](https://github.com/woodpecker-ci/woodpecker/pull/4007)]
- Use Bitbucket PR title for pipeline message [[#3984](https://github.com/woodpecker-ci/woodpecker/pull/3984)]
- Show logs if step has error [[#3979](https://github.com/woodpecker-ci/woodpecker/pull/3979)]
- Refactor docker backend and add more test coverage [[#2700](https://github.com/woodpecker-ci/woodpecker/pull/2700)]
- Make cli plugin log purge recognize steps by name [[#3953](https://github.com/woodpecker-ci/woodpecker/pull/3953)]
- Pin page size [[#3946](https://github.com/woodpecker-ci/woodpecker/pull/3946)]
- Improve cron list [[#3947](https://github.com/woodpecker-ci/woodpecker/pull/3947)]
- Add PULLREQUEST_DRONE_PULL_REQUEST drone env [[#3939](https://github.com/woodpecker-ci/woodpecker/pull/3939)]
- Make agent gRPC errors distinguishable [[#3936](https://github.com/woodpecker-ci/woodpecker/pull/3936)]
### 📦️ Dependency
- fix(deps): update module google.golang.org/grpc to v1.69.0 [[#4563](https://github.com/woodpecker-ci/woodpecker/pull/4563)]
- fix(deps): update golang-packages [[#4553](https://github.com/woodpecker-ci/woodpecker/pull/4553)]
- Update kin-openapi [[#4560](https://github.com/woodpecker-ci/woodpecker/pull/4560)]
- fix(deps): update module golang.org/x/crypto to v0.31.0 [security] [[#4557](https://github.com/woodpecker-ci/woodpecker/pull/4557)]
- fix(deps): update golang-packages [[#4546](https://github.com/woodpecker-ci/woodpecker/pull/4546)]
- chore(deps): update docker.io/woodpeckerci/plugin-ready-release-go docker tag to v3.1.0 [[#4536](https://github.com/woodpecker-ci/woodpecker/pull/4536)]
- chore(deps): update docker.io/curlimages/curl docker tag to v8.11.0 [[#4530](https://github.com/woodpecker-ci/woodpecker/pull/4530)]
- fix(deps): update golang-packages [[#4496](https://github.com/woodpecker-ci/woodpecker/pull/4496)]
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v5.1.0 [[#4524](https://github.com/woodpecker-ci/woodpecker/pull/4524)]
- chore(deps): update docker.io/woodpeckerci/plugin-prettier docker tag to v1 [[#4522](https://github.com/woodpecker-ci/woodpecker/pull/4522)]
- chore(deps): update docker.io/alpine docker tag to v3.21 [[#4520](https://github.com/woodpecker-ci/woodpecker/pull/4520)]
- chore(deps): update dependency vite to v6 [[#4485](https://github.com/woodpecker-ci/woodpecker/pull/4485)]
- chore(deps): update docker.io/woodpeckerci/plugin-ready-release-go docker tag to v3 [[#4506](https://github.com/woodpecker-ci/woodpecker/pull/4506)]
- chore(deps): update docker.io/woodpeckerci/plugin-surge-preview docker tag to v1.3.3 [[#4495](https://github.com/woodpecker-ci/woodpecker/pull/4495)]
- fix(deps): update golang-packages [[#4477](https://github.com/woodpecker-ci/woodpecker/pull/4477)]
- fix(deps): update dependency @vueuse/core to v12 [[#4486](https://github.com/woodpecker-ci/woodpecker/pull/4486)]
- fix(deps): update module github.com/google/go-github/v66 to v67 [[#4487](https://github.com/woodpecker-ci/woodpecker/pull/4487)]
- chore(deps): update woodpeckerci/plugin-release docker tag to v0.2.2 [[#4483](https://github.com/woodpecker-ci/woodpecker/pull/4483)]
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.62.2 [[#4482](https://github.com/woodpecker-ci/woodpecker/pull/4482)]
- fix(deps): update golang-packages [[#4452](https://github.com/woodpecker-ci/woodpecker/pull/4452)]
- fix(deps): update golang-packages [[#4411](https://github.com/woodpecker-ci/woodpecker/pull/4411)]
- chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.43.0 [[#4443](https://github.com/woodpecker-ci/woodpecker/pull/4443)]
- chore(deps): update postgres docker tag to v17.2 [[#4442](https://github.com/woodpecker-ci/woodpecker/pull/4442)]
- chore(deps): update docker.io/woodpeckerci/plugin-trivy docker tag to v1.3.0 [[#4434](https://github.com/woodpecker-ci/woodpecker/pull/4434)]
- chore(deps): update web npm deps non-major [[#4432](https://github.com/woodpecker-ci/woodpecker/pull/4432)]
- fix(deps): update golang-packages [[#4401](https://github.com/woodpecker-ci/woodpecker/pull/4401)]
- chore(deps): update web npm deps non-major [[#4391](https://github.com/woodpecker-ci/woodpecker/pull/4391)]
- fix(deps): update dependency @intlify/unplugin-vue-i18n to v6 [[#4397](https://github.com/woodpecker-ci/woodpecker/pull/4397)]
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.62.0 [[#4390](https://github.com/woodpecker-ci/woodpecker/pull/4390)]
- chore(deps): update postgres docker tag to v17.1 [[#4389](https://github.com/woodpecker-ci/woodpecker/pull/4389)]
- chore(deps): update docker.io/techknowlogick/xgo docker tag to go-1.23.x [[#4388](https://github.com/woodpecker-ci/woodpecker/pull/4388)]
- chore(config): migrate renovate config [[#4296](https://github.com/woodpecker-ci/woodpecker/pull/4296)]
- chore(deps): update docker.io/woodpeckerci/plugin-trivy docker tag to v1.2.0 [[#4289](https://github.com/woodpecker-ci/woodpecker/pull/4289)]
- chore(deps): update docker.io/techknowlogick/xgo docker tag to go-1.23.x [[#4282](https://github.com/woodpecker-ci/woodpecker/pull/4282)]
- fix(deps): update golang-packages [[#4251](https://github.com/woodpecker-ci/woodpecker/pull/4251)]
- fix(deps): update web npm deps non-major [[#4258](https://github.com/woodpecker-ci/woodpecker/pull/4258)]
- chore(deps): update web npm deps non-major [[#4250](https://github.com/woodpecker-ci/woodpecker/pull/4250)]
- chore(deps): update node.js to v23 [[#4239](https://github.com/woodpecker-ci/woodpecker/pull/4239)]
- chore(deps): update web npm deps non-major [[#4237](https://github.com/woodpecker-ci/woodpecker/pull/4237)]
- chore(deps): update docker.io/mysql docker tag to v9.1.0 [[#4236](https://github.com/woodpecker-ci/woodpecker/pull/4236)]
- fix(deps): update dependency simple-icons to v13.14.0 [[#4226](https://github.com/woodpecker-ci/woodpecker/pull/4226)]
- fix(deps): update web npm deps non-major [[#4223](https://github.com/woodpecker-ci/woodpecker/pull/4223)]
- fix(deps): update golang-packages [[#4215](https://github.com/woodpecker-ci/woodpecker/pull/4215)]
- fix(deps): update golang-packages [[#4210](https://github.com/woodpecker-ci/woodpecker/pull/4210)]
- fix(deps): update module github.com/google/go-github/v65 to v66 [[#4205](https://github.com/woodpecker-ci/woodpecker/pull/4205)]
- fix(deps): update dependency vue-i18n to v10.0.4 [[#4200](https://github.com/woodpecker-ci/woodpecker/pull/4200)]
- chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v5 [[#4192](https://github.com/woodpecker-ci/woodpecker/pull/4192)]
- fix(deps): update dependency simple-icons to v13.13.0 [[#4196](https://github.com/woodpecker-ci/woodpecker/pull/4196)]
- chore(deps): update web npm deps non-major [[#4174](https://github.com/woodpecker-ci/woodpecker/pull/4174)]
- chore(deps): update docker.io/postgres docker tag to v17 [[#4179](https://github.com/woodpecker-ci/woodpecker/pull/4179)]
- fix(deps): update dependency @intlify/unplugin-vue-i18n to v5 [[#4183](https://github.com/woodpecker-ci/woodpecker/pull/4183)]
- fix(deps): update dependency @vueuse/core to v11 [[#4184](https://github.com/woodpecker-ci/woodpecker/pull/4184)]
- chore(deps): update docker.io/woodpeckerci/plugin-codecov docker tag to v2.1.5 [[#4167](https://github.com/woodpecker-ci/woodpecker/pull/4167)]
- fix(deps): update module github.com/google/go-github/v64 to v65 [[#4185](https://github.com/woodpecker-ci/woodpecker/pull/4185)]
- chore(deps): update docker.io/mysql docker tag to v9 [[#4178](https://github.com/woodpecker-ci/woodpecker/pull/4178)]
- chore(deps): update docker.io/alpine docker tag to v3.20 [[#4169](https://github.com/woodpecker-ci/woodpecker/pull/4169)]
- fix(deps): update github.com/urfave/cli/v3 digest to 20ef97b [[#4166](https://github.com/woodpecker-ci/woodpecker/pull/4166)]
- chore(deps): update docker.io/woodpeckerci/plugin-surge-preview docker tag to v1.3.2 [[#4168](https://github.com/woodpecker-ci/woodpecker/pull/4168)]
- chore(deps): update woodpeckerci/plugin-release docker tag to v0.2.1 [[#4175](https://github.com/woodpecker-ci/woodpecker/pull/4175)]
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v2 [[#4182](https://github.com/woodpecker-ci/woodpecker/pull/4182)]
- fix(deps): update github.com/muesli/termenv digest to 82936c5 [[#4165](https://github.com/woodpecker-ci/woodpecker/pull/4165)]
- chore(deps): update postgres docker tag to v17 [[#4181](https://github.com/woodpecker-ci/woodpecker/pull/4181)]
- chore(deps): update pre-commit non-major [[#4173](https://github.com/woodpecker-ci/woodpecker/pull/4173)]
- chore(deps): update docker.io/golang docker tag to v1.23 [[#4170](https://github.com/woodpecker-ci/woodpecker/pull/4170)]
- chore(deps): update node.js to v22 [[#4180](https://github.com/woodpecker-ci/woodpecker/pull/4180)]
- fix(deps): update golang-packages [[#4161](https://github.com/woodpecker-ci/woodpecker/pull/4161)]
- chore(deps): update dependency @antfu/eslint-config to v3 [[#4095](https://github.com/woodpecker-ci/woodpecker/pull/4095)]
- chore(deps): update dependency jsdom to v25 [[#4094](https://github.com/woodpecker-ci/woodpecker/pull/4094)]
- chore(deps): update docker.io/golang docker tag to v1.23 [[#4081](https://github.com/woodpecker-ci/woodpecker/pull/4081)]
- chore(deps): update docker.io/woodpeckerci/plugin-prettier docker tag to v0.2.0 [[#4082](https://github.com/woodpecker-ci/woodpecker/pull/4082)]
- fix(deps): update module github.com/google/go-github/v63 to v64 [[#4073](https://github.com/woodpecker-ci/woodpecker/pull/4073)]
- fix(deps): update golang-packages [[#4059](https://github.com/woodpecker-ci/woodpecker/pull/4059)]
- Update github.com/urfave/cli/v3 digest to fc07a8c [[#4043](https://github.com/woodpecker-ci/woodpecker/pull/4043)]
- Update woodpeckerci/plugin-git Docker tag to v2.5.2 [[#4041](https://github.com/woodpecker-ci/woodpecker/pull/4041)]
- Update web npm deps non-major [[#4034](https://github.com/woodpecker-ci/woodpecker/pull/4034)]
- Update dependency simple-icons to v13 [[#4037](https://github.com/woodpecker-ci/woodpecker/pull/4037)]
- chore(deps): lock file maintenance [[#3991](https://github.com/woodpecker-ci/woodpecker/pull/3991)]
- fix(deps): update golang-packages [[#3958](https://github.com/woodpecker-ci/woodpecker/pull/3958)]
### Misc
- Move link checks into cron-curated issue dashboard [[#4515](https://github.com/woodpecker-ci/woodpecker/pull/4515)]
- Add settings title action [[#4499](https://github.com/woodpecker-ci/woodpecker/pull/4499)]
- Use same default sort for repo and org repo list [[#4461](https://github.com/woodpecker-ci/woodpecker/pull/4461)]
- Add dns config option to official feature set [[#4418](https://github.com/woodpecker-ci/woodpecker/pull/4418)]
- Remove `renovate` branch triggers [[#4437](https://github.com/woodpecker-ci/woodpecker/pull/4437)]
- Improve tab layout and add hover effect [[#4431](https://github.com/woodpecker-ci/woodpecker/pull/4431)]
- Dont run pipeline on push events to renovate branches [[#4406](https://github.com/woodpecker-ci/woodpecker/pull/4406)]
- Harden and correct fifo task queue tests [[#4377](https://github.com/woodpecker-ci/woodpecker/pull/4377)]
- Kubernetes documentation enhancements [[#4374](https://github.com/woodpecker-ci/woodpecker/pull/4374)]
- Use release-helper for release/* branches [[#4301](https://github.com/woodpecker-ci/woodpecker/pull/4301)]
- Fix wording for privileged plugins linter error [[#4280](https://github.com/woodpecker-ci/woodpecker/pull/4280)]
- Fix renovate support for `xgo` [[#4276](https://github.com/woodpecker-ci/woodpecker/pull/4276)]
- Improve nix development environment [[#4256](https://github.com/woodpecker-ci/woodpecker/pull/4256)]
- [pre-commit.ci] pre-commit autoupdate [[#4209](https://github.com/woodpecker-ci/woodpecker/pull/4209)]
- Add `.lycheeignore` [[#4154](https://github.com/woodpecker-ci/woodpecker/pull/4154)]
- Add eslint-plugin-promise back [[#4022](https://github.com/woodpecker-ci/woodpecker/pull/4022)]
- Improve wording [[#3951](https://github.com/woodpecker-ci/woodpecker/pull/3951)]
- Fix typos and optimize wording [[#3940](https://github.com/woodpecker-ci/woodpecker/pull/3940)]
## [2.7.2](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.7.2) - 2024-11-03
### Important
To secure your instance, set `WOODPECKER_PLUGINS_PRIVILEGED` to only allow specific versions of the `woodpeckerci/plugin-docker-buildx` plugin, use version 5.0.0 or above. This prevents older, potentially unstable versions from being privileged.
For example, to allow only version 5.0.0, use:
```bash
WOODPECKER_PLUGINS_PRIVILEGED=woodpeckerci/plugin-docker-buildx:5.0.0
```
To allow multiple versions, you can separate them with commas:
```bash
WOODPECKER_PLUGINS_PRIVILEGED=woodpeckerci/plugin-docker-buildx:5.0.0,woodpeckerci/plugin-docker-buildx:5.1.0
```
This setup ensures only specified, stable plugin versions are given privileged access.
Read more about it in [#4213](https://github.com/woodpecker-ci/woodpecker/pull/4213)
### ❤️ Thanks to all contributors! ❤️
@6543, @anbraten, @j04n-f, @pat-s, @qwerty287
### 🔒 Security
- Chore(deps): update dependency vite to v5.4.6 [security] ([#4163](https://github.com/woodpecker-ci/woodpecker/pull/4163)) [[#4187](https://github.com/woodpecker-ci/woodpecker/pull/4187)]
### 🐛 Bug Fixes
- Don't parse forge config files multiple times if no error occured ([#4272](https://github.com/woodpecker-ci/woodpecker/pull/4272)) [[#4273](https://github.com/woodpecker-ci/woodpecker/pull/4273)]
- Fix repo/owner parsing for gitlab ([#4255](https://github.com/woodpecker-ci/woodpecker/pull/4255)) [[#4261](https://github.com/woodpecker-ci/woodpecker/pull/4261)]
- Run queue.process() in background [[#4115](https://github.com/woodpecker-ci/woodpecker/pull/4115)]
- Only update agent.LastWork if not done recently ([#4031](https://github.com/woodpecker-ci/woodpecker/pull/4031)) [[#4100](https://github.com/woodpecker-ci/woodpecker/pull/4100)]
### Misc
- Backport JS dependency updates [[#4189](https://github.com/woodpecker-ci/woodpecker/pull/4189)]
## [2.7.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.7.1) - 2024-09-07
### ❤️ Thanks to all contributors! ❤️
@6543, @anbraten, @j04n-f, @qwerty287
### 🔒 Security
- Lint privileged plugin match and allow to be set empty [[#4084](https://github.com/woodpecker-ci/woodpecker/pull/4084)]
- Allow admins to specify privileged plugins by name **and tag** [[#4076](https://github.com/woodpecker-ci/woodpecker/pull/4076)]
- Warn if using secrets/env with plugin [[#4039](https://github.com/woodpecker-ci/woodpecker/pull/4039)]
### 🐛 Bug Fixes
- Set refspec for gitlab MR [[#4021](https://github.com/woodpecker-ci/woodpecker/pull/4021)]
- Change Bitbucket PR hook to point the source branch, commit & ref [[#3965](https://github.com/woodpecker-ci/woodpecker/pull/3965)]
- Add updated, merged and declined events to bb webhook activation [[#3963](https://github.com/woodpecker-ci/woodpecker/pull/3963)]
- Fix login via navbar [[#3962](https://github.com/woodpecker-ci/woodpecker/pull/3962)]
- Fix panic if forge is unreachable [[#3944](https://github.com/woodpecker-ci/woodpecker/pull/3944)]
- Fix org settings page [[#4093](https://github.com/woodpecker-ci/woodpecker/pull/4093)]
### Misc
- Bump github.com/docker/docker from v24.0.9 to v24.0.9+30 [[#4077](https://github.com/woodpecker-ci/woodpecker/pull/4077)]
## [2.7.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.7.0) - 2024-07-18
### ❤️ Thanks to all contributors! ❤️
@6543, @anbraten, @dvjn, @hhamalai, @lafriks, @pat-s, @qwerty287, @smainz, @tongjicoder, @zc-devs
### 🔒 Security
- Add blocklist of environment variables who could alter execution of plugins [[#3934](https://github.com/woodpecker-ci/woodpecker/pull/3934)]
- Make sure plugins only mount the workspace base in a predefinde location [[#3933](https://github.com/woodpecker-ci/woodpecker/pull/3933)]
- Disallow to set arbitrary environments for plugins [[#3909](https://github.com/woodpecker-ci/woodpecker/pull/3909)]
- Use proper oauth state [[#3847](https://github.com/woodpecker-ci/woodpecker/pull/3847)]
- Enhance token checking [[#3842](https://github.com/woodpecker-ci/woodpecker/pull/3842)]
- Bump github.com/hashicorp/go-retryablehttp v0.7.5 -> v0.7.7 [[#3834](https://github.com/woodpecker-ci/woodpecker/pull/3834)]
### ✨ Features
- Gracefully shutdown server [[#3896](https://github.com/woodpecker-ci/woodpecker/pull/3896)]
- Gracefully shutdown agent [[#3895](https://github.com/woodpecker-ci/woodpecker/pull/3895)]
- Convert urls in logs to links [[#3904](https://github.com/woodpecker-ci/woodpecker/pull/3904)]
- Allow login using multiple forges [[#3822](https://github.com/woodpecker-ci/woodpecker/pull/3822)]
- Global and organization registries [[#1672](https://github.com/woodpecker-ci/woodpecker/pull/1672)]
- Cli get repo from git remote [[#3830](https://github.com/woodpecker-ci/woodpecker/pull/3830)]
- Add api for forges [[#3733](https://github.com/woodpecker-ci/woodpecker/pull/3733)]
### 📈 Enhancement
- Cli fix pipeline logs [[#3913](https://github.com/woodpecker-ci/woodpecker/pull/3913)]
- Migrate to github.com/urfave/cli/v3 [[#2951](https://github.com/woodpecker-ci/woodpecker/pull/2951)]
- Allow to change the working directory also for plugins and services [[#3914](https://github.com/woodpecker-ci/woodpecker/pull/3914)]
- Remove `unplugin-icons` [[#3809](https://github.com/woodpecker-ci/woodpecker/pull/3809)]
- Release windows binaries as zip file [[#3906](https://github.com/woodpecker-ci/woodpecker/pull/3906)]
- Convert to openapi 3.0 [[#3897](https://github.com/woodpecker-ci/woodpecker/pull/3897)]
- Enhance pipeline list [[#3898](https://github.com/woodpecker-ci/woodpecker/pull/3898)]
- Add user registries UI [[#3888](https://github.com/woodpecker-ci/woodpecker/pull/3888)]
- Sort users by login [[#3891](https://github.com/woodpecker-ci/woodpecker/pull/3891)]
- Exclude dummy backend in production [[#3877](https://github.com/woodpecker-ci/woodpecker/pull/3877)]
- Fix deploy task env [[#3878](https://github.com/woodpecker-ci/woodpecker/pull/3878)]
- Get default branch and show message in pipeline list [[#3867](https://github.com/woodpecker-ci/woodpecker/pull/3867)]
- Add timestamp for last work done by agent [[#3844](https://github.com/woodpecker-ci/woodpecker/pull/3844)]
- Adjust logger types [[#3859](https://github.com/woodpecker-ci/woodpecker/pull/3859)]
- Cleanup state reporting [[#3850](https://github.com/woodpecker-ci/woodpecker/pull/3850)]
- Unify DB tables/columns [[#3806](https://github.com/woodpecker-ci/woodpecker/pull/3806)]
- Let webhook pass on pipeline parsing error [[#3829](https://github.com/woodpecker-ci/woodpecker/pull/3829)]
- Exclude mocks from release build [[#3831](https://github.com/woodpecker-ci/woodpecker/pull/3831)]
- K8s secrets reference from step [[#3655](https://github.com/woodpecker-ci/woodpecker/pull/3655)]
### 🐛 Bug Fixes
- Handle empty repositories in gitea when listing PRs [[#3925](https://github.com/woodpecker-ci/woodpecker/pull/3925)]
- Update alpine package dep for docker images [[#3917](https://github.com/woodpecker-ci/woodpecker/pull/3917)]
- Don't report error if agent was terminated gracefully [[#3894](https://github.com/woodpecker-ci/woodpecker/pull/3894)]
- Let agents continuously report their health [[#3893](https://github.com/woodpecker-ci/woodpecker/pull/3893)]
- Ignore warnings for cli exec [[#3868](https://github.com/woodpecker-ci/woodpecker/pull/3868)]
- Correct favicon states [[#3832](https://github.com/woodpecker-ci/woodpecker/pull/3832)]
- Cleanup of the login flow and tests [[#3810](https://github.com/woodpecker-ci/woodpecker/pull/3810)]
- Fix newlines in logs [[#3808](https://github.com/woodpecker-ci/woodpecker/pull/3808)]
- Fix authentication error handling [[#3807](https://github.com/woodpecker-ci/woodpecker/pull/3807)]
### 📚 Documentation
- Streamline docs for new users [[#3803](https://github.com/woodpecker-ci/woodpecker/pull/3803)]
- Add mastodon verification [[#3843](https://github.com/woodpecker-ci/woodpecker/pull/3843)]
- chore(deps): update docs npm deps non-major [[#3837](https://github.com/woodpecker-ci/woodpecker/pull/3837)]
- fix(deps): update docs npm deps non-major [[#3824](https://github.com/woodpecker-ci/woodpecker/pull/3824)]
- Add openSUSE package [[#3800](https://github.com/woodpecker-ci/woodpecker/pull/3800)]
- chore(deps): update docs npm deps non-major [[#3798](https://github.com/woodpecker-ci/woodpecker/pull/3798)]
- Add "Docker Tags" Plugin [[#3796](https://github.com/woodpecker-ci/woodpecker/pull/3796)]
- chore(deps): update dependency marked to v13 [[#3792](https://github.com/woodpecker-ci/woodpecker/pull/3792)]
- chore: fix some comments [[#3788](https://github.com/woodpecker-ci/woodpecker/pull/3788)]
### Misc
- chore(deps): update web npm deps non-major [[#3930](https://github.com/woodpecker-ci/woodpecker/pull/3930)]
- chore(deps): update dependency vitest to v2 [[#3905](https://github.com/woodpecker-ci/woodpecker/pull/3905)]
- fix(deps): update module github.com/google/go-github/v62 to v63 [[#3910](https://github.com/woodpecker-ci/woodpecker/pull/3910)]
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v4.1.0 [[#3908](https://github.com/woodpecker-ci/woodpecker/pull/3908)]
- Update plugin-git and add renovate trigger [[#3901](https://github.com/woodpecker-ci/woodpecker/pull/3901)]
- chore(deps): update docker.io/mstruebing/editorconfig-checker docker tag to v3.0.3 [[#3903](https://github.com/woodpecker-ci/woodpecker/pull/3903)]
- fix(deps): update golang-packages [[#3875](https://github.com/woodpecker-ci/woodpecker/pull/3875)]
- chore(deps): lock file maintenance [[#3876](https://github.com/woodpecker-ci/woodpecker/pull/3876)]
- [pre-commit.ci] pre-commit autoupdate [[#3862](https://github.com/woodpecker-ci/woodpecker/pull/3862)]
- Add dummy backend [[#3820](https://github.com/woodpecker-ci/woodpecker/pull/3820)]
- chore(deps): update dependency replace-in-file to v8 [[#3852](https://github.com/woodpecker-ci/woodpecker/pull/3852)]
- Update forgejo sdk [[#3840](https://github.com/woodpecker-ci/woodpecker/pull/3840)]
- chore(deps): lock file maintenance [[#3838](https://github.com/woodpecker-ci/woodpecker/pull/3838)]
- Allow to set dist dir using env var [[#3814](https://github.com/woodpecker-ci/woodpecker/pull/3814)]
- chore(deps): lock file maintenance [[#3805](https://github.com/woodpecker-ci/woodpecker/pull/3805)]
- chore(deps): update docker.io/lycheeverse/lychee docker tag to v0.15.1 [[#3797](https://github.com/woodpecker-ci/woodpecker/pull/3797)]
## [2.6.1](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.6.1) - 2024-07-19
### 🔒 Security
- Add blocklist of environment variables who could alter execution of plugins [[#3934](https://github.com/woodpecker-ci/woodpecker/pull/3934)]
- Make sure plugins only mount the workspace base in a predefinde location [[#3933](https://github.com/woodpecker-ci/woodpecker/pull/3933)]
- Disalow to set arbitrary environments for plugins [[#3909](https://github.com/woodpecker-ci/woodpecker/pull/3909)]
- Bump trivy plugin version and remove unused variable [[#3833](https://github.com/woodpecker-ci/woodpecker/pull/3833)]
### 🐛 Bug Fixes
- Let webhook pass on pipeline parsion error [[#3829](https://github.com/woodpecker-ci/woodpecker/pull/3829)]
- Fix newlines in logs [[#3808](https://github.com/woodpecker-ci/woodpecker/pull/3808)]
## [2.6.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.6.0) - 2024-06-13 ## [2.6.0](https://github.com/woodpecker-ci/woodpecker/releases/tag/v2.6.0) - 2024-06-13
### ❤️ Thanks to all contributors! ❤️ ### ❤️ Thanks to all contributors! ❤️

101
Makefile
View file

@ -30,7 +30,7 @@ else
endif endif
TAGS ?= TAGS ?=
LDFLAGS := -X go.woodpecker-ci.org/woodpecker/v2/version.Version=${VERSION} LDFLAGS := -X go.woodpecker-ci.org/woodpecker/v3/version.Version=${VERSION}
STATIC_BUILD ?= true STATIC_BUILD ?= true
ifeq ($(STATIC_BUILD),true) ifeq ($(STATIC_BUILD),true)
LDFLAGS := -s -w -extldflags "-static" $(LDFLAGS) LDFLAGS := -s -w -extldflags "-static" $(LDFLAGS)
@ -39,7 +39,8 @@ CGO_ENABLED ?= 1 # only used to compile server
HAS_GO = $(shell hash go > /dev/null 2>&1 && echo "GO" || echo "NOGO" ) HAS_GO = $(shell hash go > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
ifeq ($(HAS_GO),GO) ifeq ($(HAS_GO),GO)
XGO_VERSION ?= go-1.20.x # renovate: datasource=docker depName=docker.io/techknowlogick/xgo
XGO_VERSION ?= go-1.23.x
CGO_CFLAGS ?= $(shell go env CGO_CFLAGS) CGO_CFLAGS ?= $(shell go env CGO_CFLAGS)
endif endif
CGO_CFLAGS ?= CGO_CFLAGS ?=
@ -108,15 +109,15 @@ clean: ## Clean build artifacts
clean-all: clean ## Clean all artifacts clean-all: clean ## Clean all artifacts
rm -rf ${DIST_DIR} web/dist docs/build docs/node_modules web/node_modules rm -rf ${DIST_DIR} web/dist docs/build docs/node_modules web/node_modules
# delete generated # delete generated
rm -rf docs/docs/40-cli.md docs/swagger.json rm -rf docs/docs/40-cli.md docs/openapi.json
.PHONY: generate .PHONY: generate
generate: install-tools generate-swagger ## Run all code generations generate: install-tools generate-openapi ## Run all code generations
CGO_ENABLED=0 go generate ./... CGO_ENABLED=0 go generate ./...
generate-swagger: install-tools ## Run swagger code generation generate-openapi: install-tools ## Run openapi code generation and format it
swag init -g server/api/ -g cmd/server/swagger.go --outputTypes go -output cmd/server/docs go run github.com/swaggo/swag/cmd/swag fmt
CGO_ENABLED=0 go generate cmd/server/swagger.go CGO_ENABLED=0 go generate cmd/server/openapi.go
generate-license-header: install-tools generate-license-header: install-tools
addlicense -c "Woodpecker Authors" -ignore "vendor/**" **/*.go addlicense -c "Woodpecker Authors" -ignore "vendor/**" **/*.go
@ -133,9 +134,6 @@ install-tools: ## Install development tools
hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install mvdan.cc/gofumpt@latest; \ go install mvdan.cc/gofumpt@latest; \
fi ; \ fi ; \
hash swag > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install github.com/swaggo/swag/cmd/swag@latest; \
fi ; \
hash addlicense > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ hash addlicense > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install github.com/google/addlicense@latest; \ go install github.com/google/addlicense@latest; \
fi ; \ fi ; \
@ -163,20 +161,20 @@ lint-ui: ui-dependencies ## Lint UI code
(cd web/; pnpm lint --quiet) (cd web/; pnpm lint --quiet)
test-agent: ## Test agent code test-agent: ## Test agent code
go test -race -cover -coverprofile agent-coverage.out -timeout 60s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v2/cmd/agent go.woodpecker-ci.org/woodpecker/v2/agent/... go test -race -cover -coverprofile agent-coverage.out -timeout 60s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v3/cmd/agent go.woodpecker-ci.org/woodpecker/v3/agent/...
test-server: ## Test server code test-server: ## Test server code
go test -race -cover -coverprofile server-coverage.out -timeout 60s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v2/cmd/server $(shell go list go.woodpecker-ci.org/woodpecker/v2/server/... | grep -v '/store') go test -race -cover -coverprofile server-coverage.out -timeout 60s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v3/cmd/server $(shell go list go.woodpecker-ci.org/woodpecker/v3/server/... | grep -v '/store')
test-cli: ## Test cli code test-cli: ## Test cli code
go test -race -cover -coverprofile cli-coverage.out -timeout 60s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v2/cmd/cli go.woodpecker-ci.org/woodpecker/v2/cli/... go test -race -cover -coverprofile cli-coverage.out -timeout 60s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v3/cmd/cli go.woodpecker-ci.org/woodpecker/v3/cli/...
test-server-datastore: ## Test server datastore test-server-datastore: ## Test server datastore
go test -timeout 300s -tags 'test $(TAGS)' -run TestMigrate go.woodpecker-ci.org/woodpecker/v2/server/store/... go test -timeout 300s -tags 'test $(TAGS)' -run TestMigrate go.woodpecker-ci.org/woodpecker/v3/server/store/...
go test -race -timeout 100s -tags 'test $(TAGS)' -skip TestMigrate go.woodpecker-ci.org/woodpecker/v2/server/store/... go test -race -timeout 100s -tags 'test $(TAGS)' -skip TestMigrate go.woodpecker-ci.org/woodpecker/v3/server/store/...
test-server-datastore-coverage: ## Test server datastore with coverage report test-server-datastore-coverage: ## Test server datastore with coverage report
go test -race -cover -coverprofile datastore-coverage.out -timeout 300s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v2/server/store/... go test -race -cover -coverprofile datastore-coverage.out -timeout 300s -tags 'test $(TAGS)' go.woodpecker-ci.org/woodpecker/v3/server/store/...
test-ui: ui-dependencies ## Test UI code test-ui: ui-dependencies ## Test UI code
(cd web/; pnpm run lint) (cd web/; pnpm run lint)
@ -195,14 +193,14 @@ test: test-agent test-server test-server-datastore test-cli test-lib ## Run all
build-ui: ## Build UI build-ui: ## Build UI
(cd web/; pnpm install --frozen-lockfile; pnpm build) (cd web/; pnpm install --frozen-lockfile; pnpm build)
build-server: build-ui generate-swagger ## Build server build-server: build-ui generate-openapi ## Build server
CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags '$(TAGS)' -ldflags '${LDFLAGS}' -o ${DIST_DIR}/woodpecker-server${BIN_SUFFIX} go.woodpecker-ci.org/woodpecker/v2/cmd/server CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags '$(TAGS)' -ldflags '${LDFLAGS}' -o ${DIST_DIR}/woodpecker-server${BIN_SUFFIX} go.woodpecker-ci.org/woodpecker/v3/cmd/server
build-agent: ## Build agent build-agent: ## Build agent
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags '$(TAGS)' -ldflags '${LDFLAGS}' -o ${DIST_DIR}/woodpecker-agent${BIN_SUFFIX} go.woodpecker-ci.org/woodpecker/v2/cmd/agent CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags '$(TAGS)' -ldflags '${LDFLAGS}' -o ${DIST_DIR}/woodpecker-agent${BIN_SUFFIX} go.woodpecker-ci.org/woodpecker/v3/cmd/agent
build-cli: ## Build cli build-cli: ## Build cli
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags '$(TAGS)' -ldflags '${LDFLAGS}' -o ${DIST_DIR}/woodpecker-cli${BIN_SUFFIX} go.woodpecker-ci.org/woodpecker/v2/cmd/cli CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags '$(TAGS)' -ldflags '${LDFLAGS}' -o ${DIST_DIR}/woodpecker-cli${BIN_SUFFIX} go.woodpecker-ci.org/woodpecker/v3/cmd/cli
build-tarball: ## Build tar archive build-tarball: ## Build tar archive
mkdir -p ${DIST_DIR} && tar chzvf ${DIST_DIR}/woodpecker-src.tar.gz \ mkdir -p ${DIST_DIR} && tar chzvf ${DIST_DIR}/woodpecker-src.tar.gz \
@ -235,54 +233,75 @@ release-server-xgo: check-xgo ## Create server binaries for release using xgo
@echo "arch orgi:$(TARGETARCH)" @echo "arch orgi:$(TARGETARCH)"
@echo "arch (xgo):$(TARGETARCH_XGO)" @echo "arch (xgo):$(TARGETARCH_XGO)"
@echo "arch (buildx):$(TARGETARCH_BUILDX)" @echo "arch (buildx):$(TARGETARCH_BUILDX)"
# build via xgo
CGO_CFLAGS="$(CGO_CFLAGS)" xgo -go $(XGO_VERSION) -dest ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX) -tags 'netgo osusergo grpcnotrace $(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_DIR}/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"; \ # move binary into subfolder depending on target os and arch
@if [ "$${XGO_IN_XGO:-0}" -eq "1" ]; then \
echo "inside xgo image"; \
mkdir -p ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX); \ mkdir -p ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX); \
mv -vf /build/woodpecker-server* ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \ mv -vf /build/woodpecker-server* ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \
else echo "outside xgo image"; \ else \
echo "outside xgo image"; \
[ -f "${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX)" ] && rm -v ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \ [ -f "${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX)" ] && rm -v ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \
mv -v ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_XGO)/woodpecker-server* ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \ mv -v ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_XGO)/woodpecker-server* ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server$(BIN_SUFFIX); \
fi fi
@[ "$${TARGZ:-0}" -eq "1" ] && tar -cvzf ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH_BUILDX).tar.gz -C ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX) woodpecker-server$(BIN_SUFFIX) || echo "skip creating '${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH_BUILDX).tar.gz'" # if enabled package it in an archive
@if [ "$${ARCHIVE_IT:-0}" -eq "1" ]; then \
if [ "$(BIN_SUFFIX)" = ".exe" ]; then \
rm -f ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH_BUILDX).zip; \
zip -j ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH_BUILDX).zip ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX)/woodpecker-server.exe; \
else \
tar -cvzf ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH_BUILDX).tar.gz -C ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH_BUILDX) woodpecker-server$(BIN_SUFFIX); \
fi; \
else \
echo "skip creating '${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH_BUILDX).tar.gz'"; \
fi
release-server: ## Create server binaries for release release-server: ## Create server binaries for release
# compile # compile
GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) CGO_ENABLED=${CGO_ENABLED} go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/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_DIR}/server/$(TARGETOS)_$(TARGETARCH)/woodpecker-server$(BIN_SUFFIX) go.woodpecker-ci.org/woodpecker/v3/cmd/server
# tar binary files # tar binary files
tar -cvzf ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH).tar.gz -C ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH) woodpecker-server$(BIN_SUFFIX) if [ "$(BIN_SUFFIX)" == ".exe" ]; then \
zip -j ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH).zip ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH)/woodpecker-server.exe; \
else \
tar -cvzf ${DIST_DIR}/woodpecker-server_$(TARGETOS)_$(TARGETARCH).tar.gz -C ${DIST_DIR}/server/$(TARGETOS)_$(TARGETARCH) woodpecker-server$(BIN_SUFFIX); \
fi
release-agent: ## Create agent binaries for release release-agent: ## Create agent binaries for release
# compile # compile
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/linux_amd64/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_DIR}/agent/linux_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v3/cmd/agent
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/linux_arm64/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_DIR}/agent/linux_arm64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v3/cmd/agent
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/linux_arm/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_DIR}/agent/linux_arm/woodpecker-agent go.woodpecker-ci.org/woodpecker/v3/cmd/agent
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/windows_amd64/woodpecker-agent.exe go.woodpecker-ci.org/woodpecker/v2/cmd/agent GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/windows_amd64/woodpecker-agent.exe go.woodpecker-ci.org/woodpecker/v3/cmd/agent
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/darwin_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v2/cmd/agent GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/darwin_amd64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v3/cmd/agent
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -tags 'grpcnotrace $(TAGS)' -o ${DIST_DIR}/agent/darwin_arm64/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_DIR}/agent/darwin_arm64/woodpecker-agent go.woodpecker-ci.org/woodpecker/v3/cmd/agent
# tar binary files # tar binary files
tar -cvzf ${DIST_DIR}/woodpecker-agent_linux_amd64.tar.gz -C ${DIST_DIR}/agent/linux_amd64 woodpecker-agent tar -cvzf ${DIST_DIR}/woodpecker-agent_linux_amd64.tar.gz -C ${DIST_DIR}/agent/linux_amd64 woodpecker-agent
tar -cvzf ${DIST_DIR}/woodpecker-agent_linux_arm64.tar.gz -C ${DIST_DIR}/agent/linux_arm64 woodpecker-agent tar -cvzf ${DIST_DIR}/woodpecker-agent_linux_arm64.tar.gz -C ${DIST_DIR}/agent/linux_arm64 woodpecker-agent
tar -cvzf ${DIST_DIR}/woodpecker-agent_linux_arm.tar.gz -C ${DIST_DIR}/agent/linux_arm woodpecker-agent tar -cvzf ${DIST_DIR}/woodpecker-agent_linux_arm.tar.gz -C ${DIST_DIR}/agent/linux_arm woodpecker-agent
tar -cvzf ${DIST_DIR}/woodpecker-agent_windows_amd64.tar.gz -C ${DIST_DIR}/agent/windows_amd64 woodpecker-agent.exe
tar -cvzf ${DIST_DIR}/woodpecker-agent_darwin_amd64.tar.gz -C ${DIST_DIR}/agent/darwin_amd64 woodpecker-agent tar -cvzf ${DIST_DIR}/woodpecker-agent_darwin_amd64.tar.gz -C ${DIST_DIR}/agent/darwin_amd64 woodpecker-agent
tar -cvzf ${DIST_DIR}/woodpecker-agent_darwin_arm64.tar.gz -C ${DIST_DIR}/agent/darwin_arm64 woodpecker-agent tar -cvzf ${DIST_DIR}/woodpecker-agent_darwin_arm64.tar.gz -C ${DIST_DIR}/agent/darwin_arm64 woodpecker-agent
# zip binary files
rm -f ${DIST_DIR}/woodpecker-agent_windows_amd64.zip
zip -j ${DIST_DIR}/woodpecker-agent_windows_amd64.zip ${DIST_DIR}/agent/windows_amd64/woodpecker-agent.exe
release-cli: ## Create cli binaries for release release-cli: ## Create cli binaries for release
# compile # compile
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/linux_amd64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v2/cmd/cli GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/linux_amd64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v3/cmd/cli
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/linux_arm64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v2/cmd/cli GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/linux_arm64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v3/cmd/cli
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/linux_arm/woodpecker-cli go.woodpecker-ci.org/woodpecker/v2/cmd/cli GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/linux_arm/woodpecker-cli go.woodpecker-ci.org/woodpecker/v3/cmd/cli
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/windows_amd64/woodpecker-cli.exe go.woodpecker-ci.org/woodpecker/v2/cmd/cli GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/windows_amd64/woodpecker-cli.exe go.woodpecker-ci.org/woodpecker/v3/cmd/cli
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/darwin_amd64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v2/cmd/cli GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/darwin_amd64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v3/cmd/cli
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/darwin_arm64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v2/cmd/cli GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags '${LDFLAGS}' -o ${DIST_DIR}/cli/darwin_arm64/woodpecker-cli go.woodpecker-ci.org/woodpecker/v3/cmd/cli
# tar binary files # tar binary files
tar -cvzf ${DIST_DIR}/woodpecker-cli_linux_amd64.tar.gz -C ${DIST_DIR}/cli/linux_amd64 woodpecker-cli tar -cvzf ${DIST_DIR}/woodpecker-cli_linux_amd64.tar.gz -C ${DIST_DIR}/cli/linux_amd64 woodpecker-cli
tar -cvzf ${DIST_DIR}/woodpecker-cli_linux_arm64.tar.gz -C ${DIST_DIR}/cli/linux_arm64 woodpecker-cli tar -cvzf ${DIST_DIR}/woodpecker-cli_linux_arm64.tar.gz -C ${DIST_DIR}/cli/linux_arm64 woodpecker-cli
tar -cvzf ${DIST_DIR}/woodpecker-cli_linux_arm.tar.gz -C ${DIST_DIR}/cli/linux_arm woodpecker-cli tar -cvzf ${DIST_DIR}/woodpecker-cli_linux_arm.tar.gz -C ${DIST_DIR}/cli/linux_arm woodpecker-cli
tar -cvzf ${DIST_DIR}/woodpecker-cli_windows_amd64.tar.gz -C ${DIST_DIR}/cli/windows_amd64 woodpecker-cli.exe
tar -cvzf ${DIST_DIR}/woodpecker-cli_darwin_amd64.tar.gz -C ${DIST_DIR}/cli/darwin_amd64 woodpecker-cli tar -cvzf ${DIST_DIR}/woodpecker-cli_darwin_amd64.tar.gz -C ${DIST_DIR}/cli/darwin_amd64 woodpecker-cli
tar -cvzf ${DIST_DIR}/woodpecker-cli_darwin_arm64.tar.gz -C ${DIST_DIR}/cli/darwin_arm64 woodpecker-cli tar -cvzf ${DIST_DIR}/woodpecker-cli_darwin_arm64.tar.gz -C ${DIST_DIR}/cli/darwin_arm64 woodpecker-cli
# zip binary files
rm -f ${DIST_DIR}/woodpecker-cli_windows_amd64.zip
zip -j ${DIST_DIR}/woodpecker-cli_windows_amd64.zip ${DIST_DIR}/cli/windows_amd64/woodpecker-cli.exe
release-checksums: ## Create checksums for all release files release-checksums: ## Create checksums for all release files
# generate shas for tar files # generate shas for tar files
@ -321,6 +340,6 @@ spellcheck:
.PHONY: docs .PHONY: docs
docs: ## Generate docs (currently only for the cli) docs: ## Generate docs (currently only for the cli)
CGO_ENABLED=0 go generate cmd/cli/app.go CGO_ENABLED=0 go generate cmd/cli/app.go
CGO_ENABLED=0 go generate cmd/server/swagger.go CGO_ENABLED=0 go generate cmd/server/openapi.go
endif endif

View file

@ -19,11 +19,11 @@
<a href="https://matrix.to/#/#woodpecker:matrix.org" title="Join the Matrix space at https://matrix.to/#/#woodpecker:matrix.org"> <a href="https://matrix.to/#/#woodpecker:matrix.org" title="Join the Matrix space at https://matrix.to/#/#woodpecker:matrix.org">
<img src="https://img.shields.io/matrix/woodpecker:matrix.org?label=matrix" alt="Matrix space"> <img src="https://img.shields.io/matrix/woodpecker:matrix.org?label=matrix" alt="Matrix space">
</a> </a>
<a href="https://goreportcard.com/report/go.woodpecker-ci.org/woodpecker/v2" title="Go Report Card"> <a href="https://goreportcard.com/report/go.woodpecker-ci.org/woodpecker/v3" title="Go Report Card">
<img src="https://goreportcard.com/badge/go.woodpecker-ci.org/woodpecker/v2" alt="Go Report Card"> <img src="https://goreportcard.com/badge/go.woodpecker-ci.org/woodpecker/v3" alt="Go Report Card">
</a> </a>
<a href="https://pkg.go.dev/go.woodpecker-ci.org/woodpecker/v2" title="go reference"> <a href="https://pkg.go.dev/go.woodpecker-ci.org/woodpecker/v3" title="go reference">
<img src="https://pkg.go.dev/badge/go.woodpecker-ci.org/woodpecker/v2" alt="go reference"> <img src="https://pkg.go.dev/badge/go.woodpecker-ci.org/woodpecker/v3" alt="go reference">
</a> </a>
<a href="https://github.com/woodpecker-ci/woodpecker/releases/latest" title="GitHub release"> <a href="https://github.com/woodpecker-ci/woodpecker/releases/latest" title="GitHub release">
<img src="https://img.shields.io/github/v/release/woodpecker-ci/woodpecker?sort=semver" alt="GitHub release"> <img src="https://img.shields.io/github/v/release/woodpecker-ci/woodpecker?sort=semver" alt="GitHub release">
@ -43,55 +43,48 @@
</p> </p>
<br/> <br/>
Woodpecker is a simple yet powerful CI/CD engine with great extensibility. Woodpecker is a simple, yet powerful CI/CD engine with great extensibility.
![woodpecker](docs/docs/woodpecker.png) ![woodpecker](docs/woodpecker.png)
## 🫶 Support ## Installation & Resources
Please consider donating and become a backer. 🙏 [[Become a backer](https://opencollective.com/woodpecker-ci#category-CONTRIBUTE)] Woodpecker can be installed in various ways (see the [Installation Instructions](https://woodpecker-ci.org/docs/administration/getting-started)) and runs with SQLite as database by default.
It requires around 100 MB of RAM (Server) and 30 MB (Agent) at runtime in idle mode.
## Support
You can support the project by becoming a backer on [Open Collective](https://opencollective.com/woodpecker-ci#category-CONTRIBUTE) or via [GitHub Sponsors](https://github.com/sponsors/woodpecker-ci).
<a href="https://opencollective.com/woodpecker-ci" target="_blank"><img src="https://opencollective.com/woodpecker-ci/backers.svg?width=890" alt="Open Collective backers"></a> <a href="https://opencollective.com/woodpecker-ci" target="_blank"><img src="https://opencollective.com/woodpecker-ci/backers.svg?width=890" alt="Open Collective backers"></a>
## 📖 Documentation ## Documentation
<https://woodpecker-ci.org/> Our documentation can be found at <https://woodpecker-ci.org/docs/intro>.
## ✨ Contribute ## Translation
See [Contributing Guide](https://github.com/woodpecker-ci/.github/blob/main/CONTRIBUTING.md) We have a self-hosted [Weblate](https://weblate.org/en/) instance at [translate.woodpecker-ci.org](https://translate.woodpecker-ci.org).
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://woodpecker-ci.org/docs/next/development/getting-started#gitpod) An overview of the current translation state is available at <https://translate.woodpecker-ci.org/projects/woodpecker-ci/#languages>.
## 📣 Translate ## Public Woodpecker Instances
We use an own [Weblate](https://weblate.org/en/) instance at [translate.woodpecker-ci.org](https://translate.woodpecker-ci.org). Woodpecker is used as the main CI/CD engine at [Codeberg](https://codeberg.org), an alternative Git hosting platform with a focus on privacy and free software development.
<a href="https://translate.woodpecker-ci.org/engage/woodpecker-ci/"> ## Plugins
<img src="https://translate.woodpecker-ci.org/widgets/woodpecker-ci/-/ui/multi-blue.svg" alt="Translation status" />
</a>
## 👋 Who uses Woodpecker? Woodpecker can be extended via plugins.
The [plugin overview website](https://woodpecker-ci.org/plugins) helps browsing available plugins.
It combines both plugins by the Woodpecker core team and community-maintained ones.
Woodpecker is used by [itself](https://ci.woodpecker-ci.org/woodpecker/woodpecker-ci/), multiple well-known companies, organizations like [Codeberg](https://codeberg.org), hobbyists and many others. ## Star History
Leave a [comment](https://github.com/woodpecker-ci/woodpecker/discussions/2149) if you're using it as well. [![Star History Chart](https://api.star-history.com/svg?repos=woodpecker-ci/woodpecker&type=Date)](https://star-history.com/#woodpecker-ci/woodpecker&Date)
Also consider using the topic `WoodpeckerCI` in your repository, so others can learn from your config and use the hashtag `#WoodpeckerCI` when talking about the project on social media!
Here are some places where people mention Woodpecker:
- [GitHub](https://github.com/topics/WoodpeckerCI)
- [Codeberg](https://codeberg.org/explore/repos?q=woodpeckerci&topic=1)
- [Twitter](https://twitter.com/search?q=%23WoodpeckerCI&src=typed_query)
- [Fediverse](https://mastodon.social/tags/WoodpeckerCI)
## ✨ Stars over time
[![Stargazers over time](https://starchart.cc/woodpecker-ci/woodpecker.svg)](https://starchart.cc/woodpecker-ci/woodpecker)
## License ## License
Woodpecker is Apache 2.0 licensed with the source files in this repository having a header indicating which license they are under and what copyrights apply. Woodpecker is Apache 2.0 licensed.
The source files have a header indicating which license they are under and what copyrights apply.
Files under the `docs/` folder are licensed under Creative Commons Attribution-ShareAlike 4.0 International Public License. Everything in `docs/` is licensed under the Creative Commons Attribution-ShareAlike 4.0 International Public License.

View file

@ -20,16 +20,10 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.woodpecker-ci.org/woodpecker/v2/pipeline" "go.woodpecker-ci.org/woodpecker/v3/pipeline"
backend "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/log" "go.woodpecker-ci.org/woodpecker/v3/pipeline/log"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" "go.woodpecker-ci.org/woodpecker/v3/pipeline/rpc"
)
const (
// Store not more than 1mb in a log-line as 4mb is the limit of a grpc message
// and log-lines needs to be parsed by the browsers later on.
maxLogLineLength = 1024 * 1024 // 1mb
) )
func (r *Runner) createLogger(_logger zerolog.Logger, uploads *sync.WaitGroup, workflow *rpc.Workflow) pipeline.Logger { func (r *Runner) createLogger(_logger zerolog.Logger, uploads *sync.WaitGroup, workflow *rpc.Workflow) pipeline.Logger {
@ -38,7 +32,6 @@ func (r *Runner) createLogger(_logger zerolog.Logger, uploads *sync.WaitGroup, w
logger := _logger.With(). logger := _logger.With().
Str("image", step.Image). Str("image", step.Image).
Str("workflow_id", workflow.ID).
Logger() Logger()
uploads.Add(1) uploads.Add(1)
@ -51,7 +44,7 @@ func (r *Runner) createLogger(_logger zerolog.Logger, uploads *sync.WaitGroup, w
logger.Debug().Msg("log stream opened") logger.Debug().Msg("log stream opened")
logStream := log.NewLineWriter(r.client, step.UUID, secrets...) logStream := log.NewLineWriter(r.client, step.UUID, secrets...)
if err := log.CopyLineByLine(logStream, rc, maxLogLineLength); err != nil { if err := log.CopyLineByLine(logStream, rc, pipeline.MaxLogLineLength); err != nil {
logger.Error().Err(err).Msg("copy limited logStream part") logger.Error().Err(err).Msg("copy limited logStream part")
} }

View file

@ -20,9 +20,11 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto" "go.woodpecker-ci.org/woodpecker/v3/pipeline/rpc/proto"
) )
const authClientTimeout = time.Second * 5
type AuthClient struct { type AuthClient struct {
client proto.WoodpeckerAuthClient client proto.WoodpeckerAuthClient
conn *grpc.ClientConn conn *grpc.ClientConn
@ -39,8 +41,8 @@ func NewAuthGrpcClient(conn *grpc.ClientConn, agentToken string, agentID int64)
return client return client
} }
func (c *AuthClient) Auth() (string, int64, error) { func (c *AuthClient) Auth(ctx context.Context) (string, int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //nolint:mnd ctx, cancel := context.WithTimeout(ctx, authClientTimeout)
defer cancel() defer cancel()
req := &proto.AuthRequest{ req := &proto.AuthRequest{

View file

@ -30,15 +30,12 @@ type AuthInterceptor struct {
} }
// NewAuthInterceptor returns a new auth interceptor. // NewAuthInterceptor returns a new auth interceptor.
func NewAuthInterceptor( func NewAuthInterceptor(ctx context.Context, authClient *AuthClient, refreshDuration time.Duration) (*AuthInterceptor, error) {
authClient *AuthClient,
refreshDuration time.Duration,
) (*AuthInterceptor, error) {
interceptor := &AuthInterceptor{ interceptor := &AuthInterceptor{
authClient: authClient, authClient: authClient,
} }
err := interceptor.scheduleRefreshToken(refreshDuration) err := interceptor.scheduleRefreshToken(ctx, refreshDuration)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,21 +75,26 @@ func (interceptor *AuthInterceptor) attachToken(ctx context.Context) context.Con
return metadata.AppendToOutgoingContext(ctx, "token", interceptor.accessToken) return metadata.AppendToOutgoingContext(ctx, "token", interceptor.accessToken)
} }
func (interceptor *AuthInterceptor) scheduleRefreshToken(refreshDuration time.Duration) error { func (interceptor *AuthInterceptor) scheduleRefreshToken(ctx context.Context, refreshInterval time.Duration) error {
err := interceptor.refreshToken() err := interceptor.refreshToken(ctx)
if err != nil { if err != nil {
return err return err
} }
go func() { go func() {
wait := refreshDuration wait := refreshInterval
for { for {
time.Sleep(wait) select {
err := interceptor.refreshToken() case <-ctx.Done():
if err != nil { return
wait = time.Second case <-time.After(wait):
} else { err := interceptor.refreshToken(ctx)
wait = refreshDuration if err != nil {
wait = time.Second
} else {
wait = refreshInterval
}
} }
} }
}() }()
@ -100,8 +102,8 @@ func (interceptor *AuthInterceptor) scheduleRefreshToken(refreshDuration time.Du
return nil return nil
} }
func (interceptor *AuthInterceptor) refreshToken() error { func (interceptor *AuthInterceptor) refreshToken(ctx context.Context) error {
accessToken, _, err := interceptor.authClient.Auth() accessToken, _, err := interceptor.authClient.Auth(ctx)
if err != nil { if err != nil {
return err return err
} }

View file

@ -17,44 +17,57 @@ package rpc
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v5"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
grpcproto "google.golang.org/protobuf/proto"
backend "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" "go.woodpecker-ci.org/woodpecker/v3/pipeline/rpc"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto" "go.woodpecker-ci.org/woodpecker/v3/pipeline/rpc/proto"
) )
// Set grpc version on compile time to compare against server version response. const (
const ClientGrpcVersion int32 = proto.Version // Set grpc version on compile time to compare against server version response.
ClientGrpcVersion int32 = proto.Version
// Maximum size of an outgoing log message.
// Picked to prevent it from going over GRPC size limit (4 MiB) with a large safety margin.
maxLogBatchSize int = 1 * 1024 * 1024
// Maximum amount of time between sending consecutive batched log messages.
// Controls the delay between the CI job generating a log record, and web users receiving it.
maxLogFlushPeriod time.Duration = time.Second
)
type client struct { type client struct {
client proto.WoodpeckerClient client proto.WoodpeckerClient
conn *grpc.ClientConn conn *grpc.ClientConn
logs chan *proto.LogEntry
} }
// NewGrpcClient returns a new grpc Client. // NewGrpcClient returns a new grpc Client.
func NewGrpcClient(conn *grpc.ClientConn) rpc.Peer { func NewGrpcClient(ctx context.Context, conn *grpc.ClientConn) rpc.Peer {
client := new(client) client := new(client)
client.client = proto.NewWoodpeckerClient(conn) client.client = proto.NewWoodpeckerClient(conn)
client.conn = conn client.conn = conn
client.logs = make(chan *proto.LogEntry, 10) // max memory use: 10 lines * 1 MiB
go client.processLogs(ctx)
return client return client
} }
func (c *client) Close() error { func (c *client) Close() error {
close(c.logs)
return c.conn.Close() return c.conn.Close()
} }
func (c *client) newBackOff() backoff.BackOff { func (c *client) newBackOff() backoff.BackOff {
b := backoff.NewExponentialBackOff() b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 0
b.MaxInterval = 10 * time.Second //nolint:mnd b.MaxInterval = 10 * time.Second //nolint:mnd
b.InitialInterval = 10 * time.Millisecond //nolint:mnd b.InitialInterval = 10 * time.Millisecond //nolint:mnd
return b return b
@ -73,13 +86,13 @@ func (c *client) Version(ctx context.Context) (*rpc.Version, error) {
} }
// Next returns the next workflow in the queue. // Next returns the next workflow in the queue.
func (c *client) Next(ctx context.Context, f rpc.Filter) (*rpc.Workflow, error) { func (c *client) Next(ctx context.Context, filter rpc.Filter) (*rpc.Workflow, error) {
var res *proto.NextResponse var res *proto.NextResponse
var err error var err error
retry := c.newBackOff() retry := c.newBackOff()
req := new(proto.NextRequest) req := new(proto.NextRequest)
req.Filter = new(proto.Filter) req.Filter = new(proto.Filter)
req.Filter.Labels = f.Labels req.Filter.Labels = filter.Labels
for { for {
res, err = c.client.Next(ctx, req) res, err = c.client.Next(ctx, req)
if err == nil { if err == nil {
@ -90,8 +103,10 @@ func (c *client) Next(ctx context.Context, f rpc.Filter) (*rpc.Workflow, error)
case codes.Canceled: case codes.Canceled:
if ctx.Err() != nil { if ctx.Err() != nil {
// expected as context was canceled // expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: next(): context canceled")
return nil, nil return nil, nil
} }
log.Error().Err(err).Msgf("grpc error: next(): code: %v", status.Code(err))
return nil, err return nil, err
case case
codes.Aborted, codes.Aborted,
@ -105,10 +120,11 @@ func (c *client) Next(ctx context.Context, f rpc.Filter) (*rpc.Workflow, error)
// https://github.com/woodpecker-ci/woodpecker/issues/717#issuecomment-1049365104 // https://github.com/woodpecker-ci/woodpecker/issues/717#issuecomment-1049365104
log.Trace().Err(err).Msg("grpc: to many keepalive pings without sending data") log.Trace().Err(err).Msg("grpc: to many keepalive pings without sending data")
} else { } else {
log.Error().Err(err).Msgf("grpc error: next(): code: %v", status.Code(err)) log.Warn().Err(err).Msgf("grpc error: next(): code: %v", status.Code(err))
} }
default: default:
return nil, fmt.Errorf("grpc error: next(): code: %v: %w", status.Code(err), err) log.Error().Err(err).Msgf("grpc error: next(): code: %v", status.Code(err))
return nil, err
} }
select { select {
@ -143,9 +159,15 @@ func (c *client) Wait(ctx context.Context, workflowID string) (err error) {
break break
} }
log.Error().Err(err).Msgf("grpc error: wait(): code: %v", status.Code(err))
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: wait(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: wait(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -153,7 +175,9 @@ func (c *client) Wait(ctx context.Context, workflowID string) (err error) {
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: wait(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: wait(): code: %v", status.Code(err))
return err return err
} }
@ -184,6 +208,14 @@ func (c *client) Init(ctx context.Context, workflowID string, state rpc.Workflow
log.Error().Err(err).Msgf("grpc error: init(): code: %v", status.Code(err)) log.Error().Err(err).Msgf("grpc error: init(): code: %v", status.Code(err))
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: init(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: init(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -191,7 +223,9 @@ func (c *client) Init(ctx context.Context, workflowID string, state rpc.Workflow
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: init(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: init(): code: %v", status.Code(err))
return err return err
} }
@ -222,6 +256,14 @@ func (c *client) Done(ctx context.Context, workflowID string, state rpc.Workflow
log.Error().Err(err).Msgf("grpc error: done(): code: %v", status.Code(err)) log.Error().Err(err).Msgf("grpc error: done(): code: %v", status.Code(err))
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: done(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: done(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -229,7 +271,9 @@ func (c *client) Done(ctx context.Context, workflowID string, state rpc.Workflow
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: done(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: done(): code: %v", status.Code(err))
return err return err
} }
@ -256,6 +300,14 @@ func (c *client) Extend(ctx context.Context, workflowID string) (err error) {
log.Error().Err(err).Msgf("grpc error: extend(): code: %v", status.Code(err)) log.Error().Err(err).Msgf("grpc error: extend(): code: %v", status.Code(err))
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: extend(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: extend(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -263,7 +315,9 @@ func (c *client) Extend(ctx context.Context, workflowID string) (err error) {
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: extend(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: extend(): code: %v", status.Code(err))
return err return err
} }
@ -297,6 +351,14 @@ func (c *client) Update(ctx context.Context, workflowID string, state rpc.StepSt
log.Error().Err(err).Msgf("grpc error: update(): code: %v", status.Code(err)) log.Error().Err(err).Msgf("grpc error: update(): code: %v", status.Code(err))
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: update(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: update(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -304,7 +366,9 @@ func (c *client) Update(ctx context.Context, workflowID string, state rpc.StepSt
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: update(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: update(): code: %v", status.Code(err))
return err return err
} }
@ -317,25 +381,82 @@ func (c *client) Update(ctx context.Context, workflowID string, state rpc.StepSt
return nil return nil
} }
// Log writes the step log entry. // EnqueueLog queues the log entry to be written in a batch later.
func (c *client) Log(ctx context.Context, logEntry *rpc.LogEntry) (err error) { func (c *client) EnqueueLog(logEntry *rpc.LogEntry) {
retry := c.newBackOff() c.logs <- &proto.LogEntry{
req := new(proto.LogRequest) StepUuid: logEntry.StepUUID,
req.LogEntry = new(proto.LogEntry) Data: logEntry.Data,
req.LogEntry.StepUuid = logEntry.StepUUID Line: int32(logEntry.Line),
req.LogEntry.Data = logEntry.Data Time: logEntry.Time,
req.LogEntry.Line = int32(logEntry.Line) Type: int32(logEntry.Type),
req.LogEntry.Time = logEntry.Time }
req.LogEntry.Type = int32(logEntry.Type) }
func (c *client) processLogs(ctx context.Context) {
var entries []*proto.LogEntry
var bytes int
send := func() {
if len(entries) == 0 {
return
}
log.Debug().
Int("entries", len(entries)).
Int("bytes", bytes).
Msg("log drain: sending queued logs")
if err := c.sendLogs(ctx, entries); err != nil {
log.Error().Err(err).Msg("log drain: could not send logs to server")
}
// even if send failed, we don't have infinite memory; retry has already been used
entries = entries[:0]
bytes = 0
}
// ctx.Done() is covered by the log channel being closed
for { for {
_, err = c.client.Log(ctx, req) select {
case entry, ok := <-c.logs:
if !ok {
log.Info().Msg("log drain: channel closed")
send()
return
}
entries = append(entries, entry)
bytes += grpcproto.Size(entry) // cspell:words grpcproto
if bytes >= maxLogBatchSize {
send()
}
case <-time.After(maxLogFlushPeriod):
send()
}
}
}
func (c *client) sendLogs(ctx context.Context, entries []*proto.LogEntry) error {
req := &proto.LogRequest{LogEntries: entries}
retry := c.newBackOff()
for {
_, err := c.client.Log(ctx, req)
if err == nil { if err == nil {
break break
} }
log.Error().Err(err).Msgf("grpc error: log(): code: %v", status.Code(err))
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: log(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: log(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -343,7 +464,9 @@ func (c *client) Log(ctx context.Context, logEntry *rpc.LogEntry) (err error) {
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: log(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: log(): code: %v", status.Code(err))
return err return err
} }
@ -356,12 +479,15 @@ func (c *client) Log(ctx context.Context, logEntry *rpc.LogEntry) (err error) {
return nil return nil
} }
func (c *client) RegisterAgent(ctx context.Context, platform, backend, version string, capacity int) (int64, error) { func (c *client) RegisterAgent(ctx context.Context, info rpc.AgentInfo) (int64, error) {
req := new(proto.RegisterAgentRequest) req := new(proto.RegisterAgentRequest)
req.Platform = platform req.Info = &proto.AgentInfo{
req.Backend = backend Platform: info.Platform,
req.Version = version Backend: info.Backend,
req.Capacity = int32(capacity) Version: info.Version,
Capacity: int32(info.Capacity),
CustomLabels: info.CustomLabels,
}
res, err := c.client.RegisterAgent(ctx, req) res, err := c.client.RegisterAgent(ctx, req)
return res.GetAgentId(), err return res.GetAgentId(), err
@ -383,6 +509,14 @@ func (c *client) ReportHealth(ctx context.Context) (err error) {
return nil return nil
} }
switch status.Code(err) { switch status.Code(err) {
case codes.Canceled:
if ctx.Err() != nil {
// expected as context was canceled
log.Debug().Err(err).Msgf("grpc error: report_health(): context canceled")
return nil
}
log.Error().Err(err).Msgf("grpc error: report_health(): code: %v", status.Code(err))
return err
case case
codes.Aborted, codes.Aborted,
codes.DataLoss, codes.DataLoss,
@ -390,7 +524,9 @@ func (c *client) ReportHealth(ctx context.Context) (err error) {
codes.Internal, codes.Internal,
codes.Unavailable: codes.Unavailable:
// non-fatal errors // non-fatal errors
log.Warn().Err(err).Msgf("grpc error: report_health(): code: %v", status.Code(err))
default: default:
log.Error().Err(err).Msgf("grpc error: report_health(): code: %v", status.Code(err))
return err return err
} }

View file

@ -25,10 +25,11 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"go.woodpecker-ci.org/woodpecker/v2/pipeline" "go.woodpecker-ci.org/woodpecker/v3/pipeline"
backend "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" "go.woodpecker-ci.org/woodpecker/v3/pipeline/rpc"
"go.woodpecker-ci.org/woodpecker/v2/shared/utils" "go.woodpecker-ci.org/woodpecker/v3/shared/constant"
"go.woodpecker-ci.org/woodpecker/v3/shared/utils"
) )
type Runner struct { type Runner struct {
@ -49,7 +50,7 @@ func NewRunner(workEngine rpc.Peer, f rpc.Filter, h string, state *State, backen
} }
} }
func (r *Runner) Run(runnerCtx context.Context) error { //nolint:contextcheck func (r *Runner) Run(runnerCtx, shutdownCtx context.Context) error { //nolint:contextcheck
log.Debug().Msg("request next execution") log.Debug().Msg("request next execution")
meta, _ := metadata.FromOutgoingContext(runnerCtx) meta, _ := metadata.FromOutgoingContext(runnerCtx)
@ -118,7 +119,7 @@ func (r *Runner) Run(runnerCtx context.Context) error { //nolint:contextcheck
logger.Debug().Msg("pipeline done") logger.Debug().Msg("pipeline done")
return return
case <-time.After(time.Minute): case <-time.After(constant.TaskTimeout / 3):
logger.Debug().Msg("pipeline lease renewed") logger.Debug().Msg("pipeline lease renewed")
if err := r.client.Extend(workflowCtx, workflow.ID); err != nil { if err := r.client.Extend(workflowCtx, workflow.ID); err != nil {
log.Error().Err(err).Msg("extending pipeline deadline failed") log.Error().Err(err).Msg("extending pipeline deadline failed")
@ -176,7 +177,11 @@ func (r *Runner) Run(runnerCtx context.Context) error { //nolint:contextcheck
Str("error", state.Error). Str("error", state.Error).
Msg("updating workflow status") Msg("updating workflow status")
if err := r.client.Done(runnerCtx, workflow.ID, state); err != nil { doneCtx := runnerCtx
if doneCtx.Err() != nil {
doneCtx = shutdownCtx
}
if err := r.client.Done(doneCtx, workflow.ID, state); err != nil {
logger.Error().Err(err).Msg("updating workflow status failed") logger.Error().Err(err).Msg("updating workflow status failed")
} else { } else {
logger.Debug().Msg("updating workflow status complete") logger.Debug().Msg("updating workflow status complete")

View file

@ -23,8 +23,8 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.woodpecker-ci.org/woodpecker/v2/pipeline" "go.woodpecker-ci.org/woodpecker/v3/pipeline"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" "go.woodpecker-ci.org/woodpecker/v3/pipeline/rpc"
) )
func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup, logger zerolog.Logger, workflow *rpc.Workflow) pipeline.TraceFunc { func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup, logger zerolog.Logger, workflow *rpc.Workflow) pipeline.TraceFunc {
@ -74,21 +74,12 @@ func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup,
// TODO: find better way to update this state and move it to pipeline to have the same env in cli-exec // TODO: find better way to update this state and move it to pipeline to have the same env in cli-exec
state.Pipeline.Step.Environment["CI_MACHINE"] = r.hostname state.Pipeline.Step.Environment["CI_MACHINE"] = r.hostname
state.Pipeline.Step.Environment["CI_PIPELINE_STATUS"] = "success"
state.Pipeline.Step.Environment["CI_PIPELINE_STARTED"] = strconv.FormatInt(state.Pipeline.Started, 10) state.Pipeline.Step.Environment["CI_PIPELINE_STARTED"] = strconv.FormatInt(state.Pipeline.Started, 10)
state.Pipeline.Step.Environment["CI_PIPELINE_FINISHED"] = strconv.FormatInt(time.Now().Unix(), 10)
state.Pipeline.Step.Environment["CI_STEP_STATUS"] = "success"
state.Pipeline.Step.Environment["CI_STEP_STARTED"] = strconv.FormatInt(state.Pipeline.Started, 10) state.Pipeline.Step.Environment["CI_STEP_STARTED"] = strconv.FormatInt(state.Pipeline.Started, 10)
state.Pipeline.Step.Environment["CI_STEP_FINISHED"] = strconv.FormatInt(time.Now().Unix(), 10)
state.Pipeline.Step.Environment["CI_SYSTEM_PLATFORM"] = runtime.GOOS + "/" + runtime.GOARCH state.Pipeline.Step.Environment["CI_SYSTEM_PLATFORM"] = runtime.GOOS + "/" + runtime.GOARCH
if state.Pipeline.Error != nil {
state.Pipeline.Step.Environment["CI_PIPELINE_STATUS"] = "failure"
state.Pipeline.Step.Environment["CI_STEP_STATUS"] = "failure"
}
return nil return nil
} }
} }

View file

@ -15,16 +15,22 @@
package admin package admin
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/admin/registry" "go.woodpecker-ci.org/woodpecker/v3/cli/admin/loglevel"
"go.woodpecker-ci.org/woodpecker/v3/cli/admin/registry"
"go.woodpecker-ci.org/woodpecker/v3/cli/admin/secret"
"go.woodpecker-ci.org/woodpecker/v3/cli/admin/user"
) )
// Command exports the admin command set. // Command exports the admin command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "admin", Name: "admin",
Usage: "administer server settings", Usage: "manage server settings",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
loglevel.Command,
registry.Command, registry.Command,
secret.Command,
user.Command,
}, },
} }

View file

@ -15,24 +15,26 @@
package loglevel package loglevel
import ( import (
"context"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
// Command exports the log-level command used to change the servers log-level. // Command exports the log-level command used to change the servers log-level.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "log-level", Name: "log-level",
ArgsUsage: "[level]", ArgsUsage: "[level]",
Usage: "get the logging level of the server, or set it with [level]", Usage: "retrieve log level from server, or set it with [level]",
Action: logLevel, Action: logLevel,
} }
func logLevel(c *cli.Context) error { func logLevel(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -57,6 +59,6 @@ func logLevel(c *cli.Context) error {
} }
} }
log.Info().Msgf("logging level: %s", ll.Level) log.Info().Msgf("log level: %s", ll.Level)
return nil return nil
} }

View file

@ -15,18 +15,18 @@
package registry package registry
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// Command exports the registry command set. // Command exports the registry command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "registry", Name: "registry",
Usage: "manage global registries", Usage: "manage global registries",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
registryCreateCmd, registryCreateCmd,
registryDeleteCmd, registryDeleteCmd,
registryUpdateCmd,
registryInfoCmd,
registryListCmd, registryListCmd,
registryShowCmd,
registryUpdateCmd,
}, },
} }

View file

@ -15,18 +15,19 @@
package registry package registry
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var registryCreateCmd = &cli.Command{ var registryCreateCmd = &cli.Command{
Name: "add", Name: "add",
Usage: "adds a registry", Usage: "add a registry",
Action: registryCreate, Action: registryCreate,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
@ -45,14 +46,14 @@ var registryCreateCmd = &cli.Command{
}, },
} }
func registryCreate(c *cli.Context) error { func registryCreate(ctx context.Context, c *cli.Command) error {
var ( var (
hostname = c.String("hostname") hostname = c.String("hostname")
username = c.String("username") username = c.String("username")
password = c.String("password") password = c.String("password")
) )
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,13 +15,15 @@
package registry package registry
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var registryListCmd = &cli.Command{ var registryListCmd = &cli.Command{
@ -33,15 +35,17 @@ var registryListCmd = &cli.Command{
}, },
} }
func registryList(c *cli.Context) error { func registryList(ctx context.Context, c *cli.Command) error {
format := c.String("format") + "\n" format := c.String("format") + "\n"
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
list, err := client.GlobalRegistryList() opt := woodpecker.RegistryListOptions{}
list, err := client.GlobalRegistryList(opt)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,9 +15,11 @@
package registry package registry
import ( import (
"github.com/urfave/cli/v2" "context"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var registryDeleteCmd = &cli.Command{ var registryDeleteCmd = &cli.Command{
@ -33,10 +35,10 @@ var registryDeleteCmd = &cli.Command{
}, },
} }
func registryDelete(c *cli.Context) error { func registryDelete(ctx context.Context, c *cli.Command) error {
hostname := c.String("hostname") hostname := c.String("hostname")
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,14 +15,15 @@
package registry package registry
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var registryUpdateCmd = &cli.Command{ var registryUpdateCmd = &cli.Command{
@ -47,14 +48,14 @@ var registryUpdateCmd = &cli.Command{
}, },
} }
func registryUpdate(c *cli.Context) error { func registryUpdate(ctx context.Context, c *cli.Command) error {
var ( var (
hostname = c.String("hostname") hostname = c.String("hostname")
username = c.String("username") username = c.String("username")
password = c.String("password") password = c.String("password")
) )
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,19 +15,20 @@
package registry package registry
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var registryInfoCmd = &cli.Command{ var registryShowCmd = &cli.Command{
Name: "info", Name: "show",
Usage: "display registry info", Usage: "show registry information",
Action: registryInfo, Action: registryShow,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "hostname", Name: "hostname",
@ -38,13 +39,13 @@ var registryInfoCmd = &cli.Command{
}, },
} }
func registryInfo(c *cli.Context) error { func registryShow(ctx context.Context, c *cli.Command) error {
var ( var (
hostname = c.String("hostname") hostname = c.String("hostname")
format = c.String("format") + "\n" format = c.String("format") + "\n"
) )
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,10 +1,10 @@
// Copyright 2021 Woodpecker Authors // Copyright 2023 Woodpecker Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
// //
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
// //
// Unless required by applicable law or agreed to in writing, software // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
@ -12,16 +12,21 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package migration package secret
import ( import (
"src.techknowlogick.com/xormigrate" "github.com/urfave/cli/v3"
"xorm.io/xorm"
) )
var alterTableReposDropCounter = xormigrate.Migration{ // Command exports the secret command.
ID: "alter-table-drop-counter", var Command = &cli.Command{
MigrateSession: func(sess *xorm.Session) error { Name: "secret",
return dropTableColumns(sess, "repos", "repo_counter") Usage: "manage global secrets",
Commands: []*cli.Command{
secretCreateCmd,
secretDeleteCmd,
secretListCmd,
secretShowCmd,
secretUpdateCmd,
}, },
} }

View file

@ -0,0 +1,82 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"os"
"strings"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
var secretCreateCmd = &cli.Command{
Name: "add",
Usage: "add a secret",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretCreate,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "value",
Usage: "secret value",
},
&cli.StringSliceFlag{
Name: "event",
Usage: "secret limited to these events",
},
&cli.StringSliceFlag{
Name: "image",
Usage: "secret limited to these images",
},
},
}
func secretCreate(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
secret := &woodpecker.Secret{
Name: strings.ToLower(c.String("name")),
Value: c.String("value"),
Images: c.StringSlice("image"),
Events: c.StringSlice("event"),
}
if len(secret.Events) == 0 {
secret.Events = defaultSecretEvents
}
if strings.HasPrefix(secret.Value, "@") {
path := strings.TrimPrefix(secret.Value, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
secret.Value = string(out)
}
_, err = client.GlobalSecretCreate(secret)
return err
}
var defaultSecretEvents = []string{
woodpecker.EventPush,
woodpecker.EventTag,
woodpecker.EventRelease,
woodpecker.EventDeploy,
}

View file

@ -15,15 +15,16 @@
package secret package secret
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var secretListCmd = &cli.Command{ var secretListCmd = &cli.Command{
@ -32,48 +33,25 @@ var secretListCmd = &cli.Command{
ArgsUsage: "[repo-id|repo-full-name]", ArgsUsage: "[repo-id|repo-full-name]",
Action: secretList, Action: secretList,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{
Name: "global",
Usage: "global secret",
},
common.OrgFlag,
common.RepoFlag,
common.FormatFlag(tmplSecretList, true), common.FormatFlag(tmplSecretList, true),
}, },
} }
func secretList(c *cli.Context) error { func secretList(ctx context.Context, c *cli.Command) error {
format := c.String("format") + "\n" format := c.String("format") + "\n"
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
global, orgID, repoID, err := parseTargetArgs(client, c) opt := woodpecker.SecretListOptions{}
list, err := client.GlobalSecretList(opt)
if err != nil { if err != nil {
return err return err
} }
var list []*woodpecker.Secret
switch {
case global:
list, err = client.GlobalSecretList()
if err != nil {
return err
}
case orgID != -1:
list, err = client.OrgSecretList(orgID)
if err != nil {
return err
}
default:
list, err = client.SecretList(repoID)
if err != nil {
return err
}
}
tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(format) tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(format)
if err != nil { if err != nil {
return err return err

View file

@ -0,0 +1,47 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
)
var secretDeleteCmd = &cli.Command{
Name: "rm",
Usage: "remove a secret",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretDelete,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "secret name",
},
},
}
func secretDelete(ctx context.Context, c *cli.Command) error {
secretName := c.String("name")
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
return client.GlobalSecretDelete(secretName)
}

View file

@ -0,0 +1,76 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"os"
"strings"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
var secretUpdateCmd = &cli.Command{
Name: "update",
Usage: "update a secret",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretUpdate,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "secret name",
},
&cli.StringFlag{
Name: "value",
Usage: "secret value",
},
&cli.StringSliceFlag{
Name: "event",
Usage: "secret limited to these events",
},
&cli.StringSliceFlag{
Name: "image",
Usage: "secret limited to these images",
},
},
}
func secretUpdate(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
secret := &woodpecker.Secret{
Name: strings.ToLower(c.String("name")),
Value: c.String("value"),
Images: c.StringSlice("image"),
Events: c.StringSlice("event"),
}
if strings.HasPrefix(secret.Value, "@") {
path := strings.TrimPrefix(secret.Value, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
secret.Value = string(out)
}
_, err = client.GlobalSecretUpdate(secret)
return err
}

View file

@ -0,0 +1,68 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"fmt"
"html/template"
"os"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
)
var secretShowCmd = &cli.Command{
Name: "show",
Usage: "show secret information",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretShow,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "secret name",
},
common.FormatFlag(tmplSecretList, true),
},
}
func secretShow(ctx context.Context, c *cli.Command) error {
var (
secretName = c.String("name")
format = c.String("format") + "\n"
)
if secretName == "" {
return fmt.Errorf("secret name is missing")
}
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
secret, err := client.GlobalSecret(secretName)
if err != nil {
return err
}
tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(format)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, secret)
}

View file

@ -15,17 +15,17 @@
package user package user
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// Command exports the user command set. // Command exports the user command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "user", Name: "user",
Usage: "manage users", Usage: "manage users",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
userListCmd,
userInfoCmd,
userAddCmd, userAddCmd,
userListCmd,
userRemoveCmd, userRemoveCmd,
userShowCmd,
}, },
} }

View file

@ -15,25 +15,26 @@
package user package user
import ( import (
"context"
"fmt" "fmt"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var userAddCmd = &cli.Command{ var userAddCmd = &cli.Command{
Name: "add", Name: "add",
Usage: "adds a user", Usage: "add a user",
ArgsUsage: "<username>", ArgsUsage: "<username>",
Action: userAdd, Action: userAdd,
} }
func userAdd(c *cli.Context) error { func userAdd(ctx context.Context, c *cli.Command) error {
login := c.Args().First() login := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,13 +15,15 @@
package user package user
import ( import (
"context"
"os" "os"
"text/template" "text/template"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var userListCmd = &cli.Command{ var userListCmd = &cli.Command{
@ -32,13 +34,15 @@ var userListCmd = &cli.Command{
Flags: []cli.Flag{common.FormatFlag(tmplUserList)}, Flags: []cli.Flag{common.FormatFlag(tmplUserList)},
} }
func userList(c *cli.Context) error { func userList(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
users, err := client.UserList() opt := woodpecker.UserListOptions{}
users, err := client.UserList(opt)
if err != nil || len(users) == 0 { if err != nil || len(users) == 0 {
return err return err
} }

View file

@ -15,11 +15,12 @@
package user package user
import ( import (
"context"
"fmt" "fmt"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var userRemoveCmd = &cli.Command{ var userRemoveCmd = &cli.Command{
@ -29,10 +30,10 @@ var userRemoveCmd = &cli.Command{
Action: userRemove, Action: userRemove,
} }
func userRemove(c *cli.Context) error { func userRemove(ctx context.Context, c *cli.Command) error {
login := c.Args().First() login := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,26 +15,27 @@
package user package user
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"text/template" "text/template"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var userInfoCmd = &cli.Command{ var userShowCmd = &cli.Command{
Name: "info", Name: "show",
Usage: "show user details", Usage: "show user information",
ArgsUsage: "<username>", ArgsUsage: "<username>",
Action: userInfo, Action: userShow,
Flags: []cli.Flag{common.FormatFlag(tmplUserInfo)}, Flags: []cli.Flag{common.FormatFlag(tmplUserInfo)},
} }
func userInfo(c *cli.Context) error { func userShow(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,52 +15,49 @@
package common package common
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/shared/logger" "go.woodpecker-ci.org/woodpecker/v3/shared/logger"
) )
var GlobalFlags = append([]cli.Flag{ var GlobalFlags = append([]cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_CONFIG"}, Sources: cli.EnvVars("WOODPECKER_CONFIG"),
Name: "config", Name: "config",
Aliases: []string{"c"}, Aliases: []string{"c"},
Usage: "path to config file", Usage: "path to config file",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_SERVER"}, Sources: cli.EnvVars("WOODPECKER_SERVER"),
Name: "server", Name: "server",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "server address", Usage: "server address",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_TOKEN"}, Sources: cli.EnvVars("WOODPECKER_TOKEN"),
Name: "token", Name: "token",
Aliases: []string{"t"}, Aliases: []string{"t"},
Usage: "server auth token", Usage: "server auth token",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
EnvVars: []string{"WOODPECKER_DISABLE_UPDATE_CHECK"}, Sources: cli.EnvVars("WOODPECKER_DISABLE_UPDATE_CHECK"),
Name: "disable-update-check", Name: "disable-update-check",
Usage: "disable update check", Usage: "disable update check",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
EnvVars: []string{"WOODPECKER_SKIP_VERIFY"}, Sources: cli.EnvVars("WOODPECKER_SKIP_VERIFY"),
Name: "skip-verify", Name: "skip-verify",
Usage: "skip ssl verification", Usage: "skip ssl verification",
Hidden: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"SOCKS_PROXY"}, Sources: cli.EnvVars("SOCKS_PROXY"),
Name: "socks-proxy", Name: "socks-proxy",
Usage: "socks proxy address", Usage: "socks proxy address",
Hidden: true,
}, },
&cli.BoolFlag{ &cli.BoolFlag{
EnvVars: []string{"SOCKS_PROXY_OFF"}, Sources: cli.EnvVars("SOCKS_PROXY_OFF"),
Name: "socks-proxy-off", Name: "socks-proxy-off",
Usage: "socks proxy ignored", Usage: "socks proxy ignored",
Hidden: true,
}, },
}, logger.GlobalLoggerFlags...) }, logger.GlobalLoggerFlags...)

View file

@ -6,10 +6,10 @@ import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal/config" "go.woodpecker-ci.org/woodpecker/v3/cli/internal/config"
"go.woodpecker-ci.org/woodpecker/v2/cli/update" "go.woodpecker-ci.org/woodpecker/v3/cli/update"
) )
var ( var (
@ -17,12 +17,12 @@ var (
cancelWaitForUpdate context.CancelCauseFunc cancelWaitForUpdate context.CancelCauseFunc
) )
func Before(c *cli.Context) error { func Before(ctx context.Context, c *cli.Command) (context.Context, error) {
if err := setupGlobalLogger(c); err != nil { if err := setupGlobalLogger(ctx, c); err != nil {
return err return ctx, err
} }
go func() { go func(context.Context) {
if c.Bool("disable-update-check") { if c.Bool("disable-update-check") {
return return
} }
@ -35,31 +35,31 @@ func Before(c *cli.Context) error {
waitForUpdateCheck, cancelWaitForUpdate = context.WithCancelCause(context.Background()) waitForUpdateCheck, cancelWaitForUpdate = context.WithCancelCause(context.Background())
defer cancelWaitForUpdate(errors.New("update check finished")) defer cancelWaitForUpdate(errors.New("update check finished"))
log.Debug().Msg("Checking for updates ...") log.Debug().Msg("checking for updates ...")
newVersion, err := update.CheckForUpdate(waitForUpdateCheck, false) newVersion, err := update.CheckForUpdate(waitForUpdateCheck, false) //nolint:contextcheck
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Failed to check for updates") log.Error().Err(err).Msgf("failed to check for updates")
return return
} }
if newVersion != nil { if newVersion != nil {
log.Warn().Msgf("A new version of woodpecker-cli is available: %s. Update by running: %s update", newVersion.Version, c.App.Name) log.Warn().Msgf("new version of woodpecker-cli is available: %s, update with: %s update", newVersion.Version, c.Root().Name)
} else { } else {
log.Debug().Msgf("No update required") log.Debug().Msgf("no update required")
} }
}() }(ctx)
return config.Load(c) return ctx, config.Load(ctx, c)
} }
func After(_ *cli.Context) error { func After(_ context.Context, _ *cli.Command) error {
if waitForUpdateCheck != nil { if waitForUpdateCheck != nil {
select { select {
case <-waitForUpdateCheck.Done(): case <-waitForUpdateCheck.Done():
// When the actual command already finished, we still wait 500ms for the update check to finish // When the actual command already finished, we still wait 500ms for the update check to finish
case <-time.After(time.Millisecond * 500): case <-time.After(time.Millisecond * 500):
log.Debug().Msg("Update check stopped due to timeout") log.Debug().Msg("update check stopped due to timeout")
cancelWaitForUpdate(errors.New("update check timeout")) cancelWaitForUpdate(errors.New("update check timeout"))
} }
} }

View file

@ -15,13 +15,14 @@
package common package common
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant" "go.woodpecker-ci.org/woodpecker/v3/shared/constant"
) )
func DetectPipelineConfig() (isDir bool, config string, _ error) { func DetectPipelineConfig() (isDir bool, config string, _ error) {
@ -37,16 +38,16 @@ func DetectPipelineConfig() (isDir bool, config string, _ error) {
return false, "", fmt.Errorf("could not detect pipeline config") return false, "", fmt.Errorf("could not detect pipeline config")
} }
func RunPipelineFunc(c *cli.Context, fileFunc, dirFunc func(*cli.Context, string) error) error { func RunPipelineFunc(ctx context.Context, c *cli.Command, fileFunc, dirFunc func(context.Context, *cli.Command, string) error) error {
if c.Args().Len() == 0 { if c.Args().Len() == 0 {
isDir, path, err := DetectPipelineConfig() isDir, path, err := DetectPipelineConfig()
if err != nil { if err != nil {
return err return err
} }
if isDir { if isDir {
return dirFunc(c, path) return dirFunc(ctx, c, path)
} }
return fileFunc(c, path) return fileFunc(ctx, c, path)
} }
multiArgs := c.Args().Len() > 1 multiArgs := c.Args().Len() > 1
@ -59,11 +60,11 @@ func RunPipelineFunc(c *cli.Context, fileFunc, dirFunc func(*cli.Context, string
fmt.Println("#", fi.Name()) fmt.Println("#", fi.Name())
} }
if fi.IsDir() { if fi.IsDir() {
if err := dirFunc(c, arg); err != nil { if err := dirFunc(ctx, c, arg); err != nil {
return err return err
} }
} else { } else {
if err := fileFunc(c, arg); err != nil { if err := fileFunc(ctx, c, arg); err != nil {
return err return err
} }
} }

View file

@ -15,11 +15,13 @@
package common package common
import ( import (
"github.com/urfave/cli/v2" "context"
"go.woodpecker-ci.org/woodpecker/v2/shared/logger" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/shared/logger"
) )
func setupGlobalLogger(c *cli.Context) error { func setupGlobalLogger(ctx context.Context, c *cli.Command) error {
return logger.SetupGlobalLogger(c, false) return logger.SetupGlobalLogger(ctx, c, false)
} }

View file

@ -17,7 +17,7 @@
package exec package exec
import "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/dummy" import "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/dummy"
func init() { //nolint:gochecknoinits func init() { //nolint:gochecknoinits
backends = append(backends, dummy.New()) backends = append(backends, dummy.New())

View file

@ -25,23 +25,26 @@ import (
"strings" "strings"
"github.com/drone/envsubst" "github.com/drone/envsubst"
"github.com/oklog/ulid/v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/lint" "go.woodpecker-ci.org/woodpecker/v3/cli/lint"
"go.woodpecker-ci.org/woodpecker/v2/pipeline" "go.woodpecker-ci.org/woodpecker/v3/pipeline"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend" "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/docker" "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/docker"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/kubernetes" "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/kubernetes"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/local" "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/local"
backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" backend_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/compiler"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter"
pipelineLog "go.woodpecker-ci.org/woodpecker/v2/pipeline/log" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/matrix"
"go.woodpecker-ci.org/woodpecker/v2/shared/utils" pipelineLog "go.woodpecker-ci.org/woodpecker/v3/pipeline/log"
"go.woodpecker-ci.org/woodpecker/v3/shared/constant"
"go.woodpecker-ci.org/woodpecker/v3/shared/utils"
) )
// Command exports the exec command. // Command exports the exec command.
@ -59,11 +62,11 @@ var backends = []backend_types.Backend{
local.New(), local.New(),
} }
func run(c *cli.Context) error { func run(ctx context.Context, c *cli.Command) error {
return common.RunPipelineFunc(c, execFile, execDir) return common.RunPipelineFunc(ctx, c, execFile, execDir)
} }
func execDir(c *cli.Context, dir string) error { func execDir(ctx context.Context, c *cli.Command, dir string) error {
// TODO: respect pipeline dependency // TODO: respect pipeline dependency
repoPath := c.String("repo-path") repoPath := c.String("repo-path")
if repoPath != "" { if repoPath != "" {
@ -74,6 +77,7 @@ func execDir(c *cli.Context, dir string) error {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
repoPath = convertPathForWindows(repoPath) repoPath = convertPathForWindows(repoPath)
} }
// TODO: respect depends_on and do parallel runs with output to multiple _windows_ e.g. tmux like
return filepath.Walk(dir, func(path string, info os.FileInfo, e error) error { return filepath.Walk(dir, func(path string, info os.FileInfo, e error) error {
if e != nil { if e != nil {
return e return e
@ -82,7 +86,7 @@ func execDir(c *cli.Context, dir string) error {
// check if it is a regular file (not dir) // check if it is a regular file (not dir)
if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) { if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) {
fmt.Println("#", info.Name()) fmt.Println("#", info.Name())
_ = runExec(c, path, repoPath) // TODO: should we drop errors or store them and report back? _ = runExec(ctx, c, path, repoPath, false) // TODO: should we drop errors or store them and report back?
fmt.Println("") fmt.Println("")
return nil return nil
} }
@ -91,7 +95,7 @@ func execDir(c *cli.Context, dir string) error {
}) })
} }
func execFile(c *cli.Context, file string) error { func execFile(ctx context.Context, c *cli.Command, file string) error {
repoPath := c.String("repo-path") repoPath := c.String("repo-path")
if repoPath != "" { if repoPath != "" {
repoPath, _ = filepath.Abs(repoPath) repoPath, _ = filepath.Abs(repoPath)
@ -101,10 +105,10 @@ func execFile(c *cli.Context, file string) error {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
repoPath = convertPathForWindows(repoPath) repoPath = convertPathForWindows(repoPath)
} }
return runExec(c, file, repoPath) return runExec(ctx, c, file, repoPath, true)
} }
func runExec(c *cli.Context, file, repoPath string) error { func runExec(ctx context.Context, c *cli.Command, file, repoPath string, singleExec bool) error {
dat, err := os.ReadFile(file) dat, err := os.ReadFile(file)
if err != nil { if err != nil {
return err return err
@ -119,7 +123,7 @@ func runExec(c *cli.Context, file, repoPath string) error {
axes = append(axes, matrix.Axis{}) axes = append(axes, matrix.Axis{})
} }
for _, axis := range axes { for _, axis := range axes {
err := execWithAxis(c, file, repoPath, axis) err := execWithAxis(ctx, c, file, repoPath, axis, singleExec)
if err != nil { if err != nil {
return err return err
} }
@ -127,8 +131,20 @@ func runExec(c *cli.Context, file, repoPath string) error {
return nil return nil
} }
func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error { func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, axis matrix.Axis, singleExec bool) error {
metadata := metadataFromContext(c, axis) metadataWorkflow := &metadata.Workflow{}
if !singleExec {
// TODO: proper try to use the engine to generate the same metadata for workflows
// https://github.com/woodpecker-ci/woodpecker/pull/3967
metadataWorkflow.Name = strings.TrimSuffix(strings.TrimSuffix(file, ".yaml"), ".yml")
}
metadata, err := metadataFromContext(ctx, c, axis, metadataWorkflow)
if err != nil {
return fmt.Errorf("could not create metadata: %w", err)
} else if metadata == nil {
return fmt.Errorf("metadata is nil")
}
environ := metadata.Environ() environ := metadata.Environ()
var secrets []compiler.Secret var secrets []compiler.Secret
for key, val := range metadata.Workflow.Matrix { for key, val := range metadata.Workflow.Matrix {
@ -166,6 +182,9 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
return err return err
} }
// emulate server behavior https://github.com/woodpecker-ci/woodpecker/blob/eebaa10d104cbc3fa7ce4c0e344b0b7978405135/server/pipeline/stepbuilder/stepBuilder.go#L289-L295
prefix := "wp_" + ulid.Make().String()
// configure volumes for local execution // configure volumes for local execution
volumes := c.StringSlice("volumes") volumes := c.StringSlice("volumes")
if c.Bool("local") { if c.Bool("local") {
@ -180,18 +199,28 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
workspacePath = c.String("workspace-path") workspacePath = c.String("workspace-path")
} }
volumes = append(volumes, c.String("prefix")+"_default:"+workspaceBase) volumes = append(volumes, prefix+"_default:"+workspaceBase)
volumes = append(volumes, repoPath+":"+path.Join(workspaceBase, workspacePath)) volumes = append(volumes, repoPath+":"+path.Join(workspaceBase, workspacePath))
} }
privilegedPlugins := c.StringSlice("plugins-privileged")
// lint the yaml file // lint the yaml file
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{{ err = linter.New(
linter.WithTrusted(linter.TrustedConfiguration{
Security: c.Bool("repo-trusted-security"),
Network: c.Bool("repo-trusted-network"),
Volumes: c.Bool("repo-trusted-volumes"),
}),
linter.PrivilegedPlugins(privilegedPlugins),
linter.WithTrustedClonePlugins(constant.TrustedClonePlugins),
).Lint([]*linter.WorkflowConfig{{
File: path.Base(file), File: path.Base(file),
RawConfig: confStr, RawConfig: confStr,
Workflow: conf, Workflow: conf,
}}) }})
if err != nil { if err != nil {
str, err := lint.FormatLintError(file, err) str, err := lint.FormatLintError(file, err, false)
fmt.Print(str) fmt.Print(str)
if err != nil { if err != nil {
return err return err
@ -201,7 +230,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
// compiles the yaml file // compiles the yaml file
compiled, err := compiler.New( compiled, err := compiler.New(
compiler.WithEscalated( compiler.WithEscalated(
c.StringSlice("privileged")..., privilegedPlugins...,
), ),
compiler.WithVolumes(volumes...), compiler.WithVolumes(volumes...),
compiler.WithWorkspace( compiler.WithWorkspace(
@ -211,9 +240,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
compiler.WithNetworks( compiler.WithNetworks(
c.StringSlice("network")..., c.StringSlice("network")...,
), ),
compiler.WithPrefix( compiler.WithPrefix(prefix),
c.String("prefix"),
),
compiler.WithProxy(compiler.ProxyOptions{ compiler.WithProxy(compiler.ProxyOptions{
NoProxy: c.String("backend-no-proxy"), NoProxy: c.String("backend-no-proxy"),
HTTPProxy: c.String("backend-http-proxy"), HTTPProxy: c.String("backend-http-proxy"),
@ -227,7 +254,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
c.String("netrc-password"), c.String("netrc-password"),
c.String("netrc-machine"), c.String("netrc-machine"),
), ),
compiler.WithMetadata(metadata), compiler.WithMetadata(*metadata),
compiler.WithSecret(secrets...), compiler.WithSecret(secrets...),
compiler.WithEnviron(pipelineEnv), compiler.WithEnviron(pipelineEnv),
).Compile(conf) ).Compile(conf)
@ -235,7 +262,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
return err return err
} }
backendCtx := context.WithValue(c.Context, backend_types.CliContext, c) backendCtx := context.WithValue(ctx, backend_types.CliCommand, c)
backendEngine, err := backend.FindBackend(backendCtx, backends, c.String("backend-engine")) backendEngine, err := backend.FindBackend(backendCtx, backends, c.String("backend-engine"))
if err != nil { if err != nil {
return err return err
@ -245,21 +272,21 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), c.Duration("timeout")) pipelineCtx, cancel := context.WithTimeout(context.Background(), c.Duration("timeout"))
defer cancel() defer cancel()
ctx = utils.WithContextSigtermCallback(ctx, func() { pipelineCtx = utils.WithContextSigtermCallback(pipelineCtx, func() {
fmt.Println("ctrl+c received, terminating process") fmt.Printf("ctrl+c received, terminating current pipeline '%s'\n", confStr)
}) })
return pipeline.New(compiled, return pipeline.New(compiled,
pipeline.WithContext(ctx), pipeline.WithContext(pipelineCtx), //nolint:contextcheck
pipeline.WithTracer(pipeline.DefaultTracer), pipeline.WithTracer(pipeline.DefaultTracer),
pipeline.WithLogger(defaultLogger), pipeline.WithLogger(defaultLogger),
pipeline.WithBackend(backendEngine), pipeline.WithBackend(backendEngine),
pipeline.WithDescription(map[string]string{ pipeline.WithDescription(map[string]string{
"CLI": "exec", "CLI": "exec",
}), }),
).Run(c.Context) ).Run(ctx)
} }
// convertPathForWindows converts a path to use slash separators // convertPathForWindows converts a path to use slash separators
@ -281,8 +308,7 @@ func convertPathForWindows(path string) string {
return filepath.ToSlash(path) return filepath.ToSlash(path)
} }
const maxLogLineLength = 1024 * 1024 // 1mb
var defaultLogger = pipeline.Logger(func(step *backend_types.Step, rc io.ReadCloser) error { var defaultLogger = pipeline.Logger(func(step *backend_types.Step, rc io.ReadCloser) error {
logWriter := NewLineWriter(step.Name, step.UUID) logWriter := NewLineWriter(step.Name, step.UUID)
return pipelineLog.CopyLineByLine(logWriter, rc, maxLogLineLength) return pipelineLog.CopyLineByLine(logWriter, rc, pipeline.MaxLogLineLength)
}) })

View file

@ -17,53 +17,49 @@ package exec
import ( import (
"time" "time"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
) )
var flags = []cli.Flag{ var flags = []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
EnvVars: []string{"WOODPECKER_LOCAL"}, Sources: cli.EnvVars("WOODPECKER_LOCAL"),
Name: "local", Name: "local",
Usage: "run from local directory", Usage: "run from local directory",
Value: true, Value: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_REPO_PATH"}, Sources: cli.EnvVars("WOODPECKER_REPO_PATH"),
Name: "repo-path", Name: "repo-path",
Usage: "path to local repository", Usage: "path to local repository",
}, },
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_METADATA_FILE"),
Name: "metadata-file",
Usage: "path to pipeline metadata file (normally downloaded from UI). Parameters can be adjusted by applying additional cli flags",
},
&cli.DurationFlag{ &cli.DurationFlag{
EnvVars: []string{"WOODPECKER_TIMEOUT"}, Sources: cli.EnvVars("WOODPECKER_TIMEOUT"),
Name: "timeout", Name: "timeout",
Usage: "pipeline timeout", Usage: "pipeline timeout",
Value: time.Hour, Value: time.Hour,
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_VOLUMES"}, Sources: cli.EnvVars("WOODPECKER_VOLUMES"),
Name: "volumes", Name: "volumes",
Usage: "pipeline volumes", Usage: "pipeline volumes",
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_NETWORKS"}, Sources: cli.EnvVars("WOODPECKER_NETWORKS"),
Name: "network", Name: "network",
Usage: "external networks", Usage: "external networks",
}, },
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_PREFIX"},
Name: "prefix",
Value: "woodpecker",
Usage: "prefix used for containers, volumes, networks, ... created by woodpecker",
Hidden: true,
},
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "privileged", Sources: cli.EnvVars("WOODPECKER_PLUGINS_PRIVILEGED"),
Usage: "privileged plugins", Name: "plugins-privileged",
Value: cli.NewStringSlice(constant.PrivilegedPlugins...), Usage: "Allow plugins to run in privileged mode, if environment variable is defined but empty there will be none",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_BACKEND"}, Sources: cli.EnvVars("WOODPECKER_BACKEND"),
Name: "backend-engine", Name: "backend-engine",
Usage: "backend engine to run pipelines on", Usage: "backend engine to run pipelines on",
Value: "auto-detect", Value: "auto-detect",
@ -73,17 +69,17 @@ var flags = []cli.Flag{
// backend options for pipeline compiler // backend options for pipeline compiler
// //
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_BACKEND_NO_PROXY", "NO_PROXY", "no_proxy"}, Sources: cli.EnvVars("WOODPECKER_BACKEND_NO_PROXY", "NO_PROXY", "no_proxy"),
Usage: "if set, pass the environment variable down as \"NO_PROXY\" to steps", Usage: "if set, pass the environment variable down as \"NO_PROXY\" to steps",
Name: "backend-no-proxy", Name: "backend-no-proxy",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_BACKEND_HTTP_PROXY", "HTTP_PROXY", "http_proxy"}, Sources: cli.EnvVars("WOODPECKER_BACKEND_HTTP_PROXY", "HTTP_PROXY", "http_proxy"),
Usage: "if set, pass the environment variable down as \"HTTP_PROXY\" to steps", Usage: "if set, pass the environment variable down as \"HTTP_PROXY\" to steps",
Name: "backend-http-proxy", Name: "backend-http-proxy",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_BACKEND_HTTPS_PROXY", "HTTPS_PROXY", "https_proxy"}, Sources: cli.EnvVars("WOODPECKER_BACKEND_HTTPS_PROXY", "HTTPS_PROXY", "https_proxy"),
Usage: "if set, pass the environment variable down as \"HTTPS_PROXY\" to steps", Usage: "if set, pass the environment variable down as \"HTTPS_PROXY\" to steps",
Name: "backend-https-proxy", Name: "backend-https-proxy",
}, },
@ -97,12 +93,12 @@ var flags = []cli.Flag{
// workspace default // workspace default
// //
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_WORKSPACE_BASE"}, Sources: cli.EnvVars("CI_WORKSPACE_BASE"),
Name: "workspace-base", Name: "workspace-base",
Value: "/woodpecker", Value: "/woodpecker",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_WORKSPACE_PATH"}, Sources: cli.EnvVars("CI_WORKSPACE_PATH"),
Name: "workspace-path", Name: "workspace-path",
Value: "src", Value: "src",
}, },
@ -110,218 +106,298 @@ var flags = []cli.Flag{
// netrc parameters // netrc parameters
// //
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_NETRC_USERNAME"}, Sources: cli.EnvVars("CI_NETRC_USERNAME"),
Name: "netrc-username", Name: "netrc-username",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_NETRC_PASSWORD"}, Sources: cli.EnvVars("CI_NETRC_PASSWORD"),
Name: "netrc-password", Name: "netrc-password",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_NETRC_MACHINE"}, Sources: cli.EnvVars("CI_NETRC_MACHINE"),
Name: "netrc-machine", Name: "netrc-machine",
}, },
// //
// metadata parameters // metadata parameters
// //
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_SYSTEM_PLATFORM"}, Sources: cli.EnvVars("CI_SYSTEM_PLATFORM"),
Name: "system-platform", Name: "system-platform",
Usage: "Set the metadata environment variable \"CI_SYSTEM_PLATFORM\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_SYSTEM_NAME"}, Sources: cli.EnvVars("CI_SYSTEM_HOST"),
Name: "system-host",
Usage: "Set the metadata environment variable \"CI_SYSTEM_HOST\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_SYSTEM_NAME"),
Name: "system-name", Name: "system-name",
Usage: "Set the metadata environment variable \"CI_SYSTEM_NAME\".",
Value: "woodpecker", Value: "woodpecker",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_SYSTEM_URL"}, Sources: cli.EnvVars("CI_SYSTEM_URL"),
Name: "system-url", Name: "system-url",
Usage: "Set the metadata environment variable \"CI_SYSTEM_URL\".",
Value: "https://github.com/woodpecker-ci/woodpecker", Value: "https://github.com/woodpecker-ci/woodpecker",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_REPO"}, Sources: cli.EnvVars("CI_REPO"),
Name: "repo", Name: "repo",
Usage: "full repo name", Usage: "Set the full name to derive metadata environment variables \"CI_REPO\", \"CI_REPO_NAME\" and \"CI_REPO_OWNER\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_REPO_REMOTE_ID"}, Sources: cli.EnvVars("CI_REPO_REMOTE_ID"),
Name: "repo-remote-id", Name: "repo-remote-id",
Usage: "Set the metadata environment variable \"CI_REPO_REMOTE_ID\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_REPO_URL"}, Sources: cli.EnvVars("CI_REPO_URL"),
Name: "repo-url", Name: "repo-url",
Usage: "Set the metadata environment variable \"CI_REPO_URL\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_REPO_CLONE_URL"}, Sources: cli.EnvVars("CI_REPO_DEFAULT_BRANCH"),
Name: "repo-default-branch",
Usage: "Set the metadata environment variable \"CI_REPO_DEFAULT_BRANCH\".",
Value: "main",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_REPO_CLONE_URL"),
Name: "repo-clone-url", Name: "repo-clone-url",
Usage: "Set the metadata environment variable \"CI_REPO_CLONE_URL\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_REPO_CLONE_SSH_URL"}, Sources: cli.EnvVars("CI_REPO_CLONE_SSH_URL"),
Name: "repo-clone-ssh-url", Name: "repo-clone-ssh-url",
Usage: "Set the metadata environment variable \"CI_REPO_CLONE_SSH_URL\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_REPO_PRIVATE"}, Sources: cli.EnvVars("CI_REPO_PRIVATE"),
Name: "repo-private", Name: "repo-private",
Usage: "Set the metadata environment variable \"CI_REPO_PRIVATE\".",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
EnvVars: []string{"CI_REPO_TRUSTED"}, Sources: cli.EnvVars("CI_REPO_TRUSTED_NETWORK"),
Name: "repo-trusted", Name: "repo-trusted-network",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED_NETWORK\".",
},
&cli.BoolFlag{
Sources: cli.EnvVars("CI_REPO_TRUSTED_VOLUMES"),
Name: "repo-trusted-volumes",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED_VOLUMES\".",
},
&cli.BoolFlag{
Sources: cli.EnvVars("CI_REPO_TRUSTED_SECURITY"),
Name: "repo-trusted-security",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED_SECURITY\".",
}, },
&cli.IntFlag{ &cli.IntFlag{
EnvVars: []string{"CI_PIPELINE_NUMBER"}, Sources: cli.EnvVars("CI_PIPELINE_NUMBER"),
Name: "pipeline-number", Name: "pipeline-number",
Usage: "Set the metadata environment variable \"CI_PIPELINE_NUMBER\".",
}, },
&cli.IntFlag{ &cli.IntFlag{
EnvVars: []string{"CI_PIPELINE_PARENT"}, Sources: cli.EnvVars("CI_PIPELINE_PARENT"),
Name: "pipeline-parent", Name: "pipeline-parent",
Usage: "Set the metadata environment variable \"CI_PIPELINE_PARENT\".",
}, },
&cli.Int64Flag{ &cli.IntFlag{
EnvVars: []string{"CI_PIPELINE_CREATED"}, Sources: cli.EnvVars("CI_PIPELINE_CREATED"),
Name: "pipeline-created", Name: "pipeline-created",
Usage: "Set the metadata environment variable \"CI_PIPELINE_CREATED\".",
}, },
&cli.Int64Flag{ &cli.IntFlag{
EnvVars: []string{"CI_PIPELINE_STARTED"}, Sources: cli.EnvVars("CI_PIPELINE_STARTED"),
Name: "pipeline-started", Name: "pipeline-started",
}, Usage: "Set the metadata environment variable \"CI_PIPELINE_STARTED\".",
&cli.Int64Flag{
EnvVars: []string{"CI_PIPELINE_FINISHED"},
Name: "pipeline-finished",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_STATUS"}, Sources: cli.EnvVars("CI_PIPELINE_EVENT"),
Name: "pipeline-status",
},
&cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_EVENT"},
Name: "pipeline-event", Name: "pipeline-event",
Usage: "Set the metadata environment variable \"CI_PIPELINE_EVENT\".",
Value: "manual", Value: "manual",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_URL"}, Sources: cli.EnvVars("CI_PIPELINE_FORGE_URL"),
Name: "pipeline-url", Name: "pipeline-url",
Usage: "Set the metadata environment variable \"CI_PIPELINE_FORGE_URL\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_DEPLOY_TARGET", "CI_PIPELINE_TARGET"}, // TODO: remove CI_PIPELINE_TARGET in 3.x Sources: cli.EnvVars("CI_PIPELINE_DEPLOY_TARGET"),
Name: "pipeline-deploy-to", Name: "pipeline-deploy-to",
Usage: "Set the metadata environment variable \"CI_PIPELINE_DEPLOY_TARGET\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_DEPLOY_TASK", "CI_PIPELINE_TASK"}, // TODO: remove CI_PIPELINE_TASK in 3.x Sources: cli.EnvVars("CI_PIPELINE_DEPLOY_TASK"),
Name: "pipeline-deploy-task", Name: "pipeline-deploy-task",
Usage: "Set the metadata environment variable \"CI_PIPELINE_DEPLOY_TASK\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_SHA"}, Sources: cli.EnvVars("CI_PIPELINE_FILES"),
Usage: "Set the metadata environment variable \"CI_PIPELINE_FILES\", either json formatted list of strings, or comma separated string list.",
Name: "pipeline-changed-files",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_COMMIT_SHA"),
Name: "commit-sha", Name: "commit-sha",
Usage: "Set the metadata environment variable \"CI_COMMIT_SHA\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_REF"}, Sources: cli.EnvVars("CI_COMMIT_REF"),
Name: "commit-ref", Name: "commit-ref",
Usage: "Set the metadata environment variable \"CI_COMMIT_REF\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_REFSPEC"}, Sources: cli.EnvVars("CI_COMMIT_REFSPEC"),
Name: "commit-refspec", Name: "commit-refspec",
Usage: "Set the metadata environment variable \"CI_COMMIT_REFSPEC\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_BRANCH"}, Sources: cli.EnvVars("CI_COMMIT_BRANCH"),
Name: "commit-branch", Name: "commit-branch",
Usage: "Set the metadata environment variable \"CI_COMMIT_BRANCH\".",
Value: "main",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_MESSAGE"}, Sources: cli.EnvVars("CI_COMMIT_MESSAGE"),
Name: "commit-message", Name: "commit-message",
Usage: "Set the metadata environment variable \"CI_COMMIT_MESSAGE\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_AUTHOR_NAME"}, Sources: cli.EnvVars("CI_COMMIT_AUTHOR"),
Name: "commit-author-name", Name: "commit-author-name",
Usage: "Set the metadata environment variable \"CI_COMMIT_AUTHOR\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_AUTHOR_AVATAR"}, Sources: cli.EnvVars("CI_COMMIT_AUTHOR_AVATAR"),
Name: "commit-author-avatar", Name: "commit-author-avatar",
Usage: "Set the metadata environment variable \"CI_COMMIT_AUTHOR_AVATAR\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_COMMIT_AUTHOR_EMAIL"}, Sources: cli.EnvVars("CI_COMMIT_AUTHOR_EMAIL"),
Name: "commit-author-email", Name: "commit-author-email",
}, Usage: "Set the metadata environment variable \"CI_COMMIT_AUTHOR_EMAIL\".",
&cli.IntFlag{
EnvVars: []string{"CI_PREV_PIPELINE_NUMBER"},
Name: "prev-pipeline-number",
},
&cli.Int64Flag{
EnvVars: []string{"CI_PREV_PIPELINE_CREATED"},
Name: "prev-pipeline-created",
},
&cli.Int64Flag{
EnvVars: []string{"CI_PREV_PIPELINE_STARTED"},
Name: "prev-pipeline-started",
},
&cli.Int64Flag{
EnvVars: []string{"CI_PREV_PIPELINE_FINISHED"},
Name: "prev-pipeline-finished",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_PIPELINE_STATUS"},
Name: "prev-pipeline-status",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_PIPELINE_EVENT"},
Name: "prev-pipeline-event",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_PIPELINE_URL"},
Name: "prev-pipeline-url",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_SHA"},
Name: "prev-commit-sha",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_REF"},
Name: "prev-commit-ref",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_REFSPEC"},
Name: "prev-commit-refspec",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_BRANCH"},
Name: "prev-commit-branch",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_MESSAGE"},
Name: "prev-commit-message",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_AUTHOR_NAME"},
Name: "prev-commit-author-name",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_AUTHOR_AVATAR"},
Name: "prev-commit-author-avatar",
},
&cli.StringFlag{
EnvVars: []string{"CI_PREV_COMMIT_AUTHOR_EMAIL"},
Name: "prev-commit-author-email",
},
&cli.IntFlag{
EnvVars: []string{"CI_WORKFLOW_NAME"},
Name: "workflow-name",
},
&cli.IntFlag{
EnvVars: []string{"CI_WORKFLOW_NUMBER"},
Name: "workflow-number",
},
&cli.IntFlag{
EnvVars: []string{"CI_STEP_NAME"},
Name: "step-name",
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
EnvVars: []string{"CI_ENV"}, Sources: cli.EnvVars("CI_COMMIT_PULL_REQUEST_LABELS"),
Name: "commit-pull-labels",
Usage: "Set the metadata environment variable \"CI_COMMIT_PULL_REQUEST_LABELS\".",
},
&cli.BoolFlag{
Sources: cli.EnvVars("CI_COMMIT_PRERELEASE"),
Name: "commit-release-is-pre",
Usage: "Set the metadata environment variable \"CI_COMMIT_PRERELEASE\".",
},
&cli.IntFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_NUMBER"),
Name: "prev-pipeline-number",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_NUMBER\".",
},
&cli.IntFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_CREATED"),
Name: "prev-pipeline-created",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_CREATED\".",
},
&cli.IntFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_STARTED"),
Name: "prev-pipeline-started",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_STARTED\".",
},
&cli.IntFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_FINISHED"),
Name: "prev-pipeline-finished",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_FINISHED\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_STATUS"),
Name: "prev-pipeline-status",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_STATUS\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_EVENT"),
Name: "prev-pipeline-event",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_EVENT\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_FORGE_URL"),
Name: "prev-pipeline-url",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_FORGE_URL\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_DEPLOY_TARGET"),
Name: "prev-pipeline-deploy-to",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_DEPLOY_TARGET\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_PIPELINE_DEPLOY_TASK"),
Name: "prev-pipeline-deploy-task",
Usage: "Set the metadata environment variable \"CI_PREV_PIPELINE_DEPLOY_TASK\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_SHA"),
Name: "prev-commit-sha",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_SHA\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_REF"),
Name: "prev-commit-ref",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_REF\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_REFSPEC"),
Name: "prev-commit-refspec",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_REFSPEC\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_BRANCH"),
Name: "prev-commit-branch",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_BRANCH\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_MESSAGE"),
Name: "prev-commit-message",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_MESSAGE\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_AUTHOR"),
Name: "prev-commit-author-name",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_AUTHOR\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_AUTHOR_AVATAR"),
Name: "prev-commit-author-avatar",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_AUTHOR_AVATAR\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_PREV_COMMIT_AUTHOR_EMAIL"),
Name: "prev-commit-author-email",
Usage: "Set the metadata environment variable \"CI_PREV_COMMIT_AUTHOR_EMAIL\".",
},
&cli.StringFlag{
Sources: cli.EnvVars("CI_WORKFLOW_NAME"),
Name: "workflow-name",
Usage: "Set the metadata environment variable \"CI_WORKFLOW_NAME\".",
},
&cli.IntFlag{
Sources: cli.EnvVars("CI_WORKFLOW_NUMBER"),
Name: "workflow-number",
Usage: "Set the metadata environment variable \"CI_WORKFLOW_NUMBER\".",
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("CI_ENV"),
Name: "env", Name: "env",
Usage: "Set the metadata environment variable \"CI_ENV\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_FORGE_TYPE"}, Sources: cli.EnvVars("CI_FORGE_TYPE"),
Name: "forge-type", Name: "forge-type",
Usage: "Set the metadata environment variable \"CI_FORGE_TYPE\".",
}, },
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"CI_FORGE_URL"}, Sources: cli.EnvVars("CI_FORGE_URL"),
Name: "forge-url", Name: "forge-url",
Usage: "Set the metadata environment variable \"CI_FORGE_URL\".",
}, },
} }

View file

@ -15,105 +15,147 @@
package exec package exec
import ( import (
"context"
"encoding/json"
"fmt"
"os"
"runtime" "runtime"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/matrix"
"go.woodpecker-ci.org/woodpecker/v2/version" "go.woodpecker-ci.org/woodpecker/v3/version"
) )
// return the metadata from the cli context. // return the metadata from the cli context.
func metadataFromContext(c *cli.Context, axis matrix.Axis) metadata.Metadata { func metadataFromContext(_ context.Context, c *cli.Command, axis matrix.Axis, w *metadata.Workflow) (*metadata.Metadata, error) {
m := &metadata.Metadata{}
if c.IsSet("metadata-file") {
metadataFile, err := os.Open(c.String("metadata-file"))
if err != nil {
return nil, err
}
defer metadataFile.Close()
if err := json.NewDecoder(metadataFile).Decode(m); err != nil {
return nil, err
}
}
platform := c.String("system-platform") platform := c.String("system-platform")
if platform == "" { if platform == "" {
platform = runtime.GOOS + "/" + runtime.GOARCH platform = runtime.GOOS + "/" + runtime.GOARCH
} }
fullRepoName := c.String("repo-name") metadataFileAndOverrideOrDefault(c, "repo-name", func(fullRepoName string) {
repoOwner := "" if idx := strings.LastIndex(fullRepoName, "/"); idx != -1 {
repoName := "" m.Repo.Owner = fullRepoName[:idx]
if idx := strings.LastIndex(fullRepoName, "/"); idx != -1 { m.Repo.Name = fullRepoName[idx+1:]
repoOwner = fullRepoName[:idx] }
repoName = fullRepoName[idx+1:] }, c.String)
var err error
metadataFileAndOverrideOrDefault(c, "pipeline-changed-files", func(changedFilesRaw string) {
var changedFiles []string
if len(changedFilesRaw) != 0 && changedFilesRaw[0] == '[' {
if jsonErr := json.Unmarshal([]byte(changedFilesRaw), &changedFiles); jsonErr != nil {
err = fmt.Errorf("pipeline-changed-files detected json but could not parse it: %w", jsonErr)
}
} else {
for _, file := range strings.Split(changedFilesRaw, ",") {
changedFiles = append(changedFiles, strings.TrimSpace(file))
}
}
m.Curr.Commit.ChangedFiles = changedFiles
}, c.String)
if err != nil {
return nil, err
} }
return metadata.Metadata{ // Repo
Repo: metadata.Repo{ metadataFileAndOverrideOrDefault(c, "repo-remote-id", func(s string) { m.Repo.RemoteID = s }, c.String)
Name: repoName, metadataFileAndOverrideOrDefault(c, "repo-url", func(s string) { m.Repo.ForgeURL = s }, c.String)
Owner: repoOwner, metadataFileAndOverrideOrDefault(c, "repo-default-branch", func(s string) { m.Repo.Branch = s }, c.String)
RemoteID: c.String("repo-remote-id"), metadataFileAndOverrideOrDefault(c, "repo-clone-url", func(s string) { m.Repo.CloneURL = s }, c.String)
ForgeURL: c.String("repo-url"), metadataFileAndOverrideOrDefault(c, "repo-clone-ssh-url", func(s string) { m.Repo.CloneSSHURL = s }, c.String)
CloneURL: c.String("repo-clone-url"), metadataFileAndOverrideOrDefault(c, "repo-private", func(b bool) { m.Repo.Private = b }, c.Bool)
CloneSSHURL: c.String("repo-clone-ssh-url"), metadataFileAndOverrideOrDefault(c, "repo-trusted-network", func(b bool) { m.Repo.Trusted.Network = b }, c.Bool)
Private: c.Bool("repo-private"), metadataFileAndOverrideOrDefault(c, "repo-trusted-security", func(b bool) { m.Repo.Trusted.Security = b }, c.Bool)
Trusted: c.Bool("repo-trusted"), metadataFileAndOverrideOrDefault(c, "repo-trusted-volumes", func(b bool) { m.Repo.Trusted.Volumes = b }, c.Bool)
},
Curr: metadata.Pipeline{ // Current Pipeline
Number: c.Int64("pipeline-number"), metadataFileAndOverrideOrDefault(c, "pipeline-number", func(i int64) { m.Curr.Number = i }, c.Int)
Parent: c.Int64("pipeline-parent"), metadataFileAndOverrideOrDefault(c, "pipeline-parent", func(i int64) { m.Curr.Parent = i }, c.Int)
Created: c.Int64("pipeline-created"), metadataFileAndOverrideOrDefault(c, "pipeline-created", func(i int64) { m.Curr.Created = i }, c.Int)
Started: c.Int64("pipeline-started"), metadataFileAndOverrideOrDefault(c, "pipeline-started", func(i int64) { m.Curr.Started = i }, c.Int)
Finished: c.Int64("pipeline-finished"), metadataFileAndOverrideOrDefault(c, "pipeline-finished", func(i int64) { m.Curr.Finished = i }, c.Int)
Status: c.String("pipeline-status"), metadataFileAndOverrideOrDefault(c, "pipeline-status", func(s string) { m.Curr.Status = s }, c.String)
Event: c.String("pipeline-event"), metadataFileAndOverrideOrDefault(c, "pipeline-event", func(s string) { m.Curr.Event = s }, c.String)
ForgeURL: c.String("pipeline-url"), metadataFileAndOverrideOrDefault(c, "pipeline-url", func(s string) { m.Curr.ForgeURL = s }, c.String)
DeployTo: c.String("pipeline-deploy-to"), metadataFileAndOverrideOrDefault(c, "pipeline-deploy-to", func(s string) { m.Curr.DeployTo = s }, c.String)
DeployTask: c.String("pipeline-deploy-task"), metadataFileAndOverrideOrDefault(c, "pipeline-deploy-task", func(s string) { m.Curr.DeployTask = s }, c.String)
Commit: metadata.Commit{
Sha: c.String("commit-sha"), // Current Pipeline Commit
Ref: c.String("commit-ref"), metadataFileAndOverrideOrDefault(c, "commit-sha", func(s string) { m.Curr.Commit.Sha = s }, c.String)
Refspec: c.String("commit-refspec"), metadataFileAndOverrideOrDefault(c, "commit-ref", func(s string) { m.Curr.Commit.Ref = s }, c.String)
Branch: c.String("commit-branch"), metadataFileAndOverrideOrDefault(c, "commit-refspec", func(s string) { m.Curr.Commit.Refspec = s }, c.String)
Message: c.String("commit-message"), metadataFileAndOverrideOrDefault(c, "commit-branch", func(s string) { m.Curr.Commit.Branch = s }, c.String)
Author: metadata.Author{ metadataFileAndOverrideOrDefault(c, "commit-message", func(s string) { m.Curr.Commit.Message = s }, c.String)
Name: c.String("commit-author-name"), metadataFileAndOverrideOrDefault(c, "commit-author-name", func(s string) { m.Curr.Commit.Author.Name = s }, c.String)
Email: c.String("commit-author-email"), metadataFileAndOverrideOrDefault(c, "commit-author-email", func(s string) { m.Curr.Commit.Author.Email = s }, c.String)
Avatar: c.String("commit-author-avatar"), metadataFileAndOverrideOrDefault(c, "commit-author-avatar", func(s string) { m.Curr.Commit.Author.Avatar = s }, c.String)
},
}, metadataFileAndOverrideOrDefault(c, "commit-pull-labels", func(sl []string) { m.Curr.Commit.PullRequestLabels = sl }, c.StringSlice)
}, metadataFileAndOverrideOrDefault(c, "commit-release-is-pre", func(b bool) { m.Curr.Commit.IsPrerelease = b }, c.Bool)
Prev: metadata.Pipeline{
Number: c.Int64("prev-pipeline-number"), // Previous Pipeline
Created: c.Int64("prev-pipeline-created"), metadataFileAndOverrideOrDefault(c, "prev-pipeline-number", func(i int64) { m.Prev.Number = i }, c.Int)
Started: c.Int64("prev-pipeline-started"), metadataFileAndOverrideOrDefault(c, "prev-pipeline-created", func(i int64) { m.Prev.Created = i }, c.Int)
Finished: c.Int64("prev-pipeline-finished"), metadataFileAndOverrideOrDefault(c, "prev-pipeline-started", func(i int64) { m.Prev.Started = i }, c.Int)
Status: c.String("prev-pipeline-status"), metadataFileAndOverrideOrDefault(c, "prev-pipeline-finished", func(i int64) { m.Prev.Finished = i }, c.Int)
Event: c.String("prev-pipeline-event"), metadataFileAndOverrideOrDefault(c, "prev-pipeline-status", func(s string) { m.Prev.Status = s }, c.String)
ForgeURL: c.String("prev-pipeline-url"), metadataFileAndOverrideOrDefault(c, "prev-pipeline-event", func(s string) { m.Prev.Event = s }, c.String)
Commit: metadata.Commit{ metadataFileAndOverrideOrDefault(c, "prev-pipeline-url", func(s string) { m.Prev.ForgeURL = s }, c.String)
Sha: c.String("prev-commit-sha"),
Ref: c.String("prev-commit-ref"), // Previous Pipeline Commit
Refspec: c.String("prev-commit-refspec"), metadataFileAndOverrideOrDefault(c, "prev-commit-sha", func(s string) { m.Prev.Commit.Sha = s }, c.String)
Branch: c.String("prev-commit-branch"), metadataFileAndOverrideOrDefault(c, "prev-commit-ref", func(s string) { m.Prev.Commit.Ref = s }, c.String)
Message: c.String("prev-commit-message"), metadataFileAndOverrideOrDefault(c, "prev-commit-refspec", func(s string) { m.Prev.Commit.Refspec = s }, c.String)
Author: metadata.Author{ metadataFileAndOverrideOrDefault(c, "prev-commit-branch", func(s string) { m.Prev.Commit.Branch = s }, c.String)
Name: c.String("prev-commit-author-name"), metadataFileAndOverrideOrDefault(c, "prev-commit-message", func(s string) { m.Prev.Commit.Message = s }, c.String)
Email: c.String("prev-commit-author-email"), metadataFileAndOverrideOrDefault(c, "prev-commit-author-name", func(s string) { m.Prev.Commit.Author.Name = s }, c.String)
Avatar: c.String("prev-commit-author-avatar"), metadataFileAndOverrideOrDefault(c, "prev-commit-author-email", func(s string) { m.Prev.Commit.Author.Email = s }, c.String)
}, metadataFileAndOverrideOrDefault(c, "prev-commit-author-avatar", func(s string) { m.Prev.Commit.Author.Avatar = s }, c.String)
},
}, // Workflow
Workflow: metadata.Workflow{ metadataFileAndOverrideOrDefault(c, "workflow-name", func(s string) { m.Workflow.Name = s }, c.String)
Name: c.String("workflow-name"), metadataFileAndOverrideOrDefault(c, "workflow-number", func(i int64) { m.Workflow.Number = int(i) }, c.Int)
Number: c.Int("workflow-number"), m.Workflow.Matrix = axis
Matrix: axis,
}, // System
Step: metadata.Step{ metadataFileAndOverrideOrDefault(c, "system-name", func(s string) { m.Sys.Name = s }, c.String)
Name: c.String("step-name"), metadataFileAndOverrideOrDefault(c, "system-url", func(s string) { m.Sys.URL = s }, c.String)
Number: c.Int("step-number"), metadataFileAndOverrideOrDefault(c, "system-host", func(s string) { m.Sys.Host = s }, c.String)
}, m.Sys.Platform = platform
Sys: metadata.System{ m.Sys.Version = version.Version
Name: c.String("system-name"),
URL: c.String("system-url"), // Forge
Platform: platform, metadataFileAndOverrideOrDefault(c, "forge-type", func(s string) { m.Forge.Type = s }, c.String)
Version: version.Version, metadataFileAndOverrideOrDefault(c, "forge-url", func(s string) { m.Forge.URL = s }, c.String)
},
Forge: metadata.Forge{ if w != nil {
Type: c.String("forge-type"), m.Workflow = *w
URL: c.String("forge-url"), }
},
return m, nil
}
// metadataFileAndOverrideOrDefault will either use the flag default or if metadata file is set only overload if explicit set.
func metadataFileAndOverrideOrDefault[T any](c *cli.Command, flag string, setter func(T), getter func(string) T) {
if !c.IsSet("metadata-file") || c.IsSet(flag) {
setter(getter(flag))
} }
} }

142
cli/exec/metadata_test.go Normal file
View file

@ -0,0 +1,142 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package exec
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/matrix"
)
func TestMetadataFromContext(t *testing.T) {
sampleMetadata := &metadata.Metadata{
Repo: metadata.Repo{Owner: "test-user", Name: "test-repo"},
Curr: metadata.Pipeline{Number: 5},
}
runCommand := func(flags []cli.Flag, fn func(c *cli.Command)) {
c := &cli.Command{
Flags: flags,
Action: func(_ context.Context, c *cli.Command) error {
fn(c)
return nil
},
}
assert.NoError(t, c.Run(context.Background(), []string{"woodpecker-cli"}))
}
t.Run("LoadFromFile", func(t *testing.T) {
tempFileName := createTempFile(t, sampleMetadata)
flags := []cli.Flag{
&cli.StringFlag{Name: "metadata-file"},
}
runCommand(flags, func(c *cli.Command) {
_ = c.Set("metadata-file", tempFileName)
m, err := metadataFromContext(context.Background(), c, nil, nil)
require.NoError(t, err)
assert.Equal(t, "test-repo", m.Repo.Name)
assert.Equal(t, int64(5), m.Curr.Number)
})
})
t.Run("OverrideFromFlags", func(t *testing.T) {
tempFileName := createTempFile(t, sampleMetadata)
flags := []cli.Flag{
&cli.StringFlag{Name: "metadata-file"},
&cli.StringFlag{Name: "repo-name"},
&cli.IntFlag{Name: "pipeline-number"},
}
runCommand(flags, func(c *cli.Command) {
_ = c.Set("metadata-file", tempFileName)
_ = c.Set("repo-name", "aUser/override-repo")
_ = c.Set("pipeline-number", "10")
m, err := metadataFromContext(context.Background(), c, nil, nil)
require.NoError(t, err)
assert.Equal(t, "override-repo", m.Repo.Name)
assert.Equal(t, int64(10), m.Curr.Number)
})
})
t.Run("InvalidFile", func(t *testing.T) {
tempFile, err := os.CreateTemp("", "invalid.json")
require.NoError(t, err)
t.Cleanup(func() { os.Remove(tempFile.Name()) })
_, err = tempFile.Write([]byte("invalid json"))
require.NoError(t, err)
flags := []cli.Flag{
&cli.StringFlag{Name: "metadata-file"},
}
runCommand(flags, func(c *cli.Command) {
_ = c.Set("metadata-file", tempFile.Name())
_, err = metadataFromContext(context.Background(), c, nil, nil)
assert.Error(t, err)
})
})
t.Run("DefaultValues", func(t *testing.T) {
flags := []cli.Flag{
&cli.StringFlag{Name: "repo-name", Value: "test/default-repo"},
&cli.IntFlag{Name: "pipeline-number", Value: 1},
}
runCommand(flags, func(c *cli.Command) {
m, err := metadataFromContext(context.Background(), c, nil, nil)
require.NoError(t, err)
if assert.NotNil(t, m) {
assert.Equal(t, "test", m.Repo.Owner)
assert.Equal(t, "default-repo", m.Repo.Name)
assert.Equal(t, int64(1), m.Curr.Number)
}
})
})
t.Run("MatrixAxis", func(t *testing.T) {
runCommand([]cli.Flag{}, func(c *cli.Command) {
axis := matrix.Axis{"go": "1.16", "os": "linux"}
m, err := metadataFromContext(context.Background(), c, axis, nil)
require.NoError(t, err)
assert.EqualValues(t, map[string]string{"go": "1.16", "os": "linux"}, m.Workflow.Matrix)
})
})
}
func createTempFile(t *testing.T, content any) string {
t.Helper()
tempFile, err := os.CreateTemp("", "metadata.json")
require.NoError(t, err)
t.Cleanup(func() { os.Remove(tempFile.Name()) })
err = json.NewEncoder(tempFile).Encode(content)
require.NoError(t, err)
return tempFile.Name()
}

View file

@ -15,13 +15,14 @@
package info package info
import ( import (
"context"
"os" "os"
"text/template" "text/template"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
// Command exports the info command. // Command exports the info command.
@ -33,8 +34,8 @@ var Command = &cli.Command{
Flags: []cli.Flag{common.FormatFlag(tmplInfo, true)}, Flags: []cli.Flag{common.FormatFlag(tmplInfo, true)},
} }
func info(c *cli.Context) error { func info(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,6 +1,7 @@
package config package config
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"os" "os"
@ -8,7 +9,7 @@ import (
"github.com/adrg/xdg" "github.com/adrg/xdg"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
) )
@ -32,18 +33,18 @@ func (c *Config) MergeIfNotSet(c2 *Config) {
var skipSetupForCommands = []string{"setup", "help", "h", "version", "update", "lint", "exec", ""} var skipSetupForCommands = []string{"setup", "help", "h", "version", "update", "lint", "exec", ""}
func Load(c *cli.Context) error { func Load(ctx context.Context, c *cli.Command) error {
if firstArg := c.Args().First(); slices.Contains(skipSetupForCommands, firstArg) { if firstArg := c.Args().First(); slices.Contains(skipSetupForCommands, firstArg) {
return nil return nil
} }
config, err := Get(c, c.String("config")) config, err := Get(ctx, c, c.String("config"))
if err != nil { if err != nil {
return err return err
} }
if config.ServerURL == "" || config.Token == "" { 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.") log.Info().Msg("woodpecker-cli is not set up, run `woodpecker-cli setup` or provide required environment variables/flags")
return errors.New("woodpecker-cli is not configured") return errors.New("woodpecker-cli is not configured")
} }
@ -62,7 +63,7 @@ func Load(c *cli.Context) error {
return err return err
} }
log.Debug().Any("config", config).Msg("Loaded config") log.Debug().Any("config", config).Msg("loaded config")
return nil return nil
} }
@ -80,11 +81,11 @@ func getConfigPath(configPath string) (string, error) {
return configPath, nil return configPath, nil
} }
func Get(ctx *cli.Context, _configPath string) (*Config, error) { func Get(_ context.Context, c *cli.Command, _configPath string) (*Config, error) {
c := &Config{ conf := &Config{
LogLevel: ctx.String("log-level"), LogLevel: c.String("log-level"),
Token: ctx.String("token"), Token: c.String("token"),
ServerURL: ctx.String("server"), ServerURL: c.String("server"),
} }
configPath, err := getConfigPath(_configPath) configPath, err := getConfigPath(_configPath)
@ -92,16 +93,16 @@ func Get(ctx *cli.Context, _configPath string) (*Config, error) {
return nil, err return nil, err
} }
log.Debug().Str("configPath", configPath).Msg("Checking for config file") log.Debug().Str("configPath", configPath).Msg("checking for config file")
content, err := os.ReadFile(configPath) content, err := os.ReadFile(configPath)
switch { switch {
case err != nil && !os.IsNotExist(err): case err != nil && !os.IsNotExist(err):
log.Debug().Err(err).Msg("Failed to read the config file") log.Debug().Err(err).Msg("failed to read the config file")
return nil, err return nil, err
case err != nil && os.IsNotExist(err): case err != nil && os.IsNotExist(err):
log.Debug().Msg("The config file does not exist") log.Debug().Msg("config file does not exist")
default: default:
configFromFile := &Config{} configFromFile := &Config{}
@ -109,33 +110,33 @@ func Get(ctx *cli.Context, _configPath string) (*Config, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.MergeIfNotSet(configFromFile) conf.MergeIfNotSet(configFromFile)
log.Debug().Msg("Loaded config from file") log.Debug().Msg("loaded config from file")
} }
// if server or token are explicitly set, use them // if server or token are explicitly set, use them
if ctx.IsSet("server") || ctx.IsSet("token") { if c.IsSet("server") || c.IsSet("token") {
return c, nil return conf, nil
} }
// load token from keyring // load token from keyring
service := ctx.App.Name service := c.Root().Name
secret, err := keyring.Get(service, c.ServerURL) secret, err := keyring.Get(service, conf.ServerURL)
if errors.Is(err, keyring.ErrUnsupportedPlatform) { if errors.Is(err, keyring.ErrUnsupportedPlatform) {
log.Warn().Msg("Keyring is not supported on this platform") log.Warn().Msg("keyring is not supported on this platform")
return c, nil return conf, nil
} }
if errors.Is(err, keyring.ErrNotFound) { if errors.Is(err, keyring.ErrNotFound) {
log.Warn().Msg("Token not found in keyring") log.Warn().Msg("token not found in keyring")
return c, nil return conf, nil
} }
c.Token = secret conf.Token = secret
return c, nil return conf, nil
} }
func Save(ctx *cli.Context, _configPath string, c *Config) error { func Save(_ context.Context, c *cli.Command, _configPath string, conf *Config) error {
config, err := json.Marshal(c) config, err := json.Marshal(conf)
if err != nil { if err != nil {
return err return err
} }
@ -146,8 +147,8 @@ func Save(ctx *cli.Context, _configPath string, c *Config) error {
} }
// save token to keyring // save token to keyring
service := ctx.App.Name service := c.Root().Name
err = keyring.Set(service, c.ServerURL, c.Token) err = keyring.Set(service, conf.ServerURL, conf.Token)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,6 +15,7 @@
package internal package internal
import ( import (
"context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
@ -25,15 +26,15 @@ import (
vsc_url "github.com/gitsight/go-vcsurl" vsc_url "github.com/gitsight/go-vcsurl"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
// NewClient returns a new client from the CLI context. // NewClient returns a new client from the CLI context.
func NewClient(c *cli.Context) (woodpecker.Client, error) { func NewClient(ctx context.Context, c *cli.Command) (woodpecker.Client, error) {
var ( var (
skip = c.Bool("skip-verify") skip = c.Bool("skip-verify")
socks = c.String("socks-proxy") socks = c.String("socks-proxy")
@ -63,8 +64,7 @@ func NewClient(c *cli.Context) (woodpecker.Client, error) {
} }
config := new(oauth2.Config) config := new(oauth2.Config)
client := config.Client( client := config.Client(ctx,
c.Context,
&oauth2.Token{ &oauth2.Token{
AccessToken: token, AccessToken: token,
}, },
@ -161,3 +161,40 @@ func ParseKeyPair(p []string) map[string]string {
} }
return params return params
} }
/*
ParseStep parses the step id form a string which may either be the step PID (step number) or a step name.
These rules apply:
- Step PID take precedence over step name when searching for a match.
- First match is used, when there are multiple steps with the same name.
Strictly speaking, this is not parsing, but a lookup.
*/
func ParseStep(client woodpecker.Client, repoID, number int64, stepArg string) (stepID int64, err error) {
pipeline, err := client.Pipeline(repoID, number)
if err != nil {
return 0, err
}
stepPID, err := strconv.ParseInt(stepArg, 10, 64)
if err == nil {
for _, wf := range pipeline.Workflows {
for _, step := range wf.Children {
if int64(step.PID) == stepPID {
return step.ID, nil
}
}
}
}
for _, wf := range pipeline.Workflows {
for _, step := range wf.Children {
if step.Name == stepArg {
return step.ID, nil
}
}
}
return 0, fmt.Errorf("no step with number or name '%s' found", stepArg)
}

View file

@ -15,17 +15,19 @@
package lint package lint
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter"
"go.woodpecker-ci.org/woodpecker/v3/shared/constant"
) )
// Command exports the info command. // Command exports the info command.
@ -34,13 +36,31 @@ var Command = &cli.Command{
Usage: "lint a pipeline configuration file", Usage: "lint a pipeline configuration file",
ArgsUsage: "[path/to/.woodpecker.yaml]", ArgsUsage: "[path/to/.woodpecker.yaml]",
Action: lint, Action: lint,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_PRIVILEGED"),
Name: "plugins-privileged",
Usage: "allow plugins to run in privileged mode, if set empty, there is no",
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
Name: "plugins-trusted-clone",
Usage: "plugins that are trusted to handle Git credentials in cloning steps",
Value: constant.TrustedClonePlugins,
},
&cli.BoolFlag{
Sources: cli.EnvVars("WOODPECKER_LINT_STRICT"),
Name: "strict",
Usage: "treat warnings as errors",
},
},
} }
func lint(c *cli.Context) error { func lint(ctx context.Context, c *cli.Command) error {
return common.RunPipelineFunc(c, lintFile, lintDir) return common.RunPipelineFunc(ctx, c, lintFile, lintDir)
} }
func lintDir(c *cli.Context, dir string) error { func lintDir(ctx context.Context, c *cli.Command, dir string) error {
var errorStrings []string var errorStrings []string
if err := filepath.Walk(dir, func(path string, info os.FileInfo, e error) error { if err := filepath.Walk(dir, func(path string, info os.FileInfo, e error) error {
if e != nil { if e != nil {
@ -50,7 +70,7 @@ func lintDir(c *cli.Context, dir string) error {
// check if it is a regular file (not dir) // check if it is a regular file (not dir)
if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) { if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) {
fmt.Println("#", info.Name()) fmt.Println("#", info.Name())
if err := lintFile(c, path); err != nil { if err := lintFile(ctx, c, path); err != nil {
errorStrings = append(errorStrings, err.Error()) errorStrings = append(errorStrings, err.Error())
} }
fmt.Println("") fmt.Println("")
@ -68,7 +88,7 @@ func lintDir(c *cli.Context, dir string) error {
return nil return nil
} }
func lintFile(_ *cli.Context, file string) error { func lintFile(_ context.Context, c *cli.Command, file string) error {
fi, err := os.Open(file) fi, err := os.Open(file)
if err != nil { if err != nil {
return err return err
@ -82,7 +102,7 @@ func lintFile(_ *cli.Context, file string) error {
rawConfig := string(buf) rawConfig := string(buf)
c, err := yaml.ParseString(rawConfig) parsedConfig, err := yaml.ParseString(rawConfig)
if err != nil { if err != nil {
return err return err
} }
@ -90,13 +110,21 @@ func lintFile(_ *cli.Context, file string) error {
config := &linter.WorkflowConfig{ config := &linter.WorkflowConfig{
File: path.Base(file), File: path.Base(file),
RawConfig: rawConfig, RawConfig: rawConfig,
Workflow: c, Workflow: parsedConfig,
} }
// TODO: lint multiple files at once to allow checks for sth like "depends_on" to work // TODO: lint multiple files at once to allow checks for sth like "depends_on" to work
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{config}) err = linter.New(
linter.WithTrusted(linter.TrustedConfiguration{
Network: true,
Volumes: true,
Security: true,
}),
linter.PrivilegedPlugins(c.StringSlice("plugins-privileged")),
linter.WithTrustedClonePlugins(c.StringSlice("plugins-trusted-clone")),
).Lint([]*linter.WorkflowConfig{config})
if err != nil { if err != nil {
str, err := FormatLintError(config.File, err) str, err := FormatLintError(config.File, err, c.Bool("strict"))
if str != "" { if str != "" {
fmt.Print(str) fmt.Print(str)

View file

@ -7,10 +7,10 @@ import (
term_env "github.com/muesli/termenv" term_env "github.com/muesli/termenv"
pipeline_errors "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors"
) )
func FormatLintError(file string, err error) (string, error) { func FormatLintError(file string, err error, strict bool) (string, error) {
if err == nil { if err == nil {
return "", nil return "", nil
} }
@ -24,7 +24,7 @@ func FormatLintError(file string, err error) (string, error) {
for _, err := range linterErrors { for _, err := range linterErrors {
line := " " line := " "
if err.IsWarning { if !strict && err.IsWarning {
line = fmt.Sprintf("%s ⚠️ ", line) line = fmt.Sprintf("%s ⚠️ ", line)
amountWarnings++ amountWarnings++
} else { } else {

View file

@ -15,16 +15,18 @@
package org package org
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/org/registry" "go.woodpecker-ci.org/woodpecker/v3/cli/org/registry"
"go.woodpecker-ci.org/woodpecker/v3/cli/org/secret"
) )
// Command exports the org command set. // Command exports the org command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "org", Name: "org",
Usage: "manage organizations", Usage: "manage organizations",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
registry.Command, registry.Command,
secret.Command,
}, },
} }

View file

@ -17,25 +17,25 @@ package registry
import ( import (
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
// Command exports the registry command set. // Command exports the registry command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "registry", Name: "registry",
Usage: "manage organization registries", Usage: "manage organization registries",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
registryCreateCmd, registryCreateCmd,
registryDeleteCmd, registryDeleteCmd,
registryUpdateCmd,
registryInfoCmd,
registryListCmd, registryListCmd,
registryShowCmd,
registryUpdateCmd,
}, },
} }
func parseTargetArgs(client woodpecker.Client, c *cli.Context) (orgID int64, err error) { func parseTargetArgs(client woodpecker.Client, c *cli.Command) (orgID int64, err error) {
orgIDOrName := c.String("organization") orgIDOrName := c.String("organization")
if orgIDOrName == "" { if orgIDOrName == "" {
orgIDOrName = c.Args().First() orgIDOrName = c.Args().First()

View file

@ -15,19 +15,20 @@
package registry package registry
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var registryCreateCmd = &cli.Command{ var registryCreateCmd = &cli.Command{
Name: "add", Name: "add",
Usage: "adds a registry", Usage: "add a registry",
ArgsUsage: "[org-id|org-full-name]", ArgsUsage: "[org-id|org-full-name]",
Action: registryCreate, Action: registryCreate,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -48,14 +49,14 @@ var registryCreateCmd = &cli.Command{
}, },
} }
func registryCreate(c *cli.Context) error { func registryCreate(ctx context.Context, c *cli.Command) error {
var ( var (
hostname = c.String("hostname") hostname = c.String("hostname")
username = c.String("username") username = c.String("username")
password = c.String("password") password = c.String("password")
) )
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,13 +15,15 @@
package registry package registry
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var registryListCmd = &cli.Command{ var registryListCmd = &cli.Command{
@ -35,10 +37,10 @@ var registryListCmd = &cli.Command{
}, },
} }
func registryList(c *cli.Context) error { func registryList(ctx context.Context, c *cli.Command) error {
format := c.String("format") + "\n" format := c.String("format") + "\n"
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -48,7 +50,9 @@ func registryList(c *cli.Context) error {
return err return err
} }
list, err := client.OrgRegistryList(orgID) opt := woodpecker.RegistryListOptions{}
list, err := client.OrgRegistryList(orgID, opt)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,10 +15,12 @@
package registry package registry
import ( import (
"github.com/urfave/cli/v2" "context"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var registryDeleteCmd = &cli.Command{ var registryDeleteCmd = &cli.Command{
@ -36,10 +38,10 @@ var registryDeleteCmd = &cli.Command{
}, },
} }
func registryDelete(c *cli.Context) error { func registryDelete(ctx context.Context, c *cli.Command) error {
hostname := c.String("hostname") hostname := c.String("hostname")
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,14 +15,15 @@
package registry package registry
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var registryUpdateCmd = &cli.Command{ var registryUpdateCmd = &cli.Command{
@ -48,14 +49,14 @@ var registryUpdateCmd = &cli.Command{
}, },
} }
func registryUpdate(c *cli.Context) error { func registryUpdate(ctx context.Context, c *cli.Command) error {
var ( var (
hostname = c.String("hostname") hostname = c.String("hostname")
username = c.String("username") username = c.String("username")
password = c.String("password") password = c.String("password")
) )
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,20 +15,21 @@
package registry package registry
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var registryInfoCmd = &cli.Command{ var registryShowCmd = &cli.Command{
Name: "info", Name: "show",
Usage: "display registry info", Usage: "show registry information",
ArgsUsage: "[org-id|org-full-name]", ArgsUsage: "[org-id|org-full-name]",
Action: registryInfo, Action: registryShow,
Flags: []cli.Flag{ Flags: []cli.Flag{
common.OrgFlag, common.OrgFlag,
&cli.StringFlag{ &cli.StringFlag{
@ -40,13 +41,13 @@ var registryInfoCmd = &cli.Command{
}, },
} }
func registryInfo(c *cli.Context) error { func registryShow(ctx context.Context, c *cli.Command) error {
var ( var (
hostname = c.String("hostname") hostname = c.String("hostname")
format = c.String("format") + "\n" format = c.String("format") + "\n"
) )
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

60
cli/org/secret/secret.go Normal file
View file

@ -0,0 +1,60 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"strconv"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
// Command exports the secret command.
var Command = &cli.Command{
Name: "secret",
Usage: "manage secrets",
Commands: []*cli.Command{
secretCreateCmd,
secretDeleteCmd,
secretListCmd,
secretShowCmd,
secretUpdateCmd,
},
}
func parseTargetArgs(client woodpecker.Client, c *cli.Command) (orgID int64, err error) {
orgIDOrName := c.String("organization")
if orgIDOrName == "" {
orgIDOrName = c.Args().First()
}
if orgIDOrName == "" {
if err := cli.ShowSubcommandHelp(c); err != nil {
return -1, err
}
}
if orgID, err := strconv.ParseInt(orgIDOrName, 10, 64); err == nil {
return orgID, nil
}
org, err := client.OrgLookup(orgIDOrName)
if err != nil {
return -1, err
}
return org.ID, nil
}

View file

@ -15,28 +15,24 @@
package secret package secret
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var secretCreateCmd = &cli.Command{ var secretCreateCmd = &cli.Command{
Name: "add", Name: "add",
Usage: "adds a secret", Usage: "add a secret",
ArgsUsage: "[repo-id|repo-full-name]", ArgsUsage: "[repo-id|repo-full-name]",
Action: secretCreate, Action: secretCreate,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{
Name: "global",
Usage: "global secret",
},
common.OrgFlag, common.OrgFlag,
common.RepoFlag,
&cli.StringFlag{ &cli.StringFlag{
Name: "name", Name: "name",
Usage: "secret name", Usage: "secret name",
@ -56,8 +52,8 @@ var secretCreateCmd = &cli.Command{
}, },
} }
func secretCreate(c *cli.Context) error { func secretCreate(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -80,22 +76,12 @@ func secretCreate(c *cli.Context) error {
secret.Value = string(out) secret.Value = string(out)
} }
global, orgID, repoID, err := parseTargetArgs(client, c) orgID, err := parseTargetArgs(client, c)
if err != nil { if err != nil {
return err return err
} }
if global { _, err = client.OrgSecretCreate(orgID, secret)
_, err = client.GlobalSecretCreate(secret)
return err
}
if orgID != -1 {
_, err = client.OrgSecretCreate(orgID, secret)
return err
}
_, err = client.SecretCreate(repoID, secret)
return err return err
} }

View file

@ -0,0 +1,87 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"html/template"
"os"
"strings"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
var secretListCmd = &cli.Command{
Name: "ls",
Usage: "list secrets",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretList,
Flags: []cli.Flag{
common.OrgFlag,
common.FormatFlag(tmplSecretList, true),
},
}
func secretList(ctx context.Context, c *cli.Command) error {
format := c.String("format") + "\n"
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
opt := woodpecker.SecretListOptions{}
list, err := client.OrgSecretList(orgID, opt)
if err != nil {
return err
}
tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(format)
if err != nil {
return err
}
for _, secret := range list {
if err := tmpl.Execute(os.Stdout, secret); err != nil {
return err
}
}
return nil
}
// Template for secret list items.
var tmplSecretList = "\x1b[33m{{ .Name }} \x1b[0m" + `
Events: {{ list .Events }}
{{- if .Images }}
Images: {{ list .Images }}
{{- else }}
Images: <any>
{{- end }}
`
var secretFuncMap = template.FuncMap{
"list": func(s []string) string {
return strings.Join(s, ", ")
},
}

View file

@ -0,0 +1,54 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
)
var secretDeleteCmd = &cli.Command{
Name: "rm",
Usage: "remove a secret",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretDelete,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "name",
Usage: "secret name",
},
},
}
func secretDelete(ctx context.Context, c *cli.Command) error {
secretName := c.String("name")
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
return client.OrgSecretDelete(orgID, secretName)
}

View file

@ -0,0 +1,83 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"os"
"strings"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
var secretUpdateCmd = &cli.Command{
Name: "update",
Usage: "update a secret",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretUpdate,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "name",
Usage: "secret name",
},
&cli.StringFlag{
Name: "value",
Usage: "secret value",
},
&cli.StringSliceFlag{
Name: "event",
Usage: "limit secret to these event",
},
&cli.StringSliceFlag{
Name: "image",
Usage: "limit secret to these image",
},
},
}
func secretUpdate(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
secret := &woodpecker.Secret{
Name: strings.ToLower(c.String("name")),
Value: c.String("value"),
Images: c.StringSlice("image"),
Events: c.StringSlice("event"),
}
if strings.HasPrefix(secret.Value, "@") {
path := strings.TrimPrefix(secret.Value, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
secret.Value = string(out)
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
_, err = client.OrgSecretUpdate(orgID, secret)
return err
}

View file

@ -0,0 +1,74 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"fmt"
"html/template"
"os"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
)
var secretShowCmd = &cli.Command{
Name: "show",
Usage: "show secret information",
ArgsUsage: "[repo-id|repo-full-name]",
Action: secretShow,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "name",
Usage: "secret name",
},
common.FormatFlag(tmplSecretList, true),
},
}
func secretShow(ctx context.Context, c *cli.Command) error {
var (
secretName = c.String("name")
format = c.String("format") + "\n"
)
if secretName == "" {
return fmt.Errorf("secret name is missing")
}
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
secret, err := client.OrgSecret(orgID, secretName)
if err != nil {
return err
}
tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(format)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, secret)
}

View file

@ -9,7 +9,7 @@ import (
"text/tabwriter" "text/tabwriter"
"unicode" "unicode"
"github.com/mitchellh/mapstructure" "github.com/go-viper/mapstructure/v2"
) )
// NewTable creates a new Table. // NewTable creates a new Table.
@ -147,12 +147,12 @@ func (o *Table) Write(columns []string, obj any) error {
colName := strings.ToLower(col) colName := strings.ToLower(col)
if alias, ok := o.fieldAlias[colName]; ok { if alias, ok := o.fieldAlias[colName]; ok {
if fn, ok := o.fieldMapping[alias]; ok { if fn, ok := o.fieldMapping[alias]; ok {
out = append(out, fn(obj)) out = append(out, sanitizeString(fn(obj)))
continue continue
} }
} }
if fn, ok := o.fieldMapping[colName]; ok { if fn, ok := o.fieldMapping[colName]; ok {
out = append(out, fn(obj)) out = append(out, sanitizeString(fn(obj)))
continue continue
} }
if value, ok := dataL[strings.ReplaceAll(colName, "_", "")]; ok { if value, ok := dataL[strings.ReplaceAll(colName, "_", "")]; ok {
@ -165,10 +165,10 @@ func (o *Table) Write(columns []string, obj any) error {
continue continue
} }
if s, ok := value.(string); ok { if s, ok := value.(string); ok {
out = append(out, NA(s)) out = append(out, NA(sanitizeString(s)))
continue continue
} }
out = append(out, fmt.Sprintf("%v", value)) out = append(out, sanitizeString(value))
} }
} }
_, _ = fmt.Fprintln(o.w, strings.Join(out, "\t")) _, _ = fmt.Fprintln(o.w, strings.Join(out, "\t"))
@ -201,3 +201,9 @@ func fieldName(name string) string {
} }
return string(out) return string(out)
} }
func sanitizeString(value any) string {
str := fmt.Sprintf("%v", value)
replacer := strings.NewReplacer("\n", " ", "\r", " ")
return strings.TrimSpace(replacer.Replace(str))
}

View file

@ -15,12 +15,13 @@
package pipeline package pipeline
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var pipelineApproveCmd = &cli.Command{ var pipelineApproveCmd = &cli.Command{
@ -30,9 +31,9 @@ var pipelineApproveCmd = &cli.Command{
Action: pipelineApprove, Action: pipelineApprove,
} }
func pipelineApprove(c *cli.Context) (err error) { func pipelineApprove(ctx context.Context, c *cli.Command) (err error) {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,13 +15,14 @@
package pipeline package pipeline
import ( import (
"context"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var pipelineCreateCmd = &cli.Command{ var pipelineCreateCmd = &cli.Command{
@ -42,9 +43,9 @@ var pipelineCreateCmd = &cli.Command{
}...), }...),
} }
func pipelineCreate(c *cli.Context) error { func pipelineCreate(ctx context.Context, c *cli.Command) error {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -73,5 +74,5 @@ func pipelineCreate(c *cli.Context) error {
return err return err
} }
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline}) return pipelineOutput(c, []*woodpecker.Pipeline{pipeline})
} }

View file

@ -15,12 +15,13 @@
package pipeline package pipeline
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var pipelineDeclineCmd = &cli.Command{ var pipelineDeclineCmd = &cli.Command{
@ -30,9 +31,9 @@ var pipelineDeclineCmd = &cli.Command{
Action: pipelineDecline, Action: pipelineDecline,
} }
func pipelineDecline(c *cli.Context) (err error) { func pipelineDecline(ctx context.Context, c *cli.Command) (err error) {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,16 +15,17 @@
package deploy package deploy
import ( import (
"context"
"fmt" "fmt"
"html/template" "html/template"
"os" "os"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
// Command exports the deploy command. // Command exports the deploy command.
@ -52,13 +53,13 @@ var Command = &cli.Command{
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "param", Name: "param",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "custom parameters to be injected into the step environment. Format: KEY=value", Usage: "custom parameters to inject into the step environment. Format: KEY=value",
}, },
}, },
} }
func deploy(c *cli.Context) error { func deploy(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -79,14 +80,14 @@ func deploy(c *cli.Context) error {
return err return err
} }
branch = repo.DefaultBranch branch = repo.Branch
} }
pipelineArg := c.Args().Get(1) pipelineArg := c.Args().Get(1)
var number int64 var number int64
if pipelineArg == "last" { if pipelineArg == "last" {
// Fetch the pipeline number from the last pipeline // Fetch the pipeline number from the last pipeline
pipelines, err := client.PipelineList(repoID) pipelines, err := client.PipelineList(repoID, woodpecker.PipelineListOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -120,9 +121,12 @@ func deploy(c *cli.Context) error {
return fmt.Errorf("please specify the target environment (i.e. production)") return fmt.Errorf("please specify the target environment (i.e. production)")
} }
params := internal.ParseKeyPair(c.StringSlice("param")) opt := woodpecker.DeployOptions{
DeployTo: env,
Params: internal.ParseKeyPair(c.StringSlice("param")),
}
deploy, err := client.Deploy(repoID, number, env, params) deploy, err := client.Deploy(repoID, number, opt)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,12 +15,13 @@
package pipeline package pipeline
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var pipelineKillCmd = &cli.Command{ var pipelineKillCmd = &cli.Command{
@ -31,14 +32,14 @@ var pipelineKillCmd = &cli.Command{
Hidden: true, Hidden: true,
} }
func pipelineKill(c *cli.Context) (err error) { func pipelineKill(ctx context.Context, c *cli.Command) (err error) {
number, err := strconv.ParseInt(c.Args().Get(1), 10, 64) number, err := strconv.ParseInt(c.Args().Get(1), 10, 64)
if err != nil { if err != nil {
return err return err
} }
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -47,7 +48,7 @@ func pipelineKill(c *cli.Context) (err error) {
return err return err
} }
err = client.PipelineKill(repoID, number) err = client.PipelineDelete(repoID, number)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,16 +15,18 @@
package pipeline package pipeline
import ( import (
"github.com/urfave/cli/v2" "context"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var pipelineLastCmd = &cli.Command{ var pipelineLastCmd = &cli.Command{
Name: "last", Name: "last",
Usage: "show latest pipeline details", Usage: "show latest pipeline information",
ArgsUsage: "<repo-id|repo-full-name>", ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineLast, Action: pipelineLast,
Flags: append(common.OutputFlags("table"), []cli.Flag{ Flags: append(common.OutputFlags("table"), []cli.Flag{
@ -36,9 +38,9 @@ var pipelineLastCmd = &cli.Command{
}...), }...),
} }
func pipelineLast(c *cli.Context) error { func pipelineLast(ctx context.Context, c *cli.Command) error {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -47,10 +49,14 @@ func pipelineLast(c *cli.Context) error {
return err return err
} }
pipeline, err := client.PipelineLast(repoID, c.String("branch")) opt := woodpecker.PipelineLastOptions{
Branch: c.String("branch"),
}
pipeline, err := client.PipelineLast(repoID, opt)
if err != nil { if err != nil {
return err return err
} }
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline}) return pipelineOutput(c, []*woodpecker.Pipeline{pipeline})
} }

View file

@ -15,88 +15,114 @@
package pipeline package pipeline
import ( import (
"github.com/urfave/cli/v2" "context"
"time"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
//nolint:mnd //nolint:mnd
var pipelineListCmd = &cli.Command{ func buildPipelineListCmd() *cli.Command {
Name: "ls", return &cli.Command{
Usage: "show pipeline history", Name: "ls",
ArgsUsage: "<repo-id|repo-full-name>", Usage: "show pipeline history",
Action: List, ArgsUsage: "<repo-id|repo-full-name>",
Flags: append(common.OutputFlags("table"), []cli.Flag{ Action: List,
&cli.StringFlag{ Flags: append(common.OutputFlags("table"), []cli.Flag{
Name: "branch", &cli.StringFlag{
Usage: "branch filter", Name: "branch",
}, Usage: "branch filter",
&cli.StringFlag{ },
Name: "event", &cli.StringFlag{
Usage: "event filter", Name: "event",
}, Usage: "event filter",
&cli.StringFlag{ },
Name: "status", &cli.StringFlag{
Usage: "status filter", Name: "status",
}, Usage: "status filter",
&cli.IntFlag{ },
Name: "limit", &cli.IntFlag{
Usage: "limit the list size", Name: "limit",
Value: 25, Usage: "limit the list size",
}, Value: 25,
}...), },
&cli.TimestampFlag{
Name: "before",
Usage: "only return pipelines before this date (RFC3339)",
Config: cli.TimestampConfig{
Layouts: []string{
time.RFC3339,
},
},
},
&cli.TimestampFlag{
Name: "after",
Usage: "only return pipelines after this date (RFC3339)",
Config: cli.TimestampConfig{
Layouts: []string{
time.RFC3339,
},
},
},
}...),
}
} }
func List(c *cli.Context) error { func List(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
resources, err := pipelineList(c, client) pipelines, err := pipelineList(c, client)
if err != nil { if err != nil {
return err return err
} }
return pipelineOutput(c, resources) return pipelineOutput(c, pipelines)
} }
func pipelineList(c *cli.Context, client woodpecker.Client) ([]woodpecker.Pipeline, error) { func pipelineList(c *cli.Command, client woodpecker.Client) ([]*woodpecker.Pipeline, error) {
resources := make([]woodpecker.Pipeline, 0)
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
repoID, err := internal.ParseRepo(client, repoIDOrFullName) repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil { if err != nil {
return resources, err return nil, err
} }
pipelines, err := client.PipelineList(repoID) opt := woodpecker.PipelineListOptions{}
if err != nil {
return resources, err if before := c.Timestamp("before"); !before.IsZero() {
opt.Before = before
}
if after := c.Timestamp("after"); !after.IsZero() {
opt.After = after
} }
branch := c.String("branch") branch := c.String("branch")
event := c.String("event") event := c.String("event")
status := c.String("status") status := c.String("status")
limit := c.Int("limit") limit := int(c.Int("limit"))
var count int pipelines, err := shared_utils.Paginate(func(page int) ([]*woodpecker.Pipeline, error) {
for _, pipeline := range pipelines { return client.PipelineList(repoID,
if count >= limit { woodpecker.PipelineListOptions{
break ListOptions: woodpecker.ListOptions{
} Page: page,
if branch != "" && pipeline.Branch != branch { },
continue Before: opt.Before,
} After: opt.After,
if event != "" && pipeline.Event != event { Branch: branch,
continue Events: []string{event},
} Status: status,
if status != "" && pipeline.Status != status { },
continue )
} }, limit)
resources = append(resources, *pipeline) if err != nil {
count++ return nil, err
} }
return resources, nil return pipelines, nil
} }

View file

@ -1,16 +1,17 @@
package pipeline package pipeline
import ( import (
"context"
"errors" "errors"
"io" "io"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker/mocks" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker/mocks"
) )
func TestPipelineList(t *testing.T) { func TestPipelineList(t *testing.T) {
@ -21,7 +22,7 @@ func TestPipelineList(t *testing.T) {
pipelines []*woodpecker.Pipeline pipelines []*woodpecker.Pipeline
pipelineErr error pipelineErr error
args []string args []string
expected []woodpecker.Pipeline expected []*woodpecker.Pipeline
wantErr error wantErr error
}{ }{
{ {
@ -33,53 +34,12 @@ func TestPipelineList(t *testing.T) {
{ID: 3, Branch: "main", Event: "push", Status: "failure"}, {ID: 3, Branch: "main", Event: "push", Status: "failure"},
}, },
args: []string{"ls", "repo/name"}, args: []string{"ls", "repo/name"},
expected: []woodpecker.Pipeline{ expected: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"}, {ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"}, {ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"}, {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", name: "limit results",
repoID: 1, repoID: 1,
@ -89,7 +49,7 @@ func TestPipelineList(t *testing.T) {
{ID: 3, Branch: "main", Event: "push", Status: "failure"}, {ID: 3, Branch: "main", Event: "push", Status: "failure"},
}, },
args: []string{"ls", "--limit", "2", "repo/name"}, args: []string{"ls", "--limit", "2", "repo/name"},
expected: []woodpecker.Pipeline{ expected: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"}, {ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"}, {ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
}, },
@ -106,14 +66,20 @@ func TestPipelineList(t *testing.T) {
for _, tt := range testtases { for _, tt := range testtases {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockClient := mocks.NewClient(t) mockClient := mocks.NewClient(t)
mockClient.On("PipelineList", mock.Anything).Return(tt.pipelines, tt.pipelineErr) mockClient.On("PipelineList", mock.Anything, mock.Anything).Return(func(_ int64, opt woodpecker.PipelineListOptions) ([]*woodpecker.Pipeline, error) {
if tt.pipelineErr != nil {
return nil, tt.pipelineErr
}
if opt.Page == 1 {
return tt.pipelines, nil
}
return []*woodpecker.Pipeline{}, nil
}).Maybe()
mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil) mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil)
app := &cli.App{Writer: io.Discard} command := buildPipelineListCmd()
c := cli.NewContext(app, nil, nil) command.Writer = io.Discard
command.Action = func(_ context.Context, c *cli.Command) error {
command := pipelineListCmd
command.Action = func(c *cli.Context) error {
pipelines, err := pipelineList(c, mockClient) pipelines, err := pipelineList(c, mockClient)
if tt.wantErr != nil { if tt.wantErr != nil {
assert.EqualError(t, err, tt.wantErr.Error()) assert.EqualError(t, err, tt.wantErr.Error())
@ -126,7 +92,7 @@ func TestPipelineList(t *testing.T) {
return nil return nil
} }
_ = command.Run(c, tt.args...) _ = command.Run(context.Background(), tt.args)
}) })
} }
} }

View file

@ -15,14 +15,15 @@
package log package log
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// Command exports the log command set. // Command exports the log command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "log", Name: "log",
Usage: "manage logs", Usage: "manage logs",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
logPurgeCmd, logPurgeCmd,
logShowCmd,
}, },
} }

View file

@ -15,55 +15,64 @@
package log package log
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var logPurgeCmd = &cli.Command{ var logPurgeCmd = &cli.Command{
Name: "purge", Name: "purge",
Usage: "purge a log", Usage: "purge a log",
ArgsUsage: "<repo-id|repo-full-name> <pipeline> [step]", ArgsUsage: "<repo-id|repo-full-name> <pipeline> [step-number|step-name]",
Action: logPurge, Action: logPurge,
} }
func logPurge(c *cli.Context) (err error) { func logPurge(ctx context.Context, c *cli.Command) (err error) {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
if len(repoIDOrFullName) == 0 {
return fmt.Errorf("missing required argument repo-id / repo-full-name")
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName) repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil { if err != nil {
return err return fmt.Errorf("invalid repo '%s': %w", repoIDOrFullName, err)
} }
number, err := strconv.ParseInt(c.Args().Get(1), 10, 64)
pipelineArg := c.Args().Get(1)
if len(pipelineArg) == 0 {
return fmt.Errorf("missing required argument pipeline")
}
number, err := strconv.ParseInt(pipelineArg, 10, 64)
if err != nil { if err != nil {
return err return err
} }
stepArg := c.Args().Get(2) //nolint:mnd stepArg := c.Args().Get(2) //nolint:mnd
// TODO: Add lookup by name: stepID, err := internal.ParseStep(client, repoID, stepIDOrName)
var stepID int64 var stepID int64
if len(stepArg) != 0 { if len(stepArg) != 0 {
stepID, err = strconv.ParseInt(stepArg, 10, 64) stepID, err = internal.ParseStep(client, repoID, number, stepArg)
if err != nil { if err != nil {
return err return err
} }
} }
if stepID > 0 { if stepID > 0 {
fmt.Printf("Purging logs for pipeline %s#%d step %d\n", repoIDOrFullName, number, stepID)
err = client.StepLogsPurge(repoID, number, stepID) err = client.StepLogsPurge(repoID, number, stepID)
} else { } else {
fmt.Printf("Purging logs for pipeline %s#%d\n", repoIDOrFullName, number)
err = client.LogsPurge(repoID, number) err = client.LogsPurge(repoID, number)
} }
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("Purging logs for pipeline %s#%d\n", repoIDOrFullName, number)
return nil return nil
} }

View file

@ -0,0 +1,112 @@
// Copyright 2022 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
"context"
"fmt"
"os"
"strconv"
"text/template"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
var logShowCmd = &cli.Command{
Name: "show",
Usage: "show pipeline logs",
ArgsUsage: "<repo-id|repo-full-name> <pipeline> [step-number|step-name]",
Action: logShow,
}
func logShow(ctx context.Context, c *cli.Command) error {
repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
if len(repoIDOrFullName) == 0 {
return fmt.Errorf("missing required argument repo-id / repo-full-name")
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return fmt.Errorf("invalid repo '%s': %w ", repoIDOrFullName, err)
}
pipelineArg := c.Args().Get(1)
if len(pipelineArg) == 0 {
return fmt.Errorf("missing required argument pipeline")
}
number, err := strconv.ParseInt(pipelineArg, 10, 64)
if err != nil {
return fmt.Errorf("invalid pipeline '%s': %w", pipelineArg, err)
}
stepArg := c.Args().Get(2) //nolint:mnd
if len(stepArg) == 0 {
return pipelineLog(client, repoID, number)
}
step, err := internal.ParseStep(client, repoID, number, stepArg)
if err != nil {
return fmt.Errorf("invalid step '%s': %w", stepArg, err)
}
return stepLog(client, repoID, number, step)
}
func pipelineLog(client woodpecker.Client, repoID, number int64) error {
pipeline, err := client.Pipeline(repoID, number)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(tmplPipelineLogs + "\n")
if err != nil {
return err
}
for _, workflow := range pipeline.Workflows {
for _, step := range workflow.Children {
if err := tmpl.Execute(os.Stdout, map[string]any{"workflow": workflow, "step": step}); err != nil {
return err
}
err := stepLog(client, repoID, number, step.ID)
if err != nil {
return err
}
}
}
return nil
}
func stepLog(client woodpecker.Client, repoID, number, step int64) error {
logs, err := client.StepLogEntries(repoID, number, step)
if err != nil {
return err
}
for _, log := range logs {
fmt.Println(string(log.Data))
}
return nil
}
// template for pipeline ps information.
var tmplPipelineLogs = "\x1b[33m{{ .workflow.Name }} > {{ .step.Name }} (#{{ .step.PID }}):\x1b[0m"

View file

@ -1,66 +0,0 @@
// Copyright 2022 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pipeline
import (
"fmt"
"strconv"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var pipelineLogsCmd = &cli.Command{
Name: "logs",
Usage: "show pipeline logs",
ArgsUsage: "<repo-id|repo-full-name> [pipeline] [stepID]",
Action: pipelineLogs,
}
func pipelineLogs(c *cli.Context) error {
repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return err
}
numberArgIndex := 1
number, err := strconv.ParseInt(c.Args().Get(numberArgIndex), 10, 64)
if err != nil {
return err
}
stepArgIndex := 2
step, err := strconv.ParseInt(c.Args().Get(stepArgIndex), 10, 64)
if err != nil {
return err
}
logs, err := client.StepLogEntries(repoID, number, step)
if err != nil {
return err
}
for _, log := range logs {
fmt.Print(string(log.Data))
}
return nil
}

View file

@ -20,33 +20,37 @@ import (
"os" "os"
"text/template" "text/template"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/output" "go.woodpecker-ci.org/woodpecker/v3/cli/output"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/cli/pipeline/deploy"
"go.woodpecker-ci.org/woodpecker/v3/cli/pipeline/log"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
// Command exports the pipeline command set. // Command exports the pipeline command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "pipeline", Name: "pipeline",
Usage: "manage pipelines", Usage: "manage pipelines",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
pipelineListCmd,
pipelineLastCmd,
pipelineLogsCmd,
pipelineInfoCmd,
pipelineStopCmd,
pipelineStartCmd,
pipelineApproveCmd, pipelineApproveCmd,
pipelineDeclineCmd,
pipelineQueueCmd,
pipelineKillCmd,
pipelinePsCmd,
pipelineCreateCmd, pipelineCreateCmd,
pipelineDeclineCmd,
deploy.Command,
pipelineKillCmd,
pipelineLastCmd,
buildPipelineListCmd(),
log.Command,
pipelinePsCmd,
pipelinePurgeCmd,
pipelineQueueCmd,
pipelineShowCmd,
pipelineStartCmd,
pipelineStopCmd,
}, },
} }
func pipelineOutput(c *cli.Context, resources []woodpecker.Pipeline, fd ...io.Writer) error { func pipelineOutput(c *cli.Command, pipelines []*woodpecker.Pipeline, fd ...io.Writer) error {
outFmt, outOpt := output.ParseOutputOptions(c.String("output")) outFmt, outOpt := output.ParseOutputOptions(c.String("output"))
noHeader := c.Bool("output-no-headers") noHeader := c.Bool("output-no-headers")
@ -70,7 +74,7 @@ func pipelineOutput(c *cli.Context, resources []woodpecker.Pipeline, fd ...io.Wr
if err != nil { if err != nil {
return err return err
} }
if err := tmpl.Execute(out, resources); err != nil { if err := tmpl.Execute(out, pipelines); err != nil {
return err return err
} }
case "table": case "table":
@ -85,7 +89,7 @@ func pipelineOutput(c *cli.Context, resources []woodpecker.Pipeline, fd ...io.Wr
if !noHeader { if !noHeader {
table.WriteHeader(cols) table.WriteHeader(cols)
} }
for _, resource := range resources { for _, resource := range pipelines {
if err := table.Write(cols, resource); err != nil { if err := table.Write(cols, resource); err != nil {
return err return err
} }

View file

@ -2,14 +2,15 @@ package pipeline
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
func TestPipelineOutput(t *testing.T) { func TestPipelineOutput(t *testing.T) {
@ -22,7 +23,7 @@ func TestPipelineOutput(t *testing.T) {
{ {
name: "table output with default columns", name: "table output with default columns",
args: []string{}, args: []string{},
expected: "NUMBER STATUS EVENT BRANCH MESSAGE AUTHOR\n1 success push main message John Doe\n", expected: "NUMBER STATUS EVENT BRANCH MESSAGE AUTHOR\n1 success push main message multiline John Doe\n",
}, },
{ {
name: "table output with custom columns", name: "table output with custom columns",
@ -32,7 +33,7 @@ func TestPipelineOutput(t *testing.T) {
{ {
name: "table output with no header", name: "table output with no header",
args: []string{"output", "--output-no-headers"}, args: []string{"output", "--output-no-headers"},
expected: "1 success push main message John Doe\n", expected: "1 success push main message multiline John Doe\n",
}, },
{ {
name: "go-template output", name: "go-template output",
@ -46,41 +47,40 @@ func TestPipelineOutput(t *testing.T) {
}, },
} }
pipelines := []woodpecker.Pipeline{ pipelines := []*woodpecker.Pipeline{
{ {
Number: 1, Number: 1,
Status: "success", Status: "success",
Event: "push", Event: "push",
Branch: "main", Branch: "main",
Message: "message", Message: "message\nmultiline",
Author: "John Doe", Author: "John Doe\n",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
app := &cli.App{Writer: io.Discard} command := &cli.Command{
c := cli.NewContext(app, nil, nil) Writer: io.Discard,
Name: "output",
Flags: common.OutputFlags("table"),
Action: func(_ context.Context, c *cli.Command) error {
var buf bytes.Buffer
err := pipelineOutput(c, pipelines, &buf)
command := &cli.Command{} if tt.wantErr {
command.Name = "output" assert.Error(t, err)
command.Flags = common.OutputFlags("table") return nil
command.Action = func(c *cli.Context) error { }
var buf bytes.Buffer
err := pipelineOutput(c, pipelines, &buf) assert.NoError(t, err)
assert.Equal(t, tt.expected, buf.String())
if tt.wantErr {
assert.Error(t, err)
return nil return nil
} },
assert.NoError(t, err)
assert.Equal(t, tt.expected, buf.String())
return nil
} }
_ = command.Run(c, tt.args...) _ = command.Run(context.Background(), tt.args)
}) })
} }
} }

View file

@ -15,33 +15,36 @@
package pipeline package pipeline
import ( import (
"context"
"fmt"
"os" "os"
"strconv" "strconv"
"text/template" "text/template"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var pipelinePsCmd = &cli.Command{ var pipelinePsCmd = &cli.Command{
Name: "ps", Name: "ps",
Usage: "show pipeline steps", Usage: "show pipeline steps",
ArgsUsage: "<repo-id|repo-full-name> [pipeline]", ArgsUsage: "<repo-id|repo-full-name> <pipeline>",
Action: pipelinePs, Action: pipelinePs,
Flags: []cli.Flag{common.FormatFlag(tmplPipelinePs)}, Flags: []cli.Flag{common.FormatFlag(tmplPipelinePs)},
} }
func pipelinePs(c *cli.Context) error { func pipelinePs(ctx context.Context, c *cli.Command) error {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
repoID, err := internal.ParseRepo(client, repoIDOrFullName) repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil { if err != nil {
return err return fmt.Errorf("invalid repo '%s': %w", repoIDOrFullName, err)
} }
pipelineArg := c.Args().Get(1) pipelineArg := c.Args().Get(1)
@ -49,7 +52,7 @@ func pipelinePs(c *cli.Context) error {
if pipelineArg == "last" || len(pipelineArg) == 0 { if pipelineArg == "last" || len(pipelineArg) == 0 {
// Fetch the pipeline number from the last pipeline // Fetch the pipeline number from the last pipeline
pipeline, err := client.PipelineLast(repoID, "") pipeline, err := client.PipelineLast(repoID, woodpecker.PipelineLastOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -58,7 +61,7 @@ func pipelinePs(c *cli.Context) error {
} else { } else {
number, err = strconv.ParseInt(pipelineArg, 10, 64) number, err = strconv.ParseInt(pipelineArg, 10, 64)
if err != nil { if err != nil {
return err return fmt.Errorf("invalid pipeline '%s': %w", pipelineArg, err)
} }
} }
@ -72,9 +75,9 @@ func pipelinePs(c *cli.Context) error {
return err return err
} }
for _, step := range pipeline.Workflows { for _, workflow := range pipeline.Workflows {
for _, child := range step.Children { for _, step := range workflow.Children {
if err := tmpl.Execute(os.Stdout, child); err != nil { if err := tmpl.Execute(os.Stdout, map[string]any{"workflow": workflow, "step": step}); err != nil {
return err return err
} }
} }
@ -83,8 +86,11 @@ func pipelinePs(c *cli.Context) error {
return nil return nil
} }
// Template for pipeline ps information. // template for pipeline ps information.
var tmplPipelinePs = "\x1b[33mStep #{{ .PID }} \x1b[0m" + ` var tmplPipelinePs = "\x1b[33m{{ .workflow.Name }} > {{ .step.Name }} (#{{ .step.PID }}):\x1b[0m" + `
Step: {{ .Name }} Step: {{ .step.Name }}
State: {{ .State }} Started: {{ .step.Started }}
Stopped: {{ .step.Stopped }}
Type: {{ .step.Type }}
State: {{ .step.State }}
` `

164
cli/pipeline/purge.go Normal file
View file

@ -0,0 +1,164 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pipeline
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/cli/internal"
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
)
//nolint:mnd
var pipelinePurgeCmd = &cli.Command{
Name: "purge",
Usage: "purge pipelines",
ArgsUsage: "<repo-id|repo-full-name>",
Action: Purge,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "older-than",
Usage: "remove pipelines older than the specified time limit",
Required: true,
},
&cli.IntFlag{
Name: "keep-min",
Usage: "minimum number of pipelines to keep",
Value: 10,
},
&cli.BoolFlag{
Name: "dry-run",
Usage: "disable non-read api calls",
Value: false,
},
},
}
func Purge(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(ctx, c)
if err != nil {
return err
}
return pipelinePurge(c, client)
}
func pipelinePurge(c *cli.Command, client woodpecker.Client) (err error) {
repoIDOrFullName := c.Args().First()
if len(repoIDOrFullName) == 0 {
return fmt.Errorf("missing required argument repo-id / repo-full-name")
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return fmt.Errorf("invalid repo '%s': %w", repoIDOrFullName, err)
}
olderThan := c.String("older-than")
keepMin := c.Int("keep-min")
dryRun := c.Bool("dry-run")
duration, err := time.ParseDuration(olderThan)
if err != nil {
return err
}
var pipelinesKeep []*woodpecker.Pipeline
if keepMin > 0 {
pipelinesKeep, err = fetchPipelinesToKeep(client, repoID, int(keepMin))
if err != nil {
return err
}
}
pipelines, err := fetchPipelines(client, repoID, duration)
if err != nil {
return err
}
// Create a map of pipeline IDs to keep
keepMap := make(map[int64]struct{})
for _, p := range pipelinesKeep {
keepMap[p.Number] = struct{}{}
}
// Filter pipelines to only include those not in keepMap
var pipelinesToPurge []*woodpecker.Pipeline
for _, p := range pipelines {
if _, exists := keepMap[p.Number]; !exists {
pipelinesToPurge = append(pipelinesToPurge, p)
}
}
msgPrefix := ""
if dryRun {
msgPrefix = "DRY-RUN: "
}
for i, p := range pipelinesToPurge {
// cspell:words spurge
log.Debug().Msgf("%spurge %v/%v pipelines from repo '%v' (pipeline %v)", msgPrefix, i+1, len(pipelinesToPurge), repoIDOrFullName, p.Number)
if dryRun {
continue
}
err := client.PipelineDelete(repoID, p.Number)
if err != nil {
var clientErr *woodpecker.ClientError
if errors.As(err, &clientErr) && clientErr.StatusCode == http.StatusUnprocessableEntity {
log.Error().Err(err).Msgf("failed to delete pipeline %d", p.Number)
continue
}
return err
}
}
return nil
}
func fetchPipelinesToKeep(client woodpecker.Client, repoID int64, keepMin int) ([]*woodpecker.Pipeline, error) {
if keepMin <= 0 {
return nil, nil
}
return shared_utils.Paginate(func(page int) ([]*woodpecker.Pipeline, error) {
return client.PipelineList(repoID,
woodpecker.PipelineListOptions{
ListOptions: woodpecker.ListOptions{
Page: page,
},
},
)
}, keepMin)
}
func fetchPipelines(client woodpecker.Client, repoID int64, duration time.Duration) ([]*woodpecker.Pipeline, error) {
return shared_utils.Paginate(func(page int) ([]*woodpecker.Pipeline, error) {
return client.PipelineList(repoID,
woodpecker.PipelineListOptions{
ListOptions: woodpecker.ListOptions{
Page: page,
},
Before: time.Now().Add(-duration),
},
)
}, -1)
}

126
cli/pipeline/purge_test.go Normal file
View file

@ -0,0 +1,126 @@
package pipeline
import (
"context"
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker/mocks"
)
func TestPipelinePurge(t *testing.T) {
tests := []struct {
name string
repoID int64
args []string
pipelinesKeep []*woodpecker.Pipeline
pipelines []*woodpecker.Pipeline
mockDeleteError error
wantDelete int
wantErr error
}{
{
name: "success with no pipelines to purge",
repoID: 1,
args: []string{"purge", "--older-than", "1h", "repo/name"},
pipelinesKeep: []*woodpecker.Pipeline{
{Number: 1},
},
pipelines: []*woodpecker.Pipeline{},
},
{
name: "success with pipelines to purge",
repoID: 1,
args: []string{"purge", "--older-than", "1h", "repo/name"},
pipelinesKeep: []*woodpecker.Pipeline{
{Number: 1},
},
pipelines: []*woodpecker.Pipeline{
{Number: 1},
{Number: 2},
{Number: 3},
},
wantDelete: 2,
},
{
name: "error on invalid duration",
repoID: 1,
args: []string{"purge", "--older-than", "invalid", "repo/name"},
wantErr: errors.New("time: invalid duration \"invalid\""),
},
{
name: "continue on 422 error",
repoID: 1,
args: []string{"purge", "--older-than", "1h", "repo/name"},
pipelinesKeep: []*woodpecker.Pipeline{
{Number: 1},
},
pipelines: []*woodpecker.Pipeline{
{Number: 1},
{Number: 2},
{Number: 3},
},
wantDelete: 2,
mockDeleteError: &woodpecker.ClientError{
StatusCode: 422,
Message: "test error",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks.NewClient(t)
mockClient.On("RepoLookup", mock.Anything).Maybe().Return(&woodpecker.Repo{ID: tt.repoID}, nil)
mockClient.On("PipelineList", mock.Anything, mock.Anything).Return(func(_ int64, opt woodpecker.PipelineListOptions) ([]*woodpecker.Pipeline, error) {
// Return keep pipelines for first call
if opt.Before.IsZero() {
if opt.Page == 1 {
return tt.pipelinesKeep, nil
}
return []*woodpecker.Pipeline{}, nil
}
// Return pipelines to purge for calls with Before filter
if !opt.Before.IsZero() {
if opt.Page == 1 {
return tt.pipelines, nil
}
return []*woodpecker.Pipeline{}, nil
}
return []*woodpecker.Pipeline{}, nil
}).Maybe()
if tt.mockDeleteError != nil {
mockClient.On("PipelineDelete", tt.repoID, mock.Anything).Return(tt.mockDeleteError)
} else if tt.wantDelete > 0 {
mockClient.On("PipelineDelete", tt.repoID, mock.Anything).Return(nil).Times(tt.wantDelete)
}
command := pipelinePurgeCmd
command.Writer = io.Discard
command.Action = func(_ context.Context, c *cli.Command) error {
err := pipelinePurge(c, mockClient)
if tt.wantErr != nil {
assert.EqualError(t, err, tt.wantErr.Error())
return nil
}
assert.NoError(t, err)
return nil
}
_ = command.Run(context.Background(), tt.args)
})
}
}

View file

@ -15,14 +15,15 @@
package pipeline package pipeline
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"text/template" "text/template"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var pipelineQueueCmd = &cli.Command{ var pipelineQueueCmd = &cli.Command{
@ -33,8 +34,8 @@ var pipelineQueueCmd = &cli.Command{
Flags: []cli.Flag{common.FormatFlag(tmplPipelineQueue)}, Flags: []cli.Flag{common.FormatFlag(tmplPipelineQueue)},
} }
func pipelineQueue(c *cli.Context) error { func pipelineQueue(ctx context.Context, c *cli.Command) error {
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,26 +15,27 @@
package pipeline package pipeline
import ( import (
"context"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var pipelineInfoCmd = &cli.Command{ var pipelineShowCmd = &cli.Command{
Name: "info", Name: "show",
Usage: "show pipeline details", Usage: "show pipeline information",
ArgsUsage: "<repo-id|repo-full-name> [pipeline]", ArgsUsage: "<repo-id|repo-full-name> [pipeline]",
Action: pipelineInfo, Action: pipelineShow,
Flags: common.OutputFlags("table"), Flags: common.OutputFlags("table"),
} }
func pipelineInfo(c *cli.Context) error { func pipelineShow(ctx context.Context, c *cli.Command) error {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -47,7 +48,7 @@ func pipelineInfo(c *cli.Context) error {
var number int64 var number int64
if pipelineArg == "last" || len(pipelineArg) == 0 { if pipelineArg == "last" || len(pipelineArg) == 0 {
// Fetch the pipeline number from the last pipeline // Fetch the pipeline number from the last pipeline
pipeline, err := client.PipelineLast(repoID, "") pipeline, err := client.PipelineLast(repoID, woodpecker.PipelineLastOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -64,5 +65,5 @@ func pipelineInfo(c *cli.Context) error {
return err return err
} }
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline}) return pipelineOutput(c, []*woodpecker.Pipeline{pipeline})
} }

View file

@ -15,13 +15,15 @@
package pipeline package pipeline
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var pipelineStartCmd = &cli.Command{ var pipelineStartCmd = &cli.Command{
@ -33,14 +35,14 @@ var pipelineStartCmd = &cli.Command{
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "param", Name: "param",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "custom parameters to be injected into the step environment. Format: KEY=value", Usage: "custom parameters to inject into the step environment. Format: KEY=value",
}, },
}, },
} }
func pipelineStart(c *cli.Context) (err error) { func pipelineStart(ctx context.Context, c *cli.Command) (err error) {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -53,7 +55,7 @@ func pipelineStart(c *cli.Context) (err error) {
var number int64 var number int64
if pipelineArg == "last" { if pipelineArg == "last" {
// Fetch the pipeline number from the last pipeline // Fetch the pipeline number from the last pipeline
pipeline, err := client.PipelineLast(repoID, "") pipeline, err := client.PipelineLast(repoID, woodpecker.PipelineLastOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -68,9 +70,11 @@ func pipelineStart(c *cli.Context) (err error) {
} }
} }
params := internal.ParseKeyPair(c.StringSlice("param")) opt := woodpecker.PipelineStartOptions{
Params: internal.ParseKeyPair(c.StringSlice("param")),
}
pipeline, err := client.PipelineStart(repoID, number, params) pipeline, err := client.PipelineStart(repoID, number, opt)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,12 +15,13 @@
package pipeline package pipeline
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
) )
var pipelineStopCmd = &cli.Command{ var pipelineStopCmd = &cli.Command{
@ -30,9 +31,9 @@ var pipelineStopCmd = &cli.Command{
Action: pipelineStop, Action: pipelineStop,
} }
func pipelineStop(c *cli.Context) (err error) { func pipelineStop(ctx context.Context, c *cli.Command) (err error) {
repoIDOrFullName := c.Args().First() repoIDOrFullName := c.Args().First()
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,18 +15,18 @@
package cron package cron
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// Command exports the cron command set. // Command exports the cron command set.
var Command = &cli.Command{ var Command = &cli.Command{
Name: "cron", Name: "cron",
Usage: "manage cron jobs", Usage: "manage cron jobs",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
cronCreateCmd, cronCreateCmd,
cronDeleteCmd, cronDeleteCmd,
cronUpdateCmd,
cronInfoCmd,
cronListCmd, cronListCmd,
cronShowCmd,
cronUpdateCmd,
}, },
} }

View file

@ -15,14 +15,15 @@
package cron package cron
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var cronCreateCmd = &cli.Command{ var cronCreateCmd = &cli.Command{
@ -50,9 +51,9 @@ var cronCreateCmd = &cli.Command{
}, },
} }
func cronCreate(c *cli.Context) error { func cronCreate(ctx context.Context, c *cli.Command) error {
var ( var (
jobName = c.String("name") cronName = c.String("name")
branch = c.String("branch") branch = c.String("branch")
schedule = c.String("schedule") schedule = c.String("schedule")
repoIDOrFullName = c.String("repository") repoIDOrFullName = c.String("repository")
@ -62,7 +63,7 @@ func cronCreate(c *cli.Context) error {
repoIDOrFullName = c.Args().First() repoIDOrFullName = c.Args().First()
} }
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -73,7 +74,7 @@ func cronCreate(c *cli.Context) error {
} }
cron := &woodpecker.Cron{ cron := &woodpecker.Cron{
Name: jobName, Name: cronName,
Branch: branch, Branch: branch,
Schedule: schedule, Schedule: schedule,
} }

View file

@ -15,13 +15,15 @@
package cron package cron
import ( import (
"context"
"html/template" "html/template"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v3/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal" "go.woodpecker-ci.org/woodpecker/v3/cli/internal"
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
) )
var cronListCmd = &cli.Command{ var cronListCmd = &cli.Command{
@ -35,7 +37,7 @@ var cronListCmd = &cli.Command{
}, },
} }
func cronList(c *cli.Context) error { func cronList(ctx context.Context, c *cli.Command) error {
var ( var (
format = c.String("format") + "\n" format = c.String("format") + "\n"
repoIDOrFullName = c.String("repository") repoIDOrFullName = c.String("repository")
@ -43,7 +45,7 @@ func cronList(c *cli.Context) error {
if repoIDOrFullName == "" { if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First() repoIDOrFullName = c.Args().First()
} }
client, err := internal.NewClient(c) client, err := internal.NewClient(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@ -51,7 +53,8 @@ func cronList(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
list, err := client.CronList(repoID) opt := woodpecker.CronListOptions{}
list, err := client.CronList(repoID, opt)
if err != nil { if err != nil {
return err return err
} }

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