Compare commits

..

113 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
Bo-Yi Wu 627e233cc6 feat: add configurable Jenkins job wait and polling support (#40)
- Add support for waiting for Jenkins job completion with polling and timeout options
- Introduce flags and environment variables to control wait behavior, poll interval, and timeout
- Refactor the Jenkins job trigger to return queue item ID and enable job status tracking
- Implement functions for querying queue items and build status, including waiting until a build completes or times out
- Update CLI, plugin, and documentation to explain new job wait and polling capabilities
- Extend tests to cover queue item parsing, build info retrieval, and job completion scenarios with success, failure, and timeouts

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-01 21:53:42 +08:00
Bo-Yi Wu 747d7b23d1 docs: update Jenkins API token creation instructions and images
- Update instructions to select "Security" instead of "Configure" when creating a Jenkins API token
- Replace the token creation image with a new screenshot
- Remove the old jenkins-token.png image and add personal-token.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-05 13:28:14 +08:00
Bo-Yi Wu 1bb020b22d feat: add support for insecure TLS connections (#34)
- Add `crypto/tls` import in `jenkins.go`
- Add `Client` field to `Jenkins` struct
- Modify `NewJenkins` function to accept an `insecure` parameter and configure HTTP client accordingly
- Update `sendRequest` method to use the `Client` field from the `Jenkins` struct
- Update tests in `jenkins_test.go` to include the `insecure` parameter in `NewJenkins` calls
- Add `insecure` flag to CLI options in `main.go`
- Add `Insecure` field to `Plugin` struct
- Update `Plugin.Exec` method to pass `Insecure` field to `NewJenkins`

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-05 08:41:53 +08:00
appleboy 24e8004b58 docs: refactor codebase and update documentation
- Update installation command from `go get` to `go install` in README.md

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 22:19:13 +08:00
appleboy dff06f164a docs: improve CI/CD pipeline and test reliability
- Update Docker image source to `ghcr.io/appleboy/drone-jenkins` in README.md

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 22:16:58 +08:00
appleboy 895becb9a7 docs: update README badges and improve documentation
- Add lint and testing badge to the README
- Remove build status badge from the README
- Remove Docker pulls badge from the README
- Remove microbadger badge from the README

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 22:11:42 +08:00
appleboy 5344ca212b ci: improve test coverage and CI workflow reliability
- Remove `macos-latest` from the OS matrix in the lint workflow

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 21:05:40 +08:00
appleboy b370179bda refactor: refactor codebase for improved compatibility and efficiency
- Replace `ioutil.ReadAll` with `io.ReadAll` in `jenkins.go`
- Ignore the return value of `godotenv.Load` in `main.go`
- Ignore the return value of `godotenv.Overload` in `main.go`

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:34:57 +08:00
appleboy d9f9a4ddc5 ci: refactor CI pipeline to remove Docker Hub dependencies
- Remove Docker Hub login step
- Remove Docker Hub image reference

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:32:00 +08:00
appleboy 51db9357f0 ci: refactor and optimize CI/CD pipeline
- Update Docker login action from version 2 to version 3 in GitHub workflow

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:30:58 +08:00
appleboy 0326a42ccb chore: upgrade Go version and dependencies in project
- Remove Go 1.21 from the lint workflow
- Update Go version to 1.22 in go.mod
- Upgrade dependencies: `godotenv` to v1.5.1, `testify` to v1.9.0, `cli` to v1.22.15
- Add new indirect dependencies: `go-md2man/v2`, `go-spew`, `go-difflib`, `blackfriday/v2`, `yaml.v3`

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:22:51 +08:00
appleboy 4f822500f2 style: refactor codebase for improved consistency and clarity
- Reorder imports in `jenkins_test.go` and `plugin_test.go` to move `assert` import to the end
- Remove unnecessary empty line in `plugin.go`

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:22:05 +08:00
appleboy f932a0ef24 build: refactor build system and update formatting tools
- Change `GOFMT` tool from `gofmt` to `gofumpt`
- Remove DockerHub deployment configurations
- Add detection and setup for Go environment variables
- Add conditional flags and executable naming based on OS
- Update `fmt` and `fmt-check` targets to use `gofumpt`
- Remove `lint`, `misspell-check`, and `misspell` targets
- Update `test` target to remove dependency on `fmt-check`
- Change `install` and `build` targets to use `GOFILES` instead of `SOURCES`
- Update `$(EXECUTABLE)` target to output to `bin/`
- Remove various `release` and `docker` related targets

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:21:32 +08:00
appleboy 5122d14617 ci: enhance CI with multi-OS and Go version testing
- Rename `testing` job to `test`
- Add matrix strategy to run tests on multiple OS and Go versions
- Replace `actions/checkout@v4` with `actions/setup-go@v5` for setting up Go
- Add caching for Go build and module files using `actions/cache@v4`
- Change test command from `make test` to `go test -v -covermode=atomic -coverprofile=coverage.out`
- Add flags to `codecov/codecov-action@v4` for OS and Go version

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:21:23 +08:00
appleboy 65d16c52a3 chore: improve code quality and testing robustness
- Add hadolint configuration file with rules DL3018 and DL3008 ignored

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:15:35 +08:00
appleboy 86574d9456 build: consolidate Dockerfiles for multi-architecture support
- Add a new Dockerfile for multi-architecture support using Alpine 3.20
- Remove Dockerfile for linux.amd64
- Remove Dockerfile for linux.arm
- Remove Dockerfile for linux.arm64
- Remove Dockerfile for windows
- Remove Docker manifest template file

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:15:23 +08:00
appleboy 1cbab46f5c ci: add CI/CD workflows and dependency management
- Add funding configuration file with multiple supported platforms
- Add Dependabot configuration for GitHub Actions and Go modules with weekly updates
- Add CodeQL analysis workflow for Go language
- Add Docker image build and push workflow for multiple platforms
- Add GoReleaser workflow for automated releases on tag push
- Add linting and testing workflow with Go setup, Dockerfile linting, and Codecov integration

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 15:13:35 +08:00
appleboy 7c4ff53a64 chore: clean up obsolete configuration files
- Remove `.drone.jsonnet` file
- Remove `.drone.yml` file
- Remove `.editorconfig` file
- Remove `.revive.toml` file
- Remove `pipeline.libsonnet` file

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-10-04 14:28:23 +08:00
Bo-Yi Wu 4d6399606b chore: upgrade to go1.15
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2020-09-26 12:40:20 +08:00
Bo-Yi Wu 118b8f9b35 chore: upgrade to go1.14 2020-07-18 09:48:04 +08:00
Bo-Yi Wu daf0401e30 chore: load env from /run/drone/env path 2020-07-18 09:46:57 +08:00
Bo-Yi Wu 685cc312e3 chore(makefile): remove GOPACKAGE variable 2020-02-01 17:58:28 +08:00
Bo-Yi Wu cac1337e91 docs: update diff format 2019-10-19 10:17:11 +08:00
Bo-Yi Wu 742e20ea9c update docs
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2019-09-30 11:30:04 +08:00
Bo-Yi Wu 6ca333982b add arch
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2019-09-30 11:27:34 +08:00
Bo-Yi Wu 5e1b16bcf5 update docs
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2019-09-30 11:26:48 +08:00
Bo-Yi Wu 7b357075fe add arch
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2019-09-30 11:24:02 +08:00
Bo-Yi Wu bd482e9c7a docs: update
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2019-09-30 11:08:41 +08:00
Bo-Yi Wu 0d110e195c docs: update 2019-09-30 07:52:18 +08:00
Bo-Yi Wu 7441ec1d9b chore: upgrade go module 2019-09-30 07:47:20 +08:00
Bo-Yi Wu ee6e15d448 chore: support github actions 2019-09-30 07:45:18 +08:00
Bo-Yi Wu e8184fbb5c chore: upgrade to go1.13 2019-09-30 07:43:28 +08:00
Bo-Yi Wu 326b4c2dc5 chore(docker): replace ADD with COPY 2019-03-04 21:43:41 +08:00
Bo-Yi Wu e70b95f505 chore: add lint check
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2019-03-04 16:12:01 +08:00
Bo-Yi Wu beee0b43a6 chore: mirror changes 2019-03-02 10:53:03 +08:00
Bo-Yi Wu 93c81b3ee6 docs: create new jenkins token 2019-03-02 09:15:59 +08:00
Bo-Yi Wu 9e55d9c14f docs: add jenkins command 2019-03-02 09:11:42 +08:00
Bo-Yi Wu e67398a6a3 docs: update drone badge 2019-03-01 08:54:32 +08:00
Bo-Yi Wu ded8530d8e chore(docker): upgrade drone and dockerfile 2019-03-01 08:53:17 +08:00
Bo-Yi Wu eac91dc0af feat: switch to go module 2019-03-01 08:41:04 +08:00
Bo-Yi Wu c81cb4142a fix gopath 2018-12-28 00:00:05 +08:00
Bo-Yi Wu ead7714d9a remove branch
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2018-11-11 16:29:09 +08:00
Bo-Yi Wu 96b11d47cd feat: check return err. 2018-11-11 11:38:23 +08:00
Bo-Yi Wu 3362d46d26 feat: support go module in go 1.11 2018-11-11 11:36:00 +08:00
Bo-Yi Wu bba1fb3a6e feat: support windows image 2018-11-11 11:17:01 +08:00
Bo-Yi Wu 0af129e9d8 feat(ddocker): support arm and i386 version 2018-11-11 11:08:44 +08:00
Bo-Yi Wu 6777964976 add maintainer label.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-10-05 11:33:53 +08:00
Bo-Yi Wu 9eca070b01 add notify
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-06-24 23:01:01 +08:00
Bo-Yi Wu 4d3054d832 add empty line.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-08 14:05:01 +08:00
Bo-Yi Wu b007a60843 support group build (#19)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-07 15:19:17 +08:00
Bo-Yi Wu 18b163fd7b add fmt check command (#18)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-07 12:52:57 +08:00
Bo-Yi Wu 03bcfacb96 remove travis build.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-07 12:22:10 +08:00
Bo-Yi Wu 042c5d632c upgrade drone config (#17)
* upgrade drone config

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

* remove glide config

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-06 23:31:30 +08:00
Bo-Yi Wu fc238fb606 feat: replpace glide to govendor (#16) 2017-05-06 23:15:28 +08:00
Bo-Yi Wu bc987b6d01 remove drone sig file (#14)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-06 22:57:45 +08:00
Bo-Yi Wu 6e58cda4fe feat: Add unconvert command 2017-01-28 11:47:12 +08:00
Bo-Yi Wu 396719ff0f docs: [ci skip] remove secret section. 2017-01-20 15:06:16 +08:00
Bo-Yi Wu 8e578266db docs: [ci skip] update readme. 2017-01-17 11:14:19 +08:00
Bo-Yi Wu 7d040a545c docs: add drone documents. (#11) 2017-01-17 10:58:32 +08:00
Bo-Yi Wu e07592691c fix: Enforce custom LDFLAGS within makefile 2017-01-17 10:35:58 +08:00
Bo-Yi Wu 7ba6170e12 fix: show error if missing jobs. 2017-01-16 15:04:12 +08:00
Bo-Yi Wu 4ce3a1e347 docs: update 2017-01-16 14:09:27 +08:00
Bo-Yi Wu 1898550d88 feat: support build binary (#10) 2017-01-16 14:07:17 +08:00
Bo-Yi Wu 6ef9ab8751 docs: [ci skip] update docs. 2017-01-16 11:58:55 +08:00
Bo-Yi Wu 7275c0c66b feat: replace jenkins with drone (#7) 2017-01-16 11:55:28 +08:00
Bo-Yi Wu bb23a33bf0 feat: update glide 2017-01-16 10:48:37 +08:00
Bo-Yi Wu c4cd67adc5 refactor: remove 1.5.x testing and docker services. (#6) 2017-01-16 10:46:35 +08:00
Bo-Yi Wu 788418c56f refactor: remove unnecessary config. (#5) 2017-01-16 10:42:55 +08:00
Bo-Yi Wu 2211e06f29 Merge pull request #2 from appleboy/cli
Support load configuration from files.
2016-11-21 11:19:12 +08:00
Bo-Yi Wu 226713ff24 Support load configuration from files.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2016-11-21 11:16:28 +08:00
Bo-Yi Wu 186ee76d3c update Version comment.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2016-11-19 22:36:05 +08:00
Bo-Yi Wu 4917613ed8 add version command.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2016-11-19 21:19:38 +08:00
Bo-Yi Wu e6e2562e1e update to 1.7.3 testing.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2016-10-25 16:34:32 +08:00
35 changed files with 4289 additions and 426 deletions
+2
View File
@@ -0,0 +1,2 @@
*
!release/
-42
View File
@@ -1,42 +0,0 @@
# unifying the coding style for different editors and IDEs => editorconfig.org
; indicate this is the root of the project
root = true
###########################################################
; common
###########################################################
[*]
charset = utf-8
end_of_line = LF
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
###########################################################
; make
###########################################################
[Makefile]
indent_style = tab
[makefile]
indent_style = tab
###########################################################
; markdown
###########################################################
[*.md]
trim_trailing_whitespace = false
###########################################################
; golang
###########################################################
[*.go]
indent_style = tab
+13
View File
@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://www.paypal.me/appleboy46']
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
+54
View File
@@ -0,0 +1,54 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: "41 23 * * 6"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["go"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
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.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
+64
View File
@@ -0,0 +1,64 @@
name: Docker Image
on:
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- "master"
jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v6
with:
go-version: "^1"
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Build binary
run: |
make build_linux_amd64
make build_linux_arm64
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
+33
View File
@@ -0,0 +1,33 @@
name: Goreleaser
on:
push:
tags:
- "*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+68
View File
@@ -0,0 +1,68 @@
name: Lint and Testing
on:
push:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v6
with:
go-version: "stable"
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.6
args: --verbose
- uses: hadolint/hadolint-action@v3.3.0
name: hadolint for Dockerfile
with:
dockerfile: docker/Dockerfile
testing:
strategy:
matrix:
os: [ubuntu-latest]
go: ["1.25"]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
name: ${{ matrix.os }} @ Go ${{ matrix.go }}
runs-on: ${{ matrix.os }}
env:
GO111MODULE: on
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Checkout Code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- uses: actions/cache@v5
with:
path: |
${{ matrix.go-build }}
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run Tests
run: |
go test -race -cover -coverprofile=coverage.out ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
flags: ${{ matrix.os }},go-${{ matrix.go }}
+56
View File
@@ -0,0 +1,56 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run daily at 00:00 UTC
- cron: "0 0 * * *"
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
security-events: write # Required for uploading SARIF results
jobs:
trivy-scan:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: "fs"
scan-ref: "."
scanners: "vuln,secret,misconfig"
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-results.sarif"
- name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.33.1
if: always()
with:
scan-type: "fs"
scan-ref: "."
scanners: "vuln,secret,misconfig"
format: "table"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
exit-code: "1"
+2 -1
View File
@@ -22,6 +22,7 @@ _testmain.go
*.exe
*.test
*.prof
vendor
drone-jenkins
coverage.txt
.env
dist
+51
View File
@@ -0,0 +1,51 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- exhaustive
- gochecknoinits
- goconst
- gocritic
- gocyclo
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- misspell
- nakedret
- noctx
- nolintlint
- rowserrcheck
- staticcheck
- unconvert
- unparam
- unused
- whitespace
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- goimports
- golines
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+123
View File
@@ -0,0 +1,123 @@
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
goarm:
- "5"
- "6"
- "7"
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: windows
goarch: arm
goarm: "5"
- goos: windows
goarch: arm
goarm: "6"
- goos: windows
goarch: arm
goarm: "7"
- goos: windows
goarch: arm64
- goos: freebsd
goarch: ppc64le
- goos: freebsd
goarch: s390x
- goos: freebsd
goarch: arm
goarm: "5"
- goos: freebsd
goarch: arm
goarm: "6"
- goos: freebsd
goarch: arm
goarm: "7"
- goos: freebsd
goarch: arm64
flags:
- -trimpath
ldflags:
- -s -w
- -X main.Version={{.Version}}
binary: >-
{{ .ProjectName }}-
{{- if .IsSnapshot }}{{ .Branch }}-
{{- else }}{{- .Version }}-{{ end }}
{{- .Os }}-
{{- if eq .Arch "amd64" }}amd64
{{- else if eq .Arch "amd64_v1" }}amd64
{{- else if eq .Arch "386" }}386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}-{{ .Arm }}{{ end }}
no_unique_dist_dir: true
hooks:
post:
- cmd: xz -k -9 {{ .Path }}
dir: ./dist/
archives:
- format: binary
name_template: "{{ .Binary }}"
allow_different_binary_count: true
checksum:
name_template: "checksums.txt"
extra_files:
- glob: ./**.xz
snapshot:
name_template: "{{ incpatch .Version }}"
release:
# You can add extra pre-existing files to the release.
# The filename on the release will be the last part of the path (base).
# If another file with the same name exists, the last one found will be used.
#
# Templates: allowed
extra_files:
- glob: ./**.xz
changelog:
use: github
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "Bug fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "Enhancements"
regexp: "^.*chore[(\\w)]*:+.*$"
order: 2
- title: "Refactor"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Build process updates"
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
order: 4
- title: "Documentation updates"
regexp: ^.*?docs?(\(.+\))??!?:.+$
order: 4
- title: Others
order: 999
+3
View File
@@ -0,0 +1,3 @@
ignored:
- DL3018
- DL3008
-53
View File
@@ -1,53 +0,0 @@
sudo: required
language: go
services:
- docker
go:
- 1.5.4
- 1.6.3
- 1.7.1
- tip
env:
global:
- DOCKER_CACHE_FILE=${HOME}/docker/cache.tar.gz
cache:
directories:
- vendor
- ${HOME}/.glide
- ${HOME}/docker
before_install:
- mkdir -p $GOPATH/bin
- curl https://glide.sh/get | sh
- if [ -f ${DOCKER_CACHE_FILE} ]; then gunzip -c ${DOCKER_CACHE_FILE} | docker load; fi
install:
- export GO15VENDOREXPERIMENT=1
- make install
script:
- make test
- make docker
- if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
mkdir -p $(dirname ${DOCKER_CACHE_FILE});
docker save $(docker history -q $TRAVIS_REPO_SLUG:latest | grep -v '<missing>') | gzip > ${DOCKER_CACHE_FILE};
fi
after_success:
# ignore main.go coverage
- sed -i '/main.go/d' coverage.txt
- bash <(curl -s https://codecov.io/bash) -f coverage.txt
# deploy from master
- if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_GO_VERSION" == "1.7.1" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
make docker_deploy tag=latest;
fi
# deploy from tag
- if [ "$TRAVIS_GO_VERSION" == "1.7.1" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
make docker_deploy tag=$TRAVIS_TAG;
fi
+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.
+112
View File
@@ -0,0 +1,112 @@
---
date: 2019-10-19T00:00:00+00:00
title: Jenkins
author: appleboy
tags: [ infrastructure, trigger, jenkins ]
repo: appleboy/drone-jenkins
logo: jenkins.svg
image: appleboy/drone-jenkins
---
The Jenkins plugin allows you to trigger Jenkins job automatically. The below pipeline configuration demonstrates simple usage:
```yaml
- name: trigger jenkins job
image: appleboy/drone-jenkins
settings:
url: http://example.com
user: appleboy
token: xxxxxxxxxx
job: drone-jenkins-plugin-job
```
Example configuration with multiple jobs:
```diff
- name: trigger jenkins job
image: appleboy/drone-jenkins
settings:
url: http://example.com
user: appleboy
token: xxxxxxxxxx
job:
+ - drone-jenkins-plugin-job-1
+ - drone-jenkins-plugin-job-2
```
Example configuration with jobs in the folder:
```diff
- name: trigger jenkins job
image: appleboy/drone-jenkins
settings:
url: http://example.com
user: appleboy
token: xxxxxxxxxx
+ job: folder_name/job_name
```
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
: jenkins server base url.
user
: jenkins user account
token
: jenkins user 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)
-5
View File
@@ -1,5 +0,0 @@
FROM centurylink/ca-certs
ADD drone-jenkins /
ENTRYPOINT ["/drone-jenkins"]
-8
View File
@@ -1,8 +0,0 @@
FROM armhfbuild/alpine:3.4
RUN apk update && \
apk add ca-certificates && \
rm -rf /var/cache/apk/*
ADD drone-jenkins /bin/
ENTRYPOINT ["/bin/drone-jenkins"]
+84 -35
View File
@@ -1,8 +1,9 @@
.PHONY: install build test html update docker_build docker_image docker docker_deploy clean
VERSION := $(shell git describe --tags || git rev-parse --short HEAD)
DEPLOY_ACCOUNT := "appleboy"
DEPLOY_IMAGE := "drone-jenkins"
DIST := dist
EXECUTABLE := drone-jenkins
GOFMT ?= gofumpt -l -s -w
GO ?= go
GOFILES := $(shell find . -name "*.go" -type f)
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
ifneq ($(shell uname), Darwin)
EXTLDFLAGS = -extldflags "-static" $(null)
@@ -10,36 +11,84 @@ else
EXTLDFLAGS =
endif
install:
glide install
ifeq ($(HAS_GO), GO)
GOPATH ?= $(shell $(GO) env GOPATH)
export PATH := $(GOPATH)/bin:$(PATH)
build:
go build -ldflags="$(EXTLDFLAGS)-s -w -X main.Version=$(VERSION)"
test:
go test -v -coverprofile=coverage.txt
html:
go tool cover -html=coverage.txt
update:
glide up
docker_build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags="-X main.Version=$(VERSION)"
docker_image:
docker build --rm -t $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE) .
docker: docker_build docker_image
docker_deploy:
ifeq ($(tag),)
@echo "Usage: make $@ tag=<tag>"
@exit 1
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
endif
docker tag $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE):latest $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE):$(tag)
docker push $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE):$(tag)
clean:
rm -rf coverage.txt ${DEPLOY_IMAGE}
ifeq ($(OS), Windows_NT)
GOFLAGS := -v -buildmode=exe
EXECUTABLE ?= $(EXECUTABLE).exe
else ifeq ($(OS), Windows)
GOFLAGS := -v -buildmode=exe
EXECUTABLE ?= $(EXECUTABLE).exe
else
GOFLAGS := -v
EXECUTABLE ?= $(EXECUTABLE)
endif
ifneq ($(DRONE_TAG),)
VERSION ?= $(DRONE_TAG)
else
VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD)
endif
TAGS ?=
LDFLAGS ?= -X 'main.Version=$(VERSION)'
.PHONY: all
all: build ## Build the project (default target)
.PHONY: 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 binary to GOPATH/bin
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' .
.PHONY: build
build: $(EXECUTABLE) ## Build binary to bin/
$(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@ .
.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) .
.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) .
.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) .
.PHONY: clean
clean: ## Clean build artifacts
$(GO) clean -x -i ./...
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
.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)
+373 -34
View File
@@ -1,60 +1,399 @@
# drone-jenkins
[![Build Status](https://travis-ci.org/appleboy/drone-jenkins.svg?branch=master)](https://travis-ci.org/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)
[English](README.md) | [繁體中文](README.zh-TW.md) | [简体中文](README.zh-CN.md)
[Drone](https://github.com/drone/drone) plugin for trigger [Jenkins](https://jenkins.io/) jobs.
![logo](./images/logo.png)
## Build
[![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)
Build the binary with the following commands:
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.
```
$ make build
## 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)
- [Installation](#installation)
- [Download Binary](#download-binary)
- [Build from Source](#build-from-source)
- [Docker Image](#docker-image)
- [Configuration](#configuration)
- [Jenkins Server Setup](#jenkins-server-setup)
- [Authentication](#authentication)
- [Parameters Reference](#parameters-reference)
- [Usage](#usage)
- [Command Line](#command-line)
- [Docker](#docker)
- [Development](#development)
- [Building](#building)
- [Testing](#testing)
- [License](#license)
- [Contributing](#contributing)
## Features
- Trigger single or multiple Jenkins jobs
- Support for Jenkins build parameters
- Multiple authentication methods (API token or remote trigger token)
- Wait for job completion with configurable polling and timeout
- 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 CLI binary or Docker image
## Prerequisites
- Jenkins server (version 2.0 or later recommended)
- Jenkins API token or remote trigger token for authentication
- For Jenkins setup, Docker is recommended but not required
## Installation
### Download Binary
Pre-compiled binaries are available from the [release page](https://github.com/appleboy/drone-jenkins/releases) for:
- **Linux**: amd64, 386
- **macOS (Darwin)**: amd64, 386
- **Windows**: amd64, 386
With Go installed, you can also install directly:
```sh
go install github.com/appleboy/drone-jenkins@latest
```
## Testing
### Build from Source
Test the package with the following command:
Clone the repository and build:
```
$ make test
```sh
git clone https://github.com/appleboy/drone-jenkins.git
cd drone-jenkins
make build
```
## Docker
### Docker Image
Build the docker image with the following commands:
Build the Docker image:
```
$ make docker
```sh
make docker
```
Please note incorrectly building the image for the correct x64 linux and with
GCO disabled will result in an error when running the Docker image:
Or pull the pre-built image:
```sh
docker pull ghcr.io/appleboy/drone-jenkins
```
docker: Error response from daemon: Container command
'/bin/drone-jenkins' not found or does not exist..
## Configuration
### Jenkins Server Setup
Set up a Jenkins server using Docker:
```sh
docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:slim
```
### Authentication
Jenkins API tokens are recommended for authentication. To create an API token:
1. Log into Jenkins
2. Click on your username (top right)
3. Select "Security"
4. Under "API Token", click "Add new Token"
5. Give it a name and click "Generate"
6. Copy the generated token
![personal token](./images/personal-token.png)
Alternatively, you can use a remote trigger token configured in your Jenkins job settings.
### 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 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
Execute from the working directory:
### Command Line
**Single job:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job drone-jenkins-plugin
```
**Multiple jobs:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job drone-jenkins-plugin-1 \
--job drone-jenkins-plugin-2
```
**With build parameters:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--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:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--remote-token REMOTE_TOKEN_HERE \
--job my-jenkins-job
```
**Wait for job completion:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--wait \
--poll-interval 15s \
--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:**
```bash
docker run --rm \
-e PLUGIN_BASE_URL=http://example.com \
-e PLUGIN_USERNAME=xxxxxxx \
-e PLUGIN_TOKEN=xxxxxxx \
-e PLUGIN_JOB=xxxxxxx \
-e DRONE_REPO_OWNER=appleboy \
-e DRONE_REPO_NAME=go-hello \
-e DRONE_COMMIT_SHA=e5e82b5eb3737205c25955dcc3dcacc839b7be52 \
-e DRONE_COMMIT_BRANCH=master \
-e DRONE_COMMIT_AUTHOR=appleboy \
-e DRONE_BUILD_NUMBER=1 \
-e DRONE_BUILD_STATUS=success \
-e DRONE_BUILD_LINK=http://github.com/appleboy/go-hello \
-v $(pwd):$(pwd) \
-w $(pwd) \
appleboy/drone-jenkins
-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
```
**Multiple jobs:**
```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
```
**With build parameters:**
```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
```
**Wait for job completion:**
```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
```
**With debug mode:**
```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
```
**With custom CA certificate:**
```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 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).
## Development
### Building
Build the binary:
```sh
make build
```
Build the Docker image:
```sh
make docker
```
### Testing
Run the test suite:
```sh
make test
```
Run tests with coverage:
```sh
make test-coverage
```
## License
Copyright (c) 2019 Bo-Yi Wu
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
+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。
+26
View File
@@ -0,0 +1,26 @@
FROM alpine:3.22
ARG TARGETOS
ARG TARGETARCH
LABEL maintainer="Bo-Yi Wu <appleboy.tw@gmail.com>" \
org.label-schema.name="Drone Jenkins Plugin" \
org.label-schema.vendor="Bo-Yi Wu" \
org.label-schema.schema-version="1.0"
LABEL org.opencontainers.image.source=https://github.com/appleboy/drone-jenkins
LABEL org.opencontainers.image.description="Drone Jenkins"
LABEL org.opencontainers.image.licenses=MIT
RUN apk add --no-cache ca-certificates && \
rm -rf /var/cache/apk/*
RUN addgroup -g 1000 drone && \
adduser -D -u 1000 -G drone drone
COPY release/${TARGETOS}/${TARGETARCH}/drone-jenkins /bin/
RUN chown drone:drone /bin/drone-jenkins
USER drone
ENTRYPOINT ["/bin/drone-jenkins"]
Generated
-22
View File
@@ -1,22 +0,0 @@
hash: 0ddd797a78c8eef869d73907846b08ce2ba1dc91ddb0c644b80e28a949f79819
updated: 2016-10-18T10:10:10.836829677+08:00
imports:
- name: github.com/joho/godotenv
version: 4ed13390c0acd2ff4e371e64d8b97c8954138243
subpackages:
- autoload
- name: github.com/urfave/cli
version: 55f715e28c46073d0e217e2ce8eb46b0b45e3db6
testImports:
- name: github.com/davecgh/go-spew
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib
- name: github.com/stretchr/testify
version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506
subpackages:
- assert
-10
View File
@@ -1,10 +0,0 @@
package: github.com/appleboy/drone-jenkins
import:
- package: github.com/joho/godotenv
subpackages:
- autoload
- package: github.com/urfave/cli
testImport:
- package: github.com/stretchr/testify
subpackages:
- assert
+20
View File
@@ -0,0 +1,20 @@
module github.com/appleboy/drone-jenkins
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.7
github.com/yassinebenaid/godump v0.11.1
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+24
View File
@@ -0,0 +1,24 @@
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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/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.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=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+283
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+468 -27
View File
@@ -1,12 +1,22 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strings"
"time"
"github.com/appleboy/com/gh"
"github.com/yassinebenaid/godump"
)
type (
@@ -20,16 +30,152 @@ type (
Jenkins struct {
Auth *Auth
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
QueueItem struct {
Blocked bool `json:"blocked"`
Buildable bool `json:"buildable"`
ID int `json:"id"`
InQueueSince int64 `json:"inQueueSince"`
Executable *struct {
Number int `json:"number"`
URL string `json:"url"`
} `json:"executable"`
Why string `json:"why"`
}
// BuildInfo represents Jenkins build information
BuildInfo struct {
Building bool `json:"building"`
Duration int64 `json:"duration"`
Result string `json:"result"` // SUCCESS, FAILURE, ABORTED, UNSTABLE, null if building
Number int `json:"number"`
URL string `json:"url"`
Timestamp int64 `json:"timestamp"`
}
)
// 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
}
// 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(auth *Auth, url string) *Jenkins {
url = strings.TrimRight(url, "/")
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) {
@@ -44,43 +190,154 @@ 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 {
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
// 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
}
return http.DefaultClient.Do(req)
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) parseResponse(resp *http.Response, body interface{}) (err error) {
defer resp.Body.Close()
if body == nil {
return
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)
}
data, err := ioutil.ReadAll(resp.Body)
// 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(
ctx context.Context,
path string,
params url.Values,
body interface{},
) error {
requestURL := jenkins.buildURL(path, params)
req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil)
if err != nil {
return
return err
}
resp, err := jenkins.sendRequest(req, nil) // GET requests don't need crumb
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response code: %d, body: %s", resp.StatusCode, string(data))
}
if body == nil {
return nil
}
return json.Unmarshal(data, body)
}
func (jenkins *Jenkins) post(path string, params url.Values, body interface{}) (err error) {
// postAndGetLocation performs a POST request and extracts the queue ID from Location header
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.NewRequest("POST", requestURL, nil)
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, nil)
if err != nil {
fmt.Println(err)
return
return 0, err
}
resp, err := jenkins.sendRequest(req)
resp, err := jenkins.sendRequest(req, crumb)
if err != nil {
fmt.Println(err)
return
return 0, err
}
return jenkins.parseResponse(resp, body)
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf(
"unexpected response code: %d, body: %s",
resp.StatusCode,
string(data),
)
}
// Extract queue ID from Location header
// Location format: http://jenkins.example.com/queue/item/123/
location := resp.Header.Get("Location")
if location == "" {
return 0, fmt.Errorf("no Location header in response")
}
// Parse queue ID from URL
// Look for /queue/item/{id}/ or /queue/item/{id}
var queueID int
// Find the pattern "/queue/item/" and extract the number after it
queueItemPrefix := "/queue/item/"
idx := strings.Index(location, queueItemPrefix)
if idx == -1 {
return 0, fmt.Errorf("failed to parse queue ID from Location: %s", location)
}
// Extract the substring after "/queue/item/"
afterPrefix := location[idx+len(queueItemPrefix):]
// Parse the number (stop at / or end of string)
if _, err := fmt.Sscanf(afterPrefix, "%d", &queueID); err != nil {
return 0, fmt.Errorf("failed to parse queue ID from Location: %s", location)
}
return queueID, nil
}
func (jenkins *Jenkins) parseJobPath(job string) string {
@@ -100,8 +357,192 @@ func (jenkins *Jenkins) parseJobPath(job string) string {
return path
}
func (jenkins *Jenkins) trigger(job string, params url.Values) error {
path := jenkins.parseJobPath(job) + "/build"
// getQueueItem fetches information about a queue item
func (jenkins *Jenkins) getQueueItem(ctx context.Context, queueID int) (*QueueItem, error) {
path := fmt.Sprintf("/queue/item/%d/api/json", queueID)
return jenkins.post(path, params, nil)
var queueItem QueueItem
err := jenkins.get(ctx, path, nil, &queueItem)
if err != nil {
return nil, fmt.Errorf("failed to get queue item %d: %w", queueID, err)
}
return &queueItem, nil
}
// getBuildInfo fetches information about a specific build
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(ctx, path, nil, &buildInfo)
if err != nil {
return nil, fmt.Errorf("failed to get build info for %s #%d: %w", job, buildNumber, err)
}
return &buildInfo, nil
}
// 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,
) (*BuildInfo, error) {
deadline := time.Now().Add(timeout)
// Phase 1: Wait for queue item to be assigned a build number
log.Printf("waiting for job %s (queue #%d) to start...", job, queueID)
var buildNumber int
for {
if time.Now().After(deadline) {
return nil, fmt.Errorf("timeout waiting for job %s to start", job)
}
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)
time.Sleep(pollInterval)
continue
}
// Check if build has started
if queueItem.Executable != nil && queueItem.Executable.Number > 0 {
buildNumber = queueItem.Executable.Number
log.Printf("job %s started as build #%d", job, buildNumber)
break
}
// Log why the job is waiting if available
if queueItem.Why != "" {
log.Printf("job %s is queued: %s", job, queueItem.Why)
}
time.Sleep(pollInterval)
}
// Phase 2: Wait for build to complete
log.Printf("waiting for job %s (build #%d) to complete...", job, buildNumber)
for {
if time.Now().After(deadline) {
return nil, fmt.Errorf(
"timeout waiting for job %s build #%d to complete",
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)
continue
}
// Check if build is complete
if !buildInfo.Building {
log.Printf(
"job %s (build #%d) completed with status: %s",
job,
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
}
time.Sleep(pollInterval)
}
}
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 {
params = url.Values{}
}
params.Set("token", jenkins.Token)
}
var urlPath string
// Check if params contains build parameters (excluding 'token')
hasBuildParams := false
for key := range params {
if key != "token" {
hasBuildParams = true
break
}
}
if hasBuildParams {
urlPath = jenkins.parseJobPath(job) + "/buildWithParameters"
} else {
urlPath = jenkins.parseJobPath(job) + "/build"
}
// 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(ctx, urlPath, params)
}
+723 -8
View File
@@ -1,10 +1,17 @@
package main
import (
"github.com/stretchr/testify/assert"
"context"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestParseJobPath(t *testing.T) {
@@ -12,7 +19,16 @@ func TestParseJobPath(t *testing.T) {
Username: "appleboy",
Token: "1234",
}
jenkins := NewJenkins(auth, "http://example.com")
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/"))
@@ -25,19 +41,718 @@ func TestUnSupportProtocol(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "example.com")
jenkins, err := NewJenkins(context.Background(), auth, "example.com", "", false, "", false)
assert.NoError(t, err)
err := jenkins.trigger("drone-jenkins", nil)
queueID, err := jenkins.trigger(context.Background(), "drone-jenkins", nil)
assert.NotNil(t, err)
assert.Equal(t, 0, queueID)
}
func TestTriggerBuild(t *testing.T) {
// Create a mock Jenkins server
var receivedParams url.Values
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedParams = r.URL.Query()
w.Header().Set("Location", "http://jenkins.example.com/queue/item/123/")
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
auth := &Auth{
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "http://example.com")
jenkins, err := NewJenkins(
context.Background(),
auth,
server.URL,
"remote-token",
false,
"",
false,
)
assert.NoError(t, err)
err := jenkins.trigger("drone-jenkins", url.Values{"token": []string{"bar"}})
assert.Nil(t, err)
params := url.Values{"param": []string{"value"}}
queueID, err := jenkins.trigger(context.Background(), "drone-jenkins", params)
assert.NoError(t, err)
assert.Equal(t, 123, queueID)
assert.Equal(t, "value", receivedParams.Get("param"))
assert.Equal(t, "remote-token", receivedParams.Get("token"))
}
func TestPostAndGetLocation(t *testing.T) {
tests := []struct {
name string
location string
expectID int
expectError bool
}{
{
name: "valid location with trailing slash",
location: "http://jenkins.example.com/queue/item/456/",
expectID: 456,
expectError: false,
},
{
name: "valid location without trailing slash",
location: "http://jenkins.example.com/queue/item/789",
expectID: 789,
expectError: false,
},
{
name: "no location header",
location: "",
expectID: 0,
expectError: true,
},
{
name: "invalid location format",
location: "http://jenkins.example.com/invalid/path",
expectID: 0,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tt.location != "" {
w.Header().Set("Location", tt.location)
}
w.WriteHeader(http.StatusCreated)
}),
)
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
queueID, err := jenkins.postAndGetLocation(context.Background(), "/test", nil)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectID, queueID)
}
})
}
}
func TestGetQueueItem(t *testing.T) {
tests := []struct {
name string
queueID int
responseBody string
responseStatus int
expectError bool
expectBlocked bool
expectBuildNum int
}{
{
name: "queue item with build number",
queueID: 123,
responseBody: `{"id":123,"blocked":false,"buildable":true,` +
`"executable":{"number":456,"url":"http://jenkins.example.com/job/test/456/"}}`,
responseStatus: http.StatusOK,
expectError: false,
expectBlocked: false,
expectBuildNum: 456,
},
{
name: "queue item waiting",
queueID: 124,
responseBody: `{"id":124,"blocked":false,"buildable":true,"why":"Waiting for executor"}`,
responseStatus: http.StatusOK,
expectError: false,
expectBlocked: false,
expectBuildNum: 0,
},
{
name: "queue item blocked",
queueID: 125,
responseBody: `{"id":125,"blocked":true,"buildable":false,"why":"Blocked by other job"}`,
responseStatus: http.StatusOK,
expectError: false,
expectBlocked: true,
expectBuildNum: 0,
},
{
name: "queue item not found",
queueID: 999,
responseBody: "Not Found",
responseStatus: http.StatusNotFound,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Contains(t, r.URL.Path, "/queue/item/")
w.WriteHeader(tt.responseStatus)
_, _ = w.Write([]byte(tt.responseBody))
}),
)
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
queueItem, err := jenkins.getQueueItem(context.Background(), tt.queueID)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, queueItem)
assert.Equal(t, tt.queueID, queueItem.ID)
assert.Equal(t, tt.expectBlocked, queueItem.Blocked)
if queueItem.Executable != nil {
assert.Equal(t, tt.expectBuildNum, queueItem.Executable.Number)
}
}
})
}
}
func TestGetBuildInfo(t *testing.T) {
tests := []struct {
name string
jobName string
buildNumber int
responseBody string
responseStatus int
expectError bool
expectBuilding bool
expectResult string
}{
{
name: "build in progress",
jobName: "test-job",
buildNumber: 123,
responseBody: `{"number":123,"building":true,"duration":0,"result":null,` +
`"url":"http://jenkins.example.com/job/test-job/123/"}`,
responseStatus: http.StatusOK,
expectError: false,
expectBuilding: true,
expectResult: "",
},
{
name: "build completed successfully",
jobName: "test-job",
buildNumber: 124,
responseBody: `{"number":124,"building":false,"duration":5000,"result":"SUCCESS",` +
`"url":"http://jenkins.example.com/job/test-job/124/"}`,
responseStatus: http.StatusOK,
expectError: false,
expectBuilding: false,
expectResult: "SUCCESS",
},
{
name: "build failed",
jobName: "test-job",
buildNumber: 125,
responseBody: `{"number":125,"building":false,"duration":3000,"result":"FAILURE",` +
`"url":"http://jenkins.example.com/job/test-job/125/"}`,
responseStatus: http.StatusOK,
expectError: false,
expectBuilding: false,
expectResult: "FAILURE",
},
{
name: "build not found",
jobName: "test-job",
buildNumber: 999,
responseBody: "Not Found",
responseStatus: http.StatusNotFound,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Contains(t, r.URL.Path, "/job/")
w.WriteHeader(tt.responseStatus)
_, _ = w.Write([]byte(tt.responseBody))
}),
)
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(context.Background(), auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.getBuildInfo(context.Background(), tt.jobName, tt.buildNumber)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, buildInfo)
assert.Equal(t, tt.buildNumber, buildInfo.Number)
assert.Equal(t, tt.expectBuilding, buildInfo.Building)
assert.Equal(t, tt.expectResult, buildInfo.Result)
}
})
}
}
func TestWaitForCompletion(t *testing.T) {
t.Run("successful completion", func(t *testing.T) {
var callCount int32
queueID := 123
buildNumber := 456
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count := atomic.AddInt32(&callCount, 1)
switch r.URL.Path {
case testQueueItemPath:
// First call: queue item without build number
// Second call: queue item with build number
w.WriteHeader(http.StatusOK)
if count == 1 {
_, _ = w.Write([]byte(
`{"id":123,"blocked":false,"buildable":true,"why":"Waiting for executor"}`,
))
} else {
_, _ = w.Write([]byte(`{"id":123,"blocked":false,"buildable":true,` +
`"executable":{"number":456,"url":"http://example.com/job/test/456/"}}`))
}
case testBuildStatusPath:
// First call: build in progress
// Second call: build completed
w.WriteHeader(http.StatusOK)
if count <= 3 {
_, _ = w.Write(
[]byte(`{"number":456,"building":true,"duration":0,"result":null}`),
)
} else {
_, _ = w.Write([]byte(`{"number":456,"building":false,"duration":5000,"result":"SUCCESS"}`))
}
}
}))
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
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,
5*time.Second,
)
assert.NoError(t, err)
assert.NotNil(t, buildInfo)
assert.Equal(t, buildNumber, buildInfo.Number)
assert.False(t, buildInfo.Building)
assert.Equal(t, "SUCCESS", buildInfo.Result)
})
t.Run("timeout waiting for queue", func(t *testing.T) {
queueID := 123
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Always return queue item without build number
w.WriteHeader(http.StatusOK)
_, _ = w.Write(
[]byte(`{"id":123,"blocked":false,"buildable":true,"why":"Waiting forever"}`),
)
}))
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
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,
200*time.Millisecond,
)
assert.Error(t, err)
assert.Nil(t, buildInfo)
assert.Contains(t, err.Error(), "timeout")
})
t.Run("timeout waiting for build", func(t *testing.T) {
var callCount int32
queueID := 123
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count := atomic.AddInt32(&callCount, 1)
switch r.URL.Path {
case testQueueItemPath:
// Return build number immediately
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":123,"blocked":false,"buildable":true,` +
`"executable":{"number":456,"url":"http://example.com/job/test/456/"}}`))
case testBuildStatusPath:
// Always return building status
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"number":456,"building":true,"duration":0,"result":null}`))
}
_ = count
}))
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
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,
200*time.Millisecond,
)
assert.Error(t, err)
assert.Nil(t, buildInfo)
assert.Contains(t, err.Error(), "timeout")
})
t.Run("build failed", func(t *testing.T) {
var callCount int32
queueID := 123
buildNumber := 456
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count := atomic.AddInt32(&callCount, 1)
switch r.URL.Path {
case testQueueItemPath:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":123,"blocked":false,"buildable":true,` +
`"executable":{"number":456,"url":"http://example.com/job/test/456/"}}`))
case testBuildStatusPath:
// First call: building, second call: failed
w.WriteHeader(http.StatusOK)
if count == 1 {
_, _ = w.Write(
[]byte(`{"number":456,"building":true,"duration":0,"result":null}`),
)
} else {
_, _ = w.Write([]byte(`{"number":456,"building":false,"duration":3000,"result":"FAILURE"}`))
}
}
}))
defer server.Close()
auth := &Auth{
Username: "test",
Token: "test",
}
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,
5*time.Second,
)
assert.NoError(t, err)
assert.NotNil(t, buildInfo)
assert.Equal(t, buildNumber, buildInfo.Number)
assert.False(t, buildInfo.Building)
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)
})
}
+207 -89
View File
@@ -1,122 +1,240 @@
package main
import (
"fmt"
"log"
"os"
"time"
_ "github.com/joho/godotenv/autoload"
"github.com/urfave/cli"
"github.com/joho/godotenv"
"github.com/urfave/cli/v2"
"github.com/yassinebenaid/godump"
)
// Version for command line
var Version string
// Version set at compile-time
var Version = "dev"
const asciiArt = `
________ ____. __ .__
\______ \_______ ____ ____ ____ | | ____ ____ | | _|__| ____ ______
| | \_ __ \/ _ \ / \_/ __ \ ______ | |/ __ \ / \| |/ / |/ \ / ___/
| | \ | \( <_> ) | \ ___/ /_____/ /\__| \ ___/| | \ <| | | \\___ \
/_______ /__| \____/|___| /\___ > \________|___ >___| /__|_ \__|___| /____ >
\/ \/ \/ \/ \/ \/ \/ \/
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 {
if err := godotenv.Load(filename); err != nil && !os.IsNotExist(err) {
log.Printf("Warning: failed to load env file %s: %v", filename, err)
}
}
if _, err := os.Stat("/run/drone/env"); err == nil {
if err := godotenv.Overload("/run/drone/env"); err != nil {
log.Printf("Warning: failed to load /run/drone/env: %v", err)
}
}
app := cli.NewApp()
app.Name = "jenkins plugin"
app.Usage = "jenkins plugin"
app.Usage = "trigger jenkins jobs"
app.Copyright = "Copyright (c) 2019 Bo-Yi Wu"
app.Authors = []*cli.Author{
{
Name: "Bo-Yi Wu",
Email: "appleboy.tw@gmail.com",
},
}
app.Action = run
app.Version = Version
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "base.url",
Usage: "jenkins base url",
EnvVar: "PLUGIN_BASE_URL,JENKINS_BASE_URL",
&cli.StringFlag{
Name: "host",
Usage: "jenkins base url",
EnvVars: []string{"PLUGIN_URL", "JENKINS_URL", "INPUT_URL"},
},
cli.StringFlag{
Name: "username",
Usage: "jenkins username",
EnvVar: "PLUGIN_USERNAME,JENKINS_USERNAME",
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "jenkins username",
EnvVars: []string{"PLUGIN_USER", "JENKINS_USER", "INPUT_USER"},
},
cli.StringFlag{
Name: "token",
Usage: "jenkins token",
EnvVar: "PLUGIN_TOKEN,JENKINS_TOKEN",
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Usage: "jenkins API token for authentication",
EnvVars: []string{"PLUGIN_TOKEN", "JENKINS_TOKEN", "INPUT_TOKEN"},
},
cli.StringSliceFlag{
Name: "job",
Usage: "jenkins job",
EnvVar: "PLUGIN_JOB",
&cli.StringFlag{
Name: "remote-token",
Usage: "jenkins remote trigger token",
EnvVars: []string{"PLUGIN_REMOTE_TOKEN", "JENKINS_REMOTE_TOKEN", "INPUT_REMOTE_TOKEN"},
},
cli.StringFlag{
Name: "repo.owner",
Usage: "repository owner",
EnvVar: "DRONE_REPO_OWNER",
&cli.StringSliceFlag{
Name: "job",
Aliases: []string{"j"},
Usage: "jenkins job",
EnvVars: []string{"PLUGIN_JOB", "JENKINS_JOB", "INPUT_JOB"},
},
cli.StringFlag{
Name: "repo.name",
Usage: "repository name",
EnvVar: "DRONE_REPO_NAME",
&cli.BoolFlag{
Name: "insecure",
Usage: "allow insecure server connections when using SSL",
EnvVars: []string{"PLUGIN_INSECURE", "JENKINS_INSECURE", "INPUT_INSECURE"},
},
cli.StringFlag{
Name: "commit.sha",
Usage: "git commit sha",
EnvVar: "DRONE_COMMIT_SHA",
&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: "commit.branch",
Value: "master",
Usage: "git commit branch",
EnvVar: "DRONE_COMMIT_BRANCH",
&cli.StringFlag{
Name: "parameters",
Aliases: []string{"p"},
Usage: "jenkins build parameters (multi-line format: key=value, one per line)",
EnvVars: []string{"PLUGIN_PARAMETERS", "JENKINS_PARAMETERS", "INPUT_PARAMETERS"},
},
cli.StringFlag{
Name: "commit.author",
Usage: "git author name",
EnvVar: "DRONE_COMMIT_AUTHOR",
&cli.BoolFlag{
Name: "wait",
Usage: "wait for job completion",
EnvVars: []string{"PLUGIN_WAIT", "JENKINS_WAIT", "INPUT_WAIT"},
},
cli.StringFlag{
Name: "commit.message",
Usage: "commit message",
EnvVar: "DRONE_COMMIT_MESSAGE",
&cli.DurationFlag{
Name: "poll-interval",
Usage: "interval between status checks (e.g., 10s, 1m)",
Value: 10 * time.Second,
EnvVars: []string{
"PLUGIN_POLL_INTERVAL",
"JENKINS_POLL_INTERVAL",
"INPUT_POLL_INTERVAL",
},
},
cli.StringFlag{
Name: "build.event",
Value: "push",
Usage: "build event",
EnvVar: "DRONE_BUILD_EVENT",
&cli.DurationFlag{
Name: "timeout",
Usage: "maximum time to wait for job completion (e.g., 30m, 1h)",
Value: 30 * time.Minute,
EnvVars: []string{"PLUGIN_TIMEOUT", "JENKINS_TIMEOUT", "INPUT_TIMEOUT"},
},
cli.IntFlag{
Name: "build.number",
Usage: "build number",
EnvVar: "DRONE_BUILD_NUMBER",
},
cli.StringFlag{
Name: "build.status",
Usage: "build status",
Value: "success",
EnvVar: "DRONE_BUILD_STATUS",
},
cli.StringFlag{
Name: "build.link",
Usage: "build link",
EnvVar: "DRONE_BUILD_LINK",
&cli.BoolFlag{
Name: "debug",
Usage: "enable debug mode to show detailed parameter information",
EnvVars: []string{"PLUGIN_DEBUG", "JENKINS_DEBUG", "INPUT_DEBUG"},
},
}
app.Run(os.Args)
// Override a template
cli.AppHelpTemplate = asciiArt + `
NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} ` +
`{{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
{{if len .Authors}}
AUTHOR:
{{range .Authors}}{{ . }}{{end}}
{{end}}{{if .Commands}}
COMMANDS:
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}` +
`{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}{{if .Copyright }}
COPYRIGHT:
{{.Copyright}}
{{end}}{{if .Version}}
VERSION:
{{.Version}}
{{end}}
REPOSITORY:
Github: https://github.com/appleboy/drone-jenkins
`
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func run(c *cli.Context) error {
plugin := Plugin{
Repo: Repo{
Owner: c.String("repo.owner"),
Name: c.String("repo.name"),
},
Build: Build{
Number: c.Int("build.number"),
Event: c.String("build.event"),
Status: c.String("build.status"),
Commit: c.String("commit.sha"),
Branch: c.String("commit.branch"),
Author: c.String("commit.author"),
Message: c.String("commit.message"),
Link: c.String("build.link"),
},
Config: Config{
BaseURL: c.String("base.url"),
Username: c.String("username"),
Token: c.String("token"),
Job: c.StringSlice("job"),
},
// Validate required parameters
if c.String("host") == "" {
return fmt.Errorf("host is required")
}
return plugin.Exec()
if len(c.StringSlice("job")) == 0 {
return fmt.Errorf("at least one job is required")
}
// Validate authentication: either (user + token) or remote-token must be provided
hasUserAuth := c.String("user") != "" && c.String("token") != ""
hasRemoteToken := c.String("remote-token") != ""
if !hasUserAuth && !hasRemoteToken {
return fmt.Errorf("authentication required: provide either (user + token) or remote-token")
}
plugin := Plugin{
BaseURL: c.String("host"),
Username: c.String("user"),
Token: c.String("token"),
RemoteToken: c.String("remote-token"),
Job: c.StringSlice("job"),
Insecure: c.Bool("insecure"),
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"),
}
// 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)
}
+156 -50
View File
@@ -1,77 +1,183 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"strings"
"time"
)
type (
// Repo information.
Repo struct {
Owner string
Name string
}
// Build information.
Build struct {
Event string
Number int
Commit string
Message string
Branch string
Author string
Status string
Link string
}
// Config for the plugin.
Config struct {
BaseURL string
Username string
Token string
Job []string
}
// Plugin values.
// Plugin represents the configuration for the Jenkins plugin.
// It contains all necessary credentials and settings to trigger Jenkins jobs.
Plugin struct {
Repo Repo
Build Build
Config Config
BaseURL string // Jenkins server base URL
Username string // Jenkins username for authentication
Token string // Jenkins API token for authentication
RemoteToken string // Optional remote trigger token for additional security
Job []string // List of Jenkins job names to trigger
Insecure bool // Whether to skip TLS certificate verification
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
}
)
func trimElement(keys []string) []string {
var newKeys []string
// trimWhitespaceFromSlice removes empty and whitespace-only strings from a slice.
// It returns a new slice containing only non-empty trimmed strings.
func trimWhitespaceFromSlice(items []string) []string {
result := make([]string, 0, len(items))
for _, value := range keys {
value = strings.Trim(value, " ")
if len(value) == 0 {
continue
for _, item := range items {
trimmed := strings.TrimSpace(item)
if trimmed != "" {
result = append(result, trimmed)
}
newKeys = append(newKeys, value)
}
return newKeys
return result
}
// Exec executes the plugin.
func (p Plugin) Exec() error {
// 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 {
values := url.Values{}
if len(p.Config.BaseURL) == 0 || len(p.Config.Username) == 0 || len(p.Config.Token) == 0 {
log.Println("missing jenkins config")
// 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
}
return errors.New("missing jenkins config")
parts := strings.SplitN(trimmedLine, "=", 2)
if len(parts) != 2 {
log.Printf(
"warning: skipping invalid parameter format (expected key=value): %q",
trimmedLine,
)
continue
}
key := strings.TrimSpace(parts[0])
value := parts[1] // Keep value as-is to preserve intentional spaces
if key == "" {
log.Printf("warning: skipping parameter with empty key: %q", trimmedLine)
continue
}
values.Add(key, value)
}
auth := &Auth{
Username: p.Config.Username,
Token: p.Config.Token,
}
jenkins := NewJenkins(auth, p.Config.BaseURL)
return values
}
for _, value := range trimElement(p.Config.Job) {
jenkins.trigger(value, nil)
// validateConfig checks that all required plugin configuration is present.
// It returns a descriptive error if any required field is missing.
func (p Plugin) validateConfig() error {
if p.BaseURL == "" {
return errors.New("jenkins base URL is required")
}
// 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.
// 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)
}
// Clean and validate job list
jobs := trimWhitespaceFromSlice(p.Job)
if len(jobs) == 0 {
return errors.New("at least one Jenkins job name is required")
}
// 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, 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)
// Set default values for wait configuration
pollInterval := p.PollInterval
if pollInterval == 0 {
pollInterval = 10 * time.Second
}
timeout := p.Timeout
if timeout == 0 {
timeout = 30 * time.Minute
}
// Trigger each job
for _, jobName := range jobs {
queueID, err := jenkins.trigger(ctx, jobName, params)
if err != nil {
return fmt.Errorf("failed to trigger job %q: %w", jobName, err)
}
log.Printf("successfully triggered job: %s (queue #%d)", jobName, queueID)
// Wait for job completion if requested
if p.Wait {
buildInfo, err := jenkins.waitForCompletion(
ctx,
jobName,
queueID,
pollInterval,
timeout,
)
if err != nil {
return fmt.Errorf("error waiting for job %q: %w", jobName, err)
}
// Check if build was successful
if buildInfo.Result != "SUCCESS" {
return fmt.Errorf(
"job %q (build #%d) failed with status: %s",
jobName,
buildInfo.Number,
buildInfo.Result,
)
}
log.Printf("job %s (build #%d) completed successfully", jobName, buildInfo.Number)
}
}
return nil
+486 -42
View File
@@ -1,70 +1,514 @@
package main
import (
"github.com/stretchr/testify/assert"
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMissingConfig(t *testing.T) {
const (
testJobBuildPath = "/job/test-job/build"
testQueueItemPath = "/queue/item/123/api/json"
testBuildStatusPath = "/job/test-job/456/api/json"
)
// TestValidateConfig tests the validateConfig method
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
plugin Plugin
wantError bool
errorMsg string
}{
{
name: "missing all config",
plugin: Plugin{},
wantError: true,
errorMsg: "jenkins base URL is required",
},
{
name: "missing authentication",
plugin: Plugin{
BaseURL: "http://example.com",
},
wantError: true,
errorMsg: "authentication required",
},
{
name: "missing token (only username)",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
},
wantError: true,
errorMsg: "authentication required",
},
{
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",
Token: "bar",
},
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 {
t.Run(tt.name, func(t *testing.T) {
err := tt.plugin.validateConfig()
if tt.wantError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
// TestTrimWhitespaceFromSlice tests the trimWhitespaceFromSlice function
func TestTrimWhitespaceFromSlice(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "remove empty and whitespace strings",
input: []string{"1", " ", "3"},
expected: []string{"1", "3"},
},
{
name: "no whitespace strings",
input: []string{"1", "2"},
expected: []string{"1", "2"},
},
{
name: "all whitespace",
input: []string{" ", "\t", "\n"},
expected: []string{},
},
{
name: "empty slice",
input: []string{},
expected: []string{},
},
{
name: "trim surrounding whitespace",
input: []string{" foo ", " bar ", "baz"},
expected: []string{"foo", "bar", "baz"},
},
{
name: "mixed empty and valid",
input: []string{"", "valid", "", "also-valid", ""},
expected: []string{"valid", "also-valid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := trimWhitespaceFromSlice(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestParseParameters tests the parseParameters function
func TestParseParameters(t *testing.T) {
tests := []struct {
name string
input string
expected url.Values
}{
{
name: "valid parameters",
input: "key1=value1\nkey2=value2",
expected: url.Values{
"key1": []string{"value1"},
"key2": []string{"value2"},
},
},
{
name: "parameter with multiple equals signs",
input: "key=value=with=equals",
expected: url.Values{
"key": []string{"value=with=equals"},
},
},
{
name: "parameter with spaces in value",
input: "key=value with spaces",
expected: url.Values{
"key": []string{"value with spaces"},
},
},
{
name: "parameter with empty value",
input: "key=",
expected: url.Values{
"key": []string{""},
},
},
{
name: "invalid parameter format (no equals)",
input: "invalid",
expected: url.Values{},
},
{
name: "parameter with empty key",
input: "=value",
expected: url.Values{},
},
{
name: "mixed valid and invalid",
input: "valid=yes\ninvalid\nalso=valid",
expected: url.Values{
"valid": []string{"yes"},
"also": []string{"valid"},
},
},
{
name: "key with surrounding whitespace",
input: " key =value",
expected: url.Values{
"key": []string{"value"},
},
},
{
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 {
t.Run(tt.name, func(t *testing.T) {
result := parseParameters(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestExecMissingConfig tests Exec with missing configuration
func TestExecMissingConfig(t *testing.T) {
var plugin Plugin
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.NotNil(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins base URL is required")
}
func TestMissingJenkinsConfig(t *testing.T) {
// TestExecMissingJenkinsUsername tests Exec with missing username
func TestExecMissingJenkinsUsername(t *testing.T) {
plugin := Plugin{
Config: Config{
BaseURL: "http://example.com",
BaseURL: "http://example.com",
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "authentication required")
}
// TestExecMissingJenkinsToken tests Exec with missing token
func TestExecMissingJenkinsToken(t *testing.T) {
plugin := Plugin{
BaseURL: "http://example.com",
Username: "foo",
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "authentication required")
}
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
func TestExecMissingJenkinsJob(t *testing.T) {
tests := []struct {
name string
jobs []string
}{
{
name: "no jobs",
jobs: []string{},
},
{
name: "only whitespace jobs",
jobs: []string{" ", "\t", "\n"},
},
{
name: "nil jobs",
jobs: nil,
},
}
err := plugin.Exec()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
Job: tt.jobs,
}
assert.NotNil(t, err)
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one Jenkins job name is required")
})
}
}
func TestPluginTriggerBuild(t *testing.T) {
plugin := Plugin{
Repo: Repo{
Name: "go-hello",
Owner: "appleboy",
},
Build: Build{
Number: 101,
Status: "success",
Link: "https://github.com/appleboy/go-hello",
Author: "Bo-Yi Wu",
Branch: "master",
Message: "update by drone line plugin.",
Commit: "e7c4f0a63ceeb42a39ac7806f7b51f3f0d204fd2",
},
// TestExecTriggerBuild tests successful job triggering
func TestExecTriggerBuild(t *testing.T) {
// Create a mock Jenkins server
queueID := 1
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", queueID))
w.WriteHeader(http.StatusCreated)
queueID++
}))
defer server.Close()
Config: Config{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
Job: []string{"drone-jenkins"},
},
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"drone-jenkins"},
}
err := plugin.Exec()
err := plugin.Exec(context.Background())
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestTrimElement(t *testing.T) {
var input, result []string
// TestExecTriggerMultipleJobs tests triggering multiple jobs
func TestExecTriggerMultipleJobs(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 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)
}))
defer server.Close()
input = []string{"1", " ", "3"}
result = []string{"1", "3"}
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"job1", "job2", "job3"},
}
assert.Equal(t, result, trimElement(input))
err := plugin.Exec(context.Background())
input = []string{"1", "2"}
result = []string{"1", "2"}
assert.Equal(t, result, trimElement(input))
assert.NoError(t, err)
assert.Equal(t, 3, jobsTriggered)
}
// TestExecWithParameters tests job triggering with parameters
func TestExecWithParameters(t *testing.T) {
// Create a mock Jenkins server
var receivedQuery url.Values
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedQuery = r.URL.Query()
w.Header().Set("Location", "http://jenkins.example.com/queue/item/1/")
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"parameterized-job"},
Parameters: "branch=main\nenvironment=production",
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, "main", receivedQuery.Get("branch"))
assert.Equal(t, "production", receivedQuery.Get("environment"))
}
// TestExecWithRemoteToken tests job triggering with remote token
func TestExecWithRemoteToken(t *testing.T) {
// Create a mock Jenkins server
var receivedToken string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedToken = r.URL.Query().Get("token")
w.Header().Set("Location", "http://jenkins.example.com/queue/item/1/")
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
RemoteToken: "remote-token-123",
Job: []string{"secure-job"},
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, "remote-token-123", receivedToken)
}
// TestExecWithJobsContainingWhitespace tests job list with whitespace
func TestExecWithJobsContainingWhitespace(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 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)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{" job1 ", "job2", " ", "job3"},
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
// Should trigger 3 jobs (whitespace-only entry should be filtered out)
assert.Equal(t, 3, jobsTriggered)
}
// TestExecWithWaitSuccess tests job execution with wait for successful completion
func TestExecWithWaitSuccess(t *testing.T) {
// Create a mock Jenkins server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case testJobBuildPath:
// Trigger build
w.Header().Set("Location", "http://jenkins.example.com/queue/item/123/")
w.WriteHeader(http.StatusCreated)
case testQueueItemPath:
// Queue item with build number
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":123,"executable":{"number":456}}`))
case testBuildStatusPath:
// Build completed successfully
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"number":456,"building":false,"result":"SUCCESS"}`))
}
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"test-job"},
Wait: true,
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
}
// TestExecWithWaitFailure tests job execution with wait for failed build
func TestExecWithWaitFailure(t *testing.T) {
// Create a mock Jenkins server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case testJobBuildPath:
// Trigger build
w.Header().Set("Location", "http://jenkins.example.com/queue/item/123/")
w.WriteHeader(http.StatusCreated)
case testQueueItemPath:
// Queue item with build number
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":123,"executable":{"number":456}}`))
case testBuildStatusPath:
// Build completed with failure
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"number":456,"building":false,"result":"FAILURE"}`))
}
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"test-job"},
Wait: true,
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed with status: FAILURE")
}