diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27fb0ad..45e5843 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,36 +5,43 @@ on: branches: [ main ] pull_request: branches: [ main ] + # The release workflow calls this, so a tag is only ever published off a green run. + workflow_call: jobs: test: strategy: fail-fast: false matrix: - os: [ windows-latest, ubuntu-latest ] + include: + - { os: windows-latest, shell: pwsh } # PowerShell 7 + - { os: windows-latest, shell: powershell } # Windows PowerShell 5.1 - the manifest's floor + - { os: ubuntu-latest, shell: pwsh } runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.shell }} steps: - uses: actions/checkout@v4 + # -SkipPublisherCheck: Windows ships Pester 3.4 signed with a different + # certificate, and without it the 5.x install refuses to replace it. - name: Install PowerShell modules - shell: pwsh run: | Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser + Install-Module Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser -SkipPublisherCheck Install-Module PSScriptAnalyzer -Force -Scope CurrentUser - name: PSScriptAnalyzer - shell: pwsh run: ./build.ps1 -Task Analyze - name: Pester (fixtures only - no tenant needed) - shell: pwsh run: ./build.ps1 -Task Test - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: - name: test-results-${{ matrix.os }} + name: test-results-${{ matrix.os }}-${{ matrix.shell }} path: testResults.xml if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..54d61a8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +# Pushing a v* tag publishes that version to the PowerShell Gallery - but only after +# the full CI matrix passes again on the tagged commit, and only if the tag matches the +# manifest. A Gallery version can be unlisted but its number can never be reused, so +# everything that can be checked is checked before the one step that cannot be undone. + +on: + push: + tags: [ 'v*' ] + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + +jobs: + ci: + uses: ./.github/workflows/ci.yml + + publish: + needs: ci + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The tag reaches the script through env, never interpolated into it. + - name: Check the tag matches ModuleVersion + shell: pwsh + env: + TAG: ${{ github.ref_name }} + run: | + $manifest = Import-PowerShellDataFile ./src/IntuneGraphKit/IntuneGraphKit.psd1 + if ($env:TAG -ne "v$($manifest.ModuleVersion)") { + throw "Tag '$env:TAG' does not match ModuleVersion $($manifest.ModuleVersion). Nothing was published." + } + + - name: Publish to the PowerShell Gallery + shell: pwsh + env: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + run: ./build.ps1 -Task Publish diff --git a/.gitignore b/.gitignore index a4789e3..eca4b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,11 +13,11 @@ site/.astro/ out/ IntuneGraph-*/ *.html -!src/IntuneGraph/Assets/template.html +!src/IntuneGraphKit/Assets/template.html !site/public/**/*.html graph.json !tests/Fixtures/**/*.json -!src/IntuneGraph/DemoData/**/*.json +!src/IntuneGraphKit/DemoData/**/*.json # Test output TestResults/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f01490..e7529db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to IntuneGraph are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); versions follow [SemVer](https://semver.org/). +## [Unreleased] + +### Changed +- **The module is now `IntuneGraphKit`**: `Install-Module IntuneGraphKit`, + `Import-Module IntuneGraphKit`. The name `IntuneGraph` on the PowerShell Gallery + belongs to an unrelated module by another author, and a Gallery package name has to + match the module name. Cmdlet names, the `graph.json` format, the MCP server name and + the project name are unchanged. If you import from a clone, the path is now + `src/IntuneGraphKit/IntuneGraphKit.psd1`. +- The README no longer tells visitors to run `Install-Module IntuneGraph`, which + installed that unrelated module instead of this one. +- The manifest `Description` is now the same text as the repository description, and + the module has a freshly generated `GUID` ahead of its first Gallery publish. + +### Added +- Release pipeline: pushing a `v*` tag re-runs the full CI matrix and, when it is green + and the tag matches `ModuleVersion`, publishes to the PowerShell Gallery + (`.github/workflows/release.yml`, `build.ps1 -Task Publish`). +- Gallery listing metadata: `CompatiblePSEditions` plus the `PSEdition_Desktop` and + `PSEdition_Core` tags, so the module appears under both edition filters, and an icon. + ## [0.2.0] - 2026-09-06 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20b4849..2505ae2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ live tenant. Nothing in the test suite loads it. ## Repository layout ``` -src/IntuneGraph/ +src/IntuneGraphKit/ ├── Public/ one file per exported cmdlet — this is the supported surface ├── Private/ internals: Fetch, Normalize, GraphModel, Query, Emit, Mcp, Util ├── DemoData/ the Contoso tenant shipped to users via -DemoData @@ -50,15 +50,17 @@ them aware of where the data came from, it belongs somewhere else. .\build.ps1 -Task Test # Pester only .\build.ps1 -Task Analyze # PSScriptAnalyzer only .\build.ps1 -Task Fixtures # regenerate the Contoso dataset +.\build.ps1 -Task Publish # release workflow only — see Releasing ``` -CI runs `Analyze` then `Test` on `windows-latest` and `ubuntu-latest` for every -push and pull request, so run both locally before you open one. +CI runs `Analyze` then `Test` for every push and pull request on Windows +PowerShell 5.1 and PowerShell 7 on `windows-latest`, and PowerShell 7 on +`ubuntu-latest`, so run both locally before you open one. ### PSScriptAnalyzer Settings live in [`PSScriptAnalyzerSettings.psd1`](PSScriptAnalyzerSettings.psd1) -and apply to `src/IntuneGraph` recursively. Severity is `Error` and `Warning`; +and apply to `src/IntuneGraphKit` recursively. Severity is `Error` and `Warning`; **errors fail the build, warnings do not.** Three rules are excluded, each with its reason written next to it — `PSAvoidUsingWriteHost` because the report cmdlets write formatted output to the host on purpose, @@ -78,7 +80,7 @@ demo data cannot drift apart. To add a scenario: 1. Edit the identity tables and workload definitions at the top of [`tests/New-ContosoFixtures.ps1`](tests/New-ContosoFixtures.ps1). 2. Run `.\build.ps1 -Task Fixtures`. It rewrites both `tests/Fixtures/contoso` - and `src/IntuneGraph/DemoData`. + and `src/IntuneGraphKit/DemoData`. 3. Update the expected counts in `tests/IntuneGraph.Tests.ps1` — they are asserted, and a scenario nobody counted is a scenario nobody tested. 4. Commit the regenerated JSON along with your change. @@ -125,6 +127,27 @@ A pull request wants: versions follow [SemVer](https://semver.org/), - documentation updated in the same pull request when behaviour changes. +## Releasing + +Releases go out from CI, never from a workstation. The module is published to the +PowerShell Gallery as **IntuneGraphKit** — `IntuneGraph` on the Gallery is an +unrelated module by another author, which is why the two names differ (see +[docs/discoverability.md](docs/discoverability.md#the-gallery-name-intunegraphkit)). + +1. Move the *Unreleased* entries in `CHANGELOG.md` under the new version and date. +2. Set `ModuleVersion` in `src/IntuneGraphKit/IntuneGraphKit.psd1` to that version. +3. Merge to `main`, then tag the merge commit and push the tag — + `git tag v0.3.0`, then `git push origin v0.3.0`. + +The Release workflow re-runs the whole CI matrix on the tagged commit, checks that the +tag matches `ModuleVersion`, and only then publishes. A Gallery version can be unlisted +but its number can never be reused, so that check is the last point at which a mistake +costs nothing. + +The API key exists only as the `PSGALLERY_API_KEY` repository secret. +`build.ps1 -Task Publish` reads it from the environment and refuses to run without +it; there is deliberately no parameter for it. + ## Where to start Issues labelled diff --git a/README.md b/README.md index 61ecff0..6e8eb90 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ **Turn your Microsoft Intune tenant into an interactive relationship graph.** See what actually applies to a device and *why*, preview the blast radius before you touch a group, and find the orphaned junk, all from a local, read-only snapshot. +[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/IntuneGraphKit?label=PSGallery)](https://www.powershellgallery.com/packages/IntuneGraphKit) [![PowerShell](https://img.shields.io/badge/PowerShell-5.1%20%7C%207%2B-5391FE)](#requirements) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Read-only](https://img.shields.io/badge/Graph%20scopes-read--only-brightgreen)](docs/permissions.md) @@ -19,11 +20,13 @@ https://juandresrodca.github.io/IntuneGraph/demo/ ```powershell -Install-Module IntuneGraph -Scope CurrentUser # (coming to PSGallery) -Import-Module IntuneGraph +Install-Module IntuneGraphKit -Scope CurrentUser +Import-Module IntuneGraphKit Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph -Open ``` +> The Gallery package is **IntuneGraphKit**. The `IntuneGraph` package on the Gallery is an unrelated module by another author, and unlike this one it writes to Intune — check the name before you install. + That builds the bundled **Contoso** demo tenant and opens the interactive graph in your browser — zero Graph auth, zero setup. The whole tool works offline against demo data, which means you can evaluate it (and contribute to it) without ever touching a real tenant. ![IntuneGraph interactive demo — the Contoso tenant as a relationship graph](docs/img/demo.gif) diff --git a/SECURITY.md b/SECURITY.md index 78daec3..a3e0b16 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,7 +14,7 @@ If private reporting is unavailable to you, email **juandresrodca@gmail.com** wi Please include: -- the module version (`(Get-Module IntuneGraph).Version`) and PowerShell version +- the module version (`(Get-Module IntuneGraphKit).Version`) and PowerShell version (`$PSVersionTable.PSVersion`), - what you did, what happened, and what you expected, - a minimal reproduction — **against `-DemoData` wherever possible**, never against a @@ -93,7 +93,7 @@ export the way you would treat any tenant extract: To reproduce almost anything, `-DemoData` is enough: ```powershell -Import-Module IntuneGraph +Import-Module IntuneGraphKit Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph ``` diff --git a/build.ps1 b/build.ps1 index de70c79..c6fb466 100644 --- a/build.ps1 +++ b/build.ps1 @@ -5,16 +5,17 @@ .\build.ps1 -Task Test .\build.ps1 -Task Analyze .\build.ps1 -Task Fixtures + .\build.ps1 -Task Publish # release workflow only; reads $env:PSGALLERY_API_KEY #> [CmdletBinding()] param( - [ValidateSet('Test', 'Analyze', 'Fixtures', 'All')] + [ValidateSet('Test', 'Analyze', 'Fixtures', 'Publish', 'All')] [string]$Task = 'All' ) $ErrorActionPreference = 'Stop' $root = $PSScriptRoot -$moduleDir = Join-Path $root 'src\IntuneGraph' +$moduleDir = Join-Path $root 'src\IntuneGraphKit' function Invoke-Fixtures { Write-Host '== Regenerating Contoso fixtures ==' -ForegroundColor Cyan @@ -48,9 +49,26 @@ function Invoke-Test { Invoke-Pester -Configuration $conf } +function Invoke-Publish { + Write-Host '== Publish to the PowerShell Gallery ==' -ForegroundColor Cyan + # The key is read from the environment only - there is deliberately no parameter + # for it - so it never lands in a script, a default value or shell history. The + # release workflow sets it from the PSGALLERY_API_KEY repository secret. + if ([string]::IsNullOrWhiteSpace($env:PSGALLERY_API_KEY)) { + throw 'PSGALLERY_API_KEY is not set. Releases publish from .github/workflows/release.yml - see CONTRIBUTING.md.' + } + if (-not (Get-Command Publish-PSResource -ErrorAction SilentlyContinue)) { + throw 'Publish-PSResource not found. It ships with PowerShell 7.4+ (Microsoft.PowerShell.PSResourceGet).' + } + $manifest = Test-ModuleManifest -Path (Join-Path $moduleDir 'IntuneGraphKit.psd1') + Write-Host "Publishing $($manifest.Name) $($manifest.Version)" + Publish-PSResource -Path $moduleDir -Repository PSGallery -ApiKey $env:PSGALLERY_API_KEY +} + switch ($Task) { 'Fixtures' { Invoke-Fixtures } 'Analyze' { Invoke-Analyze } 'Test' { Invoke-Test } + 'Publish' { Invoke-Publish } 'All' { Invoke-Analyze; Invoke-Test } } diff --git a/docs/discoverability.md b/docs/discoverability.md index dec4a78..846a341 100644 --- a/docs/discoverability.md +++ b/docs/discoverability.md @@ -14,7 +14,7 @@ maintenance task — should change this file in the same commit, with the reason |---|---|---| | **GitHub search and topic pages** | Someone types `intune assignments` into GitHub, or lands on `github.com/topics/intune`. The topics decide whether this repo is in that result set at all. | Repository settings → Topics | | **The live demo** | `https://juandresrodca.github.io/IntuneGraph/demo/`, published by `.github/workflows/pages.yml`. Set as the repository homepage so it appears as a link in search results and in the sidebar. | Repository settings → Website | -| **PowerShell Gallery** | The intended install route, `Install-Module IntuneGraph`. Gallery search reads the `Tags` in `src/IntuneGraph/IntuneGraph.psd1`, which are a separate list from GitHub topics and should stay in step with them. | `IntuneGraph.psd1` | +| **PowerShell Gallery** | The install route, `Install-Module IntuneGraphKit` — see [*The Gallery name*](#the-gallery-name-intunegraphkit) for why it is not `IntuneGraph`. Gallery search reads the `Tags` in `src/IntuneGraphKit/IntuneGraphKit.psd1`, which are a separate list from GitHub topics and should stay in step with them. | `IntuneGraphKit.psd1` | The README's first screen is the fourth surface, and the only one that converts a visitor into a user. It leads with the demo command that needs no tenant, because the @@ -22,6 +22,24 @@ cost of trying this tool is the thing most likely to stop someone. --- +## The Gallery name: IntuneGraphKit + +The project is IntuneGraph; the module on the Gallery is **IntuneGraphKit**. That was +not a choice between two names. `IntuneGraph` on the PowerShell Gallery was already +taken — by an unrelated module from another author that uploads, assigns and removes +Intune apps — and a Gallery package name has to equal the module name. + +The difference matters in one direction. Anyone who types the project name into +`Install-Module` gets the other module: one with write cmdlets, installed on the +strength of a README whose whole pitch is *read-only*. So the README's install block +carries a one-line note naming the other package, and that note stays for as long as +the other package exists. + +Everything a user does not install kept the project name: the repository, the site, +the cmdlet names, the MCP server name (`intunegraph`) and the report title. + +--- + ## Topics GitHub allows twenty. The rule for adding one: **a real practitioner would type it into @@ -122,6 +140,10 @@ visitor who has already clicked: the three questions the tool answers, then the objections an admin raises before installing anything that touches their tenant (*does it write?* and *do I need a tenant to try it?*), answered before they are asked. +The manifest `Description` in `src/IntuneGraphKit/IntuneGraphKit.psd1` carries the same +text, so the Gallery listing and the repository make the same pitch. Change both in the +same commit. + --- ## Sibling repositories diff --git a/docs/fixtures.md b/docs/fixtures.md index 935601b..4b337de 100644 --- a/docs/fixtures.md +++ b/docs/fixtures.md @@ -31,7 +31,7 @@ A missing fixture is a hard error (silent empties would hide bugs). ## The Contoso dataset -`tests/Fixtures/contoso` (mirrored to `src/IntuneGraph/DemoData`) is designed so +`tests/Fixtures/contoso` (mirrored to `src/IntuneGraphKit/DemoData`) is designed so **each hygiene check fires exactly once** and every query scenario is covered: - **Nesting:** `SG-AllStaff` ⊃ `SG-Finance`, `SG-IT` → tests the `Via` path. diff --git a/docs/img/icon.png b/docs/img/icon.png new file mode 100644 index 0000000..c0fcfd2 Binary files /dev/null and b/docs/img/icon.png differ diff --git a/docs/mcp.md b/docs/mcp.md index eafa126..4213c69 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -100,15 +100,16 @@ claude mcp add intunegraph -- powershell.exe -NoLogo -NoProfile -NonInteractive } ``` -### If the module is installed from PSGallery +### If the module is installed from the PowerShell Gallery -You can skip the launcher script and start the server from the installed module: +You can skip the launcher script and start the server from the installed module +(the Gallery package is `IntuneGraphKit`): ```json { "command": "pwsh", "args": ["-NoLogo", "-NoProfile", "-NonInteractive", "-InputFormat", "Text", - "-Command", "Import-Module IntuneGraph; Start-IntuneGraphMcp -Path 'D:/snapshots/contoso/graph.json'"] + "-Command", "Import-Module IntuneGraphKit; Start-IntuneGraphMcp -Path 'D:/snapshots/contoso/graph.json'"] } ``` diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index c294923..bb8ac74 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -208,8 +208,8 @@ const mcpChat = `> why does Win11 Security Baseline apply to DEV-FIN-01?
1 · Zero-setup demo
-
Install-Module IntuneGraph -Scope CurrentUser
-Import-Module IntuneGraph
+          
Install-Module IntuneGraphKit -Scope CurrentUser
+Import-Module IntuneGraphKit
 Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph -Open
