Compare commits

...

101 Commits

Author SHA1 Message Date
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
32 changed files with 2914 additions and 414 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@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
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@v3
+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@v5
with:
go-version: "^1"
- name: Checkout repository
uses: actions/checkout@v4
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@v4
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v5
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@v4
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
+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"]
+62 -34
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,63 @@ 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)
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
.PHONY: test
test:
@$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: install
install: $(GOFILES)
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' .
.PHONY: build
build: $(EXECUTABLE)
$(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@ .
build_linux_amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(DEPLOY_IMAGE) .
build_linux_arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(DEPLOY_IMAGE) .
build_linux_arm:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(DEPLOY_IMAGE) .
clean:
rm -rf coverage.txt ${DEPLOY_IMAGE}
$(GO) clean -x -i ./...
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
version:
@echo $(VERSION)
+392 -34
View File
@@ -1,60 +1,418 @@
# 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)
![logo](./images/logo.png)
[Drone](https://github.com/drone/drone) plugin for trigger [Jenkins](https://jenkins.io/) jobs.
[![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
A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https://jenkins.io/) jobs with flexible authentication and parameter support.
Build the binary with the following commands:
## Table of Contents
```
$ make build
- [drone-jenkins](#drone-jenkins)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Download Binary](#download-binary)
- [Build from Source](#build-from-source)
- [Docker Image](#docker-image)
- [Configuration](#configuration)
- [Jenkins Server Setup](#jenkins-server-setup)
- [Authentication](#authentication)
- [Parameters Reference](#parameters-reference)
- [Usage](#usage)
- [Command Line](#command-line)
- [Docker](#docker)
- [Drone CI](#drone-ci)
- [Development](#development)
- [Building](#building)
- [Testing](#testing)
- [License](#license)
- [Contributing](#contributing)
## Features
- Trigger single or multiple Jenkins jobs
- Support for Jenkins build parameters
- Multiple authentication methods (API token or remote trigger token)
- Wait for job completion with configurable polling and timeout
- Debug mode with detailed parameter information and secure token masking
- SSL/TLS support with optional insecure mode
- Cross-platform support (Linux, macOS, Windows)
- Available as binary, Docker image, or Drone plugin
## Prerequisites
- Jenkins server (version 2.0 or later recommended)
- Jenkins API token or remote trigger token for authentication
- For Jenkins setup, Docker is recommended but not required
## Installation
### Download Binary
Pre-compiled binaries are available from the [release page](https://github.com/appleboy/drone-jenkins/releases) for:
- **Linux**: amd64, 386
- **macOS (Darwin)**: amd64, 386
- **Windows**: amd64, 386
With Go installed, you can also install directly:
```sh
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) |
| 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
```
### 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
```
### Drone CI
Add the plugin to your `.drone.yml`:
```yaml
kind: pipeline
name: default
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job: drone-jenkins-plugin
```
**Multiple jobs with parameters:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job:
- deploy-frontend
- deploy-backend
parameters: |
ENVIRONMENT=production
VERSION=${DRONE_TAG}
COMMIT_SHA=${DRONE_COMMIT_SHA}
BRANCH=${DRONE_BRANCH}
```
**Using remote token:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
remote_token:
from_secret: jenkins_remote_token
job: my-jenkins-job
```
**Wait for job completion:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job: deploy-production
wait: true
poll_interval: 15s
timeout: 1h
```
**With debug mode:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job: my-jenkins-job
debug: true
```
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.
+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.5
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-20240521201337-686a1a2994c1 // 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.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/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

+298 -19
View File
@@ -1,12 +1,19 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/appleboy/com/gh"
"github.com/yassinebenaid/godump"
)
type (
@@ -20,15 +27,55 @@ type (
Jenkins struct {
Auth *Auth
BaseURL string
Token string // Remote trigger token
Client *http.Client
Debug bool // Enable debug mode to show detailed information
}
// 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"`
}
)
// NewJenkins is initial Jenkins object
func NewJenkins(auth *Auth, url string) *Jenkins {
func NewJenkins(auth *Auth, url string, token string, insecure bool, debug bool) *Jenkins {
url = strings.TrimRight(url, "/")
client := http.DefaultClient
if insecure {
client = &http.Client{
Transport: &http.Transport{
// #nosec G402 -- InsecureSkipVerify is intentionally configurable by user
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
return &Jenkins{
Auth: auth,
BaseURL: url,
Token: token,
Client: client,
Debug: debug,
}
}
@@ -48,39 +95,92 @@ func (jenkins *Jenkins) sendRequest(req *http.Request) (*http.Response, error) {
if jenkins.Auth != nil {
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
}
return http.DefaultClient.Do(req)
return jenkins.Client.Do(req)
}
func (jenkins *Jenkins) parseResponse(resp *http.Response, body interface{}) (err error) {
defer resp.Body.Close()
func (jenkins *Jenkins) get(path string, params url.Values, body interface{}) error {
requestURL := jenkins.buildURL(path, params)
if body == nil {
return
req, err := http.NewRequestWithContext(context.Background(), "GET", requestURL, nil)
if err != nil {
return err
}
data, err := ioutil.ReadAll(resp.Body)
resp, err := jenkins.sendRequest(req)
if err != nil {
return
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(path string, params url.Values) (int, error) {
requestURL := jenkins.buildURL(path, params)
req, err := http.NewRequest("POST", requestURL, nil)
req, err := http.NewRequestWithContext(context.Background(), "POST", requestURL, nil)
if err != nil {
fmt.Println(err)
return
return 0, err
}
resp, err := jenkins.sendRequest(req)
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 +200,187 @@ 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(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(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(job string, buildNumber int) (*BuildInfo, error) {
path := fmt.Sprintf("%s/%d/api/json", jenkins.parseJobPath(job), buildNumber)
var buildInfo BuildInfo
err := jenkins.get(path, nil, &buildInfo)
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(
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(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(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(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(urlPath, params)
}
+432 -8
View File
@@ -1,10 +1,14 @@
package main
import (
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestParseJobPath(t *testing.T) {
@@ -12,7 +16,7 @@ func TestParseJobPath(t *testing.T) {
Username: "appleboy",
Token: "1234",
}
jenkins := NewJenkins(auth, "http://example.com")
jenkins := NewJenkins(auth, "http://example.com", "", false, false)
assert.Equal(t, "/job/foo", jenkins.parseJobPath("/foo/"))
assert.Equal(t, "/job/foo", jenkins.parseJobPath("foo/"))
@@ -25,19 +29,439 @@ func TestUnSupportProtocol(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "example.com")
jenkins := NewJenkins(auth, "example.com", "", false, false)
err := jenkins.trigger("drone-jenkins", nil)
queueID, err := jenkins.trigger("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 := NewJenkins(auth, server.URL, "remote-token", false, false)
err := jenkins.trigger("drone-jenkins", url.Values{"token": []string{"bar"}})
assert.Nil(t, err)
params := url.Values{"param": []string{"value"}}
queueID, err := jenkins.trigger("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 := NewJenkins(auth, server.URL, "", false, false)
queueID, err := jenkins.postAndGetLocation("/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 := NewJenkins(auth, server.URL, "", false, false)
queueItem, err := jenkins.getQueueItem(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 := NewJenkins(auth, server.URL, "", false, false)
buildInfo, err := jenkins.getBuildInfo(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 := NewJenkins(auth, server.URL, "", false, false)
buildInfo, err := jenkins.waitForCompletion(
"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 := NewJenkins(auth, server.URL, "", false, false)
buildInfo, err := jenkins.waitForCompletion(
"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 := NewJenkins(auth, server.URL, "", false, false)
buildInfo, err := jenkins.waitForCompletion(
"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 := NewJenkins(auth, server.URL, "", false, false)
buildInfo, err := jenkins.waitForCompletion(
"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)
})
}
+198 -88
View File
@@ -1,121 +1,231 @@
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: "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.branch",
Value: "master",
Usage: "git commit branch",
EnvVar: "DRONE_COMMIT_BRANCH",
&cli.BoolFlag{
Name: "wait",
Usage: "wait for job completion",
EnvVars: []string{"PLUGIN_WAIT", "JENKINS_WAIT", "INPUT_WAIT"},
},
cli.StringFlag{
Name: "commit.author",
Usage: "git author name",
EnvVar: "DRONE_COMMIT_AUTHOR",
&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: "commit.message",
Usage: "commit message",
EnvVar: "DRONE_COMMIT_MESSAGE",
&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.StringFlag{
Name: "build.event",
Value: "push",
Usage: "build event",
EnvVar: "DRONE_BUILD_EVENT",
},
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 {
// Validate required parameters
if c.String("host") == "" {
return fmt.Errorf("host is required")
}
if len(c.StringSlice("job")) == 0 {
return fmt.Errorf("at least one job is required")
}
// Validate authentication: either (user + token) or remote-token must be provided
hasUserAuth := c.String("user") != "" && c.String("token") != ""
hasRemoteToken := c.String("remote-token") != ""
if !hasUserAuth && !hasRemoteToken {
return fmt.Errorf("authentication required: provide either (user + token) or remote-token")
}
plugin := Plugin{
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"),
},
BaseURL: c.String("host"),
Username: c.String("user"),
Token: c.String("token"),
RemoteToken: c.String("remote-token"),
Job: c.StringSlice("job"),
Insecure: c.Bool("insecure"),
Parameters: c.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
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,
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()
+137 -49
View File
@@ -2,76 +2,164 @@ package main
import (
"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
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.
// 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{}
// Split by newlines and process each line
lines := strings.Split(params, "\n")
for _, line := range lines {
// Skip empty lines
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" {
continue
}
parts := strings.SplitN(trimmedLine, "=", 2)
if len(parts) != 2 {
log.Printf(
"warning: skipping invalid parameter format (expected key=value): %q",
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)
}
return values
}
// validateConfig checks that all required plugin configuration is present.
// It returns a descriptive error if any required field is missing.
func (p Plugin) validateConfig() error {
if p.BaseURL == "" {
return errors.New("jenkins base URL is required")
}
if p.Username == "" {
return errors.New("jenkins username is required")
}
if p.Token == "" {
return errors.New("jenkins API token is required")
}
return nil
}
// Exec executes the plugin by triggering the configured Jenkins jobs.
// It validates the configuration, parses parameters, and triggers each job sequentially.
// Returns an error if validation fails or any job trigger fails.
func (p Plugin) Exec() error {
if len(p.Config.BaseURL) == 0 || len(p.Config.Username) == 0 || len(p.Config.Token) == 0 {
log.Println("missing jenkins config")
return errors.New("missing jenkins config")
// 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
auth := &Auth{
Username: p.Config.Username,
Token: p.Config.Token,
Username: p.Username,
Token: p.Token,
}
jenkins := NewJenkins(auth, p.Config.BaseURL)
for _, value := range trimElement(p.Config.Job) {
jenkins.trigger(value, nil)
// Initialize Jenkins client
jenkins := NewJenkins(auth, p.BaseURL, p.RemoteToken, p.Insecure, p.Debug)
// 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(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(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
+451 -41
View File
@@ -1,70 +1,480 @@
package main
import (
"github.com/stretchr/testify/assert"
"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 username and token",
plugin: Plugin{
BaseURL: "http://example.com",
},
wantError: true,
errorMsg: "jenkins username is required",
},
{
name: "missing token",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
},
wantError: true,
errorMsg: "jenkins API token is required",
},
{
name: "all required config present",
plugin: Plugin{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
},
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.plugin.validateConfig()
if tt.wantError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
// TestTrimWhitespaceFromSlice tests the trimWhitespaceFromSlice function
func TestTrimWhitespaceFromSlice(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "remove empty and whitespace strings",
input: []string{"1", " ", "3"},
expected: []string{"1", "3"},
},
{
name: "no whitespace strings",
input: []string{"1", "2"},
expected: []string{"1", "2"},
},
{
name: "all whitespace",
input: []string{" ", "\t", "\n"},
expected: []string{},
},
{
name: "empty slice",
input: []string{},
expected: []string{},
},
{
name: "trim surrounding whitespace",
input: []string{" foo ", " bar ", "baz"},
expected: []string{"foo", "bar", "baz"},
},
{
name: "mixed empty and valid",
input: []string{"", "valid", "", "also-valid", ""},
expected: []string{"valid", "also-valid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := trimWhitespaceFromSlice(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestParseParameters tests the parseParameters function
func TestParseParameters(t *testing.T) {
tests := []struct {
name string
input string
expected url.Values
}{
{
name: "valid parameters",
input: "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()
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()
assert.NotNil(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins username is required")
}
func TestPluginTriggerBuild(t *testing.T) {
// TestExecMissingJenkinsToken tests Exec with missing token
func TestExecMissingJenkinsToken(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",
},
Config: Config{
BaseURL: "http://example.com",
Username: "foo",
Token: "bar",
Job: []string{"drone-jenkins"},
},
BaseURL: "http://example.com",
Username: "foo",
}
err := plugin.Exec()
assert.Nil(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins API token is required")
}
func TestTrimElement(t *testing.T) {
var input, result []string
// 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,
},
}
input = []string{"1", " ", "3"}
result = []string{"1", "3"}
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.Equal(t, result, trimElement(input))
input = []string{"1", "2"}
result = []string{"1", "2"}
assert.Equal(t, result, trimElement(input))
err := plugin.Exec()
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one Jenkins job name is required")
})
}
}
// TestExecTriggerBuild tests successful job triggering
func TestExecTriggerBuild(t *testing.T) {
// Create a mock Jenkins server
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()
plugin := Plugin{
BaseURL: server.URL,
Username: "foo",
Token: "bar",
Job: []string{"drone-jenkins"},
}
err := plugin.Exec()
assert.NoError(t, err)
}
// TestExecTriggerMultipleJobs tests triggering multiple jobs
func TestExecTriggerMultipleJobs(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jobsTriggered++
w.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()
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()
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()
assert.NoError(t, err)
assert.Equal(t, "remote-token-123", receivedToken)
}
// TestExecWithJobsContainingWhitespace tests job list with whitespace
func TestExecWithJobsContainingWhitespace(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jobsTriggered++
w.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()
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()
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()
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed with status: FAILURE")
}