From 2ad35a608b840cfc3553447961722bcf303c13bf Mon Sep 17 00:00:00 2001 From: Juan Garces Date: Tue, 29 Jul 2025 11:33:02 -0500 Subject: [PATCH 1/5] ignore local cache --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9ba664b..fe6f56c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ action/test .env .DS_Store -*.code-workspace \ No newline at end of file +*.code-workspace +__pycache__ \ No newline at end of file From 643718809d3ed053aa6033f5c3ee97528d2db72c Mon Sep 17 00:00:00 2001 From: Juan Garces Date: Tue, 29 Jul 2025 11:38:56 -0500 Subject: [PATCH 2/5] update notes on script update with timestamp --- README.md | 15 ++++++++++---- action.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1b2c608..a111f89 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/action.py b/action.py index 8e39d8e..2658c6b 100644 --- a/action.py +++ b/action.py @@ -7,6 +7,7 @@ import jmespath import hashlib import sys +from datetime import datetime from loguru import logger logger.remove() @@ -207,6 +208,59 @@ def compare_scripts(new, old): return False +#function to create a creation note with timestamp +@logger.catch +def create_creation_note(): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") + return f"created via github action on {timestamp}" + + +#function to create or update notes with proper timestamping +@logger.catch +def update_script_notes(existing_notes, action_type="updated"): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") + action_line = f"{action_type} via github action on {timestamp}" + + 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 @logger.catch def find_local_scripts(script_dir, script_extensions): @@ -276,7 +330,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 = create_creation_note() + 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() @@ -290,6 +345,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.") From 1f01ac604dc907860826846619d50b169960329e Mon Sep 17 00:00:00 2001 From: jgarcesres Date: Tue, 29 Jul 2025 11:48:20 -0500 Subject: [PATCH 3/5] Update action.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.py b/action.py index 2658c6b..119d8d3 100644 --- a/action.py +++ b/action.py @@ -211,14 +211,14 @@ def compare_scripts(new, old): #function to create a creation note with timestamp @logger.catch def create_creation_note(): - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") return f"created via github action on {timestamp}" #function to create or update notes with proper timestamping @logger.catch def update_script_notes(existing_notes, action_type="updated"): - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") action_line = f"{action_type} via github action on {timestamp}" if not existing_notes: From c8d154bce6a34f29f92b67ff4a31277ef947e511 Mon Sep 17 00:00:00 2001 From: Juan Garces Date: Tue, 29 Jul 2025 11:50:20 -0500 Subject: [PATCH 4/5] import timezone --- action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.py b/action.py index 119d8d3..f6cfe20 100644 --- a/action.py +++ b/action.py @@ -7,7 +7,7 @@ import jmespath import hashlib import sys -from datetime import datetime +from datetime import datetime, timezone from loguru import logger logger.remove() From 93c3cba081637a7d33363a7850e2a7e7a1a455ea Mon Sep 17 00:00:00 2001 From: Juan Garces Date: Tue, 29 Jul 2025 15:40:42 -0500 Subject: [PATCH 5/5] add commit hash to the note. roll both note functions into one --- action.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/action.py b/action.py index f6cfe20..72dc706 100644 --- a/action.py +++ b/action.py @@ -208,18 +208,12 @@ def compare_scripts(new, old): return False -#function to create a creation note with timestamp -@logger.catch -def create_creation_note(): - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - return f"created via github action on {timestamp}" - - #function to create or update notes with proper timestamping @logger.catch def update_script_notes(existing_notes, action_type="updated"): timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - action_line = f"{action_type} via github action on {timestamp}" + 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 @@ -330,7 +324,7 @@ 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: - creation_note = create_creation_note() + 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: