├── requirements.txt ├── .gitignore ├── Dockerfile ├── ea_script_template.xml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── pythonapp.yml ├── LICENSE ├── action.yml ├── README.md └── action.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | jmespath 3 | loguru -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | action/test 2 | .env 3 | .DS_Store 4 | *.code-workspace -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | COPY . /action 3 | WORKDIR /action 4 | RUN pip install -r requirements.txt 5 | ENTRYPOINT ["python"] 6 | CMD ["/action/action.py"] -------------------------------------------------------------------------------- /ea_script_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | name 4 | true 5 | Extension attribute script created via git2jamf 6 | String 7 | 8 | script 9 | Mac 10 | 11 | 12 | General 13 | Extension Attributes 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jgarcesres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | # 4 | name: Python application 5 | 6 | on: 7 | push: 8 | #branches: [ master ] 9 | paths: 10 | - '**/*.py' 11 | pull_request: 12 | branches: [ master ] 13 | paths: 14 | - '**/*.py' 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python 24 | uses: actions/setup-python@v1 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | - name: Lint with flake8 30 | run: | 31 | pip install flake8 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 35 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 36 | # - name: Test with pytest 37 | # run: | 38 | # pip install pytest 39 | # pytest 40 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'git2jamf' 2 | description: > 3 | creates, updates and delets scripts in jamf from your github repository 4 | inputs: 5 | jamf_url: 6 | description: 'url of your jamf instance' 7 | required: true 8 | jamf_username: 9 | description: 'username to auth against jamf' 10 | required: true 11 | jamf_password: 12 | description: 'password to auth against jamf' 13 | required: true 14 | jamf_auth_type: 15 | description: 'auth for traditional username/pwd or oauth for clientid an secret' 16 | default: auth 17 | script_dir: 18 | description: > 19 | the directory where the scripts to process are defaults to the github.workspace environment variable 20 | default: ${{ github.workspace }} 21 | ea_script_dir: 22 | description: > 23 | the directory where the extion attribute scripts to process are defaults to the github.workspace environment variable 24 | default: false 25 | script_extensions: 26 | description: > 27 | extension for the types of scripts we'll be looking to upload defaults to .sh and .py files 28 | default: 'sh py' 29 | prefix: 30 | description: > 31 | Use the branch name as a prefix 32 | default: false 33 | delete: 34 | description: > 35 | Should we delete scripts that are not in github? Careful when turning this on! 36 | default: false 37 | 38 | 39 | runs: 40 | using: 'docker' 41 | image: 'Dockerfile' 42 | branding: 43 | color: purple 44 | icon: link 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git2jamf [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 2 | This action grabs the github repository (or any subdfolder of your choice) scans it for scripts and will create or update those scripts in jamf. 3 | 4 | It starts by comparing filename of the github script (without the extension) against the name of the script in jamf: 5 | * If it doesn't exist, it will create it 6 | * if it exists, it will compare the hash of the body of both scripts and update it in jamf if they differ. Github is always treated as the source. 7 | * If enabled, it will add a prefix with the `branch name_` to a script. 8 | 9 | After creating and updating scripts, if enabled, it can delete any leftover script that is not found in github, thus keeping Github as your one source. 10 | 11 | ## Future state 12 | * handle extension attributes. 13 | * slack notifications 14 | * suggestions are welcome! 15 | 16 | ## Inputs 17 | ### `jamf_url` 18 | 19 | **Required** the url of your jamf instance 20 | 21 | ### `jamf_auth_type` 22 | 23 | **Optional** Defaults to `auth` but can be set to `oauth` to use `client_id` and `client_secret` instead of a username and password. 24 | 25 | ### `jamf_username` 26 | 27 | **Required** the username to auth against jamf. If `auth_type` is set to `oauth`, this is the `client_id` . **This user should have permission to update and create scripts.** 28 | 29 | ### `jamf_password` 30 | 31 | **Required** password for the user. If `auth_type` is set to `oauth`, this is the `client_secret` 32 | 33 | ### `script_dir` 34 | 35 | **optional** the directory where the scripts to upload will be, this could be a subdirectoy in your repository `path/to/scripts`. By default it will try to sync all .sh and .py files from the repo, so it's **greatly recommended to provide this input**, you can look for multiple subdirectories that share the same name, just provide a name like `**/scripts` 36 | 37 | ### `script_extensions` 38 | 39 | **optional** the extensions for the types of files we'll be searching for. By default it tries to look for `*.sh and *.py` files. To change the behavior, separate each extension with spaces and no periods. ie `sh py ps1` 40 | 41 | ### `delete` 42 | 43 | **optional** by default this will be `false`, if enabled it will delete any scripts that are not found in the github folder you're syncing. **Don't enable this and the prefix at the same time if you're running multiple workflows, they're not compatible** 44 | 45 | ### `prefix` 46 | 47 | **optional** by default this will be `false`, it will add the branch name as a prefix to the script before uploading it. 48 | 49 | ## Outputs 50 | 51 | ### `results` 52 | 53 | what scripts were updated 54 | 55 | 56 | ## Getting started. 57 | * First, you'll want to create the secrets that will be needed for this to work. You can do this in the settings of your repository, you'll reference those secrets in the workflow file. 58 | * Now create the workflow file in `.github/workflows/git2jamf.yml` 59 | * You can use the example bellow as a basis(replace the secret values for the names of the ones you created). 60 | * In this example, the action runs only when a push is sent to main and it's attempting to sync a folder called `scripts` at the root of the repository. 61 | * You can customize it further using githubs [workflow documentation](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions) 62 | 63 | **NOTE**: If possible, I recommend running this on a test instance first. If you can't, then try syncing just one folder with a small set of scripts so you can get a feel for how it works. 64 | 65 | ```yaml 66 | name: git2jamf 67 | on: 68 | push: 69 | branches: 70 | - main 71 | jobs: 72 | jamf_scripts: 73 | runs-on: ubuntu-latest 74 | name: git2jamf 75 | steps: 76 | - name: checkout 77 | uses: actions/checkout@v3 78 | - name: git2jamf 79 | uses: jgarcesres/git2jamf@main 80 | with: 81 | jamf_url: ${{ secrets.jamf_test_url }} 82 | jamf_username: ${{ secrets.jamf_test_username }} 83 | jamf_password: ${{ secrets.jamf_test_password }} 84 | script_dir: 'scripts' 85 | ``` 86 | 87 | 88 | ## Example usage with 2 instances 89 | you would probably have 2 sets of secrets, with url and credentials for each instance(or share the same user creds across both servers). You also will need 2 workflow files: one for pushes to the main branch and another that goes to test. 90 | 91 | ```yaml 92 | name: git2jamf_test 93 | on: 94 | pull_request: 95 | branches: 96 | - main 97 | push: 98 | branches: 99 | - test* 100 | - dev* 101 | jobs: 102 | jamf_scripts: 103 | runs-on: ubuntu-latest 104 | name: git2jgit2jamf_testamf 105 | steps: 106 | - name: checkout 107 | uses: actions/checkout@v3 108 | - name: git2jamf_test 109 | uses: jgarcesres/git2jamf@main 110 | with: 111 | jamf_url: ${{ secrets.jamf_test_url }} 112 | jamf_username: ${{ secrets.jamf_test_username }} 113 | jamf_password: ${{ secrets.jamf_test_password }} 114 | script_dir: '**/scripts' 115 | ``` 116 | ```yaml 117 | name: git2jamf 118 | on: 119 | push: 120 | branches: 121 | - main 122 | jobs: 123 | jamf_scripts: 124 | runs-on: ubuntu-latest 125 | name: git2jamf 126 | steps: 127 | - name: checkout 128 | uses: actions/checkout@v3 129 | - name: git2jamf 130 | uses: jgarcesres/git2jamf@main 131 | with: 132 | jamf_url: ${{ secrets.jamf_prod_url }} 133 | jamf_username: ${{ secrets.jamf_prod_username }} 134 | jamf_password: ${{ secrets.jamf_prod_password }} 135 | script_dir: '**/scripts' 136 | ``` 137 | 138 | 139 | ## Example usage with one instance 140 | The prefix remains enabled for the test branch. This might create a bit of "garbage" as the scripts that have a prefix won't be deleted automatically. 141 | 142 | ```yaml 143 | name: git2jamf_test 144 | on: 145 | push: 146 | branches: 147 | - test 148 | jobs: 149 | jamf_scripts: 150 | runs-on: ubuntu-latest 151 | name: git2jamf_test 152 | steps: 153 | - name: checkout 154 | uses: actions/checkout@v3 155 | - name: git2jamf_test 156 | uses: jgarcesres/git2jamf@main 157 | with: 158 | jamf_url: ${{ secrets.jamf_url }} 159 | jamf_username: ${{ secrets.jamf_username }} 160 | jamf_password: ${{ secrets.jamf_password }} 161 | script_dir: toplevelfolder/scripts 162 | enable_prefix: true 163 | ``` 164 | ```yaml 165 | name: git2jamf 166 | on: 167 | push: 168 | branches: 169 | - main 170 | jobs: 171 | jamf_scripts: 172 | runs-on: ubuntu-latest 173 | name: git2jamf 174 | steps: 175 | - name: checkout 176 | uses: actions/checkout@v3 177 | - name: git2jamf 178 | uses: jgarcesres/git2jamf@main 179 | with: 180 | jamf_url: ${{ secrets.jamf_url }} 181 | jamf_username: ${{ secrets.jamf_username }} 182 | jamf_password: ${{ secrets.jamf_password }} 183 | script_dir: toplevelfolder/scripts 184 | ``` 185 | 186 | 187 | -------------------------------------------------------------------------------- /action.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #created by Juan Garces 3 | 4 | import os 5 | import glob 6 | import requests 7 | import jmespath 8 | import hashlib 9 | import sys 10 | from loguru import logger 11 | 12 | logger.remove() 13 | logger.add(sys.stdout, colorize=True, level="INFO", format="{time:HH:mm:ss!UTC}: {message}") 14 | 15 | 16 | #function to get the token 17 | @logger.catch 18 | def get_jamf_token(url, auth_type, username, password): 19 | if auth_type == "auth": 20 | token_request = requests.post(url=f"{url}/api/v1/auth/token", auth=(username,password)) 21 | elif auth_type =='oauth': 22 | data = {"client_id": username,"client_secret": password, "grant_type": "client_credentials"} 23 | token_request = requests.post(url=f"{url}/api/oauth/token", data=data) 24 | if token_request.status_code == requests.codes.ok: 25 | if auth_type == "auth": 26 | logger.success(f"got the token! it expires in: {token_request.json()['expires']}") 27 | return token_request.json()['token'] 28 | elif auth_type == "oauth": 29 | logger.success(f"got the token! it expires in: {token_request.json()['expires_in']}") 30 | return token_request.json()['access_token'] 31 | elif token_request.status_code == requests.codes.not_found: 32 | logger.error('failed to retrieve a valid token, please check the url') 33 | raise Exception("failed to retrieve a valid token, please check the credentials") 34 | elif token_request.status_code == requests.codes.unauthorized: 35 | logger.error('failed to retrieve a valid token, please check the credentials') 36 | raise Exception("failed to retrieve a valid token, please check the credentials") 37 | else: 38 | logger.error('failed to retrieve a valid token') 39 | logger.error(token_request.text) 40 | raise Exception("failed to retrieve a valid token, please check the credentials") 41 | 42 | 43 | #function to invalidate a token so it can't be use after we're done 44 | @logger.catch 45 | def invalidate_jamf_token(url, token): 46 | header = {"Authorization": f"Bearer {token}"} 47 | token_request = requests.post(url=f"{url}/api/v1/auth/invalidate-token", headers=header) 48 | if token_request.status_code == requests.codes.no_content: 49 | logger.success("token invalidated succesfully") 50 | return True 51 | else: 52 | logger.warning("failed to invalidate the token, maybe it's already expired?") 53 | logger.warning(token_request.text) 54 | 55 | 56 | #function to create a new script 57 | @logger.catch 58 | def create_jamf_script(url, token, payload): 59 | header = {"Authorization": f"Bearer {token}"} 60 | script_request = requests.post(url=f"{url}/uapi/v1/scripts", headers=header, json=payload) 61 | if script_request.status_code == requests.codes.created: 62 | logger.success("script created") 63 | return True 64 | else: 65 | logger.warning("failed to create the script") 66 | logger.debug(f"status code for create: {script_request.status_code}") 67 | logger.warning(script_request.text) 68 | sys.exit(1) 69 | 70 | 71 | #function to update an already existing script 72 | @logger.catch 73 | def update_jamf_script(url, token, payload): 74 | header = {"Authorization": f"Bearer {token}"} 75 | script_request = requests.put(url=f"{url}/uapi/v1/scripts/{payload['id']}", headers=header, json=payload) 76 | if script_request.status_code in [requests.codes.accepted, requests.codes.ok]: 77 | logger.success("script was updated succesfully") 78 | return True 79 | else: 80 | logger.warning("failed to update the script") 81 | logger.debug(f"status code for put: {script_request.status_code}") 82 | logger.warning(script_request.text) 83 | sys.exit(1) 84 | 85 | 86 | @logger.catch 87 | def delete_jamf_script(url, token, id): 88 | header = {"Authorization": f"Bearer {token}"} 89 | script_request = requests.delete(url=f"{url}/uapi/v1/scripts/{id}", headers=header) 90 | if script_request.status_code in [requests.codes.ok, requests.codes.accepted, requests.codes.no_content]: 91 | logger.success("script was deleted succesfully") 92 | return True 93 | else: 94 | logger.warning("failed to delete the script") 95 | logger.debug(f"status code for delete: {script_request.status_code}") 96 | logger.warning(script_request.text) 97 | sys.exit(1) 98 | 99 | 100 | #retrieves all scripts in a json 101 | @logger.catch 102 | def get_all_jamf_scripts(url, token, scripts = [], page = 0): 103 | header = {"Authorization": f"Bearer {token}"} 104 | page_size=50 105 | params = {"page": page, "page-size": page_size, "sort": "name:asc"} 106 | script_list = requests.get(url=f"{url}/uapi/v1/scripts", headers=header, params=params) 107 | if script_list.status_code == requests.codes.ok: 108 | script_list = script_list.json() 109 | logger.info(f"we got {len(script_list['results'])+page} of {script_list['totalCount']} results") 110 | page+=1 111 | if (page*page_size) < script_list['totalCount']: 112 | logger.info("seems there's more to grab") 113 | scripts.extend(script_list['results']) 114 | return get_all_jamf_scripts(url, token, scripts, page) 115 | else: 116 | logger.info("reached the end of our search") 117 | scripts.extend(script_list['results']) 118 | logger.success(f"retrieved {len(scripts)} total scripts") 119 | return scripts 120 | else: 121 | logger.error(f"status code: {script_list.status_code}") 122 | logger.error("error retrevieving script list") 123 | logger.error(script_list.text) 124 | raise Exception("error retrevieving script list") 125 | 126 | 127 | #search for the script name and return the json that for it 128 | @logger.catch 129 | def find_jamf_script(url, token, script_name, page = 0): 130 | header = {"Authorization": f"Bearer {token}"} 131 | page_size=50 132 | params = {"page": page, "page-size": page_size, "sort": "name:asc"} 133 | script_list = requests.get(url=f"{url}/uapi/v1/scripts", headers=header, params=params) 134 | if script_list.status_code == requests.codes.ok: 135 | script_list = script_list.json() 136 | logger.info(f"we have searched {len(script_list['results'])+page} of {script_list['totalCount']} results") 137 | script_search = jmespath.search(f"results[?name == '{script_name}']", script_list) 138 | if len(script_search) == 1: 139 | logger.info('found the script, returning it') 140 | return script_search[0] 141 | elif len(script_search) == 0 and (page*page_size) < script_list['totalCount']: 142 | logger.info("couldn't find the script in this page, seems there's more to look through") 143 | return find_jamf_script(url, token, script_name, page+1) 144 | else: 145 | logger.info(f"did not find any script named {script_name}") 146 | return "n/a" 147 | else: 148 | logger.error(f"status code: {script_list.status_code}") 149 | logger.error("error retrevieving script list") 150 | logger.error(script_list.text) 151 | raise Exception("failed to find the script, please investigate!") 152 | 153 | 154 | #function to find a EA script using the filename as the script name 155 | @logger.catch 156 | def find_ea_script(ea_name): 157 | ea_script = requests.get(url = f"{url}/JSSResource/computerextensionattributes/name/{ea_name}", auth=(username,password)) 158 | if ea_script.status_code == requests.codes.ok: 159 | return ea_script.json()['computer_extension_attribute'] 160 | elif ea_script.status_code == requests.codes.not_found: 161 | logger.warning(f"Found no script with name: {ea_name}") 162 | return None 163 | else: 164 | logger.error("encountered an error retriving the extension attribute, stopping") 165 | logger.error(ea_script.text) 166 | raise Exception("encountered an error retriving the extension attribute, stopping") 167 | 168 | 169 | #function to create EA script 170 | @logger.catch 171 | def create_ea_script(payload, id): 172 | headers = {"Accept": "text/xml", "Content-Type": "text/xml"} 173 | ea_script = requests.post(url = f"{url}/JSSResource/computerextensionattributes/id/{id}", json=payload, auth=(username,password)) 174 | if ea_script.status_code == requests.codes.ok: 175 | return "success" 176 | else: 177 | logger.error("encountered an error creating the extension attribute, stopping") 178 | logger.error(ea_script.text) 179 | raise Exception("encountered an error creating the extension attribute, stopping") 180 | 181 | 182 | #function to update existin EA script 183 | @logger.catch 184 | def update_ea_script(payload, id): 185 | headers = {"Accept": "text/xml", "Content-Type": "text/xml"} 186 | ea_script = requests.put(url=f"{url}/JSSResource/computerextensionattributes/id/{id}", json=payload, auth=(username,password)) 187 | if ea_script.status_code == requests.codes.ok: 188 | return "success" 189 | else: 190 | logger.error("encountered an error creating the extension attribute, stopping") 191 | logger.error(ea_script.text) 192 | raise Exception("encountered an error creating the extension attribute, stopping") 193 | 194 | 195 | #function to compare sripts and see if they have changed. If they haven't, no need to update it 196 | @logger.catch 197 | def compare_scripts(new, old): 198 | md5_new = hashlib.md5(new.encode()) 199 | logger.info(f"hash of the of github script: {md5_new.hexdigest()}") 200 | md5_old = hashlib.md5(old.encode()) 201 | logger.info(f"hash of the of jamf script: {md5_old.hexdigest()}") 202 | if md5_new.hexdigest() == md5_old.hexdigest(): 203 | logger.info("scripts are the same") 204 | return True 205 | else: 206 | logger.warning("scripts are different") 207 | return False 208 | 209 | 210 | #retrieves list of files given a folder path and the list of valid file extensions to look for 211 | @logger.catch 212 | def find_local_scripts(script_dir, script_extensions): 213 | script_list = [] 214 | logger.info(f"searching for files ending in {script_extensions} in {script_dir}") 215 | for file_type in script_extensions: 216 | script_list.extend(glob.glob(f"{script_dir}/**/*.{file_type}", recursive = True)) 217 | logger.info("found these: ", script_dir) 218 | logger.info(script_list) 219 | return script_list 220 | 221 | 222 | #strips out the path and extension to get the scripts name 223 | @logger.catch 224 | def get_script_name(script_path): 225 | return script_path.split('/')[-1].rsplit('.', 1)[0] 226 | 227 | 228 | @logger.catch 229 | def push_scripts(): 230 | #grab the token from jamf 231 | logger.info('grabing the token from jamf') 232 | token = get_jamf_token(url,auth_type, username, password) 233 | logger.info('checking the list of local scripts to upload or create') 234 | scripts = {} 235 | #this retrives the full path of the scripts we're trying to sync from github 236 | scripts['github'] = find_local_scripts(script_dir, script_extensions) 237 | #I need to simplify this array down to the just the name of the script, stripping out the path. 238 | scripts['github_simple_name'] = [] 239 | for script in scripts['github']: 240 | scripts['github_simple_name'].append(get_script_name(script).lower()) 241 | logger.info('doublechecking for duplicate script names') 242 | for count, script in enumerate(scripts['github_simple_name']): 243 | if scripts['github_simple_name'].count(script) >= 2: 244 | logger.error(f"the script name {script} is duplicated {scripts['github_simple_name'].count(script)} times, please give it a unique name") 245 | #logger.error(scripts['github'][count]) 246 | sys.exit(1) 247 | #continue if no dupes are found 248 | logger.success("nice, no duplicate script names, we can continue") 249 | logger.info('now checking jamf for its list of scripts') 250 | scripts['jamf'] = get_all_jamf_scripts(url, token) 251 | logger.info("setting all script names to lower case to avoid false positives in our search.") 252 | logger.info("worry not, this won't affect the actual naming :)") 253 | #save the scripts name all in lower_case 254 | for script in scripts['jamf']: 255 | script['lower_case_name'] = script['name'].lower() 256 | #make a copy of the jamf scripts, we'll use this to determine which to delete later on 257 | scripts['to_delete'] = scripts['jamf'] 258 | logger.info("processing each script now") 259 | for count, script in enumerate(scripts['github']): 260 | logger.info("----------------------") 261 | logger.info(f"script {count+1} of {len(scripts['github'])}") 262 | logger.info(f"path of the script: {script}") 263 | script_name = get_script_name(script) 264 | if enable_prefix == "false": 265 | #don't use the prefix 266 | logger.info(f"script name is: {script_name}") 267 | else: 268 | #use the branch name as prefix 269 | prefix = branch.split('/')[-1] 270 | script_name = f"{prefix}_{script_name}" 271 | logger.info(f"the new script name: {script_name}") 272 | #check to see if the script name exists in jamf 273 | logger.info(f"now let's see if {script_name} exists in jamf already") 274 | script_search = jmespath.search(f"[?lower_case_name == '{script_name.lower()}']", scripts['jamf']) 275 | if len(script_search) == 0: 276 | logger.info("it doesn't exist, lets create it") 277 | #it doesn't exist, we can create it 278 | with open(script, 'r') as upload_script: 279 | payload = {"name": script_name, "info": "", "notes": "created via github action", "priority": "AFTER" , "categoryId": "1", "categoryName":"", "parameter4":"", "parameter5":"", "parameter6":"", "parameter7":"", "parameter8":"", "parameter9":"", "parameter10":"", "parameter11":"", "osRequirements":"", "scriptContents":f"{upload_script.read()}"} 280 | create_jamf_script(url, token, payload) 281 | elif len(script_search) == 1: 282 | jamf_script = script_search.pop() 283 | del jamf_script['lower_case_name'] 284 | scripts['to_delete'].remove(jamf_script) 285 | logger.info("it does exist, lets compare them") 286 | #it does exists, lets see if has changed 287 | with open(script, 'r') as upload_script: 288 | script_text = upload_script.read() 289 | if not compare_scripts(script_text, jamf_script['scriptContents']): 290 | logger.info("the local version is different than the one in jamf, updating jamf") 291 | #the hash of the scripts is not the same, so we'll update it 292 | jamf_script['scriptContents'] = script_text 293 | update_jamf_script(url, token, jamf_script) 294 | else: 295 | logger.info("we're skipping this one.") 296 | if delete == 'true': 297 | logger.warning(f"we have {len(scripts['to_delete'])} scripts left to delete") 298 | for script in scripts['to_delete']: 299 | logger.info(f"attempting to delete script {script['name']} in jamf") 300 | delete_jamf_script(url, token, script['id']) 301 | 302 | logger.info("expiring the token so it can't be used further") 303 | invalidate_jamf_token(url, token) 304 | logger.success("finished with the scripts") 305 | 306 | 307 | def push_ea_scripts(): 308 | return "" 309 | 310 | 311 | #run this thing 312 | if __name__ == "__main__": 313 | logger.info('reading environment variables') 314 | url = os.getenv('INPUT_JAMF_URL') 315 | auth_type = os.getenv("INPUT_JAMF_AUTH_TYPE") 316 | if auth_type not in ["auth","oauth"]: 317 | logger.error("please use 'auth' or 'oauth' as they auth_type") 318 | #if using oauth, we're just going to re-use the same variables as they are similar enough. 319 | #client_id is username 320 | username = os.getenv('INPUT_JAMF_USERNAME') 321 | #client_secret is password 322 | password = os.getenv('INPUT_JAMF_PASSWORD') 323 | script_dir = os.getenv('INPUT_SCRIPT_DIR') 324 | ea_script_dir = os.getenv('INPUT_EA_SCRIPT_DIR') 325 | workspace_dir = os.getenv('GITHUB_WORKSPACE') 326 | if script_dir != workspace_dir: 327 | script_dir = f"{workspace_dir}/{script_dir}" 328 | enable_prefix = os.getenv('INPUT_PREFIX') 329 | branch = os.getenv('GITHUB_REF') 330 | script_extensions = os.getenv('INPUT_SCRIPT_EXTENSIONS') 331 | delete = os.getenv('INPUT_DELETE') 332 | script_extensions = script_extensions.split() 333 | logger.info(f"url is: {url}") 334 | logger.info(f"workspace dir is: {workspace_dir}") 335 | logger.info(f"script_dir is: {script_dir}") 336 | logger.info(f"branch is set to: {branch}") 337 | logger.info(f"script_deletion is: {delete}") 338 | logger.info(f"scripts_extensions are: {script_extensions}") 339 | if enable_prefix == 'false': 340 | logger.warning('prefix is disabled') 341 | else: 342 | logger.warning(f"prefix enabled, using: {branch.split('/')[-1]}") 343 | #run the block to push the "normal" scripts to jamf 344 | push_scripts() 345 | #check to see if we have an EA scripts to push over 346 | if ea_script_dir != 'false': 347 | logger.info("we have some EA scripts to process") 348 | push_ea_scripts() 349 | else: 350 | logger.warning("no EA script folder set, skipping") 351 | 352 | logger.success("we're done!") --------------------------------------------------------------------------------