mirror of
https://github.com/appleboy/drone-jenkins.git
synced 2026-06-16 14:49:16 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be5114b976 |
@@ -10,11 +10,6 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -23,7 +18,6 @@ jobs:
|
|||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1"
|
go-version: "^1"
|
||||||
check-latest: true
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
@@ -35,13 +29,13 @@ jobs:
|
|||||||
make build_linux_arm64
|
make build_linux_arm64
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -49,7 +43,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
@@ -59,33 +53,8 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
- name: Build image for scanning
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: docker/Dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
tags: drone-jenkins:scan
|
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
|
||||||
uses: aquasecurity/trivy-action@v0.36.0
|
|
||||||
with:
|
|
||||||
image-ref: "drone-jenkins:scan"
|
|
||||||
format: "sarif"
|
|
||||||
output: "trivy-image-results.sarif"
|
|
||||||
severity: "CRITICAL,HIGH"
|
|
||||||
exit-code: '1'
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: "trivy-image-results.sarif"
|
|
||||||
category: "trivy-docker-image"
|
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v7
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ jobs:
|
|||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
check-latest: true
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup golangci-lint
|
- name: Setup golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.12
|
version: v2.6
|
||||||
args: --verbose
|
args: --verbose
|
||||||
|
|
||||||
- uses: hadolint/hadolint-action@v3.3.0
|
- uses: hadolint/hadolint-action@v3.3.0
|
||||||
@@ -31,7 +30,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
go: ["1.25", "1.26"]
|
go: ["1.25"]
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
go-build: ~/.cache/go-build
|
go-build: ~/.cache/go-build
|
||||||
@@ -45,7 +44,6 @@ jobs:
|
|||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
check-latest: true
|
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -65,6 +63,6 @@ jobs:
|
|||||||
go test -race -cover -coverprofile=coverage.out ./...
|
go test -race -cover -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v7
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: ${{ matrix.os }},go-${{ matrix.go }}
|
flags: ${{ matrix.os }},go-${{ matrix.go }}
|
||||||
|
|||||||
+24
-53
@@ -10,76 +10,47 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
# Run daily at 00:00 UTC
|
# Run daily at 00:00 UTC
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write # Required for uploading SARIF results
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trivy-repo-scan:
|
trivy-scan:
|
||||||
name: Trivy Repository Scan
|
name: Trivy Security Scan
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (repo)
|
- name: Run Trivy vulnerability scanner (source code)
|
||||||
uses: aquasecurity/trivy-action@v0.36.0
|
uses: aquasecurity/trivy-action@0.33.1
|
||||||
with:
|
with:
|
||||||
scan-type: "fs"
|
scan-type: "fs"
|
||||||
scan-ref: "."
|
scan-ref: "."
|
||||||
|
scanners: "vuln,secret,misconfig"
|
||||||
format: "sarif"
|
format: "sarif"
|
||||||
output: "trivy-repo-results.sarif"
|
output: "trivy-results.sarif"
|
||||||
severity: "CRITICAL,HIGH"
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- name: Upload Trivy results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: "trivy-repo-results.sarif"
|
sarif_file: "trivy-results.sarif"
|
||||||
|
|
||||||
trivy-image-scan:
|
- name: Run Trivy scanner (table output for logs)
|
||||||
name: Trivy Image Scan
|
uses: aquasecurity/trivy-action@0.33.1
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
check-latest: true
|
|
||||||
|
|
||||||
- name: Build binary
|
|
||||||
run: |
|
|
||||||
make build_linux_amd64
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Build Docker image for scanning
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: docker/Dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
tags: drone-jenkins:scan
|
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (image)
|
|
||||||
uses: aquasecurity/trivy-action@v0.36.0
|
|
||||||
with:
|
|
||||||
image-ref: "drone-jenkins:scan"
|
|
||||||
format: "sarif"
|
|
||||||
output: "trivy-image-results.sarif"
|
|
||||||
severity: "CRITICAL,HIGH"
|
|
||||||
|
|
||||||
- name: Upload Trivy image scan results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: "trivy-image-results.sarif"
|
scan-type: "fs"
|
||||||
category: "trivy-image"
|
scan-ref: "."
|
||||||
|
scanners: "vuln,secret,misconfig"
|
||||||
|
format: "table"
|
||||||
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
|
ignore-unfixed: true
|
||||||
|
exit-code: "1"
|
||||||
|
|||||||
@@ -46,16 +46,10 @@ Whether you're managing a hybrid CI/CD environment or orchestrating complex mult
|
|||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Jenkins Server Setup](#jenkins-server-setup)
|
- [Jenkins Server Setup](#jenkins-server-setup)
|
||||||
- [Authentication](#authentication)
|
- [Authentication](#authentication)
|
||||||
- [Understanding Jenkins Authentication](#understanding-jenkins-authentication)
|
|
||||||
- [CSRF Protection Notice](#csrf-protection-notice)
|
|
||||||
- [Parameters Reference](#parameters-reference)
|
- [Parameters Reference](#parameters-reference)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Command Line](#command-line)
|
- [Command Line](#command-line)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [Error: 403 No valid crumb was included in the request](#error-403-no-valid-crumb-was-included-in-the-request)
|
|
||||||
- [Error: 401 Unauthorized](#error-401-unauthorized)
|
|
||||||
- [Remote Token Not Working](#remote-token-not-working)
|
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Building](#building)
|
- [Building](#building)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
@@ -131,20 +125,7 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
|
|||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
#### Understanding Jenkins Authentication
|
Jenkins API tokens are recommended for authentication. To create an API token:
|
||||||
|
|
||||||
Jenkins supports multiple authentication methods for triggering builds. This tool supports two approaches:
|
|
||||||
|
|
||||||
**1. API Token Authentication (Recommended)**
|
|
||||||
|
|
||||||
Use Jenkins user credentials with an API token. This method:
|
|
||||||
|
|
||||||
- ✅ Works with all Jenkins configurations
|
|
||||||
- ✅ Supports CSRF protection (enabled by default in modern Jenkins)
|
|
||||||
- ✅ Supports wait mode to monitor build completion
|
|
||||||
- ✅ Provides full access to Jenkins API features
|
|
||||||
|
|
||||||
To create an API token:
|
|
||||||
|
|
||||||
1. Log into Jenkins
|
1. Log into Jenkins
|
||||||
2. Click on your username (top right)
|
2. Click on your username (top right)
|
||||||
@@ -155,42 +136,7 @@ To create an API token:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
**2. Remote Trigger Token Authentication**
|
Alternatively, you can use a remote trigger token configured in your Jenkins job settings.
|
||||||
|
|
||||||
Use a remote trigger token configured in your Jenkins job. **Important limitations**:
|
|
||||||
|
|
||||||
- ⚠️ **Does not work** with Jenkins CSRF protection enabled (default in modern Jenkins)
|
|
||||||
- ⚠️ Requires anonymous users to have read access to the job, OR
|
|
||||||
- ⚠️ Must be combined with API token authentication (see Combined Authentication below)
|
|
||||||
|
|
||||||
**3. Combined Authentication (Recommended for Remote Tokens)**
|
|
||||||
|
|
||||||
Use both API token and remote trigger token together:
|
|
||||||
|
|
||||||
- ✅ Works with CSRF protection enabled
|
|
||||||
- ✅ Provides double authentication security
|
|
||||||
- ✅ Supports all features including wait mode
|
|
||||||
|
|
||||||
```bash
|
|
||||||
drone-jenkins \
|
|
||||||
--host http://jenkins.example.com/ \
|
|
||||||
--user YOUR_USERNAME \
|
|
||||||
--token YOUR_API_TOKEN \
|
|
||||||
--remote-token YOUR_REMOTE_TOKEN \
|
|
||||||
--job my-jenkins-job
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CSRF Protection Notice
|
|
||||||
|
|
||||||
Modern Jenkins installations have CSRF protection enabled by default. If you encounter errors like:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
Error 403 No valid crumb was included in the request
|
|
||||||
```
|
|
||||||
|
|
||||||
This means your Jenkins has CSRF protection enabled. You **must** use API token authentication (option 1 or 3 above). Remote trigger token alone will not work.
|
|
||||||
|
|
||||||
For more information about Jenkins CSRF protection, see the [official Jenkins documentation](https://www.jenkins.io/doc/book/security/csrf-protection/).
|
|
||||||
|
|
||||||
### Parameters Reference
|
### Parameters Reference
|
||||||
|
|
||||||
@@ -209,19 +155,10 @@ For more information about Jenkins CSRF protection, see the [official Jenkins do
|
|||||||
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | No | Maximum time to wait for job completion (default: 30m) |
|
| 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) |
|
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | No | Enable debug mode to show detailed parameter information (default: false) |
|
||||||
|
|
||||||
**Authentication Requirements**:
|
**Authentication Requirements**: You must provide either:
|
||||||
|
|
||||||
For Jenkins with **CSRF protection enabled** (default in modern Jenkins):
|
- `user` + `token` (API token authentication), OR
|
||||||
|
- `remote-token` (remote trigger token authentication)
|
||||||
- **Required**: `user` + `token` (API token authentication)
|
|
||||||
- **Optional**: `remote-token` (for additional security)
|
|
||||||
|
|
||||||
For Jenkins with **CSRF protection disabled** (not recommended):
|
|
||||||
|
|
||||||
- **Option 1**: `user` + `token` (API token authentication)
|
|
||||||
- **Option 2**: `remote-token` only (requires anonymous read access to job)
|
|
||||||
|
|
||||||
**Important**: If you encounter "403 No valid crumb" errors, you must use API token authentication (`user` + `token`).
|
|
||||||
|
|
||||||
**Parameters Format**: The `parameters` field accepts a multi-line string where each line contains one `key=value` pair:
|
**Parameters Format**: The `parameters` field accepts a multi-line string where each line contains one `key=value` pair:
|
||||||
|
|
||||||
@@ -283,22 +220,9 @@ drone-jenkins \
|
|||||||
--job my-jenkins-job
|
--job my-jenkins-job
|
||||||
```
|
```
|
||||||
|
|
||||||
**Using combined authentication (API token + remote token - Recommended):**
|
**Using remote token authentication:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
drone-jenkins \
|
|
||||||
--host http://jenkins.example.com/ \
|
|
||||||
--user appleboy \
|
|
||||||
--token XXXXXXXX \
|
|
||||||
--remote-token REMOTE_TOKEN_HERE \
|
|
||||||
--job my-jenkins-job
|
|
||||||
```
|
|
||||||
|
|
||||||
**Using remote token only (only works without CSRF protection):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Note: This will fail if Jenkins has CSRF protection enabled
|
|
||||||
# You will get "403 No valid crumb" error
|
|
||||||
drone-jenkins \
|
drone-jenkins \
|
||||||
--host http://jenkins.example.com/ \
|
--host http://jenkins.example.com/ \
|
||||||
--remote-token REMOTE_TOKEN_HERE \
|
--remote-token REMOTE_TOKEN_HERE \
|
||||||
@@ -385,18 +309,6 @@ docker run --rm \
|
|||||||
ghcr.io/appleboy/drone-jenkins
|
ghcr.io/appleboy/drone-jenkins
|
||||||
```
|
```
|
||||||
|
|
||||||
**With combined authentication (API token + remote token):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm \
|
|
||||||
-e JENKINS_URL=http://jenkins.example.com/ \
|
|
||||||
-e JENKINS_USER=appleboy \
|
|
||||||
-e JENKINS_TOKEN=xxxxxxx \
|
|
||||||
-e JENKINS_REMOTE_TOKEN=your_remote_token \
|
|
||||||
-e JENKINS_JOB=my-jenkins-job \
|
|
||||||
ghcr.io/appleboy/drone-jenkins
|
|
||||||
```
|
|
||||||
|
|
||||||
**Wait for job completion:**
|
**Wait for job completion:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -448,57 +360,6 @@ docker run --rm \
|
|||||||
|
|
||||||
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
|
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Error: 403 No valid crumb was included in the request
|
|
||||||
|
|
||||||
**Cause**: Your Jenkins server has CSRF protection enabled (this is the default in modern Jenkins). Learn more at the [Jenkins CSRF Protection documentation](https://www.jenkins.io/doc/book/security/csrf-protection/).
|
|
||||||
|
|
||||||
**Solution**: Use API token authentication instead of remote token only:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ❌ This will fail with CSRF protection enabled
|
|
||||||
drone-jenkins \
|
|
||||||
--host http://jenkins.example.com/ \
|
|
||||||
--remote-token YOUR_REMOTE_TOKEN \
|
|
||||||
--job my-jenkins-job
|
|
||||||
|
|
||||||
# ✅ Use this instead
|
|
||||||
drone-jenkins \
|
|
||||||
--host http://jenkins.example.com/ \
|
|
||||||
--user YOUR_USERNAME \
|
|
||||||
--token YOUR_API_TOKEN \
|
|
||||||
--job my-jenkins-job
|
|
||||||
|
|
||||||
# ✅ Or combine both for additional security
|
|
||||||
drone-jenkins \
|
|
||||||
--host http://jenkins.example.com/ \
|
|
||||||
--user YOUR_USERNAME \
|
|
||||||
--token YOUR_API_TOKEN \
|
|
||||||
--remote-token YOUR_REMOTE_TOKEN \
|
|
||||||
--job my-jenkins-job
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: 401 Unauthorized
|
|
||||||
|
|
||||||
**Cause**: Invalid credentials or incorrect authentication method.
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
|
|
||||||
1. Verify your username and API token are correct
|
|
||||||
2. Ensure you're using an API token, not your Jenkins password
|
|
||||||
3. Check if you have permission to trigger the job
|
|
||||||
4. Make sure both `--user` and `--token` are provided together
|
|
||||||
|
|
||||||
### Remote Token Not Working
|
|
||||||
|
|
||||||
**Cause**: Remote trigger tokens alone only work in specific scenarios:
|
|
||||||
|
|
||||||
- Jenkins has CSRF protection disabled (not recommended), AND
|
|
||||||
- Anonymous users have read access to the job
|
|
||||||
|
|
||||||
**Solution**: Use combined authentication (API token + remote token) as shown in the examples above.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
- [配置](#配置)
|
- [配置](#配置)
|
||||||
- [Jenkins 服务器设置](#jenkins-服务器设置)
|
- [Jenkins 服务器设置](#jenkins-服务器设置)
|
||||||
- [认证](#认证)
|
- [认证](#认证)
|
||||||
- [CSRF 保护注意事项](#csrf-保护注意事项)
|
|
||||||
- [参数参考](#参数参考)
|
- [参数参考](#参数参考)
|
||||||
- [使用方式](#使用方式)
|
- [使用方式](#使用方式)
|
||||||
- [命令行](#命令行)
|
- [命令行](#命令行)
|
||||||
@@ -139,18 +138,6 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
|
|||||||
|
|
||||||
或者,您可以使用在 Jenkins 任务设置中配置的远程触发令牌。
|
或者,您可以使用在 Jenkins 任务设置中配置的远程触发令牌。
|
||||||
|
|
||||||
#### CSRF 保护注意事项
|
|
||||||
|
|
||||||
现代 Jenkins 安装默认启用 CSRF 保护。如果您遇到以下错误:
|
|
||||||
|
|
||||||
```
|
|
||||||
Error 403 No valid crumb was included in the request
|
|
||||||
```
|
|
||||||
|
|
||||||
这表示您的 Jenkins 已启用 CSRF 保护。您**必须**使用 API 令牌认证(user + token)。单独使用远程触发令牌将无法工作。
|
|
||||||
|
|
||||||
如需更多关于 Jenkins CSRF 保护的信息,请参阅 [Jenkins 官方文档](https://www.jenkins.io/doc/book/security/csrf-protection/)。
|
|
||||||
|
|
||||||
### 参数参考
|
### 参数参考
|
||||||
|
|
||||||
| 参数 | CLI 标志 | 环境变量 | 必需 | 说明 |
|
| 参数 | CLI 标志 | 环境变量 | 必需 | 说明 |
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
- [設定](#設定)
|
- [設定](#設定)
|
||||||
- [Jenkins 伺服器設定](#jenkins-伺服器設定)
|
- [Jenkins 伺服器設定](#jenkins-伺服器設定)
|
||||||
- [認證](#認證)
|
- [認證](#認證)
|
||||||
- [CSRF 保護注意事項](#csrf-保護注意事項)
|
|
||||||
- [參數參考](#參數參考)
|
- [參數參考](#參數參考)
|
||||||
- [使用方式](#使用方式)
|
- [使用方式](#使用方式)
|
||||||
- [命令列](#命令列)
|
- [命令列](#命令列)
|
||||||
@@ -139,18 +138,6 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
|
|||||||
|
|
||||||
或者,您可以使用在 Jenkins 任務設定中配置的遠端觸發令牌。
|
或者,您可以使用在 Jenkins 任務設定中配置的遠端觸發令牌。
|
||||||
|
|
||||||
#### CSRF 保護注意事項
|
|
||||||
|
|
||||||
現代 Jenkins 安裝預設啟用 CSRF 保護。如果您遇到以下錯誤:
|
|
||||||
|
|
||||||
```
|
|
||||||
Error 403 No valid crumb was included in the request
|
|
||||||
```
|
|
||||||
|
|
||||||
這表示您的 Jenkins 已啟用 CSRF 保護。您**必須**使用 API 令牌認證(user + token)。單獨使用遠端觸發令牌將無法運作。
|
|
||||||
|
|
||||||
如需更多關於 Jenkins CSRF 保護的資訊,請參閱 [Jenkins 官方文件](https://www.jenkins.io/doc/book/security/csrf-protection/)。
|
|
||||||
|
|
||||||
### 參數參考
|
### 參數參考
|
||||||
|
|
||||||
| 參數 | CLI 旗標 | 環境變數 | 必要 | 說明 |
|
| 參數 | CLI 旗標 | 環境變數 | 必要 | 說明 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/appleboy/drone-jenkins
|
module github.com/appleboy/drone-jenkins
|
||||||
|
|
||||||
go 1.25.10
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/appleboy/com v1.1.1
|
github.com/appleboy/com v1.1.1
|
||||||
|
|||||||
+8
-14
@@ -19,8 +19,6 @@ import (
|
|||||||
"github.com/yassinebenaid/godump"
|
"github.com/yassinebenaid/godump"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tokenParam = "token"
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Auth contain username and token
|
// Auth contain username and token
|
||||||
Auth struct {
|
Auth struct {
|
||||||
@@ -226,7 +224,7 @@ func (jenkins *Jenkins) sendRequest(
|
|||||||
req *http.Request,
|
req *http.Request,
|
||||||
crumb *CrumbResponse,
|
crumb *CrumbResponse,
|
||||||
) (*http.Response, error) {
|
) (*http.Response, error) {
|
||||||
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
|
if jenkins.Auth != nil {
|
||||||
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
|
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,14 +277,10 @@ func (jenkins *Jenkins) postAndGetLocation(
|
|||||||
path string,
|
path string,
|
||||||
params url.Values,
|
params url.Values,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
// Fetch CSRF crumb before POST request (only if authenticated)
|
// Fetch CSRF crumb before POST request
|
||||||
var crumb *CrumbResponse
|
crumb, err := jenkins.getCrumb(ctx)
|
||||||
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
|
if err != nil {
|
||||||
var err error
|
return 0, fmt.Errorf("failed to get crumb: %w", err)
|
||||||
crumb, err = jenkins.getCrumb(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get crumb: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestURL := jenkins.buildURL(path, params)
|
requestURL := jenkins.buildURL(path, params)
|
||||||
@@ -489,14 +483,14 @@ func (jenkins *Jenkins) trigger(ctx context.Context, job string, params url.Valu
|
|||||||
if params == nil {
|
if params == nil {
|
||||||
params = url.Values{}
|
params = url.Values{}
|
||||||
}
|
}
|
||||||
params.Set(tokenParam, jenkins.Token)
|
params.Set("token", jenkins.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
var urlPath string
|
var urlPath string
|
||||||
// Check if params contains build parameters (excluding 'token')
|
// Check if params contains build parameters (excluding 'token')
|
||||||
hasBuildParams := false
|
hasBuildParams := false
|
||||||
for key := range params {
|
for key := range params {
|
||||||
if key != tokenParam {
|
if key != "token" {
|
||||||
hasBuildParams = true
|
hasBuildParams = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -526,7 +520,7 @@ func (jenkins *Jenkins) trigger(ctx context.Context, job string, params url.Valu
|
|||||||
// Create a copy of params with masked token for display
|
// Create a copy of params with masked token for display
|
||||||
displayParams := url.Values{}
|
displayParams := url.Values{}
|
||||||
for key, values := range params {
|
for key, values := range params {
|
||||||
if key == tokenParam {
|
if key == "token" {
|
||||||
// Mask token values for security
|
// Mask token values for security
|
||||||
displayParams[key] = []string{"***MASKED***"}
|
displayParams[key] = []string{"***MASKED***"}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+43
-51
@@ -22,7 +22,7 @@ func TestParseJobPath(t *testing.T) {
|
|||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
auth,
|
auth,
|
||||||
testExampleURL,
|
"http://example.com",
|
||||||
"",
|
"",
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
@@ -38,8 +38,8 @@ func TestParseJobPath(t *testing.T) {
|
|||||||
|
|
||||||
func TestUnSupportProtocol(t *testing.T) {
|
func TestUnSupportProtocol(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, "example.com", "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, "example.com", "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -60,8 +60,8 @@ func TestTriggerBuild(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
@@ -129,8 +129,8 @@ func TestPostAndGetLocation(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -206,8 +206,8 @@ func TestGetQueueItem(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -242,7 +242,7 @@ func TestGetBuildInfo(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "build in progress",
|
name: "build in progress",
|
||||||
jobName: testJobName,
|
jobName: "test-job",
|
||||||
buildNumber: 123,
|
buildNumber: 123,
|
||||||
responseBody: `{"number":123,"building":true,"duration":0,"result":null,` +
|
responseBody: `{"number":123,"building":true,"duration":0,"result":null,` +
|
||||||
`"url":"http://jenkins.example.com/job/test-job/123/"}`,
|
`"url":"http://jenkins.example.com/job/test-job/123/"}`,
|
||||||
@@ -253,7 +253,7 @@ func TestGetBuildInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "build completed successfully",
|
name: "build completed successfully",
|
||||||
jobName: testJobName,
|
jobName: "test-job",
|
||||||
buildNumber: 124,
|
buildNumber: 124,
|
||||||
responseBody: `{"number":124,"building":false,"duration":5000,"result":"SUCCESS",` +
|
responseBody: `{"number":124,"building":false,"duration":5000,"result":"SUCCESS",` +
|
||||||
`"url":"http://jenkins.example.com/job/test-job/124/"}`,
|
`"url":"http://jenkins.example.com/job/test-job/124/"}`,
|
||||||
@@ -264,7 +264,7 @@ func TestGetBuildInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "build failed",
|
name: "build failed",
|
||||||
jobName: testJobName,
|
jobName: "test-job",
|
||||||
buildNumber: 125,
|
buildNumber: 125,
|
||||||
responseBody: `{"number":125,"building":false,"duration":3000,"result":"FAILURE",` +
|
responseBody: `{"number":125,"building":false,"duration":3000,"result":"FAILURE",` +
|
||||||
`"url":"http://jenkins.example.com/job/test-job/125/"}`,
|
`"url":"http://jenkins.example.com/job/test-job/125/"}`,
|
||||||
@@ -275,7 +275,7 @@ func TestGetBuildInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "build not found",
|
name: "build not found",
|
||||||
jobName: testJobName,
|
jobName: "test-job",
|
||||||
buildNumber: 999,
|
buildNumber: 999,
|
||||||
responseBody: "Not Found",
|
responseBody: "Not Found",
|
||||||
responseStatus: http.StatusNotFound,
|
responseStatus: http.StatusNotFound,
|
||||||
@@ -295,8 +295,8 @@ func TestGetBuildInfo(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -347,26 +347,22 @@ func TestWaitForCompletion(t *testing.T) {
|
|||||||
[]byte(`{"number":456,"building":true,"duration":0,"result":null}`),
|
[]byte(`{"number":456,"building":true,"duration":0,"result":null}`),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_, _ = w.Write(
|
_, _ = w.Write([]byte(`{"number":456,"building":false,"duration":5000,"result":"SUCCESS"}`))
|
||||||
[]byte(
|
|
||||||
`{"number":456,"building":false,"duration":5000,"result":"SUCCESS"}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
buildInfo, err := jenkins.waitForCompletion(
|
buildInfo, err := jenkins.waitForCompletion(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
testJobName,
|
"test-job",
|
||||||
queueID,
|
queueID,
|
||||||
100*time.Millisecond,
|
100*time.Millisecond,
|
||||||
5*time.Second,
|
5*time.Second,
|
||||||
@@ -392,15 +388,15 @@ func TestWaitForCompletion(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
buildInfo, err := jenkins.waitForCompletion(
|
buildInfo, err := jenkins.waitForCompletion(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
testJobName,
|
"test-job",
|
||||||
queueID,
|
queueID,
|
||||||
50*time.Millisecond,
|
50*time.Millisecond,
|
||||||
200*time.Millisecond,
|
200*time.Millisecond,
|
||||||
@@ -434,15 +430,15 @@ func TestWaitForCompletion(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
buildInfo, err := jenkins.waitForCompletion(
|
buildInfo, err := jenkins.waitForCompletion(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
testJobName,
|
"test-job",
|
||||||
queueID,
|
queueID,
|
||||||
50*time.Millisecond,
|
50*time.Millisecond,
|
||||||
200*time.Millisecond,
|
200*time.Millisecond,
|
||||||
@@ -474,26 +470,22 @@ func TestWaitForCompletion(t *testing.T) {
|
|||||||
[]byte(`{"number":456,"building":true,"duration":0,"result":null}`),
|
[]byte(`{"number":456,"building":true,"duration":0,"result":null}`),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_, _ = w.Write(
|
_, _ = w.Write([]byte(`{"number":456,"building":false,"duration":3000,"result":"FAILURE"}`))
|
||||||
[]byte(
|
|
||||||
`{"number":456,"building":false,"duration":3000,"result":"FAILURE"}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
buildInfo, err := jenkins.waitForCompletion(
|
buildInfo, err := jenkins.waitForCompletion(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
testJobName,
|
"test-job",
|
||||||
queueID,
|
queueID,
|
||||||
50*time.Millisecond,
|
50*time.Millisecond,
|
||||||
5*time.Second,
|
5*time.Second,
|
||||||
@@ -626,8 +618,8 @@ func TestLoadCACert(t *testing.T) {
|
|||||||
func TestNewJenkinsWithCACert(t *testing.T) {
|
func TestNewJenkinsWithCACert(t *testing.T) {
|
||||||
t.Run("with valid CA certificate", func(t *testing.T) {
|
t.Run("with valid CA certificate", func(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
@@ -650,8 +642,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
@@ -668,8 +660,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("with invalid CA certificate content", func(t *testing.T) {
|
t.Run("with invalid CA certificate content", func(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
@@ -687,8 +679,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("with invalid PEM format", func(t *testing.T) {
|
t.Run("with invalid PEM format", func(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
invalidPEM := "-----BEGIN CERTIFICATE-----\ninvalid-base64-data\n-----END CERTIFICATE-----"
|
invalidPEM := "-----BEGIN CERTIFICATE-----\ninvalid-base64-data\n-----END CERTIFICATE-----"
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
@@ -707,8 +699,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("with nonexistent file path", func(t *testing.T) {
|
t.Run("with nonexistent file path", func(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
@@ -726,8 +718,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("insecure flag takes precedence over CA cert", func(t *testing.T) {
|
t.Run("insecure flag takes precedence over CA cert", func(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
// When insecure is true, CA cert should be ignored
|
// When insecure is true, CA cert should be ignored
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
@@ -745,8 +737,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("without CA certificate uses default client", func(t *testing.T) {
|
t.Run("without CA certificate uses default client", func(t *testing.T) {
|
||||||
auth := &Auth{
|
auth := &Auth{
|
||||||
Username: testUserName,
|
Username: "test",
|
||||||
Token: testUserName,
|
Token: "test",
|
||||||
}
|
}
|
||||||
jenkins, err := NewJenkins(
|
jenkins, err := NewJenkins(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func main() {
|
|||||||
EnvVars: []string{"PLUGIN_USER", "JENKINS_USER", "INPUT_USER"},
|
EnvVars: []string{"PLUGIN_USER", "JENKINS_USER", "INPUT_USER"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: tokenParam,
|
Name: "token",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Usage: "jenkins API token for authentication",
|
Usage: "jenkins API token for authentication",
|
||||||
EnvVars: []string{"PLUGIN_TOKEN", "JENKINS_TOKEN", "INPUT_TOKEN"},
|
EnvVars: []string{"PLUGIN_TOKEN", "JENKINS_TOKEN", "INPUT_TOKEN"},
|
||||||
@@ -175,7 +175,7 @@ func run(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate authentication: either (user + token) or remote-token must be provided
|
// Validate authentication: either (user + token) or remote-token must be provided
|
||||||
hasUserAuth := c.String("user") != "" && c.String(tokenParam) != ""
|
hasUserAuth := c.String("user") != "" && c.String("token") != ""
|
||||||
hasRemoteToken := c.String("remote-token") != ""
|
hasRemoteToken := c.String("remote-token") != ""
|
||||||
|
|
||||||
if !hasUserAuth && !hasRemoteToken {
|
if !hasUserAuth && !hasRemoteToken {
|
||||||
@@ -185,7 +185,7 @@ func run(c *cli.Context) error {
|
|||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: c.String("host"),
|
BaseURL: c.String("host"),
|
||||||
Username: c.String("user"),
|
Username: c.String("user"),
|
||||||
Token: c.String(tokenParam),
|
Token: c.String("token"),
|
||||||
RemoteToken: c.String("remote-token"),
|
RemoteToken: c.String("remote-token"),
|
||||||
Job: c.StringSlice("job"),
|
Job: c.StringSlice("job"),
|
||||||
Insecure: c.Bool("insecure"),
|
Insecure: c.Bool("insecure"),
|
||||||
|
|||||||
@@ -88,15 +88,12 @@ func (p Plugin) validateConfig() error {
|
|||||||
if p.BaseURL == "" {
|
if p.BaseURL == "" {
|
||||||
return errors.New("jenkins base URL is required")
|
return errors.New("jenkins base URL is required")
|
||||||
}
|
}
|
||||||
|
if p.Username == "" {
|
||||||
// Validate authentication: either (user + token) or remote-token must be provided
|
return errors.New("jenkins username is required")
|
||||||
hasUserAuth := p.Username != "" && p.Token != ""
|
}
|
||||||
hasRemoteToken := p.RemoteToken != ""
|
if p.Token == "" {
|
||||||
|
return errors.New("jenkins API token is required")
|
||||||
if !hasUserAuth && !hasRemoteToken {
|
|
||||||
return errors.New("authentication required")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,13 +113,10 @@ func (p Plugin) Exec(ctx context.Context) error {
|
|||||||
return errors.New("at least one Jenkins job name is required")
|
return errors.New("at least one Jenkins job name is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up authentication (only if username and token are provided)
|
// Set up authentication
|
||||||
var auth *Auth
|
auth := &Auth{
|
||||||
if p.Username != "" && p.Token != "" {
|
Username: p.Username,
|
||||||
auth = &Auth{
|
Token: p.Token,
|
||||||
Username: p.Username,
|
|
||||||
Token: p.Token,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Jenkins client
|
// Initialize Jenkins client
|
||||||
|
|||||||
+55
-82
@@ -32,55 +32,28 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
errorMsg: "jenkins base URL is required",
|
errorMsg: "jenkins base URL is required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing authentication",
|
name: "missing username and token",
|
||||||
plugin: Plugin{
|
plugin: Plugin{
|
||||||
BaseURL: testExampleURL,
|
BaseURL: "http://example.com",
|
||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
errorMsg: testAuthRequiredErr,
|
errorMsg: "jenkins username is required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing token (only username)",
|
name: "missing token",
|
||||||
plugin: Plugin{
|
plugin: Plugin{
|
||||||
BaseURL: testExampleURL,
|
BaseURL: "http://example.com",
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
errorMsg: testAuthRequiredErr,
|
errorMsg: "jenkins API token is required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing username (only token)",
|
name: "all required config present",
|
||||||
plugin: Plugin{
|
plugin: Plugin{
|
||||||
BaseURL: testExampleURL,
|
BaseURL: "http://example.com",
|
||||||
Token: testUserBar,
|
Username: "foo",
|
||||||
},
|
Token: "bar",
|
||||||
wantError: true,
|
|
||||||
errorMsg: testAuthRequiredErr,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user and token auth",
|
|
||||||
plugin: Plugin{
|
|
||||||
BaseURL: testExampleURL,
|
|
||||||
Username: testUserFoo,
|
|
||||||
Token: testUserBar,
|
|
||||||
},
|
|
||||||
wantError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remote token auth",
|
|
||||||
plugin: Plugin{
|
|
||||||
BaseURL: testExampleURL,
|
|
||||||
RemoteToken: testRemoteTokenValue,
|
|
||||||
},
|
|
||||||
wantError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "both auth methods",
|
|
||||||
plugin: Plugin{
|
|
||||||
BaseURL: testExampleURL,
|
|
||||||
Username: testUserFoo,
|
|
||||||
Token: testUserBar,
|
|
||||||
RemoteToken: testRemoteTokenValue,
|
|
||||||
},
|
},
|
||||||
wantError: false,
|
wantError: false,
|
||||||
},
|
},
|
||||||
@@ -118,7 +91,7 @@ func TestTrimWhitespaceFromSlice(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "all whitespace",
|
name: "all whitespace",
|
||||||
input: []string{testWhitespaceVal, "\t", "\n"},
|
input: []string{" ", "\t", "\n"},
|
||||||
expected: []string{},
|
expected: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -129,12 +102,12 @@ func TestTrimWhitespaceFromSlice(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "trim surrounding whitespace",
|
name: "trim surrounding whitespace",
|
||||||
input: []string{" foo ", " bar ", "baz"},
|
input: []string{" foo ", " bar ", "baz"},
|
||||||
expected: []string{testUserFoo, testUserBar, "baz"},
|
expected: []string{"foo", "bar", "baz"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mixed empty and valid",
|
name: "mixed empty and valid",
|
||||||
input: []string{"", testValidStr, "", "also-valid", ""},
|
input: []string{"", "valid", "", "also-valid", ""},
|
||||||
expected: []string{testValidStr, "also-valid"},
|
expected: []string{"valid", "also-valid"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,29 +130,29 @@ func TestParseParameters(t *testing.T) {
|
|||||||
name: "valid parameters",
|
name: "valid parameters",
|
||||||
input: "key1=value1\nkey2=value2",
|
input: "key1=value1\nkey2=value2",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey1: []string{testParamValue1},
|
"key1": []string{"value1"},
|
||||||
testParamKey2: []string{testParamValue2},
|
"key2": []string{"value2"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parameter with multiple equals signs",
|
name: "parameter with multiple equals signs",
|
||||||
input: "key=value=with=equals",
|
input: "key=value=with=equals",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey: []string{"value=with=equals"},
|
"key": []string{"value=with=equals"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parameter with spaces in value",
|
name: "parameter with spaces in value",
|
||||||
input: "key=value with spaces",
|
input: "key=value with spaces",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey: []string{"value with spaces"},
|
"key": []string{"value with spaces"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parameter with empty value",
|
name: "parameter with empty value",
|
||||||
input: "key=",
|
input: "key=",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey: []string{""},
|
"key": []string{""},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,15 +169,15 @@ func TestParseParameters(t *testing.T) {
|
|||||||
name: "mixed valid and invalid",
|
name: "mixed valid and invalid",
|
||||||
input: "valid=yes\ninvalid\nalso=valid",
|
input: "valid=yes\ninvalid\nalso=valid",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testValidStr: []string{"yes"},
|
"valid": []string{"yes"},
|
||||||
"also": []string{testValidStr},
|
"also": []string{"valid"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "key with surrounding whitespace",
|
name: "key with surrounding whitespace",
|
||||||
input: " key =value",
|
input: " key =value",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey: []string{"value"},
|
"key": []string{"value"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -216,16 +189,16 @@ func TestParseParameters(t *testing.T) {
|
|||||||
name: "multiple empty lines",
|
name: "multiple empty lines",
|
||||||
input: "key1=value1\n\n\nkey2=value2",
|
input: "key1=value1\n\n\nkey2=value2",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey1: []string{testParamValue1},
|
"key1": []string{"value1"},
|
||||||
testParamKey2: []string{testParamValue2},
|
"key2": []string{"value2"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lines with whitespace only",
|
name: "lines with whitespace only",
|
||||||
input: "key1=value1\n \n\t\nkey2=value2",
|
input: "key1=value1\n \n\t\nkey2=value2",
|
||||||
expected: url.Values{
|
expected: url.Values{
|
||||||
testParamKey1: []string{testParamValue1},
|
"key1": []string{"value1"},
|
||||||
testParamKey2: []string{testParamValue2},
|
"key2": []string{"value2"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -252,28 +225,28 @@ func TestExecMissingConfig(t *testing.T) {
|
|||||||
// TestExecMissingJenkinsUsername tests Exec with missing username
|
// TestExecMissingJenkinsUsername tests Exec with missing username
|
||||||
func TestExecMissingJenkinsUsername(t *testing.T) {
|
func TestExecMissingJenkinsUsername(t *testing.T) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: testExampleURL,
|
BaseURL: "http://example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := plugin.Exec(context.Background())
|
err := plugin.Exec(context.Background())
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "configuration error")
|
assert.Contains(t, err.Error(), "configuration error")
|
||||||
assert.Contains(t, err.Error(), testAuthRequiredErr)
|
assert.Contains(t, err.Error(), "jenkins username is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExecMissingJenkinsToken tests Exec with missing token
|
// TestExecMissingJenkinsToken tests Exec with missing token
|
||||||
func TestExecMissingJenkinsToken(t *testing.T) {
|
func TestExecMissingJenkinsToken(t *testing.T) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: testExampleURL,
|
BaseURL: "http://example.com",
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := plugin.Exec(context.Background())
|
err := plugin.Exec(context.Background())
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "configuration error")
|
assert.Contains(t, err.Error(), "configuration error")
|
||||||
assert.Contains(t, err.Error(), testAuthRequiredErr)
|
assert.Contains(t, err.Error(), "jenkins API token is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
|
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
|
||||||
@@ -288,7 +261,7 @@ func TestExecMissingJenkinsJob(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only whitespace jobs",
|
name: "only whitespace jobs",
|
||||||
jobs: []string{testWhitespaceVal, "\t", "\n"},
|
jobs: []string{" ", "\t", "\n"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nil jobs",
|
name: "nil jobs",
|
||||||
@@ -299,9 +272,9 @@ func TestExecMissingJenkinsJob(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: testExampleURL,
|
BaseURL: "http://example.com",
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: tt.jobs,
|
Job: tt.jobs,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,8 +299,8 @@ func TestExecTriggerBuild(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: []string{"drone-jenkins"},
|
Job: []string{"drone-jenkins"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +326,8 @@ func TestExecTriggerMultipleJobs(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: []string{"job1", "job2", "job3"},
|
Job: []string{"job1", "job2", "job3"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,8 +350,8 @@ func TestExecWithParameters(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: []string{"parameterized-job"},
|
Job: []string{"parameterized-job"},
|
||||||
Parameters: "branch=main\nenvironment=production",
|
Parameters: "branch=main\nenvironment=production",
|
||||||
}
|
}
|
||||||
@@ -403,16 +376,16 @@ func TestExecWithRemoteToken(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
RemoteToken: testRemoteTokenValue,
|
RemoteToken: "remote-token-123",
|
||||||
Job: []string{"secure-job"},
|
Job: []string{"secure-job"},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := plugin.Exec(context.Background())
|
err := plugin.Exec(context.Background())
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, testRemoteTokenValue, receivedToken)
|
assert.Equal(t, "remote-token-123", receivedToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExecWithJobsContainingWhitespace tests job list with whitespace
|
// TestExecWithJobsContainingWhitespace tests job list with whitespace
|
||||||
@@ -432,9 +405,9 @@ func TestExecWithJobsContainingWhitespace(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: []string{" job1 ", "job2", testWhitespaceVal, "job3"},
|
Job: []string{" job1 ", "job2", " ", "job3"},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := plugin.Exec(context.Background())
|
err := plugin.Exec(context.Background())
|
||||||
@@ -467,9 +440,9 @@ func TestExecWithWaitSuccess(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: []string{testJobName},
|
Job: []string{"test-job"},
|
||||||
Wait: true,
|
Wait: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,9 +474,9 @@ func TestExecWithWaitFailure(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
BaseURL: server.URL,
|
BaseURL: server.URL,
|
||||||
Username: testUserFoo,
|
Username: "foo",
|
||||||
Token: testUserBar,
|
Token: "bar",
|
||||||
Job: []string{testJobName},
|
Job: []string{"test-job"},
|
||||||
Wait: true,
|
Wait: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
const (
|
|
||||||
testUserFoo = "foo"
|
|
||||||
testUserBar = "bar"
|
|
||||||
testUserName = "test"
|
|
||||||
testJobName = "test-job"
|
|
||||||
testExampleURL = "http://example.com"
|
|
||||||
testAuthRequiredErr = "authentication required"
|
|
||||||
testRemoteTokenValue = "remote-token-123"
|
|
||||||
testWhitespaceVal = " "
|
|
||||||
testValidStr = "valid"
|
|
||||||
testParamKey = "key"
|
|
||||||
testParamKey1 = "key1"
|
|
||||||
testParamKey2 = "key2"
|
|
||||||
testParamValue1 = "value1"
|
|
||||||
testParamValue2 = "value2"
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user