diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index ffc759a..137af1a 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -269,12 +269,18 @@ jobs: set -e echo "status=${status}" >> "${GITHUB_OUTPUT}" - - name: Finalize Zsh Syntax Result + - name: Run Zsh Regression Tests + id: zsh_tests + if: steps.zsh_syntax.outputs.status == '0' + run: zsh tests/installomator-label-parser.zsh + + - name: Finalize Zsh Validation Result if: success() || failure() shell: bash run: | target_count="${{ steps.collect_targets.outputs.count }}" syntax_status="${{ steps.zsh_syntax.outputs.status }}" + tests_outcome="${{ steps.zsh_tests.outcome }}" if [[ "${target_count}" == "0" ]]; then { @@ -286,19 +292,20 @@ jobs: fi { - echo "### Zsh Syntax" + echo "### Zsh Validation" echo echo "- Files scanned: ${target_count}" + echo "- Regression tests: ${tests_outcome}" } >> "${GITHUB_STEP_SUMMARY}" - if [[ "${syntax_status}" == "0" ]]; then + if [[ "${syntax_status}" == "0" && "${tests_outcome}" == "success" ]]; then echo "" >> "${GITHUB_STEP_SUMMARY}" - echo "✅ All zsh files passed \`zsh -n\`" >> "${GITHUB_STEP_SUMMARY}" + echo "✅ Zsh syntax and regression tests passed" >> "${GITHUB_STEP_SUMMARY}" exit 0 fi echo "" >> "${GITHUB_STEP_SUMMARY}" - echo "❌ zsh syntax errors found" >> "${GITHUB_STEP_SUMMARY}" + echo "❌ Zsh validation failed" >> "${GITHUB_STEP_SUMMARY}" exit 1 shellcheck: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3913b8c..66139fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project are documented in this file. +## 1.2.0 - 19-Aug-2026 +- Fixed validation of Installomator labels declared in multiline alias arms (Bug Report #16) +- Added `selectionDialogDefaultChecked` to configure default selection for interactive-mode items (while keeping already-installed items disabled and unchecked; thanks for FR #14, @jeffmw777!) +- Updated `codex` Validation Path + ## 1.1.0 - 04-Aug-2026 - Normalize surrounding straight and smart quotes in silent-mode CSV item IDs before lookup (thanks for the heads-up, @applegurutim!) - Clarify that Silent Mode Parameter 5 expects configured item identifiers, not Jamf command strings. diff --git a/README.md b/README.md index cb1cf63..d8e614c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![GitHub release (latest by date)](https://img.shields.io/github/v/release/Setup-Your-Mac/SYM-Lite?display_name=tag) ![GitHub issues](https://img.shields.io/github/issues-raw/Setup-Your-Mac/SYM-Lite) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/Setup-Your-Mac/SYM-Lite) ![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/Setup-Your-Mac/SYM-Lite) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Setup-Your-Mac/SYM-Lite) [![swiftDialog](https://img.shields.io/badge/swiftDialog-Enabled-blue)](https://swiftdialog.app) [![Semgrep Security Scan](https://img.shields.io/badge/security%20scanned%20by-Semgrep-00C7B7?style=flat&logo=semgrep&logoColor=white)](https://semgrep.dev) -# SYM-Lite (1.1.0) +# SYM-Lite (1.2.0) > **SYM-Lite** is a lean, purpose-built script for executing MDM-agnostic [Installomator labels](https://github.com/Installomator/Installomator/tree/main/fragments/labels) and [Homebrew](https://brew.sh) casks / formulas, as well as Jamf Pro-specific [policy triggers](https://learn.jamf.com/r/en-US/jamf-pro-documentation-current/Triggers_for_Policies), all through a unified [swiftDialog](https://swiftdialog.app) selection and reporting interface. @@ -71,7 +71,7 @@ installomatorLabels=( ) ``` -At runtime, SYM-Lite validates each configured label against `organizationInstallomatorFile` before building the picker or accepting silent-mode CSV input. If a label is missing from that Installomator file, or if the Installomator file is unavailable or unreadable, SYM-Lite logs a warning or error and removes Installomator labels from the current run while leaving other item types available. +At runtime, SYM-Lite validates each configured label against single-line and continued top-level alias arms in `organizationInstallomatorFile` before building the picker or accepting silent-mode CSV input. If a label is missing from that Installomator file, or if the Installomator file is unavailable, unreadable, or cannot be parsed, SYM-Lite logs a warning or error and removes Installomator labels from the current run while leaving other item types available. ### Adding Homebrew Items @@ -155,12 +155,14 @@ sudo ~/Downloads/SYM-Lite.zsh ``` **User experience:** -1. Selection dialog appears with all configured items +1. Selection dialog appears with all configured items; selectable items start checked when `selectionDialogDefaultChecked="true"` 2. User selects one or more items using checkboxes 3. Inspect Mode dialog launches showing real-time progress 4. Completion report shows one row per selected item 5. Optional restart prompt +`selectionDialogDefaultChecked` affects interactive mode only. Users can deselect prechecked items before continuing, and already-installed items disabled by status sublabels remain unchecked. Silent mode continues to select items exclusively from `operationsCSV`. + If the user clicks `Cancel` in the selection dialog, interactive mode exits cleanly without launching Inspect Mode. If `selectionDialogStatusSublabelsEnabled="true"` and every remaining valid item is already installed, interactive mode shows an informational dialog and exits without launching Inspect Mode. If no valid items remain after configuration validation, interactive mode exits cleanly with a generic unavailable-items message. **Interactive mode requirements:** @@ -330,6 +332,7 @@ swiftDialog's [Inspect Mode](https://swiftdialog.app/advanced/inspect-mode/) use | `organizationOverlayiconURL` | swiftDialog logo | Overlay icon URL | | `mainDialogIcon` | GitHub raw `SYM_icon.png` URL | Main dialog icon | | `fontSize` | `"14"` | Dialog message font size | +| `selectionDialogDefaultChecked` | `"true"` | Start selectable interactive-mode items checked | | `selectionDialogStatusSublabelsEnabled` | `"true"` | Show install-state sublabels, disable already-installed items, and exit cleanly if no selectable items remain | | `restartPromptEnabled` | `"true"` | Show restart prompt after completion | | `scriptLog` | `/var/log/...log` | Client-side log path | @@ -338,6 +341,6 @@ swiftDialog's [Inspect Mode](https://swiftdialog.app/advanced/inspect-mode/) use (The rest of the document — Logging, Troubleshooting, Testing Checklist, Next Steps, and Support — remains unchanged as the reordering was already applied where relevant.) -**Version:** 1.1.0 -**Date:** 04-Aug-2026 +**Version:** 1.2.0 +**Date:** 19-Aug-2026 **Author:** Dan K. Snelson (@dan-snelson) diff --git a/SYM-Lite.zsh b/SYM-Lite.zsh index 887c88e..c2a1ec3 100644 --- a/SYM-Lite.zsh +++ b/SYM-Lite.zsh @@ -16,11 +16,10 @@ # # HISTORY # -# Version 1.1.0, 04-Aug-2026, Dan K. Snelson (@dan-snelson) -# - Normalize surrounding straight and smart quotes in silent-mode CSV item IDs before lookup (thanks for the heads-up, @applegurutim!) -# - Clarify that Silent Mode Parameter 5 expects configured item identifiers, not Jamf command strings. -# - Fix silent-mode Parameter 5 parsing when Jamf passes multiple comma-separated item IDs wrapped in one quoted CSV string (thanks for another heads-up, @applegurutim!) -# - Updates for OpenAI renaming "Codex.app" to "ChatGPT.app" +# Version 1.2.0, 19-Aug-2026, Dan K. Snelson (@dan-snelson) +# - Fixed validation of Installomator labels declared in multiline alias arms (Bug Report #16) +# - Added `selectionDialogDefaultChecked` to configure default selection for interactive-mode items (while keeping already-installed items disabled and unchecked; thanks for FR #14, @jeffmw777!) +# - Updated `codex` Validation Path # #################################################################################################### @@ -37,7 +36,7 @@ setopt NONOMATCH setopt TYPESET_SILENT # Script Version -scriptVersion="1.1.0" +scriptVersion="1.2.0" # Script Human-readable Name humanReadableScriptName="Setup Your Mac Lite: Developer Edition" @@ -105,6 +104,7 @@ mainDialogIcon="https://raw.githubusercontent.com/setup-your-mac/Setup-Your-Mac/ # Dialog presentation defaults fontSize="14" +selectionDialogDefaultChecked="true" selectionDialogStatusSublabelsEnabled="true" # Restart prompt behavior @@ -123,7 +123,7 @@ installomatorLabels=( "awsvpnclient | AWS VPN Client | /Applications/AWS VPN Client/AWS VPN Client.app | https://usw2.ics.services.jamfcloud.com/icon/hash_1d1bef5523d9f7eca5a45f2db9a63732e85edb5f914220807ca740ba7c4881b9" "bruno | Bruno | /Applications/Bruno.app | https://usw2.ics.services.jamfcloud.com/icon/hash_48501630ad2f5dd5de3e055d6acdda07682895440cad366ee7befac71cab1399" "charles | Charles Proxy | /Applications/Charles.app | https://use2.ics.services.jamfcloud.com/icon/hash_59b395ca81889a6d83deda8e6babc5ae4bc5931d36a72b738fe30b84d027593d" - "codex | OpenAI ChatGPT Codex | /Applications/ChatGPT.app | https://usw2.ics.services.jamfcloud.com/icon/hash_be9d2917e81980484f875d9056e5e4aa45d59dffa7b03c20f8dbb5137e96ee26" + "codex | OpenAI ChatGPT Codex | /Applications/ChatGPT.localized/ChatGPT.app | https://usw2.ics.services.jamfcloud.com/icon/hash_be9d2917e81980484f875d9056e5e4aa45d59dffa7b03c20f8dbb5137e96ee26" "docker | Docker | /Applications/Docker.app | https://usw2.ics.services.jamfcloud.com/icon/hash_a344dca5fdc0e86822e8f21ec91088e6591b1e292bdcebdee1281fbd794c2724" "jetbrainsintellijidea | IntelliJ IDEA | /Applications/IntelliJ IDEA.app | https://usw2.ics.services.jamfcloud.com/icon/hash_f669d73acc06297e1fc2f65245cfbdace03263f81aebf95444a8360a101b239d" "pique | Pique | /Applications/Pique.app | https://usw2.ics.services.jamfcloud.com/icon/hash_7d2539860cca6ec5ea5a71cba2aee7d93b9534e4267c16f73c7035f3dc025b9c" @@ -426,6 +426,7 @@ function getSelectionDialogCheckboxesJSON() { local checkboxLabel="" local escapedCheckboxLabel="" local escapedIconURL="" + local checkboxChecked="false" local checkboxDisabled="false" local itemID="" local existingLabel="" @@ -482,10 +483,16 @@ function getSelectionDialogCheckboxesJSON() { selectionDialogOptionRecords+=("${itemID}") fi + if [[ "${selectionDialogDefaultChecked:l}" == "true" ]] && [[ "${checkboxDisabled}" == "false" ]]; then + checkboxChecked="true" + else + checkboxChecked="false" + fi + escapedCheckboxLabel=$(escapeJSONString "${checkboxLabel}") escapedItemID=$(escapeJSONString "${itemID}") escapedIconURL=$(escapeJSONString "${itemIconURL}") - checkboxItemsJSON="${checkboxItemsJSON}${separator}{\"label\":\"${escapedCheckboxLabel}\",\"name\":\"${escapedItemID}\",\"checked\":false,\"disabled\":${checkboxDisabled},\"icon\":\"${escapedIconURL}\"}" + checkboxItemsJSON="${checkboxItemsJSON}${separator}{\"label\":\"${escapedCheckboxLabel}\",\"name\":\"${escapedItemID}\",\"checked\":${checkboxChecked},\"disabled\":${checkboxDisabled},\"icon\":\"${escapedIconURL}\"}" separator="," done @@ -510,6 +517,7 @@ function getAvailableInstallomatorLabels() { BEGIN { inLabelCase = 0 caseDepth = 0 + continuedArm = "" } /^[[:space:]]*case[[:space:]]+\$label[[:space:]]+in[[:space:]]*$/ { @@ -519,6 +527,31 @@ function getAvailableInstallomatorLabels() { } inLabelCase { + if (continuedArm != "") { + if (caseDepth != 1) { + exit 2 + } + + if ($0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\|\\[[:space:]]*$/) { + labelFragment = $0 + sub(/^[[:space:]]*/, "", labelFragment) + sub(/\\[[:space:]]*$/, "", labelFragment) + continuedArm = continuedArm labelFragment + next + } + + if ($0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\)[[:space:]]*$/) { + labelFragment = $0 + sub(/^[[:space:]]*/, "", labelFragment) + sub(/\)[[:space:]]*$/, "", labelFragment) + print continuedArm labelFragment + continuedArm = "" + next + } + + exit 2 + } + if ($0 ~ /^[[:space:]]*case[[:space:]].*[[:space:]]+in[[:space:]]*$/) { caseDepth++ next @@ -532,6 +565,13 @@ function getAvailableInstallomatorLabels() { next } + if (caseDepth == 1 && $0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\|\\[[:space:]]*$/) { + continuedArm = $0 + sub(/^[[:space:]]*/, "", continuedArm) + sub(/\\[[:space:]]*$/, "", continuedArm) + next + } + if (caseDepth == 1 && $0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\)[[:space:]]*$/) { labelArm = $0 sub(/^[[:space:]]*/, "", labelArm) @@ -539,6 +579,12 @@ function getAvailableInstallomatorLabels() { print labelArm } } + + END { + if (continuedArm != "") { + exit 2 + } + } ' "${organizationInstallomatorFile}"); then return 1 fi diff --git a/tests/installomator-label-parser.zsh b/tests/installomator-label-parser.zsh new file mode 100644 index 0000000..246b6fc --- /dev/null +++ b/tests/installomator-label-parser.zsh @@ -0,0 +1,136 @@ +#!/bin/zsh --no-rcs + +setopt ERR_EXIT +setopt NO_UNSET +setopt PIPE_FAIL + +repoRoot="${0:A:h:h}" +scriptPath="${repoRoot}/SYM-Lite.zsh" +temporaryDirectory="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/sym-lite-parser-tests.XXXXXX")" +validFixture="${temporaryDirectory}/Installomator-valid.zsh" +incompleteFixture="${temporaryDirectory}/Installomator-incomplete.zsh" + +function cleanup() { + /bin/rm -rf -- "${temporaryDirectory}" +} +trap cleanup EXIT + +function fail() { + print -u2 -r -- "FAIL: $1" + exit 1 +} + +function extractFunction() { + local functionName="$1" + + /usr/bin/awk -v functionName="${functionName}" ' + $0 == "function " functionName "() {" { + capture = 1 + } + + capture { + print + } + + capture && /^}$/ { + exit + } + ' "${scriptPath}" +} + +function loadProductionFunction() { + local functionName="$1" + local functionSource="" + + functionSource="$(extractFunction "${functionName}")" + [[ -n "${functionSource}" ]] || fail "Unable to extract ${functionName} from SYM-Lite.zsh" + eval "${functionSource}" +} + +function assertOutputContains() { + local output="$1" + local expected="$2" + + print -r -- "${output}" | /usr/bin/grep -Fqx -- "${expected}" \ + || fail "Expected parsed label '${expected}'" +} + +function assertOutputExcludes() { + local output="$1" + local unexpected="$2" + + if print -r -- "${output}" | /usr/bin/grep -Fqx -- "${unexpected}"; then + fail "Unexpected parsed label '${unexpected}'" + fi +} + +loadProductionFunction "parseInstallomatorItem" +loadProductionFunction "getAvailableInstallomatorLabels" +loadProductionFunction "normalizeInstallomatorLabels" + +function preFlight() { :; } +function warning() { :; } +function errorOut() { :; } + +/bin/cat > "${validFixture}" <<'FIXTURE' +case $label in + singlelabel|singlealias) + ;; + multilinefirst|\ + multilinesecond|\ + multilinethird) + ;; + parentlabel) + case "${nestedValue}" in + nestedalias) + ;; + esac + ;; + longversion) + ;; + valuesfromarguments) + ;; + *) + ;; +esac +FIXTURE +/bin/chmod +x "${validFixture}" + +organizationInstallomatorFile="${validFixture}" +parsedLabels="$(getAvailableInstallomatorLabels)" \ + || fail "Valid fixture returned a parse failure" + +assertOutputContains "${parsedLabels}" "singlelabel" +assertOutputContains "${parsedLabels}" "singlealias" +assertOutputContains "${parsedLabels}" "multilinefirst" +assertOutputContains "${parsedLabels}" "multilinesecond" +assertOutputContains "${parsedLabels}" "multilinethird" +assertOutputContains "${parsedLabels}" "parentlabel" +assertOutputExcludes "${parsedLabels}" "nestedalias" +assertOutputExcludes "${parsedLabels}" "longversion" +assertOutputExcludes "${parsedLabels}" "valuesfromarguments" +assertOutputExcludes "${parsedLabels}" "*" + +configuredInstallomatorLabels=( + "multilinefirst | Multiline Label | /Applications/Multiline.app | https://example.invalid/icon.png" +) +installomatorLabels=("${configuredInstallomatorLabels[@]}") +normalizeInstallomatorLabels + +[[ ${#installomatorLabels[@]} -eq 1 ]] \ + || fail "Normalization removed a configured multiline alias" +[[ "${installomatorLabels[1]}" == "${configuredInstallomatorLabels[1]}" ]] \ + || fail "Normalization changed the configured multiline alias" + +/bin/cat > "${incompleteFixture}" <<'FIXTURE' +case $label in + incompletealias|\ +FIXTURE +/bin/chmod +x "${incompleteFixture}" + +organizationInstallomatorFile="${incompleteFixture}" +if getAvailableInstallomatorLabels >/dev/null 2>&1; then + fail "Incomplete continuation arm did not return a parse failure" +fi + +print -r -- "PASS: Installomator label parser regression tests" \ No newline at end of file