Skip to content

Migrate PSTeams surface to TeamsX cmdlets#67

Merged
PrzemyslawKlys merged 10 commits intomainfrom
feature/teamsx-csharp-migration-phase1
Apr 23, 2026
Merged

Migrate PSTeams surface to TeamsX cmdlets#67
PrzemyslawKlys merged 10 commits intomainfrom
feature/teamsx-csharp-migration-phase1

Conversation

@PrzemyslawKlys
Copy link
Copy Markdown
Member

Summary

  • move the shipping PSTeams module shell into Module/PSTeams and align module packaging/CI with that layout
  • migrate the public PSTeams surface to TeamsX-backed C# cmdlets, including adaptive, connector-card, wrapper-card, and target cmdlets
  • add the starter Graph delivery path plus richer typed adaptive and wrapper-card flows

Details

  • keep TeamsX as the reusable library and TeamsX.PowerShell as the thin cmdlet layer
  • preserve legacy PSTeams command names and aliases on main while removing the old runtime script-function path
  • add typed wrapper-card objects, direct wrapper-card sending through Send-TeamsMessage, and pipeline-friendly Send-TeamsMessageBody
  • add migration-focused module tests, Graph tests, wrapper-card tests, and docs/examples for the new typed flows
  • update PowerShell CI and module build flow, including the Module/PSTeams manifest refresh path and net10.0 module packaging alignment

Validation

  • dotnet test .\TeamsX.sln --configuration Debug --no-build
  • Invoke-Pester -Path .\Module\Tests -Output Detailed

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 71.69584% with 469 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.11%. Comparing base (9a7e2ae) to head (494c537).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
TeamsX/TeamsLegacyAdaptiveNormalizer.cs 0.00% 232 Missing ⚠️
TeamsX/WebhookMessageRenderer.cs 63.05% 69 Missing and 6 partials ⚠️
TeamsX/TeamsWrapperCardRenderer.cs 57.83% 24 Missing and 11 partials ⚠️
TeamsX/GraphMessageRenderer.cs 70.00% 23 Missing and 10 partials ⚠️
TeamsX/GraphTeamsMessageSender.cs 60.00% 11 Missing and 9 partials ⚠️
TeamsX/TeamsMessageTarget.cs 64.81% 14 Missing and 5 partials ⚠️
TeamsX/TeamsColorUtility.cs 10.00% 15 Missing and 3 partials ⚠️
TeamsX/TeamsClient.cs 33.33% 10 Missing and 2 partials ⚠️
TeamsX/TeamsAdaptiveRichTextBlock.cs 0.00% 6 Missing ⚠️
TeamsX/TeamsCardButton.cs 0.00% 4 Missing ⚠️
... and 7 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #67      +/-   ##
==========================================
+ Coverage   62.99%   70.11%   +7.11%     
==========================================
  Files          26       47      +21     
  Lines         327     1974    +1647     
  Branches       48      174     +126     
