Skip to content
Merged
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
19 changes: 13 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```

Expand Down
22 changes: 20 additions & 2 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
24 changes: 23 additions & 1 deletion docs/discoverability.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,32 @@ 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
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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Binary file added docs/img/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'"]
}
```

Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ const mcpChat = `> why does Win11 Security Baseline apply to DEV-FIN-01?
<div class="install-grid">
<div>
<div class="step">1 · Zero-setup demo</div>
<pre class="run">Install-Module IntuneGraph -Scope CurrentUser
Import-Module IntuneGraph
<pre class="run">Install-Module IntuneGraphKit -Scope CurrentUser
Import-Module IntuneGraphKit
Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph -Open</pre>
</div>
<div>
Expand Down
Loading
Loading