mirror of
https://github.com/appleboy/drone-jenkins.git
synced 2026-06-16 14:49:16 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 747d7b23d1 | |||
| 48d5425edd | |||
| cda84c122e | |||
| afbea8106e | |||
| cc67514a1a | |||
| 3d7ffaed68 | |||
| 52abef124e | |||
| 3dd86f956c | |||
| a5469c939e | |||
| a6d967789d | |||
| 3eb2242053 | |||
| 908be474f3 | |||
| de494cbda1 | |||
| 77ea4873e0 | |||
| 36013b246a | |||
| cbfb2bb51b | |||
| b6589abef3 |
+13
-12
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
@@ -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$
|
||||
@@ -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 ./...
|
||||
|
||||
@@ -3,18 +3,100 @@
|
||||

|
||||
|
||||
[](https://github.com/appleboy/drone-jenkins/actions/workflows/lint.yml)
|
||||
[](https://github.com/appleboy/drone-jenkins/actions/workflows/trivy.yml)
|
||||
[](https://godoc.org/github.com/appleboy/drone-jenkins)
|
||||
[](https://codecov.io/gh/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.
|
||||
|
||||

|
||||
### 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
|
||||

|
||||
|
||||
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
@@ -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,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
|
||||
)
|
||||
|
||||
@@ -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 |
+42
-27
@@ -1,11 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -22,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},
|
||||
},
|
||||
}
|
||||
@@ -42,6 +44,7 @@ func NewJenkins(auth *Auth, url string, insecure bool) *Jenkins {
|
||||
return &Jenkins{
|
||||
Auth: auth,
|
||||
BaseURL: url,
|
||||
Token: token,
|
||||
Client: client,
|
||||
}
|
||||
}
|
||||
@@ -65,24 +68,10 @@ 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 {
|
||||
return
|
||||
}
|
||||
@@ -92,11 +81,21 @@ func (jenkins *Jenkins) post(path string, params url.Values, body interface{}) (
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return fmt.Errorf("unexpected response code: %d", resp.StatusCode)
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return jenkins.parseResponse(resp, body)
|
||||
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 {
|
||||
@@ -117,14 +116,30 @@ func (jenkins *Jenkins) parseJobPath(job string) string {
|
||||
}
|
||||
|
||||
func (jenkins *Jenkins) trigger(job string, params url.Values) error {
|
||||
var urlPath string
|
||||
if len(params) == 0 {
|
||||
urlPath = jenkins.parseJobPath(job) + "/build"
|
||||
} else {
|
||||
urlPath = jenkins.parseJobPath(job) + "/buildWithParameters"
|
||||
// Add remote trigger token to params
|
||||
if jenkins.Token != "" {
|
||||
if params == nil {
|
||||
params = url.Values{}
|
||||
}
|
||||
params.Set("token", jenkins.Token)
|
||||
}
|
||||
|
||||
log.Println(urlPath)
|
||||
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
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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,58 +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.StringSliceFlag{
|
||||
Name: "parameters,p",
|
||||
Usage: "jenkins build parameters",
|
||||
EnvVar: "PLUGIN_PARAMETERS,JENKINS_PARAMETERS,INPUT_PARAMETERS",
|
||||
&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 }}
|
||||
@@ -96,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 {
|
||||
@@ -105,13 +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"),
|
||||
Parameters: c.StringSlice("parameters"),
|
||||
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()
|
||||
|
||||
@@ -2,69 +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
|
||||
Parameters []string
|
||||
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)
|
||||
|
||||
params := url.Values{}
|
||||
for _, v := range p.Parameters {
|
||||
kv := strings.Split(v, "=")
|
||||
if len(kv) == 2 {
|
||||
params.Add(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
// Parse job parameters
|
||||
params := parseParameters(p.Parameters)
|
||||
|
||||
for _, v := range jobs {
|
||||
if err := jenkins.trigger(v, params); err != nil {
|
||||
return err
|
||||
// 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("trigger job %s success", v)
|
||||
log.Printf("successfully triggered job: %s", jobName)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+333
-23
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user