diff --git a/src/IntuneGraph/Assets/template.html b/src/IntuneGraphKit/Assets/template.html similarity index 100% rename from src/IntuneGraph/Assets/template.html rename to src/IntuneGraphKit/Assets/template.html diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/assignmentFilters.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/assignmentFilters.json similarity index 100% rename from src/IntuneGraph/DemoData/beta/deviceManagement/assignmentFilters.json rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/assignmentFilters.json diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/configurationPolicies.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/configurationPolicies.json similarity index 100% rename from src/IntuneGraph/DemoData/beta/deviceManagement/configurationPolicies.json rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/configurationPolicies.json diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/deviceHealthScripts.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceHealthScripts.json similarity index 100% rename from src/IntuneGraph/DemoData/beta/deviceManagement/deviceHealthScripts.json rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceHealthScripts.json diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/deviceManagementScripts.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceManagementScripts.json similarity index 100% rename from src/IntuneGraph/DemoData/beta/deviceManagement/deviceManagementScripts.json rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceManagementScripts.json diff --git a/src/IntuneGraph/DemoData/manifest.json b/src/IntuneGraphKit/DemoData/manifest.json similarity index 100% rename from src/IntuneGraph/DemoData/manifest.json rename to src/IntuneGraphKit/DemoData/manifest.json diff --git a/src/IntuneGraph/DemoData/v1.0/deviceAppManagement/mobileApps.json b/src/IntuneGraphKit/DemoData/v1.0/deviceAppManagement/mobileApps.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/deviceAppManagement/mobileApps.json rename to src/IntuneGraphKit/DemoData/v1.0/deviceAppManagement/mobileApps.json diff --git a/src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json b/src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json rename to src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json diff --git a/src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceConfigurations.json b/src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceConfigurations.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceConfigurations.json rename to src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceConfigurations.json diff --git a/src/IntuneGraph/DemoData/v1.0/deviceManagement/managedDevices.json b/src/IntuneGraphKit/DemoData/v1.0/deviceManagement/managedDevices.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/deviceManagement/managedDevices.json rename to src/IntuneGraphKit/DemoData/v1.0/deviceManagement/managedDevices.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups.json b/src/IntuneGraphKit/DemoData/v1.0/groups.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups.json rename to src/IntuneGraphKit/DemoData/v1.0/groups.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json similarity index 100% rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json diff --git a/src/IntuneGraph/IntuneGraph.psd1 b/src/IntuneGraphKit/IntuneGraphKit.psd1 similarity index 67% rename from src/IntuneGraph/IntuneGraph.psd1 rename to src/IntuneGraphKit/IntuneGraphKit.psd1 index a428a12..eb3f295 100644 --- a/src/IntuneGraph/IntuneGraph.psd1 +++ b/src/IntuneGraphKit/IntuneGraphKit.psd1 @@ -1,16 +1,20 @@ @{ - RootModule = 'IntuneGraph.psm1' + RootModule = 'IntuneGraphKit.psm1' ModuleVersion = '0.2.0' - GUID = 'b7e4b0a2-4c9e-4a1f-9c3d-1a2b3c4d5e6f' + GUID = '2fd9d683-0ede-4a50-a4b5-60a39b1a93be' Author = 'Juan Andres Rodriguez' CompanyName = 'IntuneGraph' Copyright = '(c) 2026 Juan Andres Rodriguez. MIT License.' - Description = 'Turn your Microsoft Intune tenant into an interactive, queryable relationship graph. Target resolution, blast-radius impact preview, and assignment hygiene checks. Read-only, offline-capable, zero write scopes.' + # Same text as the GitHub repository description - change both together. + Description = 'Turn your Microsoft Intune tenant into an interactive relationship graph. See what applies to a device or user and why, preview the blast radius before you touch a group, and find orphaned or broken assignments. Read-only Graph, offline-capable demo, self-contained HTML viewer. Zero write scopes.' # 5.1 floor broadens adoption (many Intune admins are still on Windows PowerShell). # PowerShell 7+ is recommended but not required. PowerShellVersion = '5.1' + # Drives the Gallery's edition filter together with the PSEdition_* tags below. + CompatiblePSEditions = @('Desktop', 'Core') + # Microsoft.Graph.Authentication is loaded lazily by Connect-IntuneGraph so that # demo/fixture mode works with zero dependencies installed. It is intentionally # NOT a hard RequiredModules entry. @@ -35,9 +39,11 @@ PrivateData = @{ PSData = @{ Tags = @('Intune', 'MicrosoftGraph', 'MEM', 'Endpoint', 'MDM', 'Graph', 'Visualization', - 'MCP', 'ModelContextProtocol', 'AI', 'Copilot', 'Windows', 'macOS', 'Linux') + 'MCP', 'ModelContextProtocol', 'AI', 'Copilot', 'Windows', 'macOS', 'Linux', + 'PSEdition_Desktop', 'PSEdition_Core') LicenseUri = 'https://github.com/juandresrodca/IntuneGraph/blob/main/LICENSE' ProjectUri = 'https://github.com/juandresrodca/IntuneGraph' + IconUri = 'https://raw.githubusercontent.com/juandresrodca/IntuneGraph/main/docs/img/icon.png' ReleaseNotes = 'https://github.com/juandresrodca/IntuneGraph/blob/main/CHANGELOG.md' } } diff --git a/src/IntuneGraph/IntuneGraph.psm1 b/src/IntuneGraphKit/IntuneGraphKit.psm1 similarity index 100% rename from src/IntuneGraph/IntuneGraph.psm1 rename to src/IntuneGraphKit/IntuneGraphKit.psm1 diff --git a/src/IntuneGraph/Private/DataSource.ps1 b/src/IntuneGraphKit/Private/DataSource.ps1 similarity index 100% rename from src/IntuneGraph/Private/DataSource.ps1 rename to src/IntuneGraphKit/Private/DataSource.ps1 diff --git a/src/IntuneGraph/Private/Emit.ps1 b/src/IntuneGraphKit/Private/Emit.ps1 similarity index 100% rename from src/IntuneGraph/Private/Emit.ps1 rename to src/IntuneGraphKit/Private/Emit.ps1 diff --git a/src/IntuneGraph/Private/Fetch.ps1 b/src/IntuneGraphKit/Private/Fetch.ps1 similarity index 100% rename from src/IntuneGraph/Private/Fetch.ps1 rename to src/IntuneGraphKit/Private/Fetch.ps1 diff --git a/src/IntuneGraph/Private/GraphModel.ps1 b/src/IntuneGraphKit/Private/GraphModel.ps1 similarity index 100% rename from src/IntuneGraph/Private/GraphModel.ps1 rename to src/IntuneGraphKit/Private/GraphModel.ps1 diff --git a/src/IntuneGraph/Private/Mcp.ps1 b/src/IntuneGraphKit/Private/Mcp.ps1 similarity index 100% rename from src/IntuneGraph/Private/Mcp.ps1 rename to src/IntuneGraphKit/Private/Mcp.ps1 diff --git a/src/IntuneGraph/Private/Normalize.ps1 b/src/IntuneGraphKit/Private/Normalize.ps1 similarity index 100% rename from src/IntuneGraph/Private/Normalize.ps1 rename to src/IntuneGraphKit/Private/Normalize.ps1 diff --git a/src/IntuneGraph/Private/Query.ps1 b/src/IntuneGraphKit/Private/Query.ps1 similarity index 100% rename from src/IntuneGraph/Private/Query.ps1 rename to src/IntuneGraphKit/Private/Query.ps1 diff --git a/src/IntuneGraph/Private/Util.ps1 b/src/IntuneGraphKit/Private/Util.ps1 similarity index 100% rename from src/IntuneGraph/Private/Util.ps1 rename to src/IntuneGraphKit/Private/Util.ps1 diff --git a/src/IntuneGraph/Public/Connect-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Connect-IntuneGraph.ps1 similarity index 100% rename from src/IntuneGraph/Public/Connect-IntuneGraph.ps1 rename to src/IntuneGraphKit/Public/Connect-IntuneGraph.ps1 diff --git a/src/IntuneGraph/Public/Disconnect-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Disconnect-IntuneGraph.ps1 similarity index 100% rename from src/IntuneGraph/Public/Disconnect-IntuneGraph.ps1 rename to src/IntuneGraphKit/Public/Disconnect-IntuneGraph.ps1 diff --git a/src/IntuneGraph/Public/Export-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Export-IntuneGraph.ps1 similarity index 100% rename from src/IntuneGraph/Public/Export-IntuneGraph.ps1 rename to src/IntuneGraphKit/Public/Export-IntuneGraph.ps1 diff --git a/src/IntuneGraph/Public/Find-IntuneOrphan.ps1 b/src/IntuneGraphKit/Public/Find-IntuneOrphan.ps1 similarity index 100% rename from src/IntuneGraph/Public/Find-IntuneOrphan.ps1 rename to src/IntuneGraphKit/Public/Find-IntuneOrphan.ps1 diff --git a/src/IntuneGraph/Public/Get-IntuneBlastRadius.ps1 b/src/IntuneGraphKit/Public/Get-IntuneBlastRadius.ps1 similarity index 100% rename from src/IntuneGraph/Public/Get-IntuneBlastRadius.ps1 rename to src/IntuneGraphKit/Public/Get-IntuneBlastRadius.ps1 diff --git a/src/IntuneGraph/Public/Get-IntuneGraphNode.ps1 b/src/IntuneGraphKit/Public/Get-IntuneGraphNode.ps1 similarity index 100% rename from src/IntuneGraph/Public/Get-IntuneGraphNode.ps1 rename to src/IntuneGraphKit/Public/Get-IntuneGraphNode.ps1 diff --git a/src/IntuneGraph/Public/Get-IntuneTarget.ps1 b/src/IntuneGraphKit/Public/Get-IntuneTarget.ps1 similarity index 100% rename from src/IntuneGraph/Public/Get-IntuneTarget.ps1 rename to src/IntuneGraphKit/Public/Get-IntuneTarget.ps1 diff --git a/src/IntuneGraph/Public/Import-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Import-IntuneGraph.ps1 similarity index 100% rename from src/IntuneGraph/Public/Import-IntuneGraph.ps1 rename to src/IntuneGraphKit/Public/Import-IntuneGraph.ps1 diff --git a/src/IntuneGraph/Public/Show-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Show-IntuneGraph.ps1 similarity index 100% rename from src/IntuneGraph/Public/Show-IntuneGraph.ps1 rename to src/IntuneGraphKit/Public/Show-IntuneGraph.ps1 diff --git a/src/IntuneGraph/Public/Start-IntuneGraphMcp.ps1 b/src/IntuneGraphKit/Public/Start-IntuneGraphMcp.ps1 similarity index 100% rename from src/IntuneGraph/Public/Start-IntuneGraphMcp.ps1 rename to src/IntuneGraphKit/Public/Start-IntuneGraphMcp.ps1 diff --git a/tests/IntuneGraph.Tests.ps1 b/tests/IntuneGraph.Tests.ps1 index 614e4d2..79ab66b 100644 --- a/tests/IntuneGraph.Tests.ps1 +++ b/tests/IntuneGraph.Tests.ps1 @@ -3,7 +3,7 @@ #requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } BeforeAll { - $script:ModulePath = Join-Path $PSScriptRoot '..\src\IntuneGraph\IntuneGraph.psd1' + $script:ModulePath = Join-Path $PSScriptRoot '..\src\IntuneGraphKit\IntuneGraphKit.psd1' Import-Module $script:ModulePath -Force $script:FixtureRoot = Join-Path $PSScriptRoot 'Fixtures\contoso' $script:GraphPath = Join-Path $TestDrive 'graph.json' @@ -16,7 +16,7 @@ BeforeAll { $script:McpSend = { param($Graph, [string[]]$Lines) $payload = ($Lines -join "`n") - $raw = & (Get-Module IntuneGraph) { + $raw = & (Get-Module IntuneGraphKit) { param($g, $text) $sw = New-Object System.IO.StringWriter Invoke-IgMcpLoop -Reader (New-Object System.IO.StringReader($text)) -Writer $sw -State (New-IgMcpState -Graph $g -Path $null) @@ -44,9 +44,21 @@ Describe 'Module' { $expected = 'Connect-IntuneGraph', 'Disconnect-IntuneGraph', 'Export-IntuneGraph', 'Import-IntuneGraph', 'Get-IntuneGraphNode', 'Get-IntuneTarget', 'Get-IntuneBlastRadius', 'Find-IntuneOrphan', 'Show-IntuneGraph', 'Start-IntuneGraphMcp' - $actual = (Get-Command -Module IntuneGraph -CommandType Function).Name + $actual = (Get-Command -Module IntuneGraphKit -CommandType Function).Name ($expected | Sort-Object) | Should -Be ($actual | Sort-Object) } + It 'carries the metadata the Gallery listing needs' { + # A published version's listing can never be corrected in place; only a new + # version fixes it. Catch a missing field here instead. + $m = Import-PowerShellDataFile $script:ModulePath + $m.CompatiblePSEditions | Should -Contain 'Desktop' + $m.CompatiblePSEditions | Should -Contain 'Core' + $m.PrivateData.PSData.Tags | Should -Contain 'PSEdition_Desktop' + $m.PrivateData.PSData.Tags | Should -Contain 'PSEdition_Core' + foreach ($uri in 'ProjectUri', 'LicenseUri', 'IconUri', 'ReleaseNotes') { + $m.PrivateData.PSData[$uri] | Should -Match '^https://' -Because "$uri is shown on the listing" + } + } } Describe 'Export / build' { @@ -304,7 +316,7 @@ Describe 'MCP transport (feature 4)' { $out.Raw | Should -Match '\\n' # the rendered table's newlines, escaped } It 'serves every tool with the session disconnected, so no live tenant call is reachable' { - (InModuleScope IntuneGraph { $script:IgSession.Mode }) | Should -Be 'None' + (InModuleScope IntuneGraphKit { $script:IgSession.Mode }) | Should -Be 'None' foreach ($tool in 'intune_summary', 'intune_orphans', 'intune_node') { $out = & $script:McpSend $script:G @(& $script:McpToolRequest $tool @{}) $out.Responses[0].result.isError | Should -BeFalse diff --git a/tests/New-ContosoFixtures.ps1 b/tests/New-ContosoFixtures.ps1 index ef61625..51c73e3 100644 --- a/tests/New-ContosoFixtures.ps1 +++ b/tests/New-ContosoFixtures.ps1 @@ -11,7 +11,7 @@ param( [string[]]$TargetRoot = @( (Join-Path $PSScriptRoot 'Fixtures\contoso'), - (Join-Path $PSScriptRoot '..\src\IntuneGraph\DemoData') + (Join-Path $PSScriptRoot '..\src\IntuneGraphKit\DemoData') ) ) diff --git a/tools/icon/New-IntuneGraphIcon.ps1 b/tools/icon/New-IntuneGraphIcon.ps1 new file mode 100644 index 0000000..0bebd25 --- /dev/null +++ b/tools/icon/New-IntuneGraphIcon.ps1 @@ -0,0 +1,75 @@ +<# + Draws the module icon the PowerShell Gallery listing shows (the manifest's IconUri). + Run: .\tools\icon\New-IntuneGraphIcon.ps1 (writes docs\img\icon.png) + + Windows only: it uses System.Drawing, which ships with Windows PowerShell, so the + icon needs no package install. Colours are the node palette from the HTML viewer, + and the two bright edges are the "via" path - device -> group -> policy - which is + the one thing this tool shows that a flat list does not. +#> +[CmdletBinding()] +param( + [string]$OutputPath, + [int]$Size = 256 +) +$ErrorActionPreference = 'Stop' +# Resolved here rather than as a parameter default: Windows PowerShell leaves +# $PSScriptRoot empty in parameter defaults when the script is started with -File. +if (-not $OutputPath) { $OutputPath = Join-Path $PSScriptRoot '..\..\docs\img\icon.png' } +Add-Type -AssemblyName System.Drawing + +function New-IgColor([string]$Hex) { [System.Drawing.ColorTranslator]::FromHtml($Hex) } + +$s = $Size / 256.0 # drawn on a 256 grid, scaled to $Size +$bmp = New-Object System.Drawing.Bitmap $Size, $Size +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias +$g.Clear([System.Drawing.Color]::Transparent) + +# Rounded-square tile, inset so the border stroke is not clipped. +$inset = 3 * $s; $d = 2 * 52 * $s; $far = $Size - $inset - $d +$tile = New-Object System.Drawing.Drawing2D.GraphicsPath +$tile.AddArc($inset, $inset, $d, $d, 180, 90) +$tile.AddArc($far, $inset, $d, $d, 270, 90) +$tile.AddArc($far, $far, $d, $d, 0, 90) +$tile.AddArc($inset, $far, $d, $d, 90, 90) +$tile.CloseFigure() +$bg = New-IgColor '#0d1320' +$g.FillPath((New-Object System.Drawing.SolidBrush $bg), $tile) +$g.DrawPath((New-Object System.Drawing.Pen (New-IgColor '#27395a'), (4 * $s)), $tile) + +# x, y, radius, colour - group hub in the middle, one of each workload kind around it. +$nodes = [ordered]@{ + group = @(128, 128, 30, '#9fef00') + config = @(194, 64, 19, '#ffb454') + device = @(196, 192, 19, '#58a6ff') + user = @( 60, 192, 19, '#c792ea') + app = @( 62, 64, 19, '#ff7b72') +} +$edges = @( + @('app', 'group', '#34496e'), + @('user', 'group', '#34496e'), + @('device', 'group', '#9fef00'), + @('group', 'config', '#9fef00') +) + +foreach ($e in $edges) { + $a = $nodes[$e[0]]; $b = $nodes[$e[1]] + $pen = New-Object System.Drawing.Pen (New-IgColor $e[2]), (8 * $s) + $pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round + $pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round + $g.DrawLine($pen, $a[0] * $s, $a[1] * $s, $b[0] * $s, $b[1] * $s) + $pen.Dispose() +} + +# Each node sits in a ring of tile colour so edges stop short of it. +foreach ($n in $nodes.Values) { + $x = $n[0] * $s; $y = $n[1] * $s; $r = $n[2] * $s; $ring = 7 * $s + $g.FillEllipse((New-Object System.Drawing.SolidBrush $bg), ($x - $r - $ring), ($y - $r - $ring), (2 * ($r + $ring)), (2 * ($r + $ring))) + $g.FillEllipse((New-Object System.Drawing.SolidBrush (New-IgColor $n[3])), ($x - $r), ($y - $r), (2 * $r), (2 * $r)) +} + +$OutputPath = [System.IO.Path]::GetFullPath($OutputPath) +$bmp.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png) +$g.Dispose(); $bmp.Dispose() +Write-Host "Wrote $OutputPath ($Size x $Size)" diff --git a/tools/mcp/mcp-server.ps1 b/tools/mcp/mcp-server.ps1 index 2493710..2078d39 100644 --- a/tools/mcp/mcp-server.ps1 +++ b/tools/mcp/mcp-server.ps1 @@ -9,5 +9,5 @@ param( [switch]$LogRequests ) $ErrorActionPreference = 'Stop' -Import-Module (Join-Path $PSScriptRoot '..\..\src\IntuneGraph\IntuneGraph.psd1') -Force +Import-Module (Join-Path $PSScriptRoot '..\..\src\IntuneGraphKit\IntuneGraphKit.psd1') -Force Start-IntuneGraphMcp -Path $Path -LogRequests:$LogRequests