==========================================
+ Hits          206     1384    +1178     
- Misses        108      529     +421     
- Partials       13       61      +48     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb096b0491

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Module/PSTeams/PSTeams.psm1 Outdated
Comment on lines +5 to +8
if ($runtimeMajor -ge 10) {
@('net10.0', 'net8.0', 'netstandard2.0')
} else {
@('net8.0', 'netstandard2.0')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Select netstandard build for pre-.NET 8 PowerShell runtimes

The Core runtime selection currently prefers net8.0 for every runtime below 10, which includes PowerShell 7.2/7.3 running on .NET 6/7. Those hosts cannot load a net8.0 assembly, so module import fails even though a netstandard2.0 build is available. This creates a runtime compatibility regression for Core users not yet on .NET 8.

Useful? React with 👍 / 👎.

Comment thread Module/Tests/Import-Module.Tests.ps1 Outdated
Comment on lines +123 to +125
Import-Module 'C:\Support\GitHub\PSTeams\PSTeams.psd1' -Force
Get-Command -Module PSTeams | Select-Object -ExpandProperty Name | Sort-Object | ConvertTo-Json
'@
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove hardcoded local path from legacy parity tests

These tests shell out to pwsh and import a module from C:\Support\GitHub\PSTeams\PSTeams.psd1, which is a machine-specific path and not present in GitHub Actions runners or most contributor environments. As a result, the parity tests fail due to missing files rather than regressions, making the suite unreliable in CI.

Useful? React with 👍 / 👎.

Comment on lines +29 to +33
if (!string.IsNullOrWhiteSpace(ActionUrl)) {
WriteObject(new TeamsAdaptiveOpenUrlAction {
Title = Title ?? string.Empty,
Url = ActionUrl ?? string.Empty
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor -Type Action.OpenUrl when ActionUrl is omitted

The cmdlet currently creates an open-url action only when ActionUrl is provided, and otherwise falls through to a show-card action. This means callers who explicitly pass -Type Action.OpenUrl can receive an Action.ShowCard payload instead, which changes legacy behavior and can produce incorrect card actions when URL is supplied later or omitted by mistake.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1614410e0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +23
var resolved = Color.FromName(candidate);
if (resolved.ToArgb() == 0 &&
!string.Equals(candidate, "Transparent", StringComparison.OrdinalIgnoreCase)) {
throw new ArgumentException("The Input value is not a valid colorname nor an valid color hex code.", nameof(color));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve legacy color-name compatibility

This migration now resolves named colors through Color.FromName, which only supports the built-in System.Drawing set. Legacy PSTeams accepted a much larger palette (for example AlbescentWhite, still used in this repo’s examples), so previously valid color inputs now throw ArgumentException or lose color values in cmdlets that call NormalizeToHex, breaking existing scripts during a 1:1 surface migration.

Useful? React with 👍 / 👎.

Comment thread TeamsX/GraphMessageRenderer.cs Outdated
Comment on lines +76 to +77
foreach (var section in request.Sections) {
fragments.Add(RenderSection(section));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip empty section fragments in Graph body rendering

BuildBodyFragments appends every rendered section even when RenderSection returns an empty string, so fragments.Count becomes non-zero and the summary fallback is skipped. For messages that only carry empty/start-group-style sections plus Summary, this can emit an empty Graph body.content, resulting in blank messages or Graph validation failures for empty body content.

Useful? React with 👍 / 👎.

Comment on lines +18 to +21
var httpClient = new HttpClient(handler, disposeHandler: true);
var sender = new WebhookTeamsMessageSender(httpClient, disposeHttpClient: true);

return new TeamsClient(new ITeamsMessageSender[] { sender });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Dispose proxy HTTP resources after send

In the proxy path, a new HttpClientHandler and HttpClient are created for each call, but the returned TeamsClient has no disposal path and callers do not dispose it. In long-lived PowerShell sessions that repeatedly send with -Proxy, this can accumulate undisposed handlers/sockets and eventually cause connection exhaustion.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90e6e57e05

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

AdaptiveCard = AdaptiveCard,
ThemeColor = ResolveThemeColor(),
HideOriginalBody = HideOriginalBody.IsPresent,
UseConnectorCardFormat = UseConnectorCardFormat.IsPresent || (AdaptiveCard is null && Sections.Length > 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enable connector format when connector-only fields are set

New-TeamsMessage now accepts -ThemeColor/-Color and -HideOriginalBody, but UseConnectorCardFormat is only turned on for explicit -UseConnectorCardFormat or when sections are present. When a caller sets color or hide-body on a plain title/text message, those fields are silently dropped because WebhookMessageRenderer.Render takes the non-connector path unless this flag is true, so the output no longer reflects the parameters the user supplied.

Useful? React with 👍 / 👎.

Comment on lines +152 to +155
.Invoke()
.Select(item => item?.BaseObject)
.OfType<TeamsMessageSection>()
.ToArray();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve legacy IDictionary sections from scriptblock input

In the legacy scriptblock path, ResolveLegacySections now keeps only TeamsMessageSection objects via OfType<TeamsMessageSection>(). Older PSTeams usage accepted raw section dictionaries/ordered hashtables from scriptblocks, but those are now filtered out entirely, so facts/buttons/images can disappear and Send-TeamsMessage can send a message with no sections for previously working inputs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a300e09a55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

return AdaptiveCard is null &&
(Sections.Length > 0 ||
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle null -Sections input before reading Length

When callers pass an optional variable that is $null into New-TeamsMessage -Sections, ShouldUseConnectorCardFormat() dereferences Sections.Length and throws a NullReferenceException before any message object is produced. This makes a common conditional-splat pattern fail at runtime instead of simply treating missing sections as empty input.

Useful? React with 👍 / 👎.

Comment on lines +202 to +206
private static string? GetButtonLink(IDictionary dictionary) {
if (dictionary.Contains("target")) {
var targetValue = dictionary["target"];
if (targetValue is IEnumerable enumerable && targetValue is not string) {
foreach (var entry in enumerable) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Extract ActionCard targets from nested actions

In New-TeamsSection's legacy dictionary compatibility path, GetButtonLink only checks top-level target/Target/Targets keys and never reads ActionCard-style nested actions[*].target. Legacy ActionCard button dictionaries therefore lose their URL during conversion, and the renderer emits submit buttons with a null target, breaking previously functional button actions.

Useful? React with 👍 / 👎.

Comment on lines +232 to +233
"ActionCard" => TeamsMessageButtonType.TextInput,
"HttpPOST" => TeamsMessageButtonType.HttpPost,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve DateInput subtype for ActionCard buttons

The legacy dictionary mapper currently converts every @type='ActionCard' button to TeamsMessageButtonType.TextInput, so DateInput cards are silently rewritten as text-input cards. Older payloads that relied on DateInput semantics now render the wrong input control after migration through New-TeamsSection dictionary conversion.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 124eebb57e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread TeamsX.PowerShell/CmdletNewCardList.cs Outdated
Comment on lines +64 to +66
if (value is IDictionary dictionary) {
if (CmdletNewHeroCard.TryCreateCardButton(dictionary, out var fallbackButton)) {
ApplyItem(card, fallbackButton);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse list-item dictionaries before generic button fallback

In ApplyItem, dictionary inputs are passed to CmdletNewHeroCard.TryCreateCardButton before TryCreateListItem. Legacy list-card item dictionaries contain title, so they satisfy the button check first and get converted into TeamsCardButton instead of TeamsListCardItem, which removes them from content.items and produces malformed button payloads. This is a compatibility regression for scriptblocks that still emit hashtable-style list items (for example, reused legacy payload fragments).

Useful? React with 👍 / 👎.

var assemblyDirectory = Path.GetDirectoryName(typeof(TeamsPowerShellImageSupport).Assembly.Location) ?? string.Empty;
var candidates = new[] {
Path.GetFullPath(Path.Combine(assemblyDirectory, "..", "..", "Images", $"{imageName}.jpg")),
Path.GetFullPath(Path.Combine(assemblyDirectory, "..", "..", "..", "..", "Module", "PSTeams", "Images", $"{imageName}.jpg"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fix built-in image fallback path for source-tree imports

ResolveBuiltInImage uses a fallback path that climbs four directories from the loaded assembly and then appends Module/PSTeams/Images. When the module is imported from source (the module shell explicitly prefers TeamsX.PowerShell/bin/Debug/<tfm>), this resolves under .../TeamsX.PowerShell/Module/PSTeams/Images, which does not exist; the first candidate under .../bin/Debug/Images also does not exist. As a result, built-in image names like -Image Add silently fail in development/source-tree usage.

Useful? React with 👍 / 👎.

@PrzemyslawKlys PrzemyslawKlys merged commit 98f19d3 into main Apr 23, 2026
8 checks passed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 494c537421

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

NETProjectName = 'TeamsX.PowerShell'
NETBinaryModule = 'TeamsX.PowerShell.dll'
NETConfiguration = 'Release'
NETFramework = 'net472', 'net8.0', 'net10.0'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict packaged Core target to PowerShell-supported TFM

New-ConfigurationBuild now includes net10.0 in the packed module build, but the module loader imports a single Lib\Core\TeamsX.PowerShell.dll (Module/PSTeams/PSTeams.psm1), so whichever Core binary is emitted there must be loadable by all supported PowerShell Core hosts. If Lib\Core ends up with the net10.0 binary, PowerShell 7.2/7.3 (.NET 6/7) cannot load the module at all. Keep the packaged Core target pinned to a runtime those hosts can load (e.g., net8.0) and test newer TFMs separately.

Useful? React with 👍 / 👎.

Comment on lines 97 to 99
if (!result.IsSuccessStatusCode) {
WriteError(CreateDeliveryFailureError(result));
WriteError(TeamsPowerShellDeliverySupport.CreateDeliveryFailureError(result, "Send-TeamsMessage"));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Run delivery-body failure checks in typed send path

The typed Send-TeamsMessage path only treats non-2xx responses as failures and never calls TeamsPowerShellDeliverySupport.WriteDeliveryIssue, so it misses the same body-level failure detection ("failed"/"error") used by legacy and wrapper-card senders. For webhook endpoints that reply 200 with an error payload, typed sends will look successful and scripts can proceed as if delivery worked.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants