Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
action/test
.env
.DS_Store
*.code-workspace
*.code-workspace
__pycache__
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# git2jamf [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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.
This action grabs the github repository (or any subfolder of your choice) scans it for scripts and will create or update those scripts in jamf.

It starts by comparing filename of the github script (without the extension) against the name of the script in jamf:
* If it doesn't exist, it will create it
* 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.
* If enabled, it will add a prefix with the `branch name_` to a script.
* If it doesn't exist, it will create it with a timestamped note indicating when it was created
* If it exists, it will compare the hash of the body of both scripts and update it in jamf if they differ. When updating, it will also update the notes with a timestamp of when the script was last updated, preserving any existing custom notes
* If enabled, it will add a prefix with the `branch name_` to a script.

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.

## Notes Management
The action automatically manages notes in Jamf scripts:
- **New scripts**: Get a "created via github action on [timestamp]" note
- **Updated scripts**: Get an "updated via github action on [timestamp]" note added/updated
- **Existing notes**: Custom notes are preserved and GitHub action timestamps are kept at the top
- **Order**: Created timestamp (if present) appears first, followed by updated timestamp, then any custom notes

## Future state
* handle extension attributes.
* slack notifications
Expand Down
54 changes: 53 additions & 1 deletion action.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jmespath
import hashlib
import sys
from datetime import datetime, timezone
from loguru import logger

logger.remove()
Expand Down Expand Up @@ -207,6 +208,53 @@ def compare_scripts(new, old):
return False


#function to create or update notes with proper timestamping
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should start with a capital letter and end with a period: 'Function to create or update notes with proper timestamping.'

Suggested change
#function to create or update notes with proper timestamping
# Function to create or update notes with proper timestamping.

Copilot uses AI. Check for mistakes.
@logger.catch
def update_script_notes(existing_notes, action_type="updated"):
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
commit_hash = os.getenv('GITHUB_SHA', 'unknown')[:7] # Get first 7 characters of commit hash
action_line = f"{action_type} via github action on {timestamp} (commit: {commit_hash})"

if not existing_notes:
# No existing notes, just add the action line
return action_line

lines = existing_notes.strip().split('\n')
updated_lines = []
found_created_line = False
found_updated_line = False

# Look for existing github action lines
for line in lines:
line = line.strip()
if line.startswith("created via github action"):
if not found_created_line:
updated_lines.append(line) # Keep the original created line
found_created_line = True
# Skip duplicate created lines
elif line.startswith("updated via github action"):
if not found_updated_line:
updated_lines.append(action_line) # Replace with new updated line
found_updated_line = True
# Skip old updated lines
else:
# Keep other notes
if line: # Only add non-empty lines
updated_lines.append(line)

# If we didn't find an existing updated line, add it after created line (if exists) or at the top
if not found_updated_line:
if found_created_line:
# Insert after the created line
insert_index = 1 if len(updated_lines) > 0 else 0
updated_lines.insert(insert_index, action_line)
else:
# Insert at the beginning
updated_lines.insert(0, action_line)

return '\n'.join(updated_lines)


#retrieves list of files given a folder path and the list of valid file extensions to look for
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should start with a capital letter and end with a period: 'Retrieves list of files given a folder path and the list of valid file extensions to look for.'

Suggested change
#retrieves list of files given a folder path and the list of valid file extensions to look for
# Retrieves list of files given a folder path and the list of valid file extensions to look for.

Copilot uses AI. Check for mistakes.
@logger.catch
def find_local_scripts(script_dir, script_extensions):
Expand Down Expand Up @@ -276,7 +324,8 @@ def push_scripts():
logger.info("it doesn't exist, lets create it")
#it doesn't exist, we can create it
with open(script, 'r') as upload_script:
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()}"}
creation_note = update_script_notes("", "created")
payload = {"name": script_name, "info": "", "notes": creation_note, "priority": "AFTER" , "categoryId": "1", "categoryName":"", "parameter4":"", "parameter5":"", "parameter6":"", "parameter7":"", "parameter8":"", "parameter9":"", "parameter10":"", "parameter11":"", "osRequirements":"", "scriptContents":f"{upload_script.read()}"}
create_jamf_script(url, token, payload)
elif len(script_search) == 1:
jamf_script = script_search.pop()
Expand All @@ -290,6 +339,9 @@ def push_scripts():
logger.info("the local version is different than the one in jamf, updating jamf")
#the hash of the scripts is not the same, so we'll update it
jamf_script['scriptContents'] = script_text
# Update the notes with timestamp
existing_notes = jamf_script.get('notes', '')
jamf_script['notes'] = update_script_notes(existing_notes, "updated")
update_jamf_script(url, token, jamf_script)
else:
logger.info("we're skipping this one.")
Expand Down