Compare commits

..

22 Commits

Author SHA1 Message Date
Bo-Yi Wu 747d7b23d1 docs: update Jenkins API token creation instructions and images
- Update instructions to select "Security" instead of "Configure" when creating a Jenkins API token
- Replace the token creation image with a new screenshot
- Remove the old jenkins-token.png image and add personal-token.png

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:45:49 +08:00
Bo-Yi Wu 48d5425edd ci: use Docker-based linting in CI workflow
- Update the lint workflow to use the Dockerfile located in the docker directory

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:41:10 +08:00
Bo-Yi Wu cda84c122e feat: refactor plugin config validation and enhance job error handling
- Refactor plugin configuration validation into a dedicated method with improved error messages
- Add utility functions for trimming whitespace from slices and parsing key=value parameters
- Improve error handling for missing or invalid job names and parameters
- Enhance Exec to validate configuration, clean job list, and parse parameters before triggering jobs
- Update job triggering to provide more descriptive error reporting
- Replace deprecated and less precise test assertions with more explicit ones
- Add extensive unit tests for configuration validation, whitespace trimming, parameter parsing, and job triggering (including multiple jobs, parameters, remote token, and job lists with whitespace)
- Improve test coverage and reliability for plugin behavior and edge cases

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:39:36 +08:00
Bo-Yi Wu afbea8106e refactor: refactor Jenkins struct by removing unused methods
- Remove the parseResponse method from the Jenkins struct

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:29:58 +08:00
Bo-Yi Wu cc67514a1a docs: revamp README with expanded setup, usage, and integration docs
- Rewrite and expand the README with a new structure, including a Table of Contents, Features, Prerequisites, Installation, Configuration, Usage, Development, License, and Contributing sections
- Add detailed installation instructions for binary, source, and Docker image
- Provide step-by-step Jenkins server setup and authentication guidance
- Introduce a comprehensive parameters reference table for configuration
- Clarify authentication requirements and usage scenarios for CLI, Docker, and Drone CI
- Update environment variable names for consistency (use JENKINS_URL instead of JENKINS_BASE_URL)
- Add multiple usage examples for single/multiple jobs and parameterized builds
- Include instructions and examples for integrating with Drone CI pipelines
- Add development instructions for building, testing, and test coverage
- Add license and contributing information at the end of the README

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:28:52 +08:00
Bo-Yi Wu 3d7ffaed68 build: ensure Go build/install uses explicit current directory
- Add explicit current directory (.) to Go build and install commands to ensure correct build context

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:25:51 +08:00
Bo-Yi Wu 52abef124e refactor: improve CLI robustness and Jenkins integration
- Add missing .PHONY targets to the Makefile for better build reliability
- Ensure HTTP response bodies are always read and closed in Jenkins post requests
- Replace custom response parsing with direct JSON unmarshalling in Jenkins post
- Set a default value for the Version variable
- Move ASCII art to a constant and reuse for CLI help template
- Improve dotenv loading error handling and logging in main
- Update repository link in CLI help output
- Add validation for required CLI parameters and authentication in run function

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:23:52 +08:00
Bo-Yi Wu 3dd86f956c feat: add remote trigger token support for Jenkins job execution
- Add support for passing a remote trigger token to Jenkins jobs
- Update the Jenkins constructor to accept a token parameter
- Ensure the token is included as a query parameter when triggering jobs
- Improve error reporting by including response body in error messages
- Remove unnecessary logging and refactor build parameter logic
- Update tests to use the new Jenkins constructor and token handling
- Add CLI option for specifying a remote trigger token
- Extend plugin configuration to support remote token injection

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 17:16:39 +08:00
Bo-Yi Wu a5469c939e refactor: run container as non-root dedicated drone user
- Add a dedicated drone user and group for running the container
- Change file ownership of the drone-jenkins binary to the drone user
- Switch container execution to use the drone user instead of root

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 16:32:52 +08:00
Steve Liang a6d967789d chore(api): check Jenkins response status (#39)
When triggering a Jenkins job, Jenkins API call returns 200 OK but drone-jenkins expects 201.  So, the trigger succeeded but drone-jenkins returns false negative.
2025-12-01 16:29:54 +08:00
Bo-Yi Wu 3eb2242053 chore: migrate CLI to urfave/cli/v2 and update flag handling
- Remove formatting and vetting targets from the Makefile
- Upgrade urfave/cli dependency from v1 to v2
- Update CLI flag definitions to use urfave/cli/v2 API, including new struct field names and flag aliasing
- Add indirect dependency on github.com/xrash/smetrics

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 16:27:57 +08:00
Bo-Yi Wu 908be474f3 docs: add Trivy Security Scan badge to README
- Add a Trivy Security Scan badge to the README

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 16:24:13 +08:00
Bo-Yi Wu de494cbda1 feat: add context-aware HTTP POST and improve CLI help formatting
- Add context support to HTTP POST requests for improved cancellation and timeout handling
- Fix formatting of usage and commands help text in the CLI output

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 16:23:07 +08:00
Bo-Yi Wu 77ea4873e0 ci: modernize CI workflows and enhance security scanning
- Update GitHub Actions to use newer versions for setup-go, checkout, golangci-lint, and codecov
- Change Go version specification to use "stable" and update test matrix to only "1.25"
- Rename the test job to testing
- Adjust hadolint to use a newer version and reference the Dockerfile at the root
- Modify go test command to enable race detection and test all packages
- Add a new Trivy security scan workflow for vulnerability, secret, and misconfiguration checks, including SARIF upload and log output

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 16:21:24 +08:00
Bo-Yi Wu 36013b246a build: configure golangci-lint with custom rules and exclusions
- Add golangci-lint configuration with a custom set of enabled linters and formatters
- Exclude generated files and specific directories from linting and formatting

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 15:36:14 +08:00
Bo-Yi Wu cbfb2bb51b chore: update documentation and Docker base image version
- Fix formatting of platform list in the README
- Update Docker base image from alpine:3.20 to alpine:3.22

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 15:35:05 +08:00
Bo-Yi Wu b6589abef3 chore: update dependencies to latest stable versions
- Update testify dependency to v1.10.0
- Update urfave/cli dependency to v1.22.17
- Update go-md2man/v2 indirect dependency to v2.0.7

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-01 15:33:45 +08:00
appleboy 626b9e4bfa refactor: refactor codebase to use plural form 'parameters'
- Rename `parameter` to `parameters` in CLI flags and environment variables
- Update function calls to use `parameters` instead of `parameter`
- Change struct field `Parameter` to `Parameters`
- Modify loop variable to iterate over `Parameters` instead of `Parameter`

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-06 08:34:10 +08:00
appleboy e60e4b9161 chore: improve logging and testing across the codebase
- Add logging for URL path in the `trigger` function
- Remove commented-out form data encoding line
- Import the `log` package

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-06 07:59:12 +08:00
appleboy c213337305 ci: enhance CI/CD pipeline with GoReleaser integration
- Replace `go-version` with `go-version-file` and add `check-latest` in GitHub Actions workflow
- Add `.goreleaser.yaml` configuration file with build, archive, checksum, snapshot, release, and changelog settings

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-05 22:26:32 +08:00
Bo-Yi Wu a7c6b81621 feat: support parameter handling for dynamic URL paths (#35)
- Comment out unused form data encoding in `post` method
- Remove redundant error logging in `post` method
- Add conditional URL path selection in `trigger` method based on parameters
- Add `parameter` flag to CLI options in `main.go`
- Include `parameter` in the `run` function configuration
- Import `net/url` package in `plugin.go`
- Add `Parameter` field to `plugin.go` struct
- Parse and add parameters to URL values in `Exec` method
- Pass parsed parameters to `trigger` method in `Exec` function

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-05 20:25:01 +08:00
appleboy bb1e9fe4e0 feat: improve logging and error handling in Jenkins plugin
- Add error handling for unexpected response codes in `post` method of `jenkins.go`
- Import `log` package in `plugin.go`
- Add logging for successful job trigger in `Exec` method of `plugin.go`

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-05 13:28:14 +08:00
17 changed files with 1075 additions and 231 deletions
+2 -1
View File
@@ -19,7 +19,8 @@ jobs:
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: "^1"
go-version-file: go.mod
check-latest: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
+13 -12
View File
@@ -9,27 +9,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "^1"
go-version: "stable"
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
version: v2.6
args: --verbose
- uses: hadolint/hadolint-action@v3.1.0
- uses: hadolint/hadolint-action@v3.3.0
name: hadolint for Dockerfile
with:
dockerfile: docker/Dockerfile
test:
testing:
strategy:
matrix:
os: [ubuntu-latest]
go: [1.22, 1.23]
go: ["1.25"]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
@@ -40,12 +41,12 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
@@ -59,9 +60,9 @@ jobs:
${{ runner.os }}-go-
- name: Run Tests
run: |
go test -v -covermode=atomic -coverprofile=coverage.out
go test -race -cover -coverprofile=coverage.out ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: ${{ matrix.os }},go-${{ matrix.go }}
+56
View File
@@ -0,0 +1,56 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run daily at 00:00 UTC
- cron: "0 0 * * *"
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
security-events: write # Required for uploading SARIF results
jobs:
trivy-scan:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: "fs"
scan-ref: "."
scanners: "vuln,secret,misconfig"
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-results.sarif"
- name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.33.1
if: always()
with:
scan-type: "fs"
scan-ref: "."
scanners: "vuln,secret,misconfig"
format: "table"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
exit-code: "1"
+51
View File
@@ -0,0 +1,51 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- exhaustive
- gochecknoinits
- goconst
- gocritic
- gocyclo
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- misspell
- nakedret
- noctx
- nolintlint
- rowserrcheck
- staticcheck
- unconvert
- unparam
- unused
- whitespace
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- goimports
- golines
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+123
View File
@@ -0,0 +1,123 @@
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
goarm:
- "5"
- "6"
- "7"
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: windows
goarch: arm
goarm: "5"
- goos: windows
goarch: arm
goarm: "6"
- goos: windows
goarch: arm
goarm: "7"
- goos: windows
goarch: arm64
- goos: freebsd
goarch: ppc64le
- goos: freebsd
goarch: s390x
- goos: freebsd
goarch: arm
goarm: "5"
- goos: freebsd
goarch: arm
goarm: "6"
- goos: freebsd
goarch: arm
goarm: "7"
- goos: freebsd
goarch: arm64
flags:
- -trimpath
ldflags:
- -s -w
- -X main.Version={{.Version}}
binary: >-
{{ .ProjectName }}-
{{- if .IsSnapshot }}{{ .Branch }}-
{{- else }}{{- .Version }}-{{ end }}
{{- .Os }}-
{{- if eq .Arch "amd64" }}amd64
{{- else if eq .Arch "amd64_v1" }}amd64
{{- else if eq .Arch "386" }}386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}-{{ .Arm }}{{ end }}
no_unique_dist_dir: true
hooks:
post:
- cmd: xz -k -9 {{ .Path }}
dir: ./dist/
archives:
- format: binary
name_template: "{{ .Binary }}"
allow_different_binary_count: true
checksum:
name_template: "checksums.txt"
extra_files:
- glob: ./**.xz
snapshot:
name_template: "{{ incpatch .Version }}"
release:
# You can add extra pre-existing files to the release.
# The filename on the release will be the last part of the path (base).
# If another file with the same name exists, the last one found will be used.
#
# Templates: allowed
extra_files:
- glob: ./**.xz
changelog:
use: github
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "Bug fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "Enhancements"
regexp: "^.*chore[(\\w)]*:+.*$"
order: 2
- title: "Refactor"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Build process updates"
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
order: 4
- title: "Documentation updates"
regexp: ^.*?docs?(\(.+\))??!?:.+$
order: 4
- title: Others
order: 999
+9 -26
View File
@@ -39,48 +39,31 @@ endif
TAGS ?=
LDFLAGS ?= -X 'main.Version=$(VERSION)'
.PHONY: all
all: build
fmt:
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install mvdan.cc/gofumpt; \
fi
$(GOFMT) -w $(GOFILES)
vet:
$(GO) vet ./...
.PHONY: fmt-check
fmt-check:
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install mvdan.cc/gofumpt; \
fi
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \
exit 1; \
fi;
.PHONY: test
test:
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: install
install: $(GOFILES)
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' .
.PHONY: build
build: $(EXECUTABLE)
$(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@ .
build_linux_amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(DEPLOY_IMAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(DEPLOY_IMAGE) .
build_linux_arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(DEPLOY_IMAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(DEPLOY_IMAGE) .
build_linux_arm:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(DEPLOY_IMAGE)
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(DEPLOY_IMAGE) .
clean:
$(GO) clean -x -i ./...
+237 -55
View File
@@ -3,18 +3,100 @@
![logo](./images/logo.png)
[![Lint and Testing](https://github.com/appleboy/drone-jenkins/actions/workflows/lint.yml/badge.svg)](https://github.com/appleboy/drone-jenkins/actions/workflows/lint.yml)
[![Trivy Security Scan](https://github.com/appleboy/drone-jenkins/actions/workflows/trivy.yml/badge.svg)](https://github.com/appleboy/drone-jenkins/actions/workflows/trivy.yml)
[![GoDoc](https://godoc.org/github.com/appleboy/drone-jenkins?status.svg)](https://godoc.org/github.com/appleboy/drone-jenkins)
[![codecov](https://codecov.io/gh/appleboy/drone-jenkins/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/drone-jenkins)
[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/drone-jenkins)](https://goreportcard.com/report/github.com/appleboy/drone-jenkins)
[Drone](https://github.com/drone/drone) plugin for trigger [Jenkins](https://jenkins.io/) jobs.
A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https://jenkins.io/) jobs with flexible authentication and parameter support.
## Setup the Jenkins Server
## Table of Contents
Setup the Jenkins server using the docker command:
- [drone-jenkins](#drone-jenkins)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Download Binary](#download-binary)
- [Build from Source](#build-from-source)
- [Docker Image](#docker-image)
- [Configuration](#configuration)
- [Jenkins Server Setup](#jenkins-server-setup)
- [Authentication](#authentication)
- [Parameters Reference](#parameters-reference)
- [Usage](#usage)
- [Command Line](#command-line)
- [Docker](#docker)
- [Drone CI](#drone-ci)
- [Development](#development)
- [Building](#building)
- [Testing](#testing)
- [License](#license)
- [Contributing](#contributing)
## Features
- Trigger single or multiple Jenkins jobs
- Support for Jenkins build parameters
- Multiple authentication methods (API token or remote trigger token)
- SSL/TLS support with optional insecure mode
- Cross-platform support (Linux, macOS, Windows)
- Available as binary, Docker image, or Drone plugin
## Prerequisites
- Jenkins server (version 2.0 or later recommended)
- Jenkins API token or remote trigger token for authentication
- For Jenkins setup, Docker is recommended but not required
## Installation
### Download Binary
Pre-compiled binaries are available from the [release page](https://github.com/appleboy/drone-jenkins/releases) for:
- **Linux**: amd64, 386
- **macOS (Darwin)**: amd64, 386
- **Windows**: amd64, 386
With Go installed, you can also install directly:
```sh
$ docker run \
go install github.com/appleboy/drone-jenkins@latest
```
### Build from Source
Clone the repository and build:
```sh
git clone https://github.com/appleboy/drone-jenkins.git
cd drone-jenkins
make build
```
### Docker Image
Build the Docker image:
```sh
make docker
```
Or pull the pre-built image:
```sh
docker pull ghcr.io/appleboy/drone-jenkins
```
## Configuration
### Jenkins Server Setup
Set up a Jenkins server using Docker:
```sh
docker run \
--name jenkins \
-d --restart always \
-p 8080:8080 -p 50000:50000 \
@@ -22,45 +104,45 @@ $ docker run \
jenkins/jenkins:lts
```
Please make sure that you create the `/data/jenkins` before starting the Jenkins. Create the new API token as below:
**Note**: Create the `/data/jenkins` directory before starting Jenkins.
![jenkins token](./images/jenkins-token.png)
### Authentication
## Build or Download a binary
Jenkins API tokens are recommended for authentication. To create an API token:
The pre-compiled binaries can be downloaded from [release page](https://github.com/appleboy/drone-jenkins/releases). Support the following OS type.
1. Log into Jenkins
2. Click on your username (top right)
3. Select "Security"
4. Under "API Token", click "Add new Token"
5. Give it a name and click "Generate"
6. Copy the generated token
* Windows amd64/386
* Linux amd64/386
* Darwin amd64/386
![personal token](./images/personal-token.png)
With `Go` installed
Alternatively, you can use a remote trigger token configured in your Jenkins job settings.
```sh
go install github.com/appleboy/drone-jenkins
```
### Parameters Reference
or build the binary with the following command:
| Parameter | CLI Flag | Environment Variable | Required | Description |
| ------------ | -------------------- | --------------------------------------------- | ------------- | ------------------------------------------------------ |
| Host | `--host` | `PLUGIN_URL`, `JENKINS_URL` | Yes | Jenkins base URL (e.g., `http://jenkins.example.com/`) |
| User | `--user`, `-u` | `PLUGIN_USER`, `JENKINS_USER` | Conditional\* | Jenkins username |
| Token | `--token`, `-t` | `PLUGIN_TOKEN`, `JENKINS_TOKEN` | Conditional\* | Jenkins API token |
| Remote Token | `--remote-token` | `PLUGIN_REMOTE_TOKEN`, `JENKINS_REMOTE_TOKEN` | Conditional\* | Jenkins remote trigger token |
| Job | `--job`, `-j` | `PLUGIN_JOB`, `JENKINS_JOB` | Yes | Jenkins job name(s) - can specify multiple |
| Parameters | `--parameters`, `-p` | `PLUGIN_PARAMETERS`, `JENKINS_PARAMETERS` | No | Build parameters in `key=value` format |
| Insecure | `--insecure` | `PLUGIN_INSECURE`, `JENKINS_INSECURE` | No | Allow insecure SSL connections (default: false) |
```sh
make build
```
**Authentication Requirements**: You must provide either:
## Docker
Build the docker image with the following commands:
```sh
make docker
```
- `user` + `token` (API token authentication), OR
- `remote-token` (remote trigger token authentication)
## Usage
There are three ways to trigger jenkins jobs.
### Command Line
### Usage from binary
trigger single job.
**Single job:**
```bash
drone-jenkins \
@@ -70,7 +152,7 @@ drone-jenkins \
--job drone-jenkins-plugin
```
trigger multiple jobs.
**Multiple jobs:**
```bash
drone-jenkins \
@@ -81,51 +163,151 @@ drone-jenkins \
--job drone-jenkins-plugin-2
```
### Usage from docker
**With build parameters:**
trigger single job.
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--parameters "ENVIRONMENT=production" \
--parameters "VERSION=1.0.0"
```
**Using remote token authentication:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--remote-token REMOTE_TOKEN_HERE \
--job my-jenkins-job
```
### Docker
**Single job:**
```bash
docker run --rm \
-e JENKINS_BASE_URL=http://jenkins.example.com/
-e JENKINS_USER=appleboy
-e JENKINS_TOKEN=xxxxxxx
-e JENKINS_JOB=drone-jenkins-plugin
-e JENKINS_URL=http://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=drone-jenkins-plugin \
ghcr.io/appleboy/drone-jenkins
```
trigger multiple jobs.
**Multiple jobs:**
```bash
docker run --rm \
-e JENKINS_BASE_URL=http://jenkins.example.com/
-e JENKINS_USER=appleboy
-e JENKINS_TOKEN=xxxxxxx
-e JENKINS_JOB=drone-jenkins-plugin-1,drone-jenkins-plugin-2
-e JENKINS_URL=http://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=drone-jenkins-plugin-1,drone-jenkins-plugin-2 \
ghcr.io/appleboy/drone-jenkins
```
### Usage from drone ci
**With build parameters:**
Execute from the working directory:
```bash
docker run --rm \
-e JENKINS_URL=http://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_PARAMETERS="ENVIRONMENT=production,VERSION=1.0.0" \
ghcr.io/appleboy/drone-jenkins
```
### Drone CI
Add the plugin to your `.drone.yml`:
```yaml
kind: pipeline
name: default
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job: drone-jenkins-plugin
```
**Multiple jobs with parameters:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job:
- deploy-frontend
- deploy-backend
parameters:
- ENVIRONMENT=production
- VERSION=${DRONE_TAG}
- COMMIT_SHA=${DRONE_COMMIT_SHA}
```
**Using remote token:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
remote_token:
from_secret: jenkins_remote_token
job: my-jenkins-job
```
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
## Development
### Building
Build the binary:
```sh
docker run --rm \
-e PLUGIN_URL=http://example.com \
-e PLUGIN_USER=xxxxxxx \
-e PLUGIN_TOKEN=xxxxxxx \
-e PLUGIN_JOB=xxxxxxx \
-v $(pwd):$(pwd) \
-w $(pwd) \
ghcr.io/appleboy/drone-jenkins
make build
```
You can get more [information](DOCS.md) about how to use scp plugin in drone.
Build the Docker image:
## Testing
```sh
make docker
```
Test the package with the following command:
### Testing
Run the test suite:
```sh
make test
```
Run tests with coverage:
```sh
make test-coverage
```
## License
Copyright (c) 2019 Bo-Yi Wu
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
+7 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.20
FROM alpine:3.22
ARG TARGETOS
ARG TARGETARCH
@@ -15,6 +15,12 @@ LABEL org.opencontainers.image.licenses=MIT
RUN apk add --no-cache ca-certificates && \
rm -rf /var/cache/apk/*
RUN addgroup -g 1000 drone && \
adduser -D -u 1000 -G drone drone
COPY release/${TARGETOS}/${TARGETARCH}/drone-jenkins /bin/
RUN chown drone:drone /bin/drone-jenkins
USER drone
ENTRYPOINT ["/bin/drone-jenkins"]
+4 -3
View File
@@ -4,14 +4,15 @@ go 1.22
require (
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.9.0
github.com/urfave/cli v1.22.15
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.5
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+8 -18
View File
@@ -1,8 +1,5 @@
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -11,20 +8,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+47 -22
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -21,18 +22,20 @@ type (
Jenkins struct {
Auth *Auth
BaseURL string
Token string // Remote trigger token
Client *http.Client
}
)
// NewJenkins is initial Jenkins object
func NewJenkins(auth *Auth, url string, insecure bool) *Jenkins {
func NewJenkins(auth *Auth, url string, token string, insecure bool) *Jenkins {
url = strings.TrimRight(url, "/")
client := http.DefaultClient
if insecure {
client = &http.Client{
Transport: &http.Transport{
// #nosec G402 -- InsecureSkipVerify is intentionally configurable by user
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
@@ -41,6 +44,7 @@ func NewJenkins(auth *Auth, url string, insecure bool) *Jenkins {
return &Jenkins{
Auth: auth,
BaseURL: url,
Token: token,
Client: client,
}
}
@@ -64,36 +68,34 @@ func (jenkins *Jenkins) sendRequest(req *http.Request) (*http.Response, error) {
return jenkins.Client.Do(req)
}
func (jenkins *Jenkins) parseResponse(resp *http.Response, body interface{}) (err error) {
defer resp.Body.Close()
if body == nil {
return
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return
}
return json.Unmarshal(data, body)
}
func (jenkins *Jenkins) post(path string, params url.Values, body interface{}) (err error) {
requestURL := jenkins.buildURL(path, params)
req, err := http.NewRequest("POST", requestURL, nil)
req, err := http.NewRequestWithContext(context.Background(), "POST", requestURL, nil)
if err != nil {
fmt.Println(err)
return
}
resp, err := jenkins.sendRequest(req)
if err != nil {
fmt.Println(err)
return
}
return jenkins.parseResponse(resp, body)
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response code: %d, body: %s", resp.StatusCode, string(data))
}
if body == nil {
return nil
}
return json.Unmarshal(data, body)
}
func (jenkins *Jenkins) parseJobPath(job string) string {
@@ -114,7 +116,30 @@ func (jenkins *Jenkins) parseJobPath(job string) string {
}
func (jenkins *Jenkins) trigger(job string, params url.Values) error {
path := jenkins.parseJobPath(job) + "/build"
// Add remote trigger token to params
if jenkins.Token != "" {
if params == nil {
params = url.Values{}
}
params.Set("token", jenkins.Token)
}
return jenkins.post(path, params, nil)
var urlPath string
// Check if params contains build parameters (excluding 'token')
hasBuildParams := false
for key := range params {
if key != "token" {
hasBuildParams = true
break
}
}
if hasBuildParams {
urlPath = jenkins.parseJobPath(job) + "/buildWithParameters"
} else {
urlPath = jenkins.parseJobPath(job) + "/build"
}
// All params (including token) are passed as query parameters
return jenkins.post(urlPath, params, nil)
}
+19 -5
View File
@@ -1,6 +1,8 @@
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
@@ -12,7 +14,7 @@ func TestParseJobPath(t *testing.T) {
Username: "appleboy",
Token: "1234",
}
jenkins := NewJenkins(auth, "http://example.com", false)
jenkins := NewJenkins(auth, "http://example.com", "", false)
assert.Equal(t, "/job/foo", jenkins.parseJobPath("/foo/"))
assert.Equal(t, "/job/foo", jenkins.parseJobPath("foo/"))
@@ -25,19 +27,31 @@ func TestUnSupportProtocol(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "example.com", false)
jenkins := NewJenkins(auth, "example.com", "", false)
err := jenkins.trigger("drone-jenkins", nil)
assert.NotNil(t, err)
}
func TestTriggerBuild(t *testing.T) {
// Create a mock Jenkins server
var receivedParams url.Values
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedParams = r.URL.Query()
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
auth := &Auth{
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "http://example.com", false)
jenkins := NewJenkins(auth, server.URL, "remote-token", false)
err := jenkins.trigger("drone-jenkins", url.Values{"token": []string{"bar"}})
assert.Nil(t, err)
params := url.Values{"param": []string{"value"}}
err := jenkins.trigger("drone-jenkins", params)
assert.NoError(t, err)
assert.Equal(t, "value", receivedParams.Get("param"))
assert.Equal(t, "remote-token", receivedParams.Get("token"))
}
+84 -41
View File
@@ -1,31 +1,46 @@
package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// Version set at compile-time
var Version string
var Version = "dev"
const asciiArt = `
________ ____. __ .__
\______ \_______ ____ ____ ____ | | ____ ____ | | _|__| ____ ______
| | \_ __ \/ _ \ / \_/ __ \ ______ | |/ __ \ / \| |/ / |/ \ / ___/
| | \ | \( <_> ) | \ ___/ /_____/ /\__| \ ___/| | \ <| | | \\___ \
/_______ /__| \____/|___| /\___ > \________|___ >___| /__|_ \__|___| /____ >
\/ \/ \/ \/ \/ \/ \/ \/
version: {{.Version}}
`
func main() {
// Load env-file if it exists first
if filename, found := os.LookupEnv("PLUGIN_ENV_FILE"); found {
_ = godotenv.Load(filename)
if err := godotenv.Load(filename); err != nil && !os.IsNotExist(err) {
log.Printf("Warning: failed to load env file %s: %v", filename, err)
}
}
if _, err := os.Stat("/run/drone/env"); err == nil {
_ = godotenv.Overload("/run/drone/env")
if err := godotenv.Overload("/run/drone/env"); err != nil {
log.Printf("Warning: failed to load /run/drone/env: %v", err)
}
}
app := cli.NewApp()
app.Name = "jenkins plugin"
app.Usage = "trigger jenkins jobs"
app.Copyright = "Copyright (c) 2019 Bo-Yi Wu"
app.Authors = []cli.Author{
app.Authors = []*cli.Author{
{
Name: "Bo-Yi Wu",
Email: "appleboy.tw@gmail.com",
@@ -34,53 +49,62 @@ func main() {
app.Action = run
app.Version = Version
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "host",
Usage: "jenkins base url",
EnvVar: "PLUGIN_URL,JENKINS_URL,INPUT_URL",
&cli.StringFlag{
Name: "host",
Usage: "jenkins base url",
EnvVars: []string{"PLUGIN_URL", "JENKINS_URL", "INPUT_URL"},
},
cli.StringFlag{
Name: "user,u",
Usage: "jenkins username",
EnvVar: "PLUGIN_USER,JENKINS_USER,INPUT_USER",
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "jenkins username",
EnvVars: []string{"PLUGIN_USER", "JENKINS_USER", "INPUT_USER"},
},
cli.StringFlag{
Name: "token,t",
Usage: "jenkins token",
EnvVar: "PLUGIN_TOKEN,JENKINS_TOKEN,INPUT_TOKEN",
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Usage: "jenkins API token for authentication",
EnvVars: []string{"PLUGIN_TOKEN", "JENKINS_TOKEN", "INPUT_TOKEN"},
},
cli.StringSliceFlag{
Name: "job,j",
Usage: "jenkins job",
EnvVar: "PLUGIN_JOB,JENKINS_JOB,INPUT_JOB",
&cli.StringFlag{
Name: "remote-token",
Usage: "jenkins remote trigger token",
EnvVars: []string{"PLUGIN_REMOTE_TOKEN", "JENKINS_REMOTE_TOKEN", "INPUT_REMOTE_TOKEN"},
},
cli.BoolFlag{
Name: "insecure",
Usage: "allow insecure server connections when using SSL",
EnvVar: "PLUGIN_INSECURE,JENKINS_INSECURE,INPUT_INSECURE",
&cli.StringSliceFlag{
Name: "job",
Aliases: []string{"j"},
Usage: "jenkins job",
EnvVars: []string{"PLUGIN_JOB", "JENKINS_JOB", "INPUT_JOB"},
},
&cli.BoolFlag{
Name: "insecure",
Usage: "allow insecure server connections when using SSL",
EnvVars: []string{"PLUGIN_INSECURE", "JENKINS_INSECURE", "INPUT_INSECURE"},
},
&cli.StringSliceFlag{
Name: "parameters",
Aliases: []string{"p"},
Usage: "jenkins build parameters",
EnvVars: []string{"PLUGIN_PARAMETERS", "JENKINS_PARAMETERS", "INPUT_PARAMETERS"},
},
}
// Override a template
cli.AppHelpTemplate = `
________ ____. __ .__
\______ \_______ ____ ____ ____ | | ____ ____ | | _|__| ____ ______
| | \_ __ \/ _ \ / \_/ __ \ ______ | |/ __ \ / \| |/ / |/ \ / ___/
| | \ | \( <_> ) | \ ___/ /_____/ /\__| \ ___/| | \ <| | | \\___ \
/_______ /__| \____/|___| /\___ > \________|\___ >___| /__|_ \__|___| /____ >
\/ \/ \/ \/ \/ \/ \/ \/
version: {{.Version}}
cli.AppHelpTemplate = asciiArt + `
NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} ` +
`{{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
{{if len .Authors}}
AUTHOR:
{{range .Authors}}{{ . }}{{end}}
{{end}}{{if .Commands}}
COMMANDS:
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}` +
`{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}{{if .Copyright }}
@@ -91,7 +115,7 @@ VERSION:
{{.Version}}
{{end}}
REPOSITORY:
Github: https://github.com/appleboy/drone-line
Github: https://github.com/appleboy/drone-jenkins
`
if err := app.Run(os.Args); err != nil {
@@ -100,12 +124,31 @@ REPOSITORY:
}
func run(c *cli.Context) error {
// Validate required parameters
if c.String("host") == "" {
return fmt.Errorf("host is required")
}
if len(c.StringSlice("job")) == 0 {
return fmt.Errorf("at least one job is required")
}
// Validate authentication: either (user + token) or remote-token must be provided
hasUserAuth := c.String("user") != "" && c.String("token") != ""
hasRemoteToken := c.String("remote-token") != ""
if !hasUserAuth && !hasRemoteToken {
return fmt.Errorf("authentication required: provide either (user + token) or remote-token")
}
plugin := Plugin{
BaseURL: c.String("host"),
Username: c.String("user"),
Token: c.String("token"),
Job: c.StringSlice("job"),
Insecure: c.Bool("insecure"),
BaseURL: c.String("host"),
Username: c.String("user"),
Token: c.String("token"),
RemoteToken: c.String("remote-token"),
Job: c.StringSlice("job"),
Insecure: c.Bool("insecure"),
Parameters: c.StringSlice("parameters"),
}
return plugin.Exec()
+82 -24
View File
@@ -2,57 +2,115 @@ package main
import (
"errors"
"fmt"
"log"
"net/url"
"strings"
)
type (
// Plugin values.
// Plugin represents the configuration for the Jenkins plugin.
// It contains all necessary credentials and settings to trigger Jenkins jobs.
Plugin struct {
BaseURL string
Username string
Token string
Job []string
Insecure bool
BaseURL string // Jenkins server base URL
Username string // Jenkins username for authentication
Token string // Jenkins API token for authentication
RemoteToken string // Optional remote trigger token for additional security
Job []string // List of Jenkins job names to trigger
Insecure bool // Whether to skip TLS certificate verification
Parameters []string // Job parameters in key=value format
}
)
func trimElement(keys []string) []string {
newKeys := []string{}
// trimWhitespaceFromSlice removes empty and whitespace-only strings from a slice.
// It returns a new slice containing only non-empty trimmed strings.
func trimWhitespaceFromSlice(items []string) []string {
result := make([]string, 0, len(items))
for _, value := range keys {
value = strings.Trim(value, " ")
if len(value) == 0 {
continue
for _, item := range items {
trimmed := strings.TrimSpace(item)
if trimmed != "" {
result = append(result, trimmed)
}
newKeys = append(newKeys, value)
}
return newKeys
return result
}
// Exec executes the plugin.
// parseParameters converts a slice of key=value strings into url.Values.
// It logs a warning for any parameters that don't match the expected format.
func parseParameters(params []string) url.Values {
values := url.Values{}
for _, param := range params {
parts := strings.SplitN(param, "=", 2)
if len(parts) != 2 {
log.Printf("warning: skipping invalid parameter format (expected key=value): %q", param)
continue
}
key := strings.TrimSpace(parts[0])
value := parts[1] // Keep value as-is to preserve intentional spaces
if key == "" {
log.Printf("warning: skipping parameter with empty key: %q", param)
continue
}
values.Add(key, value)
}
return values
}
// validateConfig checks that all required plugin configuration is present.
// It returns a descriptive error if any required field is missing.
func (p Plugin) validateConfig() error {
if p.BaseURL == "" {
return errors.New("jenkins base URL is required")
}
if p.Username == "" {
return errors.New("jenkins username is required")
}
if p.Token == "" {
return errors.New("jenkins API token is required")
}
return nil
}
// Exec executes the plugin by triggering the configured Jenkins jobs.
// It validates the configuration, parses parameters, and triggers each job sequentially.
// Returns an error if validation fails or any job trigger fails.
func (p Plugin) Exec() error {
if len(p.BaseURL) == 0 || len(p.Username) == 0 || len(p.Token) == 0 {
return errors.New("missing jenkins config")
// Validate required configuration
if err := p.validateConfig(); err != nil {
return fmt.Errorf("configuration error: %w", err)
}
jobs := trimElement(p.Job)
// Clean and validate job list
jobs := trimWhitespaceFromSlice(p.Job)
if len(jobs) == 0 {
return errors.New("missing jenkins job")
return errors.New("at least one Jenkins job name is required")
}
// Set up authentication
auth := &Auth{
Username: p.Username,
Token: p.Token,
}
jenkins := NewJenkins(auth, p.BaseURL, p.Insecure)
// Initialize Jenkins client
jenkins := NewJenkins(auth, p.BaseURL, p.RemoteToken, p.Insecure)
for _, v := range jobs {
if err := jenkins.trigger(v, nil); err != nil {
return err
// Parse job parameters
params := parseParameters(p.Parameters)
// Trigger each job
for _, jobName := range jobs {
if err := jenkins.trigger(jobName, params); err != nil {
return fmt.Errorf("failed to trigger job %q: %w", jobName, err)
}
log.Printf("successfully triggered job: %s", jobName)
}
return nil
+333 -23
View File
@@ -1,48 +1,276 @@
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMissingConfig(t *testing.T) {
// TestValidateConfig tests the validateConfig method
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
plugin Plugin
wantError bool
errorMsg string
}{
{
name: "missing all config",
plugin: Plugin{},
wantError: true,
errorMsg: "jenkins base URL is required",
},
{
name: "missing username and token",
plugin: Plugin{
BaseURL: "http://example.com",
},
wantError: true,
errorMsg: "jenkins username is required",
},
{
name: "missing token",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
},
wantError: true,
errorMsg: "jenkins API token is required",
},
{
name: "all required config present",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
},
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.plugin.validateConfig()
if tt.wantError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
// TestTrimWhitespaceFromSlice tests the trimWhitespaceFromSlice function
func TestTrimWhitespaceFromSlice(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "remove empty and whitespace strings",
input: []string{"1", " ", "3"},
expected: []string{"1", "3"},
},
{
name: "no whitespace strings",
input: []string{"1", "2"},
expected: []string{"1", "2"},
},
{
name: "all whitespace",
input: []string{" ", "\t", "\n"},
expected: []string{},
},
{
name: "empty slice",
input: []string{},
expected: []string{},
},
{
name: "trim surrounding whitespace",
input: []string{" foo ", " bar ", "baz"},
expected: []string{"foo", "bar", "baz"},
},
{
name: "mixed empty and valid",
input: []string{"", "valid", "", "also-valid", ""},
expected: []string{"valid", "also-valid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := trimWhitespaceFromSlice(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestParseParameters tests the parseParameters function
func TestParseParameters(t *testing.T) {
tests := []struct {
name string
input []string
expected url.Values
}{
{
name: "valid parameters",
input: []string{"key1=value1", "key2=value2"},
expected: url.Values{
"key1": []string{"value1"},
"key2": []string{"value2"},
},
},
{
name: "parameter with multiple equals signs",
input: []string{"key=value=with=equals"},
expected: url.Values{
"key": []string{"value=with=equals"},
},
},
{
name: "parameter with spaces in value",
input: []string{"key=value with spaces"},
expected: url.Values{
"key": []string{"value with spaces"},
},
},
{
name: "parameter with empty value",
input: []string{"key="},
expected: url.Values{
"key": []string{""},
},
},
{
name: "invalid parameter format (no equals)",
input: []string{"invalid"},
expected: url.Values{},
},
{
name: "parameter with empty key",
input: []string{"=value"},
expected: url.Values{},
},
{
name: "mixed valid and invalid",
input: []string{"valid=yes", "invalid", "also=valid"},
expected: url.Values{
"valid": []string{"yes"},
"also": []string{"valid"},
},
},
{
name: "key with surrounding whitespace",
input: []string{" key =value"},
expected: url.Values{
"key": []string{"value"},
},
},
{
name: "empty slice",
input: []string{},
expected: url.Values{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseParameters(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestExecMissingConfig tests Exec with missing configuration
func TestExecMissingConfig(t *testing.T) {
var plugin Plugin
err := plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins base URL is required")
}
func TestMissingJenkinsConfig(t *testing.T) {
// TestExecMissingJenkinsUsername tests Exec with missing username
func TestExecMissingJenkinsUsername(t *testing.T) {
plugin := Plugin{
BaseURL: "http://example.com",
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins username is required")
}
func TestMissingJenkinsJob(t *testing.T) {
// TestExecMissingJenkinsToken tests Exec with missing token
func TestExecMissingJenkinsToken(t *testing.T) {
plugin := Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
}
err := plugin.Exec()
assert.NotNil(t, err)
plugin.Job = []string{" "}
err = plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins API token is required")
}
func TestPluginTriggerBuild(t *testing.T) {
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
func TestExecMissingJenkinsJob(t *testing.T) {
tests := []struct {
name string
jobs []string
}{
{
name: "no jobs",
jobs: []string{},
},
{
name: "only whitespace jobs",
jobs: []string{" ", "\t", "\n"},
},
{
name: "nil jobs",
jobs: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
Job: tt.jobs,
}
err := plugin.Exec()
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one Jenkins job name is required")
})
}
}
// TestExecTriggerBuild tests successful job triggering
func TestExecTriggerBuild(t *testing.T) {
// Create a mock Jenkins server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
plugin := Plugin{
BaseURL: "http://example.com",
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"drone-jenkins"},
@@ -50,19 +278,101 @@ func TestPluginTriggerBuild(t *testing.T) {
err := plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestTrimElement(t *testing.T) {
var input, result []string
// TestExecTriggerMultipleJobs tests triggering multiple jobs
func TestExecTriggerMultipleJobs(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jobsTriggered++
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
input = []string{"1", " ", "3"}
result = []string{"1", "3"}
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"job1", "job2", "job3"},
}
assert.Equal(t, result, trimElement(input))
err := plugin.Exec()
input = []string{"1", "2"}
result = []string{"1", "2"}
assert.Equal(t, result, trimElement(input))
assert.NoError(t, err)
assert.Equal(t, 3, jobsTriggered)
}
// TestExecWithParameters tests job triggering with parameters
func TestExecWithParameters(t *testing.T) {
// Create a mock Jenkins server
var receivedQuery url.Values
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedQuery = r.URL.Query()
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"parameterized-job"},
Parameters: []string{"branch=main", "environment=production"},
}
err := plugin.Exec()
assert.NoError(t, err)
assert.Equal(t, "main", receivedQuery.Get("branch"))
assert.Equal(t, "production", receivedQuery.Get("environment"))
}
// TestExecWithRemoteToken tests job triggering with remote token
func TestExecWithRemoteToken(t *testing.T) {
// Create a mock Jenkins server
var receivedToken string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedToken = r.URL.Query().Get("token")
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
RemoteToken: "remote-token-123",
Job: []string{"secure-job"},
}
err := plugin.Exec()
assert.NoError(t, err)
assert.Equal(t, "remote-token-123", receivedToken)
}
// TestExecWithJobsContainingWhitespace tests job list with whitespace
func TestExecWithJobsContainingWhitespace(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jobsTriggered++
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{" job1 ", "job2", " ", "job3"},
}
err := plugin.Exec()
assert.NoError(t, err)
// Should trigger 3 jobs (whitespace-only entry should be filtered out)
assert.Equal(t, 3, jobsTriggered)
}