Skip to content
This repository has been archived by the owner on Mar 20, 2024. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
HoldenLucas committed Oct 19, 2023
0 parents commit 93461ab
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
layout python3

# secrets
dotenv_if_exists .env
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.direnv
.env
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.9

WORKDIR /usr/src/app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY entrypoint.py .

ENTRYPOINT ["./entrypoint.py"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Holden Lucas

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
all: build test

build: Dockerfile entrypoint.py
docker build -t build-jenkins-job .

test: entrypoint.py
docker run -it build-jenkins-job '${JOB_URL}' '${JENKINS_TOKEN}' '${JENKINS_USER}' '${JOB_PARAMS}'
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Build jenkins job docker action

This action runs a Jenkins job at a given URL with optional parameters.

## Inputs

```
inputs:
job_url:
description: 'URL for the Jenkins job. i.e. http://jenkins.company.io/job/some-folder/job/test'
required: true
jenkins_token:
description: 'Jekins API Token. 34 characters.'
required: true
jenkins_user:
description: 'Your Jenkins User ID.'
required: true
job_params:
description: 'JSON string of job parameters. i.e "{"foo": "bar", "baz": true, "qux": 3}" '
required: false
default: "{}"
```

## Outputs

```
outputs:
build_result:
description: 'Result of the Jenkins build; SUCCESS UNSTABLE FAILURE NOT_BUILT ABORTED'
```

### `build_result`

- SUCCESS
- UNSTABLE
- FAILURE
- NOT_BUILT
- ABORTED

[source](https://github.com/jenkinsci/jenkins/blob/6c07309cb1467f44413c58452cf99c42cfcf84ff/core/src/main/java/hudson/model/Result.java#L51)

## example
```
- uses: HoldenLucas/build-jenkins-job@v1
id: build
with:
job_url: ${{ secrets.JENKINS_URL }}/job/some-folder/job/test
jenkins_token: ${{ secrets.JENKINS_TOKEN }}
jenkins_user: ${{ secrets.JENKINS_USER }}
job_params: "{"foo": "bar", "baz": true, "qux": 3}"
- run: echo "Result: ${{ steps.build.outputs.build_result }}"
```
30 changes: 30 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: 'Jenkins: Build Job'
description: 'Builds a Jenkins job, returning its result.'
branding:
icon: 'activity'
color: 'red'
inputs:
job_url:
description: 'URL for the job. i.e. http://jenkins.infrastructure.company.io/job/build/job/test'
required: true
jenkins_token:
description: 'Jekins API Token. 34 characters.'
required: true
jenkins_user:
description: 'Your Jenkins User ID.'
required: true
job_params:
description: 'JSON string of job parameters. i.e "{"foo": "bar", "baz": true, "qux": 3}" '
required: false
default: "{}"
outputs:
build_result:
description: 'Build result'
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.job_url}}
- ${{ inputs.jenkins_token}}
- ${{ inputs.jenkins_user}}
- ${{ inputs.job_params}}
123 changes: 123 additions & 0 deletions entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python

import dataclasses
import json
import os
import sys
import time
from urllib.parse import urlparse, urlunsplit

import jenkins # https://python-jenkins.readthedocs.io/en/latest/index.html


@dataclasses.dataclass
class Jenkins:
QUIET_PERIOD = 5
EXPECTED_BUILD_RESULT = "SUCCESS"

jenkins_token: str
jenkins_user: str

job_url: str
job_params: str = "{}"

_short_job_name: str = dataclasses.field(init=False)
_jenkins_url: str = dataclasses.field(init=False)

def __post_init__(self):
u = urlparse(self.job_url)

self._short_job_name = "".join(u.path.split("job/"))[1:]

self._jenkins_url = urlunsplit((u.scheme, u.netloc, "", "", ""))

def _j(self):
return jenkins.Jenkins(self._jenkins_url, self.jenkins_user, self.jenkins_token)

def print_auth(self):
user_id = self._j().get_whoami()["id"]

print(f"Authenticated with {self._jenkins_url} as {user_id}")
print()

def queue_job(self):
return self._j().build_job(
self._short_job_name,
parameters=json.loads(self.job_params),
token=self.jenkins_token,
)

def get_run_number(self, queue_id):
# `executable` shows up when the build has started
# https://python-jenkins.readthedocs.io/en/latest/api.html?highlight=build_job#jenkins.Jenkins.get_queue_item
while "executable" not in (q_item := self._j().get_queue_item(queue_id)):
# "The returned dict will have a “why” key if the queued item is still waiting for an executor."
print(f"Waiting for {q_item['task']['url']} because: {q_item['why']}")
print()

time.sleep(self.QUIET_PERIOD)

return q_item["executable"]["number"]

def wait_for_build_to_finish(self, number) -> dict:
JOB_POLLING_RATE = 30

# `building` is true until all stages are done (including `post`)
while (
build_info := self._j().get_build_info(
name=self._short_job_name, number=number
)
).get("building"):
print(f"{build_info['fullDisplayName']} is running at {build_info['url']}")
print()

time.sleep(JOB_POLLING_RATE)

return build_info


# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files
def set_gh_env_file(file: str, msg: str):
try:
with open(os.environ[file], "a") as fh:
print(msg, file=fh)
except KeyError:
print(f"Failed to append {msg} to ${file}")
print()


if __name__ == "__main__":
token = sys.argv[2]
print(f"::add-mask::{token}")

j = Jenkins(
job_url=sys.argv[1],
jenkins_token=sys.argv[2],
jenkins_user=sys.argv[3],
job_params=sys.argv[4],
)

print()

# print(j)
# print()

j.print_auth()

q_id = j.queue_job()
print(f"In queue for {j.job_url} with ID {q_id}")
print()

print(f"Waiting {j.QUIET_PERIOD} seconds until Jenkins' quiet period has ended.")
print()
time.sleep(j.QUIET_PERIOD)

run_number = j.get_run_number(q_id)

build_result = j.wait_for_build_to_finish(run_number)["result"]

set_gh_env_file("GITHUB_OUTPUT", f"build_result={build_result}")
set_gh_env_file("GITHUB_STEP_SUMMARY", build_result)

if build_result != j.EXPECTED_BUILD_RESULT:
exit(1)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python_jenkins==1.8.1

0 comments on commit 93461ab

Please sign in to comment.