Compare commits

..

19 Commits

Author SHA1 Message Date
appleboy 1fbf3e5cd6 style: streamline authentication error handling in config validation
- Simplify authentication error message in config validation

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-27 11:23:25 +08:00
appleboy 95a5eb125f feat: refactor authentication logic and broaden test coverage
- Improve authentication checks to only require username and token when both are provided
- Update validation logic to allow either (username and token) or remote-token for authentication
- Enhance test coverage for various authentication scenarios
- Refine error messages to indicate a generic authentication requirement instead of specifying missing username or token

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-27 11:13:06 +08:00
Bo-Yi Wu 984ca01afc feat: add CSRF crumb support and session management for Jenkins API (#49)
- Add support for Jenkins CSRF protection by managing and adding CSRF crumb to POST requests
- Store and cache CSRF crumb after fetching from Jenkins for session reuse
- Use an HTTP client with CookieJar for session management to support CSRF crumb handling
- Update existing tests to check for presence of CookieJar in the HTTP client
- Improve test servers to only count job trigger POSTs, ignoring GET requests for crumbs

fix #48

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-27 10:48:26 +08:00
appleboy d30890d257 ci: update GitHub Actions dependencies to latest major versions
- Update actions/checkout to version 6 in all workflows
- Update github/codeql-action steps to version 4 in the CodeQL workflow
- Upgrade actions/setup-go to version 6 in docker and goreleaser workflows
- Upgrade actions/cache to version 5 in the lint workflow

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-26 16:50:50 +08:00
appleboy 3309475595 refactor: add context.Context support across Jenkins client and execution
- Add support for passing context.Context throughout Jenkins client and related functions for better cancellation and timeout handling
- Update plugin execution and main entrypoint to accept context, enabling propagating cancellation signals
- Refactor tests to provide context when calling functions that now require it

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-26 16:41:00 +08:00
appleboy 2b5c542196 docs: revise documentation for multi-platform CI/CD support
- Update project description to emphasize multi-platform CI/CD compatibility beyond Drone
- Remove outdated references to Drone CI and simplify the supported usage forms; project is now offered as a CLI tool or Docker image
- Add support mentions for GitHub Actions, GitLab CI, and Gitea Action across documentation
- Eliminate usage examples and documentation sections specific to Drone CI from all language versions
- Clarify that integration examples are covered in a separate documentation file

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-26 15:54:17 +08:00
appleboy d8cceb3839 docs: add multilingual README and enhance usage documentation
- Add links for English, Traditional Chinese, and Simplified Chinese translations at the top of the README
- Introduce a new "Why drone-jenkins?" section explaining the use cases and benefits for hybrid CI/CD environments
- Expand the table of contents with new sections reflecting the README structure
- Correct the formatting of the parameters table for clarity
- Add a Simplified Chinese README translation (README.zh-CN.md)
- Add a Traditional Chinese README translation (README.zh-TW.md)

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-26 15:44:17 +08:00
appleboy 6a8250d7e6 docs: document Makefile target discovery with make help
- Document the usage of the `make help` command to display available Makefile targets

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-20 12:00:39 +08:00
appleboy 8dfbb92314 build: refactor Makefile to enhance usability and add cross-compilation
- Add help descriptions to Makefile targets using double hashes, improving 'make help' documentation
- Implement a 'help' target to print available Makefile commands and their descriptions
- Add explicit .PHONY lines for all make targets to clarify intent
- Add linux/amd64, linux/arm64, and linux/arm build targets for cross-compilation
- Improve test, lint, fmt, install, build, and clean target implementations
- Add clean target to remove build artifacts and coverage files
- Add version target to print current project version

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-06 18:31:26 +08:00
appleboy b0761290c2 docs: document project and enforce style with lint targets
- Add a CLAUDE.md guide describing project purpose, build instructions, architecture, and developer patterns
- Add linting and formatting targets (lint, fmt) to Makefile using golangci-lint

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-06 18:27:45 +08:00
Bo-Yi Wu 02829360ad feat: add support for custom CA certificates for SSL/TLS connections (#42)
* feat: add support for custom CA certificates for SSL/TLS connections

- Add support for custom CA certificates (via PEM content, file path, or HTTP/HTTPS URL) for SSL/TLS connections.
- Document the new CA certificate option and usage examples for CLI, Docker, and Drone CI in the README.
- Update Jenkins client initialization to load and validate a custom CA certificate if provided, using a priority where insecure mode overrides custom CA.
- Introduce comprehensive tests for CA certificate loading and Jenkins client initialization with different CA certificate sources and error scenarios.
- Register the new ca-cert command-line flag and propagate its value through configuration and debug output.
- Ensure that error handling for certificate loading fully propagates failures.

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* test: update test CA certificate with new sample

- Replace the sample CA certificate used in tests with a new certificate

Signed-off-by: appleboy <appleboy.tw@gmail.com>

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-06 13:11:29 +08:00
appleboy f3a67c62a6 chore: update third-party libraries to latest compatible versions
- Update urfave/cli library to version 2.27.7
- Update xrash/smetrics library to a newer commit

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-06 10:58:35 +08:00
Bo-Yi Wu 069e6455cc ci: update build workflow and dependencies for Go 1.24 compatibility
- Update Go version requirement from 1.22 to 1.24.0
- Add github.com/appleboy/com as a dependency
- Set GitHub Actions output with build result and URL after Jenkins build completion
- Log a warning if setting GitHub output fails

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 19:42:44 +08:00
Bo-Yi Wu eb51e55e81 build: enable detailed build info logging in debug mode
- Add debug logging to display final build information when debug mode is enabled

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 19:40:30 +08:00
Bo-Yi Wu da87ddb86b docs: document and illustrate debug mode configuration options
- Document the new debug mode, including its purpose and usage
- Add examples showing how to enable debug mode via CLI, Docker, and YAML configuration
- Update the configuration table to describe the debug option and its environment variables

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 17:25:40 +08:00
Bo-Yi Wu 60874908e6 docs: document and standardize multi-line build parameter configurations
- Add example configurations for build parameters and waiting for job completion
- Document new settings: wait, poll_interval, timeout, insecure, and remote_token
- Update parameters format to require multi-line key=value pairs (one per line)
- Clarify handling of whitespace, empty lines, and multiple equals signs in parameter values
- Revise CLI and Docker usage examples to use multi-line parameters format
- Update YAML configuration example to use multi-line parameters block

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 17:09:52 +08:00
Bo-Yi Wu f6e62d9c49 feat: switch Jenkins build parameters to multi-line string format
- Change the Jenkins build parameters input from a string slice to a multi-line string format
- Update parameter parsing to handle multi-line strings, skipping empty and whitespace-only lines
- Adjust all usages and tests to support the new string-based parameter format
- Add test cases for multiple empty lines and lines containing only whitespace in parameters

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 16:55:46 +08:00
Bo-Yi Wu cf9b9a0a0d feat: add debug mode with enhanced logging and token masking (#41)
* feat: add debug mode with enhanced logging and token masking

- Add godump library dependency for improved debug output
- Introduce a debug mode flag to the Jenkins and Plugin structs
- Update NewJenkins constructor and all usages to support the debug flag
- Mask sensitive tokens in debug output for security
- Log detailed parameter and configuration information when debug mode is enabled
- Add CLI flag to enable debug mode via environment variables

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* fix: add error handling and logging for godump.Dump failures

- Add error handling for godump.Dump calls, logging a warning if dumping fails

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 15:11:39 +08:00
Bo-Yi Wu 4c54d13899 docs: simplify Jenkins Docker setup and improve reliability
- Update Jenkins Docker run instructions to use a named volume and the slim image
- Change container restart policy to on-failure
- Remove note about creating a host directory for Jenkins data

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 14:36:48 +08:00
17 changed files with 1827 additions and 212 deletions
+3 -3
View File
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,4 +51,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+2 -2
View File
@@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "^1"
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
with:
ref: ${{ github.ref }}
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: |
${{ matrix.go-build }}
+50
View File
@@ -0,0 +1,50 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
drone-jenkins is a Drone CI plugin (and standalone CLI tool) for triggering Jenkins jobs. It supports multiple authentication methods, build parameters, and can wait for job completion with configurable polling.
## Build Commands
```sh
make build # Build binary to bin/drone-jenkins
make test # Run tests with coverage
make lint # Run golangci-lint
make fmt # Format code with golangci-lint
make docker # Build Docker image
make clean # Clean build artifacts
make help # Show all available targets
```
To run a single test:
```sh
go test -v -run TestFunctionName ./...
```
## Architecture
The codebase is structured as a simple Go CLI application:
- **main.go** - CLI entry point using `urfave/cli/v2`. Defines all command-line flags and environment variable mappings. Handles debug mode display with token masking.
- **plugin.go** - Plugin struct and configuration validation. Contains `Exec()` which orchestrates job triggering. Includes `parseParameters()` for converting multi-line `key=value` strings to URL values.
- **jenkins.go** - Jenkins HTTP client implementation. Handles:
- Authentication (basic auth with API token)
- SSL/TLS with custom CA certificates (PEM content, file path, or URL)
- Job triggering via `/build` or `/buildWithParameters` endpoints
- Queue monitoring and build status polling for wait mode
- Nested job path parsing (converts `folder/job` to `/job/folder/job/job`)
## Key Patterns
- **Authentication**: Either `user + token` (API token auth) OR `remote-token` (remote trigger token). Validated in `main.go:run()`.
- **Parameters format**: Multi-line string with one `key=value` per line. Parsed in `plugin.go:parseParameters()`.
- **Wait mode**: Uses two-phase polling - first waits for queue item to get a build number, then polls build status until completion.
- **Environment variables**: Each flag accepts multiple env vars (e.g., `PLUGIN_URL`, `JENKINS_URL`, `INPUT_URL`) for compatibility with different CI systems.
+49
View File
@@ -48,6 +48,37 @@ Example configuration with jobs in the folder:
It will trigger the URL of Jenkins job like as `http://example.com/job/folder_name/job/job_name/`
Example configuration with build parameters:
```yaml
- name: trigger jenkins job
image: appleboy/drone-jenkins
settings:
url: http://example.com
user: appleboy
token: xxxxxxxxxx
job: parameterized-job
parameters: |
ENVIRONMENT=production
VERSION=${DRONE_TAG}
COMMIT_SHA=${DRONE_COMMIT_SHA}
```
Example configuration with wait for completion:
```yaml
- name: trigger jenkins job and wait
image: appleboy/drone-jenkins
settings:
url: http://example.com
user: appleboy
token: xxxxxxxxxx
job: deploy-job
wait: true
poll_interval: 15s
timeout: 1h
```
## Parameter Reference
url
@@ -61,3 +92,21 @@ token
job
: jenkins job name
parameters
: build parameters in multi-line `key=value` format (one per line)
wait
: wait for job completion (default: false)
poll_interval
: interval between status checks when waiting (default: 10s)
timeout
: maximum time to wait for job completion (default: 30m)
insecure
: allow insecure SSL connections (default: false)
remote_token
: jenkins remote trigger token (alternative to user/token authentication)
+30 -9
View File
@@ -40,34 +40,55 @@ TAGS ?=
LDFLAGS ?= -X 'main.Version=$(VERSION)'
.PHONY: all
all: build
all: build ## Build the project (default target)
.PHONY: test
test:
test: ## Run tests with coverage
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: lint
lint: ## Run golangci-lint
golangci-lint run -v
.PHONY: fmt
fmt: ## Format code with golangci-lint
golangci-lint fmt
.PHONY: install
install: $(GOFILES)
install: $(GOFILES) ## Install binary to GOPATH/bin
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' .
.PHONY: build
build: $(EXECUTABLE)
build: $(EXECUTABLE) ## Build binary to bin/
$(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@ .
build_linux_amd64:
.PHONY: build_linux_amd64
build_linux_amd64: ## Build for 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) .
build_linux_arm64:
.PHONY: build_linux_arm64
build_linux_arm64: ## Build for 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) .
build_linux_arm:
.PHONY: build_linux_arm
build_linux_arm: ## Build for Linux arm (armv7)
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:
.PHONY: clean
clean: ## Clean build artifacts
$(GO) clean -x -i ./...
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
version:
.PHONY: help
help: ## Print this help message.
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@echo ""
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: version
version: ## Print current version
@echo $(VERSION)
+127 -89
View File
@@ -1,5 +1,7 @@
# drone-jenkins
[English](README.md) | [繁體中文](README.zh-TW.md) | [简体中文](README.zh-CN.md)
![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)
@@ -8,11 +10,32 @@
[![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)
A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https://jenkins.io/) jobs with flexible authentication and parameter support.
A CLI tool and CI/CD plugin for triggering [Jenkins](https://jenkins.io/) jobs. Works with [GitHub Actions](https://github.com/features/actions), [GitLab CI](https://docs.gitlab.com/ee/ci/), [Gitea Action](https://docs.gitea.com/usage/actions/overview), and any platform that supports Docker containers or shell commands.
## Why drone-jenkins?
In modern enterprise environments, teams often adopt different CI/CD platforms based on their specific needs, project requirements, or historical decisions. It's common to find:
- **Multiple CI platforms coexisting**: Some teams use Jenkins for its extensive plugin ecosystem, while others prefer GitHub Actions or GitLab CI for their simplicity and container-native approach.
- **Legacy systems integration**: Organizations with established Jenkins pipelines need to integrate with newer CI/CD workflows without rewriting everything.
- **Cross-team collaboration**: Different departments may standardize on different tools, requiring seamless communication between platforms.
**drone-jenkins** bridges this gap by allowing CI/CD pipelines to trigger Jenkins jobs as part of their workflow. It works seamlessly with **GitHub Actions**, **GitLab CI**, **Gitea Action**, and any CI platform that supports Docker containers or shell commands.
This enables:
- **Unified deployment pipelines**: Trigger existing Jenkins deployment jobs from any CI platform without migration
- **Gradual migration**: Teams can incrementally move to modern CI platforms while still leveraging Jenkins jobs
- **Best of both worlds**: Use GitHub Actions or GitLab CI for modern containerized builds and Jenkins for specialized tasks with specific plugins
- **Centralized orchestration**: Coordinate builds across multiple CI systems from a single pipeline
- **Flexibility**: Available as a CLI binary or Docker image—use it however fits your workflow
Whether you're managing a hybrid CI/CD environment or orchestrating complex multi-platform deployments, drone-jenkins provides the connectivity you need.
## Table of Contents
- [drone-jenkins](#drone-jenkins)
- [Why drone-jenkins?](#why-drone-jenkins)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Prerequisites](#prerequisites)
@@ -27,7 +50,6 @@ A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https:
- [Usage](#usage)
- [Command Line](#command-line)
- [Docker](#docker)
- [Drone CI](#drone-ci)
- [Development](#development)
- [Building](#building)
- [Testing](#testing)
@@ -40,9 +62,10 @@ A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https:
- Support for Jenkins build parameters
- Multiple authentication methods (API token or remote trigger token)
- Wait for job completion with configurable polling and timeout
- SSL/TLS support with optional insecure mode
- Debug mode with detailed parameter information and secure token masking
- SSL/TLS support with custom CA certificates (PEM content, file path, or URL)
- Cross-platform support (Linux, macOS, Windows)
- Available as binary, Docker image, or Drone plugin
- Available as CLI binary or Docker image
## Prerequisites
@@ -97,16 +120,9 @@ docker pull ghcr.io/appleboy/drone-jenkins
Set up a Jenkins server using Docker:
```sh
docker run \
--name jenkins \
-d --restart always \
-p 8080:8080 -p 50000:50000 \
-v /data/jenkins:/var/jenkins_home \
jenkins/jenkins:lts
docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:slim
```
**Note**: Create the `/data/jenkins` directory before starting Jenkins.
### Authentication
Jenkins API tokens are recommended for authentication. To create an API token:
@@ -124,24 +140,36 @@ Alternatively, you can use a remote trigger token configured in your Jenkins job
### Parameters Reference
| 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) |
| Wait | `--wait` | `PLUGIN_WAIT`, `JENKINS_WAIT` | No | Wait for job completion (default: false) |
| Poll Interval | `--poll-interval` | `PLUGIN_POLL_INTERVAL`, `JENKINS_POLL_INTERVAL` | No | Interval between status checks (default: 10s) |
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | No | Maximum time to wait for job completion (default: 30m) |
| 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 multi-line `key=value` format (one per line) |
| Insecure | `--insecure` | `PLUGIN_INSECURE`, `JENKINS_INSECURE` | No | Allow insecure SSL connections (default: false) |
| CA Cert | `--ca-cert` | `PLUGIN_CA_CERT`, `JENKINS_CA_CERT` | No | Custom CA certificate (PEM content, file path, or HTTP URL) |
| Wait | `--wait` | `PLUGIN_WAIT`, `JENKINS_WAIT` | No | Wait for job completion (default: false) |
| Poll Interval | `--poll-interval` | `PLUGIN_POLL_INTERVAL`, `JENKINS_POLL_INTERVAL` | No | Interval between status checks (default: 10s) |
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | No | Maximum time to wait for job completion (default: 30m) |
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | No | Enable debug mode to show detailed parameter information (default: false) |
**Authentication Requirements**: You must provide either:
- `user` + `token` (API token authentication), OR
- `remote-token` (remote trigger token authentication)
**Parameters Format**: The `parameters` field accepts a multi-line string where each line contains one `key=value` pair:
- Each parameter should be on a separate line
- Format: `KEY=VALUE` (one per line)
- Empty lines are automatically ignored
- Whitespace-only lines are skipped
- Keys are trimmed of surrounding whitespace
- Values preserve intentional spaces
- Values can contain `=` signs (everything after the first `=` is treated as the value)
## Usage
### Command Line
@@ -175,8 +203,21 @@ drone-jenkins \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--parameters "ENVIRONMENT=production" \
--parameters "VERSION=1.0.0"
--parameters $'ENVIRONMENT=production\nVERSION=1.0.0'
```
Or using environment variable:
```bash
export JENKINS_PARAMETERS="ENVIRONMENT=production
VERSION=1.0.0
BRANCH=main"
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job
```
**Using remote token authentication:**
@@ -201,6 +242,37 @@ drone-jenkins \
--timeout 1h
```
**With debug mode:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--debug
```
**With custom CA certificate:**
```bash
# Using a file path
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert /path/to/ca.pem
# Using a URL
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert https://example.com/ca-bundle.crt
```
### Docker
**Single job:**
@@ -233,7 +305,7 @@ docker run --rm \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_PARAMETERS="ENVIRONMENT=production,VERSION=1.0.0" \
-e JENKINS_PARAMETERS=$'ENVIRONMENT=production\nVERSION=1.0.0\nBRANCH=main' \
ghcr.io/appleboy/drone-jenkins
```
@@ -251,73 +323,39 @@ docker run --rm \
ghcr.io/appleboy/drone-jenkins
```
### Drone CI
**With debug mode:**
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
```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_DEBUG=true \
ghcr.io/appleboy/drone-jenkins
```
**Multiple jobs with parameters:**
**With custom CA certificate:**
```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}
```
```bash
# Using a mounted certificate file
docker run --rm \
-v /path/to/ca.pem:/ca.pem:ro \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=/ca.pem \
ghcr.io/appleboy/drone-jenkins
**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
```
**Wait for job completion:**
```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-production
wait: true
poll_interval: 15s
timeout: 1h
# Using a URL
docker run --rm \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=https://example.com/ca-bundle.crt \
ghcr.io/appleboy/drone-jenkins
```
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
+399
View File
@@ -0,0 +1,399 @@
# drone-jenkins
[English](README.md) | [繁體中文](README.zh-TW.md) | [简体中文](README.zh-CN.md)
![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)
一个用于触发 [Jenkins](https://jenkins.io/) 任务的 CLI 工具与 CI/CD 插件。支持 [GitHub Actions](https://github.com/features/actions)、[GitLab CI](https://docs.gitlab.com/ee/ci/)、[Gitea Action](https://docs.gitea.com/usage/actions/overview) 以及任何支持 Docker 容器或 Shell 命令的平台。
## 为什么选择 drone-jenkins
在现代企业环境中,团队经常根据特定需求、项目要求或历史决策采用不同的 CI/CD 平台。常见的情况包括:
- **多个 CI 平台并存**:有些团队因为 Jenkins 丰富的插件生态系统而使用它,而其他团队则偏好 GitHub Actions 或 GitLab CI 的简洁性和容器原生方式。
- **遗留系统集成**:拥有既有 Jenkins 流水线的组织需要与新的 CI/CD 工作流程集成,而不需要重写所有内容。
- **跨团队协作**:不同部门可能标准化使用不同的工具,需要平台之间的无缝沟通。
**drone-jenkins** 弥补了这个差距,让 CI/CD 流水线能够将触发 Jenkins 任务作为工作流程的一部分。它可以与 **GitHub Actions**、**GitLab CI**、**Gitea Action** 以及任何支持 Docker 容器或 Shell 命令的 CI 平台无缝协作。
这使得以下场景成为可能:
- **统一的部署流水线**:从任何 CI 平台触发现有的 Jenkins 部署任务,无需迁移
- **渐进式迁移**:团队可以逐步迁移到现代 CI 平台,同时继续使用 Jenkins 任务
- **两全其美**:使用 GitHub Actions 或 GitLab CI 进行现代容器化构建,并使用 Jenkins 处理需要特定插件的专门任务
- **集中式编排**:从单一流水线协调跨多个 CI 系统的构建
- **灵活使用**:提供 CLI 可执行文件或 Docker 镜像——根据您的工作流程选择使用方式
无论您是在管理混合 CI/CD 环境还是编排复杂的多平台部署,drone-jenkins 都能提供您所需的连接能力。
## 目录
- [drone-jenkins](#drone-jenkins)
- [为什么选择 drone-jenkins](#为什么选择-drone-jenkins)
- [目录](#目录)
- [功能特性](#功能特性)
- [前置条件](#前置条件)
- [安装](#安装)
- [下载可执行文件](#下载可执行文件)
- [从源码构建](#从源码构建)
- [Docker 镜像](#docker-镜像)
- [配置](#配置)
- [Jenkins 服务器设置](#jenkins-服务器设置)
- [认证](#认证)
- [参数参考](#参数参考)
- [使用方式](#使用方式)
- [命令行](#命令行)
- [Docker](#docker)
- [开发](#开发)
- [构建](#构建)
- [测试](#测试)
- [许可证](#许可证)
- [贡献](#贡献)
## 功能特性
- 触发单个或多个 Jenkins 任务
- 支持 Jenkins 构建参数
- 多种认证方式(API 令牌或远程触发令牌)
- 等待任务完成,可配置轮询间隔和超时时间
- 调试模式,显示详细参数信息并安全遮蔽令牌
- SSL/TLS 支持,可使用自定义 CA 证书(PEM 内容、文件路径或 URL)
- 跨平台支持(Linux、macOS、Windows
- 提供 CLI 可执行文件或 Docker 镜像
## 前置条件
- Jenkins 服务器(建议版本 2.0 或更新版本)
- 用于认证的 Jenkins API 令牌或远程触发令牌
- 对于 Jenkins 设置,建议使用 Docker,但非必需
## 安装
### 下载可执行文件
预编译的可执行文件可从[发布页面](https://github.com/appleboy/drone-jenkins/releases)下载,支持:
- **Linux**: amd64, 386
- **macOS (Darwin)**: amd64, 386
- **Windows**: amd64, 386
如果已安装 Go,也可以直接安装:
```sh
go install github.com/appleboy/drone-jenkins@latest
```
### 从源码构建
克隆仓库并构建:
```sh
git clone https://github.com/appleboy/drone-jenkins.git
cd drone-jenkins
make build
```
### Docker 镜像
构建 Docker 镜像:
```sh
make docker
```
或拉取预构建的镜像:
```sh
docker pull ghcr.io/appleboy/drone-jenkins
```
## 配置
### Jenkins 服务器设置
使用 Docker 设置 Jenkins 服务器:
```sh
docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:slim
```
### 认证
建议使用 Jenkins API 令牌进行认证。创建 API 令牌的步骤:
1. 登录 Jenkins
2. 点击右上角的用户名
3. 选择"安全"
4. 在"API 令牌"下,点击"添加新令牌"
5. 输入名称并点击"生成"
6. 复制生成的令牌
![personal token](./images/personal-token.png)
或者,您可以使用在 Jenkins 任务设置中配置的远程触发令牌。
### 参数参考
| 参数 | CLI 标志 | 环境变量 | 必需 | 说明 |
| ------------- | -------------------- | ----------------------------------------------- | ------------- | ------------------------------------------------------------------------- |
| Host | `--host` | `PLUGIN_URL`, `JENKINS_URL` | 是 | Jenkins 基础 URL(例如 `http://jenkins.example.com/`) |
| User | `--user`, `-u` | `PLUGIN_USER`, `JENKINS_USER` | 条件式\* | Jenkins 用户名 |
| Token | `--token`, `-t` | `PLUGIN_TOKEN`, `JENKINS_TOKEN` | 条件式\* | Jenkins API 令牌 |
| Remote Token | `--remote-token` | `PLUGIN_REMOTE_TOKEN`, `JENKINS_REMOTE_TOKEN` | 条件式\* | Jenkins 远程触发令牌 |
| Job | `--job`, `-j` | `PLUGIN_JOB`, `JENKINS_JOB` | 是 | Jenkins 任务名称 - 可指定多个 |
| Parameters | `--parameters`, `-p` | `PLUGIN_PARAMETERS`, `JENKINS_PARAMETERS` | 否 | 构建参数,多行 `key=value` 格式(每行一个) |
| Insecure | `--insecure` | `PLUGIN_INSECURE`, `JENKINS_INSECURE` | 否 | 允许不安全的 SSL 连接(默认:false) |
| CA Cert | `--ca-cert` | `PLUGIN_CA_CERT`, `JENKINS_CA_CERT` | 否 | 自定义 CA 证书(PEM 内容、文件路径或 HTTP URL) |
| Wait | `--wait` | `PLUGIN_WAIT`, `JENKINS_WAIT` | 否 | 等待任务完成(默认:false) |
| Poll Interval | `--poll-interval` | `PLUGIN_POLL_INTERVAL`, `JENKINS_POLL_INTERVAL` | 否 | 状态检查间隔(默认:10s) |
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | 否 | 等待任务完成的最长时间(默认:30m) |
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | 否 | 启用调试模式以显示详细参数信息(默认:false) |
**认证要求**:您必须提供以下其中一种:
- `user` + `token`API 令牌认证),或
- `remote-token`(远程触发令牌认证)
**参数格式**`parameters` 字段接受多行字符串,每行包含一个 `key=value` 配对:
- 每个参数应该在单独一行
- 格式:`KEY=VALUE`(每行一个)
- 空行会自动忽略
- 只有空白的行会被跳过
- 键名会去除前后空白
- 值会保留有意义的空格
- 值可以包含 `=` 符号(第一个 `=` 之后的所有内容都视为值)
## 使用方式
### 命令行
**单个任务:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job drone-jenkins-plugin
```
**多个任务:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job drone-jenkins-plugin-1 \
--job drone-jenkins-plugin-2
```
**带构建参数:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--parameters $'ENVIRONMENT=production\nVERSION=1.0.0'
```
或使用环境变量:
```bash
export JENKINS_PARAMETERS="ENVIRONMENT=production
VERSION=1.0.0
BRANCH=main"
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job
```
**使用远程令牌认证:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--remote-token REMOTE_TOKEN_HERE \
--job my-jenkins-job
```
**等待任务完成:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--wait \
--poll-interval 15s \
--timeout 1h
```
**使用调试模式:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--debug
```
**使用自定义 CA 证书:**
```bash
# 使用文件路径
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert /path/to/ca.pem
# 使用 URL
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert https://example.com/ca-bundle.crt
```
### Docker
**单个任务:**
```bash
docker run --rm \
-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
```
**多个任务:**
```bash
docker run --rm \
-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
```
**带构建参数:**
```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\nVERSION=1.0.0\nBRANCH=main' \
ghcr.io/appleboy/drone-jenkins
```
**等待任务完成:**
```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_WAIT=true \
-e JENKINS_POLL_INTERVAL=15s \
-e JENKINS_TIMEOUT=1h \
ghcr.io/appleboy/drone-jenkins
```
**使用调试模式:**
```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_DEBUG=true \
ghcr.io/appleboy/drone-jenkins
```
**使用自定义 CA 证书:**
```bash
# 使用挂载的证书文件
docker run --rm \
-v /path/to/ca.pem:/ca.pem:ro \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=/ca.pem \
ghcr.io/appleboy/drone-jenkins
# 使用 URL
docker run --rm \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=https://example.com/ca-bundle.crt \
ghcr.io/appleboy/drone-jenkins
```
更多详细示例和高级配置,请参阅 [DOCS.md](DOCS.md)。
## 开发
### 构建
构建可执行文件:
```sh
make build
```
构建 Docker 镜像:
```sh
make docker
```
### 测试
运行测试套件:
```sh
make test
```
运行测试并生成覆盖率报告:
```sh
make test-coverage
```
## 许可证
Copyright (c) 2019 Bo-Yi Wu
## 贡献
欢迎贡献!请随时提交 Pull Request。
+399
View File
@@ -0,0 +1,399 @@
# drone-jenkins
[English](README.md) | [繁體中文](README.zh-TW.md) | [简体中文](README.zh-CN.md)
![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)
一個用於觸發 [Jenkins](https://jenkins.io/) 任務的 CLI 工具與 CI/CD 外掛。支援 [GitHub Actions](https://github.com/features/actions)、[GitLab CI](https://docs.gitlab.com/ee/ci/)、[Gitea Action](https://docs.gitea.com/usage/actions/overview) 以及任何支援 Docker 容器或 Shell 命令的平台。
## 為什麼選擇 drone-jenkins
在現代企業環境中,團隊經常根據特定需求、專案要求或歷史決策採用不同的 CI/CD 平台。常見的情況包括:
- **多個 CI 平台並存**:有些團隊因為 Jenkins 豐富的外掛生態系統而使用它,而其他團隊則偏好 GitHub Actions 或 GitLab CI 的簡潔性和容器原生方式。
- **舊有系統整合**:擁有既有 Jenkins 流水線的組織需要與新的 CI/CD 工作流程整合,而不需要重寫所有內容。
- **跨團隊協作**:不同部門可能標準化使用不同的工具,需要平台之間的無縫溝通。
**drone-jenkins** 彌補了這個差距,讓 CI/CD 流水線能夠將觸發 Jenkins 任務作為工作流程的一部分。它可以與 **GitHub Actions**、**GitLab CI**、**Gitea Action** 以及任何支援 Docker 容器或 Shell 命令的 CI 平台無縫協作。
這使得以下情境成為可能:
- **統一的部署流水線**:從任何 CI 平台觸發現有的 Jenkins 部署任務,無需遷移
- **漸進式遷移**:團隊可以逐步遷移到現代 CI 平台,同時繼續使用 Jenkins 任務
- **兩全其美**:使用 GitHub Actions 或 GitLab CI 進行現代容器化建置,並使用 Jenkins 處理需要特定外掛的專門任務
- **集中式協調**:從單一流水線協調跨多個 CI 系統的建置
- **彈性使用**:提供 CLI 執行檔或 Docker 映像檔——依照您的工作流程選擇使用方式
無論您是在管理混合 CI/CD 環境還是協調複雜的多平台部署,drone-jenkins 都能提供您所需的連接能力。
## 目錄
- [drone-jenkins](#drone-jenkins)
- [為什麼選擇 drone-jenkins](#為什麼選擇-drone-jenkins)
- [目錄](#目錄)
- [功能特色](#功能特色)
- [先決條件](#先決條件)
- [安裝](#安裝)
- [下載執行檔](#下載執行檔)
- [從原始碼建置](#從原始碼建置)
- [Docker 映像檔](#docker-映像檔)
- [設定](#設定)
- [Jenkins 伺服器設定](#jenkins-伺服器設定)
- [認證](#認證)
- [參數參考](#參數參考)
- [使用方式](#使用方式)
- [命令列](#命令列)
- [Docker](#docker)
- [開發](#開發)
- [建置](#建置)
- [測試](#測試)
- [授權條款](#授權條款)
- [貢獻](#貢獻)
## 功能特色
- 觸發單一或多個 Jenkins 任務
- 支援 Jenkins 建置參數
- 多種認證方式(API 令牌或遠端觸發令牌)
- 等待任務完成,可設定輪詢間隔和逾時時間
- 除錯模式,顯示詳細參數資訊並安全遮蔽令牌
- SSL/TLS 支援,可使用自訂 CA 憑證(PEM 內容、檔案路徑或 URL)
- 跨平台支援(Linux、macOS、Windows
- 提供 CLI 執行檔或 Docker 映像檔
## 先決條件
- Jenkins 伺服器(建議版本 2.0 或更新版本)
- 用於認證的 Jenkins API 令牌或遠端觸發令牌
- 對於 Jenkins 設定,建議使用 Docker,但非必要
## 安裝
### 下載執行檔
預先編譯的執行檔可從[發布頁面](https://github.com/appleboy/drone-jenkins/releases)下載,支援:
- **Linux**: amd64, 386
- **macOS (Darwin)**: amd64, 386
- **Windows**: amd64, 386
如果已安裝 Go,也可以直接安裝:
```sh
go install github.com/appleboy/drone-jenkins@latest
```
### 從原始碼建置
複製儲存庫並建置:
```sh
git clone https://github.com/appleboy/drone-jenkins.git
cd drone-jenkins
make build
```
### Docker 映像檔
建置 Docker 映像檔:
```sh
make docker
```
或拉取預先建置的映像檔:
```sh
docker pull ghcr.io/appleboy/drone-jenkins
```
## 設定
### Jenkins 伺服器設定
使用 Docker 設定 Jenkins 伺服器:
```sh
docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:slim
```
### 認證
建議使用 Jenkins API 令牌進行認證。建立 API 令牌的步驟:
1. 登入 Jenkins
2. 點擊右上角的使用者名稱
3. 選擇「安全性」
4. 在「API 令牌」下,點擊「新增令牌」
5. 輸入名稱並點擊「產生」
6. 複製產生的令牌
![personal token](./images/personal-token.png)
或者,您可以使用在 Jenkins 任務設定中配置的遠端觸發令牌。
### 參數參考
| 參數 | CLI 旗標 | 環境變數 | 必要 | 說明 |
| ------------- | -------------------- | ----------------------------------------------- | ------------- | ------------------------------------------------------------------------- |
| Host | `--host` | `PLUGIN_URL`, `JENKINS_URL` | 是 | Jenkins 基礎 URL(例如 `http://jenkins.example.com/`) |
| User | `--user`, `-u` | `PLUGIN_USER`, `JENKINS_USER` | 條件式\* | Jenkins 使用者名稱 |
| Token | `--token`, `-t` | `PLUGIN_TOKEN`, `JENKINS_TOKEN` | 條件式\* | Jenkins API 令牌 |
| Remote Token | `--remote-token` | `PLUGIN_REMOTE_TOKEN`, `JENKINS_REMOTE_TOKEN` | 條件式\* | Jenkins 遠端觸發令牌 |
| Job | `--job`, `-j` | `PLUGIN_JOB`, `JENKINS_JOB` | 是 | Jenkins 任務名稱 - 可指定多個 |
| Parameters | `--parameters`, `-p` | `PLUGIN_PARAMETERS`, `JENKINS_PARAMETERS` | 否 | 建置參數,多行 `key=value` 格式(每行一個) |
| Insecure | `--insecure` | `PLUGIN_INSECURE`, `JENKINS_INSECURE` | 否 | 允許不安全的 SSL 連線(預設:false) |
| CA Cert | `--ca-cert` | `PLUGIN_CA_CERT`, `JENKINS_CA_CERT` | 否 | 自訂 CA 憑證(PEM 內容、檔案路徑或 HTTP URL) |
| Wait | `--wait` | `PLUGIN_WAIT`, `JENKINS_WAIT` | 否 | 等待任務完成(預設:false) |
| Poll Interval | `--poll-interval` | `PLUGIN_POLL_INTERVAL`, `JENKINS_POLL_INTERVAL` | 否 | 狀態檢查間隔(預設:10s) |
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | 否 | 等待任務完成的最長時間(預設:30m) |
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | 否 | 啟用除錯模式以顯示詳細參數資訊(預設:false) |
**認證要求**:您必須提供以下其中一種:
- `user` + `token`API 令牌認證),或
- `remote-token`(遠端觸發令牌認證)
**參數格式**`parameters` 欄位接受多行字串,每行包含一個 `key=value` 配對:
- 每個參數應該在單獨一行
- 格式:`KEY=VALUE`(每行一個)
- 空行會自動忽略
- 只有空白的行會被跳過
- 鍵名會去除前後空白
- 值會保留有意義的空格
- 值可以包含 `=` 符號(第一個 `=` 之後的所有內容都視為值)
## 使用方式
### 命令列
**單一任務:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job drone-jenkins-plugin
```
**多個任務:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job drone-jenkins-plugin-1 \
--job drone-jenkins-plugin-2
```
**帶建置參數:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--parameters $'ENVIRONMENT=production\nVERSION=1.0.0'
```
或使用環境變數:
```bash
export JENKINS_PARAMETERS="ENVIRONMENT=production
VERSION=1.0.0
BRANCH=main"
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job
```
**使用遠端令牌認證:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--remote-token REMOTE_TOKEN_HERE \
--job my-jenkins-job
```
**等待任務完成:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--wait \
--poll-interval 15s \
--timeout 1h
```
**使用除錯模式:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--debug
```
**使用自訂 CA 憑證:**
```bash
# 使用檔案路徑
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert /path/to/ca.pem
# 使用 URL
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert https://example.com/ca-bundle.crt
```
### Docker
**單一任務:**
```bash
docker run --rm \
-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
```
**多個任務:**
```bash
docker run --rm \
-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
```
**帶建置參數:**
```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\nVERSION=1.0.0\nBRANCH=main' \
ghcr.io/appleboy/drone-jenkins
```
**等待任務完成:**
```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_WAIT=true \
-e JENKINS_POLL_INTERVAL=15s \
-e JENKINS_TIMEOUT=1h \
ghcr.io/appleboy/drone-jenkins
```
**使用除錯模式:**
```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_DEBUG=true \
ghcr.io/appleboy/drone-jenkins
```
**使用自訂 CA 憑證:**
```bash
# 使用掛載的憑證檔案
docker run --rm \
-v /path/to/ca.pem:/ca.pem:ro \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=/ca.pem \
ghcr.io/appleboy/drone-jenkins
# 使用 URL
docker run --rm \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=https://example.com/ca-bundle.crt \
ghcr.io/appleboy/drone-jenkins
```
更多詳細範例和進階設定,請參閱 [DOCS.md](DOCS.md)。
## 開發
### 建置
建置執行檔:
```sh
make build
```
建置 Docker 映像檔:
```sh
make docker
```
### 測試
執行測試套件:
```sh
make test
```
執行測試並產生覆蓋率報告:
```sh
make test-coverage
```
## 授權條款
Copyright (c) 2019 Bo-Yi Wu
## 貢獻
歡迎貢獻!請隨時提交 Pull Request。
+5 -3
View File
@@ -1,11 +1,13 @@
module github.com/appleboy/drone-jenkins
go 1.22
go 1.24.0
require (
github.com/appleboy/com v1.1.1
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.5
github.com/urfave/cli/v2 v2.27.7
github.com/yassinebenaid/godump v0.11.1
)
require (
@@ -13,6 +15,6 @@ require (
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
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+8 -4
View File
@@ -1,3 +1,5 @@
github.com/appleboy/com v1.1.1 h1:iqu+BzrEcO3Towwi4E0GDRLSEeMBix3gf3LRjn9h8ow=
github.com/appleboy/com v1.1.1/go.mod h1:WKU8+CaWcyLkpm0NLhGA8Wl/yGi3KXfTIXsp7T2ceZc=
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=
@@ -10,10 +12,12 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+249 -28
View File
@@ -3,14 +3,20 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strings"
"time"
"github.com/appleboy/com/gh"
"github.com/yassinebenaid/godump"
)
type (
@@ -26,6 +32,14 @@ type (
BaseURL string
Token string // Remote trigger token
Client *http.Client
Debug bool // Enable debug mode to show detailed information
crumb *CrumbResponse // Cached CSRF crumb
}
// CrumbResponse represents Jenkins crumb issuer response for CSRF protection
CrumbResponse struct {
Crumb string `json:"crumb"`
CrumbRequestField string `json:"crumbRequestField"`
}
// QueueItem represents a Jenkins queue item response
@@ -52,26 +66,116 @@ type (
}
)
// NewJenkins is initial Jenkins object
func NewJenkins(auth *Auth, url string, token string, insecure bool) *Jenkins {
url = strings.TrimRight(url, "/")
// loadCACert loads a CA certificate from various sources:
// - PEM content (if it starts with "-----BEGIN")
// - File path (if the file exists)
// - HTTP/HTTPS URL (if it starts with "http://" or "https://")
func loadCACert(ctx context.Context, caCert string) ([]byte, error) {
if caCert == "" {
return nil, nil
}
client := http.DefaultClient
if insecure {
client = &http.Client{
Transport: &http.Transport{
// #nosec G402 -- InsecureSkipVerify is intentionally configurable by user
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
// Check if it's PEM content (starts with BEGIN marker)
if strings.HasPrefix(strings.TrimSpace(caCert), "-----BEGIN") {
return []byte(caCert), nil
}
// Check if it's an HTTP/HTTPS URL
if strings.HasPrefix(caCert, "http://") || strings.HasPrefix(caCert, "https://") {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, caCert, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for CA certificate URL: %w", err)
}
resp, err := http.DefaultClient.Do(req) // #nosec G107 -- URL is user-provided configuration
if err != nil {
return nil, fmt.Errorf("failed to fetch CA certificate from URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch CA certificate: HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate from URL: %w", err)
}
return data, nil
}
// Otherwise, treat it as a file path
data, err := os.ReadFile(caCert)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate file: %w", err)
}
return data, nil
}
// NewJenkins is initial Jenkins object
func NewJenkins(
ctx context.Context,
auth *Auth,
baseURL string,
token string,
insecure bool,
caCert string,
debug bool,
) (*Jenkins, error) {
baseURL = strings.TrimRight(baseURL, "/")
// Load CA certificate if provided
caCertData, err := loadCACert(ctx, caCert)
if err != nil {
return nil, fmt.Errorf("failed to load CA certificate: %w", err)
}
// Build TLS configuration
var tlsConfig *tls.Config
if insecure {
// #nosec G402 -- InsecureSkipVerify is intentionally configurable by user
tlsConfig = &tls.Config{InsecureSkipVerify: true}
} else if caCertData != nil {
// Create certificate pool with custom CA
certPool, err := x509.SystemCertPool()
if err != nil {
// Fall back to empty pool if system pool unavailable
certPool = x509.NewCertPool()
}
if !certPool.AppendCertsFromPEM(caCertData) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
tlsConfig = &tls.Config{
RootCAs: certPool,
MinVersion: tls.VersionTLS12,
}
}
// Create CookieJar for session management (required for CSRF crumb)
jar, err := cookiejar.New(nil)
if err != nil {
return nil, fmt.Errorf("failed to create cookie jar: %w", err)
}
// Create HTTP Transport with optional TLS configuration
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
// Create HTTP client with CookieJar and Transport
client := &http.Client{
Jar: jar,
Transport: transport,
}
return &Jenkins{
Auth: auth,
BaseURL: url,
BaseURL: baseURL,
Token: token,
Client: client,
}
Debug: debug,
}, nil
}
func (jenkins *Jenkins) buildURL(path string, params url.Values) (requestURL string) {
@@ -86,22 +190,66 @@ func (jenkins *Jenkins) buildURL(path string, params url.Values) (requestURL str
return
}
func (jenkins *Jenkins) sendRequest(req *http.Request) (*http.Response, error) {
if jenkins.Auth != nil {
// getCrumb fetches CSRF crumb from Jenkins
// Returns nil if CSRF protection is disabled on Jenkins
//
//nolint:unparam // Error return kept for future extensibility and API consistency
func (jenkins *Jenkins) getCrumb(ctx context.Context) (*CrumbResponse, error) {
// Return cached crumb if available
if jenkins.crumb != nil {
return jenkins.crumb, nil
}
path := "/crumbIssuer/api/json"
var crumb CrumbResponse
err := jenkins.get(ctx, path, nil, &crumb)
if err != nil {
// CSRF protection might be disabled, log and continue
if jenkins.Debug {
log.Printf("crumb not available (CSRF may be disabled): %v", err)
}
return nil, nil
}
// Cache the crumb for subsequent requests
jenkins.crumb = &crumb
if jenkins.Debug {
log.Printf("obtained crumb: %s=%s", crumb.CrumbRequestField, crumb.Crumb)
}
return jenkins.crumb, nil
}
func (jenkins *Jenkins) sendRequest(
req *http.Request,
crumb *CrumbResponse,
) (*http.Response, error) {
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
}
// Add CSRF crumb header if available
if crumb != nil && crumb.CrumbRequestField != "" {
req.Header.Set(crumb.CrumbRequestField, crumb.Crumb)
}
return jenkins.Client.Do(req)
}
func (jenkins *Jenkins) get(path string, params url.Values, body interface{}) error {
func (jenkins *Jenkins) get(
ctx context.Context,
path string,
params url.Values,
body interface{},
) error {
requestURL := jenkins.buildURL(path, params)
req, err := http.NewRequestWithContext(context.Background(), "GET", requestURL, nil)
req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil)
if err != nil {
return err
}
resp, err := jenkins.sendRequest(req)
resp, err := jenkins.sendRequest(req, nil) // GET requests don't need crumb
if err != nil {
return err
}
@@ -124,15 +272,29 @@ func (jenkins *Jenkins) get(path string, params url.Values, body interface{}) er
}
// postAndGetLocation performs a POST request and extracts the queue ID from Location header
func (jenkins *Jenkins) postAndGetLocation(path string, params url.Values) (int, error) {
func (jenkins *Jenkins) postAndGetLocation(
ctx context.Context,
path string,
params url.Values,
) (int, error) {
// Fetch CSRF crumb before POST request (only if authenticated)
var crumb *CrumbResponse
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
var err error
crumb, err = jenkins.getCrumb(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get crumb: %w", err)
}
}
requestURL := jenkins.buildURL(path, params)
req, err := http.NewRequestWithContext(context.Background(), "POST", requestURL, nil)
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, nil)
if err != nil {
return 0, err
}
resp, err := jenkins.sendRequest(req)
resp, err := jenkins.sendRequest(req, crumb)
if err != nil {
return 0, err
}
@@ -196,11 +358,11 @@ func (jenkins *Jenkins) parseJobPath(job string) string {
}
// getQueueItem fetches information about a queue item
func (jenkins *Jenkins) getQueueItem(queueID int) (*QueueItem, error) {
func (jenkins *Jenkins) getQueueItem(ctx context.Context, queueID int) (*QueueItem, error) {
path := fmt.Sprintf("/queue/item/%d/api/json", queueID)
var queueItem QueueItem
err := jenkins.get(path, nil, &queueItem)
err := jenkins.get(ctx, path, nil, &queueItem)
if err != nil {
return nil, fmt.Errorf("failed to get queue item %d: %w", queueID, err)
}
@@ -209,11 +371,15 @@ func (jenkins *Jenkins) getQueueItem(queueID int) (*QueueItem, error) {
}
// getBuildInfo fetches information about a specific build
func (jenkins *Jenkins) getBuildInfo(job string, buildNumber int) (*BuildInfo, error) {
func (jenkins *Jenkins) getBuildInfo(
ctx context.Context,
job string,
buildNumber int,
) (*BuildInfo, error) {
path := fmt.Sprintf("%s/%d/api/json", jenkins.parseJobPath(job), buildNumber)
var buildInfo BuildInfo
err := jenkins.get(path, nil, &buildInfo)
err := jenkins.get(ctx, path, nil, &buildInfo)
if err != nil {
return nil, fmt.Errorf("failed to get build info for %s #%d: %w", job, buildNumber, err)
}
@@ -224,6 +390,7 @@ func (jenkins *Jenkins) getBuildInfo(job string, buildNumber int) (*BuildInfo, e
// waitForCompletion waits for a Jenkins build to complete
// It first polls the queue to get the build number, then polls the build status until completion
func (jenkins *Jenkins) waitForCompletion(
ctx context.Context,
job string,
queueID int,
pollInterval, timeout time.Duration,
@@ -239,7 +406,7 @@ func (jenkins *Jenkins) waitForCompletion(
return nil, fmt.Errorf("timeout waiting for job %s to start", job)
}
queueItem, err := jenkins.getQueueItem(queueID)
queueItem, err := jenkins.getQueueItem(ctx, queueID)
if err != nil {
// Queue item might be deleted after build starts, try to continue
log.Printf("warning: failed to get queue item: %v", err)
@@ -274,7 +441,7 @@ func (jenkins *Jenkins) waitForCompletion(
)
}
buildInfo, err := jenkins.getBuildInfo(job, buildNumber)
buildInfo, err := jenkins.getBuildInfo(ctx, job, buildNumber)
if err != nil {
log.Printf("warning: failed to get build info: %v", err)
time.Sleep(pollInterval)
@@ -289,6 +456,24 @@ func (jenkins *Jenkins) waitForCompletion(
buildNumber,
buildInfo.Result,
)
// Debug: Display final build info
if jenkins.Debug {
log.Println("=== Debug Mode: Build Result ===")
if err := godump.Dump(buildInfo); err != nil {
log.Printf("warning: failed to dump build info: %v", err)
}
log.Println("================================")
}
// Set GitHub Actions output
if err := gh.SetOutput(map[string]string{
"result": buildInfo.Result,
"url": buildInfo.URL,
}); err != nil {
log.Printf("warning: failed to set GitHub output: %v", err)
}
return buildInfo, nil
}
@@ -296,7 +481,7 @@ func (jenkins *Jenkins) waitForCompletion(
}
}
func (jenkins *Jenkins) trigger(job string, params url.Values) (int, error) {
func (jenkins *Jenkins) trigger(ctx context.Context, job string, params url.Values) (int, error) {
// Add remote trigger token to params
if jenkins.Token != "" {
if params == nil {
@@ -321,7 +506,43 @@ func (jenkins *Jenkins) trigger(job string, params url.Values) (int, error) {
urlPath = jenkins.parseJobPath(job) + "/build"
}
// Debug: Display parameters being sent
if jenkins.Debug {
log.Println("=== Debug Mode: Jenkins Job Trigger ===")
log.Printf("Job: %s", job)
log.Printf("URL Path: %s", urlPath)
// Build the full URL for display
fullURL := jenkins.buildURL(urlPath, params)
// Mask token in URL for display
if jenkins.Token != "" {
fullURL = strings.Replace(fullURL, "token="+jenkins.Token, "token=***MASKED***", 1)
}
log.Printf("Full URL: %s", fullURL)
if len(params) > 0 {
// Create a copy of params with masked token for display
displayParams := url.Values{}
for key, values := range params {
if key == "token" {
// Mask token values for security
displayParams[key] = []string{"***MASKED***"}
} else {
displayParams[key] = values
}
}
log.Println("Parameters:")
if err := godump.Dump(displayParams); err != nil {
log.Printf("warning: failed to dump parameters: %v", err)
}
} else {
log.Println("Parameters: (none)")
}
log.Println("======================================")
}
// All params (including token) are passed as query parameters
// Returns the queue item ID for tracking
return jenkins.postAndGetLocation(urlPath, params)
return jenkins.postAndGetLocation(ctx, urlPath, params)
}
+306 -15
View File
@@ -1,9 +1,12 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
@@ -16,7 +19,16 @@ func TestParseJobPath(t *testing.T) {
Username: "appleboy",
Token: "1234",
}
jenkins := NewJenkins(auth, "http://example.com", "", false)
jenkins, err := NewJenkins(
context.Background(),
auth,
"http://example.com",
"",
false,
"",
false,
)
assert.NoError(t, err)
assert.Equal(t, "/job/foo", jenkins.parseJobPath("/foo/"))
assert.Equal(t, "/job/foo", jenkins.parseJobPath("foo/"))
@@ -29,9 +41,10 @@ func TestUnSupportProtocol(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "example.com", "", false)
jenkins, err := NewJenkins(context.Background(), auth, "example.com", "", false, "", false)
assert.NoError(t, err)
queueID, err := jenkins.trigger("drone-jenkins", nil)
queueID, err := jenkins.trigger(context.Background(), "drone-jenkins", nil)
assert.NotNil(t, err)
assert.Equal(t, 0, queueID)
}
@@ -50,10 +63,19 @@ func TestTriggerBuild(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, server.URL, "remote-token", false)
jenkins, err := NewJenkins(
context.Background(),
auth,
server.URL,
"remote-token",
false,
"",
false,
)
assert.NoError(t, err)
params := url.Values{"param": []string{"value"}}
queueID, err := jenkins.trigger("drone-jenkins", params)
queueID, err := jenkins.trigger(context.Background(), "drone-jenkins", params)
assert.NoError(t, err)
assert.Equal(t, 123, queueID)
@@ -110,9 +132,10 @@ func TestPostAndGetLocation(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
queueID, err := jenkins.postAndGetLocation("/test", nil)
queueID, err := jenkins.postAndGetLocation(context.Background(), "/test", nil)
if tt.expectError {
assert.Error(t, err)
@@ -186,9 +209,10 @@ func TestGetQueueItem(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
queueItem, err := jenkins.getQueueItem(tt.queueID)
queueItem, err := jenkins.getQueueItem(context.Background(), tt.queueID)
if tt.expectError {
assert.Error(t, err)
@@ -274,9 +298,10 @@ func TestGetBuildInfo(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.getBuildInfo(tt.jobName, tt.buildNumber)
buildInfo, err := jenkins.getBuildInfo(context.Background(), tt.jobName, tt.buildNumber)
if tt.expectError {
assert.Error(t, err)
@@ -332,9 +357,11 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
context.Background(),
"test-job",
queueID,
100*time.Millisecond,
@@ -364,9 +391,11 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
context.Background(),
"test-job",
queueID,
50*time.Millisecond,
@@ -404,9 +433,11 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
context.Background(),
"test-job",
queueID,
50*time.Millisecond,
@@ -449,9 +480,11 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false)
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
context.Background(),
"test-job",
queueID,
50*time.Millisecond,
@@ -465,3 +498,261 @@ func TestWaitForCompletion(t *testing.T) {
assert.Equal(t, "FAILURE", buildInfo.Result)
})
}
// Sample CA certificate for testing (self-signed, not for production use)
const testCACert = `-----BEGIN CERTIFICATE-----
MIIDAzCCAeugAwIBAgIUGYBGBr+t20UAWJorEPULxzGIXUEwDQYJKoZIhvcNAQEL
BQAwETEPMA0GA1UEAwwGdGVzdGNhMB4XDTI1MTIwNjA1MDgzMloXDTM1MTIwNDA1
MDgzMlowETEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAq4bwnABqFenRVUoHLKhPiJXkh6TBFUaCWiEpKYNPywptBJNdyWNf
ouDxJ8gvQOMCkp3trnAHFcT6W5s8QLM1Hf/70QZI9GU/BtYm0KijU8aM+GJawNto
sK103TeCd0tVenDkxfamBGYnh3L5jtk0V/jeIsAIfFoe9Citu3MttRfxnSmZ4w2K
qlS14vKhFlO4WrXAh9j4PaVE5DL7jya/UKe6VVQIONCwUipRN6nU3UXhR7akVSmF
/bYkFsfdcErXJHjDpg+0xOsa5LJhzRkx5Uoqtviq2oRVVYhZc0eTwjq/407ocJ25
6WmerfKrtFDpzOZPa4XPVX9Am4vWugtrwQIDAQABo1MwUTAdBgNVHQ4EFgQUh7kL
LqmsvQP3TI6eiLVK7Gs7A00wHwYDVR0jBBgwFoAUh7kLLqmsvQP3TI6eiLVK7Gs7
A00wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApLOdWacya+Zi
w0Fd3UfSveuRsayAkMkZ4p0L9XKlADzwKtSF1Ykn6wiEiYfXd2TvffsR2XglOXFc
181IpBhP5u2mzK6pRvH9mqTs3w8JTcXMFmg8AKE2Vg5k22tBM2OUJJgKXkiACuHS
pZeOOvJcnjGunbTRwqais0TLYnkOcFsbgrSBKv82HiVootH/iKZahf1ViFMOURTh
MqjwIous7Y53Rq4RmfycIjNwODlDW0i5atKe8incDBiIYKw6sH8WN+nuhnHC/vJ5
5ZQvGCUsGOvma5ojWAiLs8wu4dODuF5ZNID3t+M36PQs7JDaQNN+AkZROOTSMqa/
ud3vS1A5+g==
-----END CERTIFICATE-----`
func TestLoadCACert(t *testing.T) {
t.Run("empty string returns nil", func(t *testing.T) {
data, err := loadCACert(context.Background(), "")
assert.NoError(t, err)
assert.Nil(t, data)
})
t.Run("PEM content directly", func(t *testing.T) {
data, err := loadCACert(context.Background(), testCACert)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("PEM content with leading whitespace", func(t *testing.T) {
data, err := loadCACert(context.Background(), " \n"+testCACert)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("file path", func(t *testing.T) {
// Create a temporary file with the certificate
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "ca.pem")
err := os.WriteFile(certFile, []byte(testCACert), 0o600)
assert.NoError(t, err)
data, err := loadCACert(context.Background(), certFile)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("file not found", func(t *testing.T) {
data, err := loadCACert(context.Background(), "/nonexistent/path/ca.pem")
assert.Error(t, err)
assert.Nil(t, data)
assert.Contains(t, err.Error(), "failed to read CA certificate file")
})
t.Run("HTTP URL", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(testCACert))
}))
defer server.Close()
data, err := loadCACert(context.Background(), server.URL)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("HTTPS URL", func(t *testing.T) {
server := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(testCACert))
}),
)
defer server.Close()
// Note: This test uses the test server's self-signed cert
// In real scenarios, the URL would be to a trusted source
// We skip HTTPS verification for this test
data, err := loadCACert(context.Background(), server.URL)
// This may fail due to certificate verification, which is expected
if err != nil {
assert.Contains(t, err.Error(), "certificate")
} else {
assert.NotNil(t, data)
}
})
t.Run("HTTP URL returns error status", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
data, err := loadCACert(context.Background(), server.URL)
assert.Error(t, err)
assert.Nil(t, data)
assert.Contains(t, err.Error(), "HTTP 404")
})
t.Run("HTTP URL unreachable", func(t *testing.T) {
data, err := loadCACert(context.Background(), "http://localhost:59999/nonexistent")
assert.Error(t, err)
assert.Nil(t, data)
assert.Contains(t, err.Error(), "failed to fetch CA certificate from URL")
})
}
func TestNewJenkinsWithCACert(t *testing.T) {
t.Run("with valid CA certificate", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
false,
testCACert,
false,
)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
assert.NotNil(t, jenkins.Client)
})
t.Run("with CA certificate from file", func(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "ca.pem")
err := os.WriteFile(certFile, []byte(testCACert), 0o600)
assert.NoError(t, err)
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
false,
certFile,
false,
)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
})
t.Run("with invalid CA certificate content", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
false,
"invalid-cert-data",
false,
)
assert.Error(t, err)
assert.Nil(t, jenkins)
assert.Contains(t, err.Error(), "failed to read CA certificate file")
})
t.Run("with invalid PEM format", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
invalidPEM := "-----BEGIN CERTIFICATE-----\ninvalid-base64-data\n-----END CERTIFICATE-----"
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
false,
invalidPEM,
false,
)
assert.Error(t, err)
assert.Nil(t, jenkins)
assert.Contains(t, err.Error(), "failed to parse CA certificate")
})
t.Run("with nonexistent file path", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
false,
"/nonexistent/ca.pem",
false,
)
assert.Error(t, err)
assert.Nil(t, jenkins)
assert.Contains(t, err.Error(), "failed to load CA certificate")
})
t.Run("insecure flag takes precedence over CA cert", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
// When insecure is true, CA cert should be ignored
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
true,
testCACert,
false,
)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
})
t.Run("without CA certificate uses default client", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
context.Background(),
auth,
"https://example.com",
"",
false,
"",
false,
)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
assert.NotNil(t, jenkins.Client)
// Client should have CookieJar for CSRF session management
assert.NotNil(t, jenkins.Client.Jar)
})
}
+64 -4
View File
@@ -8,6 +8,7 @@ import (
"github.com/joho/godotenv"
"github.com/urfave/cli/v2"
"github.com/yassinebenaid/godump"
)
// Version set at compile-time
@@ -23,6 +24,14 @@ ________ ____. __ .__
version: {{.Version}}
`
// maskToken masks a token string for secure display
func maskToken(token string) string {
if token == "" {
return ""
}
return "***MASKED***"
}
func main() {
// Load env-file if it exists first
if filename, found := os.LookupEnv("PLUGIN_ENV_FILE"); found {
@@ -83,10 +92,15 @@ func main() {
Usage: "allow insecure server connections when using SSL",
EnvVars: []string{"PLUGIN_INSECURE", "JENKINS_INSECURE", "INPUT_INSECURE"},
},
&cli.StringSliceFlag{
&cli.StringFlag{
Name: "ca-cert",
Usage: "custom CA certificate (PEM content, file path, or HTTP URL)",
EnvVars: []string{"PLUGIN_CA_CERT", "JENKINS_CA_CERT", "INPUT_CA_CERT"},
},
&cli.StringFlag{
Name: "parameters",
Aliases: []string{"p"},
Usage: "jenkins build parameters",
Usage: "jenkins build parameters (multi-line format: key=value, one per line)",
EnvVars: []string{"PLUGIN_PARAMETERS", "JENKINS_PARAMETERS", "INPUT_PARAMETERS"},
},
&cli.BoolFlag{
@@ -110,6 +124,11 @@ func main() {
Value: 30 * time.Minute,
EnvVars: []string{"PLUGIN_TIMEOUT", "JENKINS_TIMEOUT", "INPUT_TIMEOUT"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "enable debug mode to show detailed parameter information",
EnvVars: []string{"PLUGIN_DEBUG", "JENKINS_DEBUG", "INPUT_DEBUG"},
},
}
// Override a template
@@ -170,11 +189,52 @@ func run(c *cli.Context) error {
RemoteToken: c.String("remote-token"),
Job: c.StringSlice("job"),
Insecure: c.Bool("insecure"),
Parameters: c.StringSlice("parameters"),
CACert: c.String("ca-cert"),
Parameters: c.String("parameters"),
Wait: c.Bool("wait"),
PollInterval: c.Duration("poll-interval"),
Timeout: c.Duration("timeout"),
Debug: c.Bool("debug"),
}
return plugin.Exec()
// Display plugin configuration in debug mode
if plugin.Debug {
log.Println("=== Debug Mode: Plugin Configuration ===")
// Create a display copy with masked sensitive data
displayPlugin := struct {
BaseURL string
Username string
Token string
RemoteToken string
Job []string
Insecure bool
CACert string
Parameters string
Wait bool
PollInterval time.Duration
Timeout time.Duration
Debug bool
}{
BaseURL: plugin.BaseURL,
Username: plugin.Username,
Token: maskToken(plugin.Token),
RemoteToken: maskToken(plugin.RemoteToken),
Job: plugin.Job,
Insecure: plugin.Insecure,
CACert: plugin.CACert,
Parameters: plugin.Parameters,
Wait: plugin.Wait,
PollInterval: plugin.PollInterval,
Timeout: plugin.Timeout,
Debug: plugin.Debug,
}
if err := godump.Dump(displayPlugin); err != nil {
log.Printf("warning: failed to dump plugin configuration: %v", err)
}
log.Println("========================================")
}
return plugin.Exec(c.Context)
}
+51 -20
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"errors"
"fmt"
"log"
@@ -19,10 +20,12 @@ type (
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
CACert string // Custom CA certificate (PEM content, file path, or HTTP URL)
Parameters string // Job parameters in key=value format (one per line)
Wait bool // Whether to wait for job completion
PollInterval time.Duration // Interval between status checks (default: 10s)
Timeout time.Duration // Maximum time to wait for job completion (default: 30m)
Debug bool // Enable debug mode to show detailed parameter information
}
)
@@ -41,15 +44,27 @@ func trimWhitespaceFromSlice(items []string) []string {
return result
}
// parseParameters converts a slice of key=value strings into url.Values.
// parseParameters converts a multi-line string of key=value pairs into url.Values.
// Each line should contain one key=value pair.
// It logs a warning for any parameters that don't match the expected format.
func parseParameters(params []string) url.Values {
func parseParameters(params string) url.Values {
values := url.Values{}
for _, param := range params {
parts := strings.SplitN(param, "=", 2)
// Split by newlines and process each line
lines := strings.Split(params, "\n")
for _, line := range lines {
// Skip empty lines
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" {
continue
}
parts := strings.SplitN(trimmedLine, "=", 2)
if len(parts) != 2 {
log.Printf("warning: skipping invalid parameter format (expected key=value): %q", param)
log.Printf(
"warning: skipping invalid parameter format (expected key=value): %q",
trimmedLine,
)
continue
}
@@ -57,7 +72,7 @@ func parseParameters(params []string) url.Values {
value := parts[1] // Keep value as-is to preserve intentional spaces
if key == "" {
log.Printf("warning: skipping parameter with empty key: %q", param)
log.Printf("warning: skipping parameter with empty key: %q", trimmedLine)
continue
}
@@ -73,19 +88,23 @@ 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")
// Validate authentication: either (user + token) or remote-token must be provided
hasUserAuth := p.Username != "" && p.Token != ""
hasRemoteToken := p.RemoteToken != ""
if !hasUserAuth && !hasRemoteToken {
return errors.New("authentication 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 {
// The context can be used to cancel operations mid-execution.
func (p Plugin) Exec(ctx context.Context) error {
// Validate required configuration
if err := p.validateConfig(); err != nil {
return fmt.Errorf("configuration error: %w", err)
@@ -97,14 +116,20 @@ func (p Plugin) Exec() error {
return errors.New("at least one Jenkins job name is required")
}
// Set up authentication
auth := &Auth{
Username: p.Username,
Token: p.Token,
// Set up authentication (only if username and token are provided)
var auth *Auth
if p.Username != "" && p.Token != "" {
auth = &Auth{
Username: p.Username,
Token: p.Token,
}
}
// Initialize Jenkins client
jenkins := NewJenkins(auth, p.BaseURL, p.RemoteToken, p.Insecure)
jenkins, err := NewJenkins(ctx, auth, p.BaseURL, p.RemoteToken, p.Insecure, p.CACert, p.Debug)
if err != nil {
return fmt.Errorf("failed to initialize Jenkins client: %w", err)
}
// Parse job parameters
params := parseParameters(p.Parameters)
@@ -122,7 +147,7 @@ func (p Plugin) Exec() error {
// Trigger each job
for _, jobName := range jobs {
queueID, err := jenkins.trigger(jobName, params)
queueID, err := jenkins.trigger(ctx, jobName, params)
if err != nil {
return fmt.Errorf("failed to trigger job %q: %w", jobName, err)
}
@@ -130,7 +155,13 @@ func (p Plugin) Exec() error {
// Wait for job completion if requested
if p.Wait {
buildInfo, err := jenkins.waitForCompletion(jobName, queueID, pollInterval, timeout)
buildInfo, err := jenkins.waitForCompletion(
ctx,
jobName,
queueID,
pollInterval,
timeout,
)
if err != nil {
return fmt.Errorf("error waiting for job %q: %w", jobName, err)
}
+82 -32
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
@@ -31,24 +32,33 @@ func TestValidateConfig(t *testing.T) {
errorMsg: "jenkins base URL is required",
},
{
name: "missing username and token",
name: "missing authentication",
plugin: Plugin{
BaseURL: "http://example.com",
},
wantError: true,
errorMsg: "jenkins username is required",
errorMsg: "authentication required",
},
{
name: "missing token",
name: "missing token (only username)",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
},
wantError: true,
errorMsg: "jenkins API token is required",
errorMsg: "authentication required",
},
{
name: "all required config present",
name: "missing username (only token)",
plugin: Plugin{
BaseURL: "http://example.com",
Token: "bar",
},
wantError: true,
errorMsg: "authentication required",
},
{
name: "user and token auth",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
@@ -56,6 +66,24 @@ func TestValidateConfig(t *testing.T) {
},
wantError: false,
},
{
name: "remote token auth",
plugin: Plugin{
BaseURL: "http://example.com",
RemoteToken: "remote-token-123",
},
wantError: false,
},
{
name: "both auth methods",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
RemoteToken: "remote-token-123",
},
wantError: false,
},
}
for _, tt := range tests {
@@ -122,12 +150,12 @@ func TestTrimWhitespaceFromSlice(t *testing.T) {
func TestParseParameters(t *testing.T) {
tests := []struct {
name string
input []string
input string
expected url.Values
}{
{
name: "valid parameters",
input: []string{"key1=value1", "key2=value2"},
input: "key1=value1\nkey2=value2",
expected: url.Values{
"key1": []string{"value1"},
"key2": []string{"value2"},
@@ -135,38 +163,38 @@ func TestParseParameters(t *testing.T) {
},
{
name: "parameter with multiple equals signs",
input: []string{"key=value=with=equals"},
input: "key=value=with=equals",
expected: url.Values{
"key": []string{"value=with=equals"},
},
},
{
name: "parameter with spaces in value",
input: []string{"key=value with spaces"},
input: "key=value with spaces",
expected: url.Values{
"key": []string{"value with spaces"},
},
},
{
name: "parameter with empty value",
input: []string{"key="},
input: "key=",
expected: url.Values{
"key": []string{""},
},
},
{
name: "invalid parameter format (no equals)",
input: []string{"invalid"},
input: "invalid",
expected: url.Values{},
},
{
name: "parameter with empty key",
input: []string{"=value"},
input: "=value",
expected: url.Values{},
},
{
name: "mixed valid and invalid",
input: []string{"valid=yes", "invalid", "also=valid"},
input: "valid=yes\ninvalid\nalso=valid",
expected: url.Values{
"valid": []string{"yes"},
"also": []string{"valid"},
@@ -174,16 +202,32 @@ func TestParseParameters(t *testing.T) {
},
{
name: "key with surrounding whitespace",
input: []string{" key =value"},
input: " key =value",
expected: url.Values{
"key": []string{"value"},
},
},
{
name: "empty slice",
input: []string{},
name: "empty string",
input: "",
expected: url.Values{},
},
{
name: "multiple empty lines",
input: "key1=value1\n\n\nkey2=value2",
expected: url.Values{
"key1": []string{"value1"},
"key2": []string{"value2"},
},
},
{
name: "lines with whitespace only",
input: "key1=value1\n \n\t\nkey2=value2",
expected: url.Values{
"key1": []string{"value1"},
"key2": []string{"value2"},
},
},
}
for _, tt := range tests {
@@ -198,7 +242,7 @@ func TestParseParameters(t *testing.T) {
func TestExecMissingConfig(t *testing.T) {
var plugin Plugin
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
@@ -211,11 +255,11 @@ func TestExecMissingJenkinsUsername(t *testing.T) {
BaseURL: "http://example.com",
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins username is required")
assert.Contains(t, err.Error(), "authentication required")
}
// TestExecMissingJenkinsToken tests Exec with missing token
@@ -225,11 +269,11 @@ func TestExecMissingJenkinsToken(t *testing.T) {
Username: "foo",
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins API token is required")
assert.Contains(t, err.Error(), "authentication required")
}
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
@@ -261,7 +305,7 @@ func TestExecMissingJenkinsJob(t *testing.T) {
Job: tt.jobs,
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one Jenkins job name is required")
})
@@ -287,7 +331,7 @@ func TestExecTriggerBuild(t *testing.T) {
Job: []string{"drone-jenkins"},
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NoError(t, err)
}
@@ -297,7 +341,10 @@ 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++
// Only count POST requests (job triggers), not GET requests (crumb)
if r.Method == "POST" {
jobsTriggered++
}
w.Header().
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
w.WriteHeader(http.StatusCreated)
@@ -311,7 +358,7 @@ func TestExecTriggerMultipleJobs(t *testing.T) {
Job: []string{"job1", "job2", "job3"},
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, 3, jobsTriggered)
@@ -333,10 +380,10 @@ func TestExecWithParameters(t *testing.T) {
Username: "foo",
Token: "bar",
Job: []string{"parameterized-job"},
Parameters: []string{"branch=main", "environment=production"},
Parameters: "branch=main\nenvironment=production",
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, "main", receivedQuery.Get("branch"))
@@ -362,7 +409,7 @@ func TestExecWithRemoteToken(t *testing.T) {
Job: []string{"secure-job"},
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, "remote-token-123", receivedToken)
@@ -373,7 +420,10 @@ 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++
// Only count POST requests (job triggers), not GET requests (crumb)
if r.Method == "POST" {
jobsTriggered++
}
w.Header().
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
w.WriteHeader(http.StatusCreated)
@@ -387,7 +437,7 @@ func TestExecWithJobsContainingWhitespace(t *testing.T) {
Job: []string{" job1 ", "job2", " ", "job3"},
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NoError(t, err)
// Should trigger 3 jobs (whitespace-only entry should be filtered out)
@@ -423,7 +473,7 @@ func TestExecWithWaitSuccess(t *testing.T) {
Wait: true,
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NoError(t, err)
}
@@ -457,7 +507,7 @@ func TestExecWithWaitFailure(t *testing.T) {
Wait: true,
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed with status: FAILURE")