├── 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 [](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!")
--------------------------------------------------------------------------------